tuigoff

module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2026 License: MIT

README

tuigoff

Meet Mr. Tuigoff: the TUI Go Friendly Framework. Terminal UIs in Go that feel good to use. The house style behind DataTug, SpecScore, inGitDB, CodeGrapher, OVDB and Sneat CLIs.

The name: TUI + Go + the Russian "-off" surname ending, as in Smirnoff. Mr. Tuigoff is a gentleman who keeps your terminal tidy.

Components are listed in docs/components. Design rules live in docs/design-language.md; the original visual reference is docs/design-language.html.

Go CI Coverage Status

The shared terminal UI toolkit of Sneat Co., DataTug and FileTug, built on Bubble Tea v2, Bubbles and Lip Gloss, in the Elm architecture those libraries are made for. It provides:

  • a navigation shell (pkg/nav): header with breadcrumbs, menu and content panels, actions bar, alerts, focus zones, a stack of pages driven by messages;
  • components (pkg/widgets): list, tree, form, tabs, text pane, modal, breadcrumbs, frame, layout helpers, all following the conventions of charm.land/bubbles;
  • the result grid (pkg/grid): a sortable, filterable, virtualised table on top of bubble-table, with per-cell styles, fixed columns, master/detail messages and lazy row sources;
  • the shared theme (pkg/theme), focus ring (pkg/focus), syntax highlighting (pkg/highlight) and the test helpers (pkg/uitest, pkg/nav/navtest).

Our approach to development

We build with our own tooling:

  • SpecScore — specify requirements as SpecScore.md artifacts
  • SpecStudio — author & manage specs across their lifecycle
  • inGitDB — store structured data in Git where applicable
  • DALgo — data access layer for Go
  • cover100.dev — drive toward 100% test coverage
  • DataTug — query & explore data

Installation

go get github.com/tuigoff/tuigoff

The architecture in five rules

  1. A component is a model. Update(tea.Msg) (T, tea.Cmd) and View() string; state lives in the value; setters have pointer receivers; the parent gives it a size with SetSize or a tea.WindowSizeMsg. No component holds a pointer to its parent or starts a goroutine.
  2. Behaviour is a message. A list does not call you back; it returns a command that delivers widgets.ItemSelectedMsg{ID, Index, Item}, and your Update handles it. Every message carries the component's ID, so a screen with two lists can tell them apart.
  3. Asynchronous work is a command. A load is a tea.Cmd that returns a result message. Nothing calls Send, nothing mutates the UI from a goroutine.
  4. Navigation is a message. A screen returns nav.Push(page), nav.Pop(), nav.Alert(...); the shell owns the stack, the breadcrumbs, the focus and the layout.
  5. Keys are bindings. key.Binding and key.Matches, collected in a KeyMap that implements help.KeyMap, so keys can be rebound and shown in the actions bar.

A screen

type projects struct {
	grid    *grid.Model
	loading bool
}

func newProjects() projects { return projects{loading: true} }

func (p projects) Init() tea.Cmd { return loadProjects() }        // async: a Cmd

func (p projects) Update(msg tea.Msg) (nav.Screen, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:                                       // size from the shell
		if p.grid != nil { p.grid.SetSize(msg.Width, msg.Height) }
	case nav.ScreenFocusMsg:                                      // focus from the shell
		if p.grid != nil { p.grid.SetFocused(msg.Focused) }
	case projectsLoaded:                                          // the result of the Cmd
		p.loading = false
		p.grid = newProjectGrid(msg.rows)
	case grid.RowActivatedMsg:                                    // a message from a component
		return p, nav.Push(nav.Page{Title: title(msg.Row), Content: newProject(msg.Row)})
	case tea.KeyPressMsg:
		if p.grid != nil { _, cmd := p.grid.Update(msg); return p, cmd }
	}
	return p, nil
}

func (p projects) View() string { /* p.grid.View(w, focused) or "Loading..." */ }
func (p projects) Title() string { return "Projects" }             // optional: border title

Wire it up:

shell := nav.New(nav.Page{Title: "Home", Menu: newMenu(), Content: nav.Static("Welcome", "...")})
if err := nav.Run(shell); err != nil { log.Fatal(err) }

A complete runnable application is in examples/demo: a menu list, a grid of people loaded asynchronously, a drill-down page, a tree, a form and highlighted text.

The shell (pkg/nav)

