rocket9labs.com/content/post/tview-and-you.md

313 lines
9.0 KiB
Markdown
Raw Normal View History

2019-11-20 12:48:44 +00:00
---
title: "tview and you - Creating Rich Terminal User Interfaces"
date: 2019-11-08T01:42:18-07:00
categories: [tutorial]
---
2020-01-18 02:14:28 +00:00
[![Recording of presentation demo](https://raw.githubusercontent.com/rivo/tview/master/tview.gif)](https://github.com/rivo/tview/tree/master/demos/presentation)
2019-11-20 12:48:44 +00:00
2020-01-23 00:12:32 +00:00
This is an introduction to using [tview](https://github.com/rivo/tview) (or [cview](https://gitlab.com/tslocum/cview)) to create rich terminal-based user interfaces with [Go](https://golang.org).
2019-11-20 12:48:44 +00:00
2020-01-18 02:14:28 +00:00
## Primitives
2019-11-20 12:48:44 +00:00
The [Primitive](https://godoc.org/github.com/rivo/tview#Primitive) interface is as follows:
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
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
}
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
[Box](https://godoc.org/github.com/rivo/tview#Box) is the only primitive implemented.
It has a size, padding amount, optional border, optional title and background color.
2020-01-18 02:14:28 +00:00
## Widgets
2019-11-20 12:48:44 +00:00
Widgets are structs which embed a [Box](https://godoc.org/github.com/rivo/tview#Box) and build upon it.
From the [TextView](https://godoc.org/github.com/rivo/tview#TextView) declaration:
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
type TextView struct {
*Box
// The text buffer.
buffer []string
// The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int
// ...
}
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
Some widgets allow nesting other widgets within them, such as [Grid](https://godoc.org/github.com/rivo/tview#Grid).
Most widget commands may be chained together:
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
nameLabel := tview.NewTextView().
SetTextAlign(tview.AlignRight).
SetDynamicColors(true).
SetWrap(true).
SetWordWrap(true).
SetText("Please enter your name:")
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
New widgets may be defined, as documented in the [Primitive demo](https://github.com/rivo/tview/tree/master/demos/primitive).
2020-01-18 02:14:28 +00:00
### Widget Elements
2019-11-20 12:48:44 +00:00
---
[**Button**](https://godoc.org/github.com/rivo/tview#Button) is a labeled box that triggers an action when selected.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
button := tview.NewButton("OK").SetSelectedFunc(func() {
pressedOK()
})
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
[**Checkbox**](https://godoc.org/github.com/rivo/tview#Checkbox) holds a label and boolean value which may be checked and unchecked.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
checkbox := tview.NewCheckbox().SetLabel("Toggle value with Enter: ")
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
[**DropDown**](https://godoc.org/github.com/rivo/tview#DropDown) holds one or more options which may be selected as a dropdown list.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
dropdown := tview.NewDropDown().
SetLabel("Select an option with Enter: ").
SetOptions([]string{"Foo", "Bar", "Baz"}, nil)
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
[**InputField**](https://godoc.org/github.com/rivo/tview#InputField) is a box where text may be entered.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
inputField := tview.NewInputField().
SetLabel("Name: ").
SetPlaceholder("John Smith").
SetFieldWidth(14).
SetDoneFunc(func(key tcell.Key) {
processName()
})
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
[**Modal**](https://godoc.org/github.com/rivo/tview#Modal) is a centered message window which may have one or more buttons.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
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()
}
})
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
[**TextView**](https://godoc.org/github.com/rivo/tview#TextView) is a box containing text.
[Colored text](https://godoc.org/github.com/rivo/tview#hdr-Colors) is supported when enabled.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
textView := tview.NewTextView().
SetWrap(true).
SetWordWrap(true).
SetText("Hello, World!")
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
---
2020-01-18 02:14:28 +00:00
### Widget Containers
2019-11-20 12:48:44 +00:00
---
[**Flex**](https://godoc.org/github.com/rivo/tview#Flex) is a [flexbox layout](https://en.wikipedia.org/wiki/CSS_Flexible_Box_Layout) container.
See the [Flex demo](https://github.com/rivo/tview/tree/master/demos/flex) for example usage.
---
[**Grid**](https://godoc.org/github.com/rivo/tview#Grid) is a [grid layout](https://en.wikipedia.org/wiki/CSS_grid_layout) container.
See the [Grid demo](https://github.com/rivo/tview/tree/master/demos/grid) for example usage.
---
[**Form**](https://godoc.org/github.com/rivo/tview#Form) displays one or more form elements in a vertical or horizontal layout.
See the [Form demo](https://github.com/rivo/tview/tree/master/demos/form) for example usage.
---
[**List**](https://godoc.org/github.com/rivo/tview#List) displays one or more widgets as a selectable list.
See the [List demo](https://github.com/rivo/tview/tree/master/demos/list) for example usage.
---
[**Pages**](https://godoc.org/github.com/rivo/tview#Pages) displays one or more widgets at a time.
See the [Pages demo](https://github.com/rivo/tview/tree/master/demos/pages) for example usage.
---
[**Table**](https://godoc.org/github.com/rivo/tview#Table) displays one or more widgets in rows and columns.
See the [Table demo](https://github.com/rivo/tview/tree/master/demos/table) for example usage.
---
[**TreeView**](https://godoc.org/github.com/rivo/tview#TreeView) displays one or more widgets in a tree.
See the [TreeView demo](https://github.com/rivo/tview/tree/master/demos/treeview) for example usage.
---
2020-01-18 02:14:28 +00:00
## Thread Safety
2019-11-20 12:48:44 +00:00
**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.
* [Application.QueueUpdate](https://godoc.org/github.com/rivo/tview#Application.QueueUpdate)
* [Application.QueueUpdateDraw](https://godoc.org/github.com/rivo/tview#Application.QueueUpdateDraw)
The only difference between the two is that QueueUpdateDraw calls [Application.Draw](https://godoc.org/github.com/rivo/tview#Application.Draw) after the queued function returns.
One exception is [TextView.Write](https://godoc.org/github.com/rivo/tview#TextView.Write), which may safely be called from multiple goroutines.
Below is an example of setting a new root primitive from another goroutine.
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
app.QueueUpdateDraw(func() {
app.SetRoot(appGrid, true)
})
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
2020-01-18 02:14:28 +00:00
## Example Application
2019-11-20 12:48:44 +00:00
A tview application is constructed of a running [Application](https://godoc.org/github.com/rivo/tview#Application) with at least one root widget.
To display a primitive (and its contents), we call [Application.SetRoot](https://godoc.org/github.com/rivo/tview#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.
2020-01-23 00:12:32 +00:00
In this example, the root is a Grid containing a label, an input and a submit button. For a more complex example, see [netris](https://gitlab.com/tslocum/netris).
2019-11-20 12:48:44 +00:00
Install tview if you haven't already:
2020-01-18 02:14:28 +00:00
{{< highlight command >}}
2019-11-20 12:48:44 +00:00
go get -u github.com/rivo/tview
2020-01-18 02:14:28 +00:00
{{< / highlight >}}
2019-11-20 12:48:44 +00:00
Then create a file named greet.go:
2020-01-18 02:14:28 +00:00
{{< highlight go >}}
2019-11-20 12:48:44 +00:00
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)
}
}
2020-01-18 02:14:28 +00:00
{{< / highlight >}}