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

313 lines
9.0 KiB

---
title: "tview and you - Creating Rich Terminal User Interfaces"
date: 2019-11-08T01:42:18-07:00
categories: [tutorial]
---
3 years ago
[![Recording of presentation demo](https://raw.githubusercontent.com/rivo/tview/master/tview.gif)](https://github.com/rivo/tview/tree/master/demos/presentation)
3 years ago
This is an introduction to using [tview](https://github.com/rivo/tview) (or [cview](https://git.sr.ht/~tslocum/cview)) to create rich terminal-based user interfaces with [Go](https://golang.org).
3 years ago
## Primitives
The [Primitive](https://godoc.org/github.com/rivo/tview#Primitive) interface is as follows:
3 years ago
{{< 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
}
3 years ago
{{< / highlight >}}
[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.
3 years ago
## Widgets
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:
3 years ago
{{< highlight go >}}
type TextView struct {
*Box
// The text buffer.
buffer []string
// The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int
// ...
}
3 years ago
{{< / highlight >}}
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:
3 years ago
{{< highlight go >}}
nameLabel := tview.NewTextView().
SetTextAlign(tview.AlignRight).
SetDynamicColors(true).
SetWrap(true).
SetWordWrap(true).
SetText("Please enter your name:")
3 years ago
{{< / highlight >}}
New widgets may be defined, as documented in the [Primitive demo](https://github.com/rivo/tview/tree/master/demos/primitive).
3 years ago
### Widget Elements
---
[**Button**](https://godoc.org/github.com/rivo/tview#Button) is a labeled box that triggers an action when selected.
3 years ago
{{< highlight go >}}
button := tview.NewButton("OK").SetSelectedFunc(func() {
pressedOK()
})
3 years ago
{{< / highlight >}}
---
[**Checkbox**](https://godoc.org/github.com/rivo/tview#Checkbox) holds a label and boolean value which may be checked and unchecked.
3 years ago
{{< highlight go >}}
checkbox := tview.NewCheckbox().SetLabel("Toggle value with Enter: ")
3 years ago
{{< / highlight >}}
---
[**DropDown**](https://godoc.org/github.com/rivo/tview#DropDown) holds one or more options which may be selected as a dropdown list.
3 years ago
{{< highlight go >}}
dropdown := tview.NewDropDown().
SetLabel("Select an option with Enter: ").
SetOptions([]string{"Foo", "Bar", "Baz"}, nil)
3 years ago
{{< / highlight >}}
---
[**InputField**](https://godoc.org/github.com/rivo/tview#InputField) is a box where text may be entered.
3 years ago
{{< highlight go >}}
inputField := tview.NewInputField().
SetLabel("Name: ").
SetPlaceholder("John Smith").
SetFieldWidth(14).
SetDoneFunc(func(key tcell.Key) {
processName()
})
3 years ago
{{< / highlight >}}
---
[**Modal**](https://godoc.org/github.com/rivo/tview#Modal) is a centered message window which may have one or more buttons.
3 years ago
{{< 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()
}
})
3 years ago
{{< / highlight >}}
---
[**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.
3 years ago
{{< highlight go >}}
textView := tview.NewTextView().
SetWrap(true).
SetWordWrap(true).
SetText("Hello, World!")
3 years ago
{{< / highlight >}}
---
3 years ago
### Widget Containers
---
[**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.
---
3 years ago
## 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.
* [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.
3 years ago
{{< highlight go >}}
app.QueueUpdateDraw(func() {
app.SetRoot(appGrid, true)
})
3 years ago
{{< / highlight >}}
3 years ago
## Example Application
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.
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://git.sr.ht/~tslocum/netris).
Install tview if you haven't already:
3 years ago
{{< highlight command >}}
go get -u github.com/rivo/tview
3 years ago
{{< / highlight >}}
Then create a file named greet.go:
3 years ago
{{< 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)
}
}
3 years ago
{{< / highlight >}}