+--------------------------------------------------------------+
| Home > Projects > Demo                             (l) Login |  header
+----------------+---------------------------------------------+
| menu (30 cols) | content                                     |  body
+----------------+---------------------------------------------+
| enter open  ctrl+q quit  f1 help                             |  actions bar
+--------------------------------------------------------------+

The shell draws the border of each panel, takes its title from a screen that implements Titled, and tells the screen the size of the area inside the border with a tea.WindowSizeMsg. The menu is hidden below 100 columns.

Pages form a stack; their titles are the breadcrumbs. Push, Pop, PopTo, Replace, Reset change the stack; SetPanels swaps the screens of the current page; SetBreadcrumbs shows a custom trail; SetFocus moves focus; Alert and ShowError show a modal or an error; SetActions replaces the application's actions. A page with a nil Menu keeps the menu of the page below.

Optional interfaces a screen may implement: Titled, Borderless, widgets.Boundary (should this arrow key leave the screen?), widgets.Editor (the screen is editing text), KeyCapturer (the screen claims a key the shell would take), and ShortHelper (bindings listed in the actions bar).

Focus lives in four zones: breadcrumbs, login button, menu, content. Arrow keys move it when the focused screen is AtEdge in that direction: Up at the top goes to the breadcrumbs, Right from the menu to the content, Left from the content to the menu, Shift+Tab to the breadcrumbs, Right past the last crumb to the login button, Down from the header back where focus came from. The shell sends a screen nav.ScreenFocusMsg{Focused} when it gains or loses focus; forward it to your components' Focus/Blur. Screens that do not implement Boundary keep their arrow keys.

Keys: Ctrl+Q always quits, Ctrl+C quits unless the focused screen claims it. nav.WithActions(nav.Action{ID, Binding, Msg}) binds application-wide keys to messages; they are ignored while the focused screen is editing text. The shell reports nav.LoginMsg (header button) and nav.HelpMsg (F1).

Mouse is on: clicks focus and activate, the wheel scrolls; a screen receives mouse messages with coordinates relative to its own top-left cell.

Components (pkg/widgets)

Component Built on Messages
List, MenuItem bubbles/list ItemHighlightedMsg, ItemSelectedMsg
Tree, TreeNode own (data-driven; bubbles/tree has no unselectable nodes, ID-keyed expansion or exact sizing) NodeHighlightedMsg, NodeSelectedMsg
Form, Field, FormButton bubbles/textinput FieldChangedMsg, SubmitMsg, CancelMsg, ButtonPressedMsg
TextPane bubbles/viewport
Tabs own strip TabChangedMsg, TabCloseMsg
Modal own ModalDoneMsg
Breadcrumbs, Crumb own CrumbSelectedMsg
Frame, Button, Split, Fit, Overlay, Center pure functions and values

Every component has a KeyMap of key.Bindings, ShortHelp/FullHelp, SetSize, Focus/Blur/Focused, and an AtEdge query where navigation applies. The conventions are documented in pkg/widgets/doc.go.

The grid (pkg/grid)

The result grid moved here from strongo/aichat so that every product can use it without depending on the chat surface. See pkg/grid/doc.go.

Testing

Components are tested by driving Update with messages and asserting on the view and on the messages they emit:

list, cmd := list.Update(uitest.Key("enter"))
msgs := uitest.Msgs(cmd) // []tea.Msg{widgets.ItemSelectedMsg{ID: "menu", Index: 0, ...}}
got := uitest.Plain(list.View())

Whole screens run under navtest, a thin harness over the Elm loop:

h := navtest.New(t, nav.Page{Title: "Home", Menu: menu, Content: welcome})
h.Press("down", "enter")             // keys, typing, clicks, wheel, resize
h.RequireContains("Projects")
h.Send(projectsLoaded{rows: rows})   // any message
h.Advance(3 * time.Second)           // fake clock for alert timers

nav.Run starts the program through a seam (runTeaProgram), so no test touches a terminal.

Development

go build ./... && go vet ./... && go test ./...
go run ./examples/demo

CI requires 100% statement coverage of every package.

License

Apache License 2.0 - see LICENSE.

Directories

