|
|
|
@ -0,0 +1,321 @@
|
|
|
|
|
---
|
|
|
|
|
title: "tview and you - Creating Rich Terminal User Interfaces"
|
|
|
|
|
date: 2019-11-08T01:42:18-07:00
|
|
|
|
|
categories: [tutorial]
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<a href="https://github.com/rivo/tview/tree/master/demos/presentation"><img src="https://raw.githubusercontent.com/rivo/tview/master/tview.gif" width=640" height="446" border="0" title="tview presentation demo"></a>
|
|
|
|
|
|
|
|
|
|
This is an introduction to using [tview](https://github.com/rivo/tview) to create rich terminal-based user interfaces with Go.
|
|
|
|
|
|
|
|
|
|
## Contents
|
|
|
|
|
|
|
|
|
|
* [Primitives](#primitives)
|
|
|
|
|
* [Widgets](#widgets)
|
|
|
|
|
* [Elements](#widget-elements)
|
|
|
|
|
* [Containers](#widget-containers)
|
|
|
|
|
* [Thread Safety](#thread-safety)
|
|
|
|
|
* [Example Application](#example-application)
|
|
|
|
|
|
|
|
|
|
# Primitives
|
|
|
|
|
|
|
|
|
|
The [Primitive](https://godoc.org/github.com/rivo/tview#Primitive) interface is as follows:
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
[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.
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
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](https://godoc.org/github.com/rivo/tview#Grid).
|
|
|
|
|
|
|
|
|
|
Most widget commands may be chained together:
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
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](https://github.com/rivo/tview/tree/master/demos/primitive).
|
|
|
|
|
|
|
|
|
|
## Widget Elements
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**Button**](https://godoc.org/github.com/rivo/tview#Button) is a labeled box that triggers an action when selected.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
button := tview.NewButton("OK").SetSelectedFunc(func() {
|
|
|
|
|
pressedOK()
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**Checkbox**](https://godoc.org/github.com/rivo/tview#Checkbox) holds a label and boolean value which may be checked and unchecked.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
checkbox := tview.NewCheckbox().SetLabel("Toggle value with Enter: ")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**DropDown**](https://godoc.org/github.com/rivo/tview#DropDown) holds one or more options which may be selected as a dropdown list.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
dropdown := tview.NewDropDown().
|
|
|
|
|
SetLabel("Select an option with Enter: ").
|
|
|
|
|
SetOptions([]string{"Foo", "Bar", "Baz"}, nil)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**InputField**](https://godoc.org/github.com/rivo/tview#InputField) is a box where text may be entered.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
inputField := tview.NewInputField().
|
|
|
|
|
SetLabel("Name: ").
|
|
|
|
|
SetPlaceholder("John Smith").
|
|
|
|
|
SetFieldWidth(14).
|
|
|
|
|
SetDoneFunc(func(key tcell.Key) {
|
|
|
|
|
processName()
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**Modal**](https://godoc.org/github.com/rivo/tview#Modal) is a centered message window which may have one or more buttons.
|
|
|
|
|
|
|
|
|
|
```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()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
[**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.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
textView := tview.NewTextView().
|
|
|
|
|
SetWrap(true).
|
|
|
|
|
SetWordWrap(true).
|
|
|
|
|
SetText("Hello, World!")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
app.QueueUpdateDraw(func() {
|
|
|
|
|
app.SetRoot(appGrid, true)
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
|
|
|
|
|
```command
|
|
|
|
|
go get -u github.com/rivo/tview
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then create a file named greet.go:
|
|
|
|
|
|
|
|
|
|
```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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|