You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

9.0 KiB

title date categories
tview and you - Creating Rich Terminal User Interfaces 2019-11-08T01:42:18-07:00
tutorial

Recording of presentation demo

This is an introduction to using tview (or cview) to create rich terminal-based user interfaces with Go.

Primitives

The Primitive interface is as follows:

{{< highlight go >}} type Primitive interface { Draw(screen tcell.Screen) GetRect() (int, int, int, int) SetRect(x, y, width, height int) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) Focus(delegate func(p Primitive)) Blur() GetFocusable() Focusable } {{< / highlight >}}

Box is the only primitive implemented. It has a size, padding amount, optional border, optional title and background color.

Widgets

Widgets are structs which embed a Box and build upon it.

From the TextView declaration:

{{< highlight go >}} type TextView struct { *Box

// The text buffer.
buffer []string

// The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int

// ...

} {{< / highlight >}}

Some widgets allow nesting other widgets within them, such as Grid.

Most widget commands may be chained together:

{{< highlight go >}} nameLabel := tview.NewTextView(). SetTextAlign(tview.AlignRight). SetDynamicColors(true). SetWrap(true). SetWordWrap(true). SetText("Please enter your name:") {{< / highlight >}}

New widgets may be defined, as documented in the Primitive demo.

Widget Elements


Button is a labeled box that triggers an action when selected.

{{< highlight go >}} button := tview.NewButton("OK").SetSelectedFunc(func() { pressedOK() }) {{< / highlight >}}


Checkbox holds a label and boolean value which may be checked and unchecked.

{{< highlight go >}} checkbox := tview.NewCheckbox().SetLabel("Toggle value with Enter: ") {{< / highlight >}}


DropDown holds one or more options which may be selected as a dropdown list.

{{< highlight go >}} dropdown := tview.NewDropDown(). SetLabel("Select an option with Enter: "). SetOptions([]string{"Foo", "Bar", "Baz"}, nil) {{< / highlight >}}


InputField is a box where text may be entered.

{{< highlight go >}} inputField := tview.NewInputField(). SetLabel("Name: "). SetPlaceholder("John Smith"). SetFieldWidth(14). SetDoneFunc(func(key tcell.Key) { processName() }) {{< / highlight >}}


Modal is a centered message window which may have one or more buttons.

{{< highlight go >}} modal := tview.NewModal(). SetText("Are you sure you want to exit?"). AddButtons([]string{"Cancel", "Quit"}). SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonIndex == 1 { app.Stop() } }) {{< / highlight >}}


TextView is a box containing text. Colored text is supported when enabled.

{{< highlight go >}} textView := tview.NewTextView(). SetWrap(true). SetWordWrap(true). SetText("Hello, World!") {{< / highlight >}}


Widget Containers


Flex is a flexbox layout container.

See the Flex demo for example usage.


Grid is a grid layout container.

See the Grid demo for example usage.


Form displays one or more form elements in a vertical or horizontal layout.

See the Form demo for example usage.


List displays one or more widgets as a selectable list.

See the List demo for example usage.


Pages displays one or more widgets at a time.

See the Pages demo for example usage.


Table displays one or more widgets in rows and columns.

See the Table demo for example usage.


TreeView displays one or more widgets in a tree.

See the TreeView demo for example usage.


Thread Safety

Most tview functions cannot safely be called from any thread except the main one.

Using either of the following functions, we can queue a function to be executed in the main thread.

The only difference between the two is that QueueUpdateDraw calls Application.Draw after the queued function returns.

One exception is TextView.Write, which may safely be called from multiple goroutines.

Below is an example of setting a new root primitive from another goroutine.

{{< highlight go >}} app.QueueUpdateDraw(func() { app.SetRoot(appGrid, true) }) {{< / highlight >}}

Example Application

A tview application is constructed of a running Application with at least one root widget.

To display a primitive (and its contents), we call Application.SetRoot.

This function has two arguments, a primitive which will become the root of the screen, and a boolean which controls whether the primitive will be resized to fit the screen.

In this example, the root is a Grid containing a label, an input and a submit button. For a more complex example, see netris.

Install tview if you haven't already:

{{< highlight command >}} go get -u github.com/rivo/tview {{< / highlight >}}

Then create a file named greet.go:

{{< highlight go >}} package main

import ( "fmt" "log" "strings"

"github.com/gdamore/tcell"
"github.com/rivo/tview"

)

func main() { // Initialize application app := tview.NewApplication()

// Create label
label := tview.NewTextView().SetText("Please enter your name:")

// Create input field
input := tview.NewInputField()

// Create submit button
btn := tview.NewButton("Submit")

// Create empty Box to pad each side of appGrid
bx := tview.NewBox()

// Create Grid containing the application's widgets
appGrid := tview.NewGrid().
	SetColumns(-1, 24, 16, -1).
	SetRows(-1, 2, 3, -1).
	AddItem(bx, 0, 0, 3, 1, 0, 0, false). // Left - 3 rows
	AddItem(bx, 0, 1, 1, 1, 0, 0, false). // Top - 1 row
	AddItem(bx, 0, 3, 3, 1, 0, 0, false). // Right - 3 rows
	AddItem(bx, 3, 1, 1, 1, 0, 0, false). // Bottom - 1 row
	AddItem(label, 1, 1, 1, 1, 0, 0, false).
	AddItem(input, 1, 2, 1, 1, 0, 0, false).
	AddItem(btn, 2, 1, 1, 2, 0, 0, false)

// submittedName is toggled each time Enter is pressed
var submittedName bool

// Capture user input
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
	// Anything handled here will be executed on the main thread
	switch event.Key() {
	case tcell.KeyEnter:
		submittedName = !submittedName

		if submittedName {
			name := input.GetText()
			if strings.TrimSpace(name) == "" {
				name = "Anonymous"
			}

			// Create a modal dialog
			m := tview.NewModal().
				SetText(fmt.Sprintf("Greetings, %s!", name)).
				AddButtons([]string{"Hello"})

			// Display and focus the dialog
			app.SetRoot(m, true).SetFocus(m)
		} else {
			// Clear the input field
			input.SetText("")

			// Display appGrid and focus the input field
			app.SetRoot(appGrid, true).SetFocus(input)
		}
		return nil
	case tcell.KeyEsc:
		// Exit the application
		app.Stop()
		return nil
	}

	return event
})

// Set the grid as the application root and focus the input field
app.SetRoot(appGrid, true).SetFocus(input)

// Run the application
err := app.Run()
if err != nil {
	log.Fatal(err)
}

} {{< / highlight >}}