Path Synopsis
examples
demo command
Command demo is a complete tuigoff application in the Elm architecture: a menu list, a grid loaded asynchronously, a drill-down page, a tree, a form and highlighted text, all inside the navigation shell.
Command demo is a complete tuigoff application in the Elm architecture: a menu list, a grid loaded asynchronously, a drill-down page, a tree, a form and highlighted text, all inside the navigation shell.
pkg
entity
Package entity defines Ref, the plain-data reference to "a thing the user is looking at" that tuigoff components pass around: the row under a grid's cursor, an entry pinned in a sidebar.
Package entity defines Ref, the plain-data reference to "a thing the user is looking at" that tuigoff components pass around: the row under a grid's cursor, an entry pinned in a sidebar.
focus
Package focus is a small, pure focus-ring state machine shared by every chatshell screen.
Package focus is a small, pure focus-ring state machine shared by every chatshell screen.
grid
Package grid is a product-neutral result grid: a bubble-table-backed table with sort, per-cell column selection, a scrollbar, style presets, and slots for a product's own secondary views (charts, a current-row card, raw responses, ...) and split-pane layout.
Package grid is a product-neutral result grid: a bubble-table-backed table with sort, per-cell column selection, a scrollbar, style presets, and slots for a product's own secondary views (charts, a current-row card, raw responses, ...) and split-pane layout.
highlight
Package highlight turns source text into ANSI-styled text using the Chroma lexers and styles, so that YAML, JSON, SQL and friends can be shown in a TextPane or any other widget that renders ANSI.
Package highlight turns source text into ANSI-styled text using the Chroma lexers and styles, so that YAML, JSON, SQL and friends can be shown in a TextPane or any other widget that renders ANSI.
mdrender
Package mdrender renders Markdown for a terminal card: one glamour-backed implementation, styled from the theme package, so a Markdown message looks identical in every tuigoff app with no glamour import of its own.
Package mdrender renders Markdown for a terminal card: one glamour-backed implementation, styled from the theme package, so a Markdown message looks identical in every tuigoff app with no glamour import of its own.
nav
Package nav is the navigation shell of tuigoff: one Bubble Tea model that lays out a header with breadcrumbs, a menu panel, a content panel and an actions bar, routes keys, mouse events and messages to the screens it hosts, keeps a stack of pages, and shows alerts.
Package nav is the navigation shell of tuigoff: one Bubble Tea model that lays out a header with breadcrumbs, a menu panel, a content panel and an actions bar, routes keys, mouse events and messages to the screens it hosts, keeps a stack of pages, and shows alerts.
nav/navtest
Package navtest drives a nav.Model without a terminal, so screens can be tested by pressing keys and reading what would be on screen.
Package navtest drives a nav.Model without a terminal, so screens can be tested by pressing keys and reading what would be on screen.
sidebar
Package sidebar is the right-hand working-context panel shared by tuigoff apps: an ordered list of entity.Ref, rendered by a product-supplied renderer, with a cursor, remove ("x"/Delete), open (Enter) and a resizable split width — generalised from DataTug chat's F6 workspace dock and Ctrl+←/→ split handling (ui.go splitEnabled/ resizeChatPane, workspace_ui.go).
Package sidebar is the right-hand working-context panel shared by tuigoff apps: an ordered list of entity.Ref, rendered by a product-supplied renderer, with a cursor, remove ("x"/Delete), open (Enter) and a resizable split width — generalised from DataTug chat's F6 workspace dock and Ctrl+←/→ split handling (ui.go splitEnabled/ resizeChatPane, workspace_ui.go).
theme
Package theme is the shared visual language of Sneat Co., DataTug and FileTug terminal applications: colours, card framing, and chrome (bars, composer frame, panel rows) that every product renders through, so all of them look and behave alike with no styling code of their own.
Package theme is the shared visual language of Sneat Co., DataTug and FileTug terminal applications: colours, card framing, and chrome (bars, composer frame, panel rows) that every product renders through, so all of them look and behave alike with no styling code of their own.
transcript
Package transcript renders the scrolling chat history shared by every tuigoff app: plain user/assistant messages, streamed assistant text and rich Block entries (e.g.
Package transcript renders the scrolling chat history shared by every tuigoff app: plain user/assistant messages, streamed assistant text and rich Block entries (e.g.
uitest
Package uitest holds the TTY-free helpers used to test widgets and screens: building key presses from their textual names and reading rendered views as plain text.
Package uitest holds the TTY-free helpers used to test widgets and screens: building key presses from their textual names and reading rendered views as plain text.
widgets
Package widgets is the component kit of tuigoff.
Package widgets is the component kit of tuigoff.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL