© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_16

16. TUI Framework

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 

You saw in Chapter 15 that ANSI codes contain a different variety of code that can be used to develop text-based user interfaces. You also saw examples of using ANSI codes and learned what the different codes mean. There are a number of user interface libraries for Go that take care of user interface operations, thereby making development easier and faster. In this chapter, you will look at these libraries and explore in detail how they work internally.

In this chapter, you will look at two libraries. The first library is a simple library called uiprogress that allows an application to create a text-based progress bar. The other is called bubbletea and it is a more comprehensive library that allows an application to create different kinds of text-based UIs such as text input, boxes, spinners, and more.

By the end of this chapter, you will learn the following:
  • How to use the libraries

  • How the libraries work internally

uiprogress

In this section, you will look at the uiprogress library, which is hosted at https://github.com/gosuri/uiprogress. The library provides a progress bar user interface, as shown in Figure 16-1. The application uses the library to create a progress bar as a feedback mechanism to show that an operation is currently in progress.

A screenshot of the output screen. The text reads app deployment started app 1 and app 2. app 1 starts from 4s and goes to 86 percent and app 2 starts from 4s and goes to 100 percent.

Figure 16-1

uiprogress progress bar

Check the project out from GitHub to your local environment and run the sample application that is provided inside the example/simple directory.
go run main.go
The output is shown in Figure 16-2.

A screenshot of the output screen. The text reads 1s 84 percent.

Figure 16-2

Progress bar output from simple.go

The sample code is quite simple.
func main() {
  uiprogress.Start()            // start rendering
  bar := uiprogress.AddBar(100) // Add a new bar
  // optionally, append and prepend completion and elapsed time
  bar.AppendCompleted()
  bar.PrependElapsed()
  for bar.Incr() {
     time.Sleep(time.Millisecond * 20)
  }
}

Code Flow

You will use this sample application as the basis to do a walk-through of the library. Figure 16-3 shows how the application interacts with the library and shows what is actually happening behind the scenes inside the library.

A flow diagram of a code starts from simple. go and divided into 5 factors named to start, add a bar, app end completed, prepend lapsed, and Inc to bars, print, and listen with the help of append and loop.

Figure 16-3

Code flow from simple.go to the library

Let’s walk through the diagram to understand what’s happening. The first thing the app does is call the Start() function. This is to initialize the internals of the library. The function spins off a goroutine and calls the Listen() function, which reside inside the progress.go file shown here:
func (p *Progress) Listen() {
  for {
     p.mtx.Lock()
     interval := p.RefreshInterval
     p.mtx.Unlock()
     select {
     case <-time.After(interval):
        p.print()
     case <-p.tdone:
        p.print()
        close(p.tdone)
        return
     }
  }
}

The function is in a for{} loop and calls the print() function at an interval that has been set at the default of 10 milliseconds.

Upon completing the Start() function, the sample app calls the AddBar() function to create a new progress bar that will be shown to the user. The library can process multiple progress bar at the same time, so any new bar created will be stored in the Bars slice, as shown:
func (p *Progress) AddBar(total int) *Bar {
  ...
  bar := NewBar(total)
  bar.Width = p.Width
  p.Bars = append(p.Bars, bar)
  ...
}

Updating Progress

Upon expiry of the 10 milliseconds interval, the library updates each of the registered progress bars using the print() function running in the background. The code snippet of running the print() function is as follows:
func (p *Progress) print() {
  ...
  for _, bar := range p.Bars {
     fmt.Fprintln(p.lw, bar.String())
  }
  ...
}
The print() function loops through the Bars slice and calls the String() function, which in turn calls the Bytes() function. The Bytes() function performs calculations to get the correct value for the progress bar and prints this with a suffix and prefix.
func (b *Bar) Bytes() []byte {
  completedWidth := int(float64(b.Width) * (b.CompletedPercent() / 100.00))
  for i := 0; i < completedWidth; i++ {
     ...
  }
  ...
  pb := buf.Bytes()
  if completedWidth > 0 && completedWidth < b.Width {
     pb[completedWidth-1] = b.Head
  }
  ...
  return pb
}
The function calls AppendCompleted() and PrependElapsed() are used to define the following:
  • AppendCompleted() adds a function that will print out the percentage completed when the progress bar has completed its operation.

func (b *Bar) AppendCompleted() *Bar {
  b.AppendFunc(func(b *Bar) string {
     return b.CompletedPercentString()
  })
  return b
}
  • PrependElapsed() prefixes the progress bar with the time it has taken to complete so far.

func (b *Bar) PrependElapsed() *Bar {
  b.PrependFunc(func(b *Bar) string {
     return strutil.PadLeft(b.TimeElapsedString(), 5, ' ')
  })
  return b
}
Lastly, the application needs to specify the increment or decrement of the progress bar value. In the sample code case, it increments as follows:
func main() {
  ...
  for bar.Incr() {
     time.Sleep(time.Millisecond * 20)
  }
}

The code will look as long as the bar.Incr() returns true and will sleep for 20 milliseconds before incrementing again.

From your code perspective, the library takes care of updating and managing the progress bar, allowing your application to focus on its main task. All the application needs to do is just inform the library about the new value of the bar by calling the Incr() or Decr() function.

In the next section, you will look at a more comprehensive library that provides a better user interface for an application.

Bubbletea

In the previous section, you saw the uiprogress progress bar library and looked at how it works internally. In this section, you will take a look at another user interface framework called bubbletea. The code can be checked out from https://github.com/charmbracelet/bubbletea.

Run the sample application inside the examples/tui-daemon-combo folder as follows:
go run main.go
You will get output that looks like Figure 16-4.

