tview and you - Creating Rich Terminal User Interfaces

Page content

Recording of presentation demo

Donate

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:

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
}

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:

type TextView struct {
	*Box

	// The text buffer.
	buffer []string

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

	// ...
}

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

Most widget commands may be chained together:

nameLabel := tview.NewTextView().
	SetTextAlign(tview.AlignRight).
	SetDynamicColors(true).
	SetWrap(true).
	SetWordWrap(true).
	SetText("Please enter your name:")

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

Widget Elements


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

button := tview.NewButton("OK").SetSelectedFunc(func() {
	pressedOK()
})

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

checkbox := tview.NewCheckbox().SetLabel("Toggle value with Enter: ")

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

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

InputField is a box where text may be entered.

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

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

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()
		}
	})

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

textView := tview.NewTextView().
	SetWrap(true).
	SetWordWrap(true).
	SetText("Hello, World!")

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.

app.QueueUpdateDraw(func() {
	app.SetRoot(appGrid, true)
})

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:

go get -u github.com/rivo/tview

Then create a file named greet.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)
	}
}