Cookbook
Short recipes. Each is copied from an Example function, which go test runs
and checks against its // Output: comment, so the code compiles and the output
is real. The source is named under each recipe. In layout/example_test.go,
show is a helper that prints each line with trailing spaces trimmed.
Recipes on topic pages: follow the terminal’s light or dark background, click regions from a layout, focus across mixed widgets, a widget inside a sized layout.
Header and footer that stay put around a long body
Section titled “Header and footer that stay put around a long body”Wrap the body in layout.Fill. A plain Grow: 1 child that measures taller
than the screen pushes the footer out of view instead (Example_growTrap).
lines := make([]string, 100)for i := range lines { lines[i] = fmt.Sprintf("log %02d", i)}ui := layout.Column(0, layout.FlexChild{Node: layout.Block("== header ==")}, layout.Fill(layout.Block(strings.Join(lines, "\n"))), layout.FlexChild{Node: layout.Block("== footer ==")},)show(layout.Draw(ui, layout.Constraints{MinW: 12, MaxW: 12, MinH: 5, MaxH: 5}))// Output:// == header ==// log 00// log 01// log 02// == footer ==Source: ExampleFill, layout/example_test.go.
Columns that line up across rows
Section titled “Columns that line up across rows”A zero Track sizes the column to its widest cell, Grow takes the leftover
width, and Size fixes it.
g := layout.GridNode( []layout.Track{{}, {Grow: 1}, {Size: 3}}, 1, layout.Block("id"), layout.Block("name"), layout.Block("ok"), layout.Block("7"), layout.Block("widget"), layout.Block("no"),)// MaxH must be set: a zero Max means "at most 0".show(layout.Draw(g, layout.Constraints{MinW: 20, MaxW: 20, MaxH: layout.Unbounded}))// Output:// id name ok//// 7 widget noSource: ExampleGridNode, layout/example_test.go. For different column and
row gaps, see ExampleGridNodeGaps.
A different screen for narrow terminals
Section titled “A different screen for narrow terminals”ui := layout.Responsive([]layout.Break{{MaxW: 20}}, layout.Block("menu"), layout.Block("home | search | settings"))fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 20, H: 3})))fmt.Println(layout.Draw(ui, layout.Loose(layout.Size{W: 40, H: 3})))// Output:// menu// home | search | settingsSource: ExampleResponsive, layout/example_test.go. The first node whose
Break admits the available width and height is used. A node with no Break
is the fallback for larger sizes.
Tab order that follows the screen
Section titled “Tab order that follows the screen”Number the fields in the order your model keeps them, label them with
layout.Named, and let focus.LayoutOrder sort them by position.
cell := func(name string) layout.FlexChild { return layout.FlexChild{Node: layout.Named(name, layout.Block(name)), Basis: 1}}form := layout.Row(2, layout.FlexChild{Node: layout.Column(0, cell("name"), cell("email")), Basis: 8}, layout.FlexChild{Node: layout.Column(0, cell("city"), cell("submit")), Basis: 8},)names := []string{"name", "email", "city", "submit"}size := layout.Size{W: 20, H: 2}
ring := focus.New(len(names)).WithOrder(focus.LayoutOrder(form, size, names...))for range names { fmt.Print(names[ring.Current()], " ") ring = ring.Next()}fmt.Println()// Output: name city email submitSource: ExampleLayoutOrder, focus/order_test.go.
Scroll text in a fixed window
Section titled “Scroll text in a fixed window”m := viewport.New(10, 3)m.SetContent(strings.Join([]string{"one", "two", "three", "four", "five"}, "\n"))fmt.Println(m.View())m.LineDown(2)fmt.Println("--")fmt.Println(m.View())// Output:// one// two// three// --// three// four// fiveSource: Example, viewport/example_test.go.
A selectable table
Section titled “A selectable table”m := datatable.New([]string{"name", "qty"}, [][]string{{"apple", "3"}, {"pear", "5"}, {"plum", "8"}})m.Height = 3m, _ = m.Update(tui.Key{Type: tui.KeyDown})for _, line := range strings.Split(ansi.StripANSI(m.View()), "\n") { fmt.Println(strings.TrimRight(line, " "))}fmt.Println("cursor row:", m.Cursor())// Output:// name qty// ──────────// apple 3// > pear 5// plum 8// cursor row: 1Source: Example, datatable/example_test.go. examples/table is the full
program, confirming a row with Enter via datatable.SelectedMsg.
A dialog and its dismissal message
Section titled “A dialog and its dismissal message”dialog.New returns an open dialog. Enter closes it and returns a Cmd whose
message tells your Update it was dismissed.
d := dialog.New("Saved", "Your changes were saved.")fmt.Println(d.Open())d, cmd := d.Update(tui.Key{Type: tui.KeyEnter})fmt.Println(d.Open())fmt.Printf("%T\n", cmd())d.Show()fmt.Println(d.Open())// Output:// true// false// dialog.DismissedMsg// trueSource: Example, dialog/example_test.go. To draw it over your screen, see
overlays.
Resize a split pane from the keyboard
Section titled “Resize a split pane from the keyboard”m := splitpane.New(layout.Block("files"), layout.Block("preview"))m.SetTotal(21)m.Min1, m.Min2 = 4, 4m, _ = m.Update(tui.Key{Type: tui.KeyRight, Mod: input.ModCtrl})first, second := m.Sizes()fmt.Println(first, second)// Output: 11 9Source: Example, splitpane/example_test.go. By default, Ctrl+Right or
Ctrl+Down grows the first pane, Ctrl+Left or Ctrl+Up shrinks it, and Ctrl+E
centres the divider.
Detect conflicting key bindings
Section titled “Detect conflicting key bindings”var r keymap.Registryr.Add(keymap.NewBinding("quit", "q"))conflicts, _ := r.Add(keymap.NewBinding("close", "q"))fmt.Println(len(conflicts))for _, h := range r.Hints("") { fmt.Println(h.Key, h.Desc)}// Output:// 1// q quit// q closeSource: Example, keymap/example_test.go. The second binding is still
registered. Add reports the conflict and returns a non-nil error.