A screenshot explains the output. The output is five works finished at 817, 727, 658, 219, and 190-meter seconds.

Figure 16-4

tui-daemon-combo sample output

The interesting thing about this TUI framework is it provides a variety of user interfaces: progress bars, spinners, lists, and more. Figure 16-5 shows the different functions the application must provide in order to use the library.

A screenshot explains the three application functions such as int, update, and view. All three are connected parallel to the bubble tea function.

Figure 16-5

Application functions for bubbletea interaction

In the next few sections, you will use the tui-daemon-combo sample code to work out how the code flows inside the library.

Using bubbletea is quite straightforward, as shown here:
func main() {
  ...
  p := tea.NewProgram(newModel(), opts...)
  if err := p.Start(); err != nil {
     fmt.Println("Error starting Bubble Tea program:", err)
     os.Exit(1)
  }
}
The code calls tea.NewProgram(), passing in the Model interface and options that need to be set. The Model interface defined by the library is as follows:
type Model interface {
  Init() Cmd
  Update(Msg) (Model, Cmd)
  View() string
}
The newModel() function returns the implementation of the Model interface, which is defined as follows:
func (m model) Init() tea.Cmd {
  ...
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  ...
}
func (m model) View() string {
  ...
}

Now you have defined the different functions that will be called by the library when constructing and updating the UI. Next, you will look at how each of these functions are used by the library.

Init

The Init() function is the first function called by bubbletea after calling the Start() function. You saw that Init() must return a Cmd type, which is declared as the function type shown here:
type Cmd func() Msg
The Init() functions use batches to return different kinds of function types: spinner.Tick and runPretendProcess. This is done by using the tea.Batch() function, as shown here:
func (m model) Init() tea.Cmd {
  ...
  return tea.Batch(
     spinner.Tick,
     runPretendProcess,
  )
}
Internally, tea.Batch() returns an anonymous function that wraps the different Cmd function types into an array of Cmd, as shown in this snippet:
type batchMsg []Cmd
func Batch(cmds ...Cmd) Cmd {
  ...
  return func() Msg {
     return batchMsg(validCmds)
  }
}

After bubbletea completes calling the application Init() function, it kickstarts the process. Internally, it uses channels to read different incoming messages to perform different user interface operations, so in your sample code case, it processes the batchMsg array and starts calling the Cmd function types.

The Cmd function type implementation returns Msg, which is an interface as defined in the library.
type Msg interface{}
In the sample code, you uses spinner.Tick and runPretendProcess, which are defined as follows:
type processFinishedMsg time.Duration
func Tick() tea.Msg {
  return TickMsg{Time: time.Now()}
}
func runPretendProcess() tea.Msg {
  ...
  return processFinishedMsg(pause)
}
Figure 16-6 shows that the library uses a number of goroutines to do several things in the background including processing the Msg that are returned by the function types that will be used in the Update() function, which you will look at in the next section.

A framework of the sample code starts from the main function and then starts. Start returns the model and then classified into three parts named S I G N T, initCmD, and Msg.

Figure 16-6

Initialization of the internal execution flow

Update

The Update function is called to update the state of the user interface. In the sample app, it is defined as follows:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  switch msg := msg.(type) {
  case tea.KeyMsg:
     ...
     return m, tea.Quit
  case spinner.TickMsg:
     ...
     m.spinner, cmd = m.spinner.Update(msg)
     ...
  case processFinishedMsg:
     ...
     m.results = append(m.results[1:], res)
     ...
  default:
     return m, nil
  }
}

The Update function receives different kinds of tea.Msg because it is defined as an interface, so the code needs to do type checking and handle the type it wants to handle. For example, when the function receives spinner.TickMsg, it updates the spinner by calling the spinner.Update() function, and when it receives tea.KeyMsg, it quits the application.

The function only needs to process messages that it is interested in and process any user interface state management that it needs to do. Other heavy operations must be avoided in the function.

View

The last function, View(), is called by the library to update the user interface. The application is given the freedom to update the user interface as it sees fit. This flexibility allows the application to render a user interface that suits its needs.

This does not mean that the application needs to know how to draw the user interface. This is taken care of by the functions available for each user interface. Here is the View() function:
func (m model) View() string {
  s := " " +
     m.spinner.View() + " Doing some work... "
  for _, res := range m.results {
     ...
  }
  ...
  if m.quitting {
     s += " "
  }
  return indent.String(s, 1)
}

The app combines all the user interfaces that it needs to display to the user by extracting the different values from the different variables. For example, it extract the results array values to show it to the user. The results array is populated in the Update function when it receives the processFinishedMsg message type.

The function returns a string containing the user interface that will be rendered by the library to the terminal.

Figure 16-7 shows at a high level the different goroutines that are spun off by the library and that take care of the different parts of the user interfaces such as user input using the keyboard, mouse, terminal resizing, and more.

The architecture is like a pub/sub model where the central goroutine process all the different messages and calls the relevant functions internally to perform the operations.

A framework of process messages has six factors such as user input, process Cmds, terminal resize, tea dot msg function, update function, and view function.

Figure 16-7

Centralized processing of messages

Summary

In this chapter, you look at two different terminal-based user interface frameworks that provide APIs for developers to build command-line user interfaces. You looked at sample applications of how to use the frameworks to build simple command-line user interfaces.

You looked at the internals of the frameworks to understand how they work. Knowing this gives you better insight into how to troubleshoot issues when using these kinds of frameworks. And understanding the complexity of these frameworks helps you build your own asynchronous applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.181.196