Help us shape the future of Go! Take the 2026 Go Developer Survey

terminu

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: GPL-3.0 Imports: 42 Imported by: 0

README

terminu-go

Language: English · 繁體中文

A Go library that draws and drives a terminal UI the way terminu design says.

You register your panels and your actions — key, name, scope (item, panel or global), whether to confirm first, and what to do. terminu-go draws the whole screen from that one registration and handles the core keys: the Space menu, the ? key reference, the App Log on ! (every toast and error, and what the app logs), the hint on each panel's bottom border, the footer, confirm popups, fading what is not in focus, Esc closing one layer at a time, and popups stacked on popups. Because the menu, the key reference and the keys themselves come from the same registration, they cannot drift apart.

Screens the library does not own (a full-screen lock screen, several terminals side by side) can still use its parts: draw such a screen yourself with App.Canvas, and confirm, toast and every kind of input open over it as they do anywhere else. A popup of your own (a grid to draw on) goes on the same stack as the library's: you draw what is inside and handle its keys; the frame, the animation, the fading and Esc are the library's, and its confirms and inputs open over yours.

It is built on Bubble Tea v1 and Lip Gloss v1.

Install

go get github.com/vulcanshen/terminu-go

Requires Go 1.26 or later. Apps built with it need what terminu design needs from the user's terminal: a Nerd Font and 24-bit colour (truecolor). The terminal component (a program running in a PTY) works on Unix only.

Some fonts make an icon take two cells, and some make the powerline symbols (the ends of the capsules on the frames, the arrow at the end of the cursor) take two as well. The library measures both when the app starts; the user can set them instead with <APP>__ICON_WIDTH and <APP>__POWERLINE_WIDTH (for one app, e.g. LOCKU__ICON_WIDTH=2) or TERMINU__ICON_WIDTH and TERMINU__POWERLINE_WIDTH (shared by every terminu app: set once, every app in the family reads them). An app that runs a terminal inside itself sets both TERMINU__ variables for the programs it starts.

Quick start

A whole app, a list of tasks with one action on it (examples/minimal):

// minimal is the smallest terminu app: one list and one action on it.
package main

import (
	_ "embed"
	"log"
	"slices"

	tea "github.com/charmbracelet/bubbletea"
	terminu "github.com/vulcanshen/terminu-go"
)

//go:embed icon.svg
var icon []byte // the app's mark, drawn by its splash (V)

func main() {
	app := terminu.New("todo")
	list := &terminu.List{Columns: []terminu.Column{{Title: "Task", Shrink: true}}}
	list.SetRows([]terminu.Row{{ID: "1", Cells: []string{"Write the README"}}, {ID: "2", Cells: []string{"Tag v1.0.0"}}})
	done := terminu.Action{Key: "x", Label: "Done", Desc: "this task", Scope: terminu.ItemScope, Hint: "done",
		Do: func() tea.Cmd {
			row, _ := list.Current()
			list.SetRows(slices.DeleteFunc(list.Rows(), func(r terminu.Row) bool { return r.ID == row.ID }))
			return app.Toast(terminu.Info, "Done: "+row.Cells[0])
		}}
	app.AddScreen(&terminu.Screen{Name: "Tasks", Panels: []*terminu.Panel{{Label: "Tasks", Body: list, Actions: []terminu.Action{done}}}})
	var err error
	if app.Splash, err = terminu.NewSplash(icon, "todo", "v0.1.0", "Things to do"); err != nil {
		log.Fatal(err)
	}
	if err := app.Run(); err != nil {
		log.Fatal(err)
	}
}

x marks the task under the cursor done. Space lists what can be done there, ? lists the keys, V opens the splash, q leaves. Every terminu app has a splash with its own mark (tdp Rules S); icon.svg is that mark, and the example uses a copy of terminu's.

To see everything it does, run the showcase, an example app that shows every component on fake data:

go run github.com/vulcanshen/terminu-go/examples/showcase@latest

Text and colours

The strings you give the library are text. Whatever comes from outside — a file's name, a web page's title, a program's output — never reaches the terminal as it is:

  • Names and short texts (a list's cells, values on forms and panels, menu labels, titles, toasts, confirms, options, the statusbar) draw every control character by its name in Red: a tab is \t, a colour code is \x1b then [31m. To colour one yourself, give spans: Row.Styled, Field.Styled, FormField.Styled, and App.Status returns them. On a Canvas or in a popup of your own, Escape draws text from outside the same way.
  • What is read (a viewer, a panel's log, previews) is drawn as a terminal would: colour codes colour, \n starts a line, a progress bar's \r leaves its last state, anything else is its name in Red. A body of your own that shows a log draws its lines with Reading.

The App Log

! on any panel opens the App Log: what the app did and what went wrong, newest last, in memory only. Every toast and every error popup is in it by itself; app.Log(kind, text) adds the rest — an action's result, what the app does in the background — from any goroutine. While errors or warnings came in unseen, the footer's !:log says how many.

A file of your own it can't read

When the app can't read its settings, state or data — a bad format, a broken file, no permission — call app.MoveAside(path, err, startFresh) at the start. The user is asked to move the file aside: Enter renames it to .bak (never over another) and runs startFresh, Esc leaves, Run returning the path and the reason for main to print.

Settings

Two of the user's settings are shared by every terminu app (tdp Rules E3). The library does not read config files: the app reads them from its own and hands them over.

Key What it does Default Set
hide_secrets_in_preview Previews of files hide private keys and files only their owner may read false App.HideSecretsInPreview
input_tab_width How many spaces Tab puts in a multi-line text, and how wide a tab in text on the screen is drawn 4 App.InputTabWidth

Testing an app

The tdptest package drives an app in a Go test the way a user does, and after every message it checks what terminu design asks of every screen: each row exactly as wide as the terminal (tdp L4).

func TestDone(t *testing.T) {
	d := tdptest.Start(t, newApp(), 80, 40) // newApp is all of main but Run
	d.Press("x")
	if !d.Has("Done: Write the README") {
		t.Error("no toast")
	}
	d.Advance(10 * time.Second)
	if d.Has("Done:") {
		t.Error("the toast stays")
	}
}

// The app's first launch, walked on many terminal sizes.
func TestFirstLaunch(t *testing.T) {
	tdptest.Check(t, tdptest.App{Name: "todo", New: newApp})
}
  • Keys and text: Press takes keys by the names terminu design writes ("j", "Enter", "Ctrl-C", "Alt-Esc"); Type types text a key at a time, Burst sends it all at once, Paste pastes it. Resize changes the terminal's size, Send sends a message of the app's own, Do runs code where the app may be changed (app.Focus).
  • Time is the test's: after each input the popups' animations play to their end; Advance moves the time on (a toast going, a timer); with d.Auto = false, Step plays one frame at a time. The app's own timers follow it when they are made with App.Tick (below).
  • Commands run as they do in the program. One that takes long (a file read, a program's output) comes back with a later input; WaitFor waits for it.
  • What is on the screen: Screen is the rows as text, Styled with their colours, Has looks for text, Quit reports that the app asked to quit, Clipboard is what it copied. A failure says the size, the keys pressed and the screen; Note adds more to it (which of Tricky the app was given).
  • EachIconWidth runs a test once with icons and powerline symbols one cell wide, once with icons two, once with both two.
  • Check launches the app as for the first time on terminals of many sizes and every icon width, each in a new sandbox (HOME and the XDG directories under /tmp, the app's own variables cleared), and opens the Space menu, the global operation popup, the App Log and ? on every screen and panel, closing each with Esc; then it shrinks the terminal under the app's least size and back. A colour code drawn as text — one left in a cell or a value instead of Styled — fails it. Env gets the sandbox's HOME, so a variable can point to a file Seed put there. With go test -short it launches on the least size and 80×24 only, icons one cell wide.
  • Tricky, Sizes and Mashes are values that have broken screens: control characters, escape sequences, wide and joined characters, very long values; odd terminal sizes; keys pressed faster than the animations (Mash).

The widths of icons are the whole test program's, so a test that drives an app does not run in parallel with another.

Timers: App.Tick

Make the app's own timers with App.Tick instead of tea.Tick; it takes the same arguments:

return app.Tick(300*time.Millisecond, func(time.Time) tea.Msg { return searchNow{} }) // was tea.Tick

In the program it is tea.Tick. Under a test it waits for the test's time, so Advance fires it and nothing fires on its own. A timer made with tea.Tick runs on the real clock there too: the test cannot move it, and what it does comes in whenever it is due.

Which terminu design

terminu-go implements tdp v0.9.0. The tdp version each release implements is listed in the CHANGELOG.

License

GPL-3.0

Documentation

Overview

Package terminu draws and drives a terminal UI the way terminu design (tdp) says, on top of Bubble Tea v1.

Index

Constants

View Source
const (
	FamilyIconWidth      = "TERMINU__ICON_WIDTH"
	FamilyPowerlineWidth = "TERMINU__POWERLINE_WIDTH"
)

The variables a family app sets for what runs in its PTY (tdp E2, E5).

View Source
const (
	RemoteIconWidth      = "LC_TERMINU_ICON_WIDTH"
	RemotePowerlineWidth = "LC_TERMINU_POWERLINE_WIDTH"
)

The variables an app with ssh sends to the remote end when it connects, for the family app that runs there: OpenSSH passes on only LANG and LC_* (tdp E2, E5). Sending them is that app's; Probe reads them.

Variables

View Source
var (
	Base     = Hex("#1e1e2e") // text on blocks, the unfilled side of a capsule join, what dimming fades to
	Blue     = Hex("#89b4fa") // focus: frames, keys you can press now, the screen chips
	Surface2 = Hex("#585b70") // not usable: unfocused frames, disabled rows
	Lavender = Hex("#b4befe") // being edited, the user's footprint
	Sapphire = Hex("#74c7ec") // the last layer colour
	Green    = Hex("#a6e3a1") // in effect: the chosen value, live sessions, progress
	Yellow   = Hex("#f9e2af") // a mode
	Peach    = Hex("#fab387") // a warning
	Red      = Hex("#f38ba8") // an error
	Overlay0 = Hex("#6c7086") // quiet text: descriptions, hint words, separators
	Text     = Hex("#cdd6f4") // ordinary text
	Subtext1 = Hex("#bac2de") // the panel cursor block
	Surface1 = Hex("#45475a") // an unfocused button
)

The family colours, catppuccin-mocha (components/color). Each meaning has one colour (Principle P4); the comment is what tdp uses it for.

View Source
var ErrEmpty = errors.New("can't be empty")

ErrEmpty is a ready "may not be empty" check. The input names the value before it: "Name can't be empty" (components/input/README).

Functions

func Center

func Center(s string, w int) string

Center centres one line in w cells; the smaller half of the gap goes left.

func Clipboard added in v0.2.0

func Clipboard(text string)

Clipboard puts text on the terminal's clipboard (OSC 52), as a viewer's selection mode copies by default (components/dialog/note). It is sent with the next frame.

func Composite

func Composite(base, top []string, x, y, width int) []string

Composite lays the lines of top over base, top's first cell at column x and row y. Whatever falls off the screen is cut; it never panics, however big top is (tdp E5). base is changed in place and returned; every line keeps base's width.

func Cut

func Cut(s string, from, to int) string

Cut keeps the cells [from, to) of one line. Escape sequences are kept so the styles stay right, and the result ends with a style reset. A wide character cut in half becomes spaces, so the result is always exactly as wide as the part of [from, to) the line covers.

func Dim

func Dim(line string) string

Dim redraws one drawn line dimmed: every foreground and background colour in its SGR sequences fades toward Base, always written as 24-bit, and text with no colour of its own gets dim(Text). Bold, reverse, cursor movement and the text itself are left as they are.

func DimLines

func DimLines(lines []string) []string

DimLines dims every line.

func EmptyState

func EmptyState(w, h int, fact, hint string) []string

EmptyState draws an empty panel's two sentences, the fact and the hint naming keys, both in Overlay0 (layout/screen); keys written as [A] in the hint stay as they are.

func EnvName

func EnvName(app, name string) string

EnvName is the name of an environment variable app reads, tdp E2: EnvName("filu", "icon width") is "FILU__ICON_WIDTH".

func Escape added in v0.4.0

func Escape(s string) string

Escape is text as a name or a short text is drawn (components, control characters): each control character (C0, DEL, C1), Tab, \n and ESC too, by its name in Red (\t, \n, \x1b, as a pasted one in an input), nothing read as a style or a line break, nothing dropped. The lib draws every name and short text an app gives it this way; an app drawing text from outside itself (on a Canvas, in a popup of its own) uses it too.

func Filter

func Filter(items []string, typed string) []int

Filter returns the indexes of the items that match typed, in their order: a filter never reorders.

func Fit

func Fit(s string, w int) string

Fit pads or truncates (with "…") one line to exactly w cells.

func FitLeft

func FitLeft(s string, w int) string

FitLeft is Fit cutting from the front.

func HexText

func HexText(c Color) string

HexText writes a colour as the family does: "#2A2A3C".

func IconWidth

func IconWidth() int

IconWidth is how many cells a Nerd Font icon takes on this terminal.

func KeyName

func KeyName(msg tea.KeyMsg) string

KeyName is the name of the key in msg as tdp writes it (Rules M5): the keycap name in upper camel case (Enter, Esc, Tab, Space, Backspace, Delete, Home, End, PgUp, PgDn), arrows as ↑ ↓ ← →, a letter as typed (q, A), modifiers joined with "-" and Ctrl's letter in upper case (Ctrl-C, Alt-t, Shift-Tab, Alt-Esc). A paste has no name: it is "".

func LoadingFrame added in v0.2.0

func LoadingFrame() string

LoadingFrame is the loading icon as it is now, for a row of the app's own that is loading (a transfer in a work list): the same icon, turning with the lib's. App.Spinning keeps it turning (components/layout/popup).

func MarkLabel

func MarkLabel(key, label string) string

MarkLabel writes a hotkey into its label the way Rules M5 says. A letter that is in the label, in either case, goes in place of the first one, written as the key is pressed ("[r]ename", "[n]ew", "D[e]lete", "Ex[P]ort": Shift-P); one that is not goes in front ("[x] Reset"). A modifier key goes in place of the letter it ends with the same way ("[Alt-t]erm"), several letters in place at the start ("[go]to"), a digit, a core key or a symbol always in front ("[3] Favorites", "[Enter] Edit", "[/] Search"). The label keeps its own case. A label that already shows its key, and a row with no key, are left as they are.

func Matches

func Matches(text, typed string) bool

Matches reports whether text matches what was typed in a filter (components/input/finder, matching): every space-separated word must appear in text as typed, in one piece, ignoring case; the words may come in any order. Nothing typed matches everything.

func Pad

func Pad(s string, w int) string

Pad cuts or pads one line with spaces to exactly w cells.

func Pad2

func Pad2(lines []string, w, h int) []string

Pad2 makes lines exactly w × h.

func PadLeft

func PadLeft(s string, w int) string

PadLeft right-aligns one line in w cells.

func PowerlineWidth

func PowerlineWidth() int

PowerlineWidth is how many cells a powerline symbol takes on this terminal.

func Reading added in v0.4.0

func Reading(lines ...string) []string

Reading is lines of reading content (a log, a program's output) as a terminal draws them, the way the lib's viewer and Pager draw theirs (components, control characters): a line with \n in it is two rows, a \r inside a line leaves its last state, the \r of a CRLF goes, colour codes colour, a tab is input_tab_width spaces, any other control character or escape sequence is its name in Red. For a body of the app's own that shows what is read (a container's log behind a prefix, folded): each string it gives back is one row's text, to fold or cut as the app does; Width, Truncate and Cut measure it right.

func Repeat

func Repeat(s string, n int) string

Repeat is strings.Repeat that takes a negative count as zero.

func SetIconWidth

func SetIconWidth(icon, powerline int)

SetIconWidth sets how many cells a Nerd Font icon takes, and a powerline symbol. Probe sets them; tests and apps that already know may set them.

func StripStyles

func StripStyles(s string) string

StripStyles drops the escape sequences of s.

func Truncate

func Truncate(s string, w int) string

Truncate fits one line in w cells, cutting from the end and adding "…" when it had to cut. The result may be narrower than w when a wide character would straddle the cut.

func TruncateLeft

func TruncateLeft(s string, w int) string

TruncateLeft fits one line in w cells, cutting from the front and putting "…" first: for paths and for a value whose end matters.

func TruncateMiddle added in v0.2.0

func TruncateMiddle(s string, w int) string

TruncateMiddle fits one line in w cells, cutting from the middle and putting "…" there: for a value whose two ends both matter, a pod's name with its deployment in front and what tells it apart at the end ("demo-app…66-gm72n"). The end keeps the extra cell of an odd split. The result may be narrower than w when a wide character would straddle a cut.

func Upper

func Upper(text string) string

Upper writes text in upper case for a capsule, a title, a header or a section title (components, "Uppercase"), leaving the letters in a hotkey marker as they are: "[m]anage" becomes "[m]ANAGE".

func Width

func Width(s string) int

Width is how many cells s takes on the screen: its widest line, escape sequences ignored.

func Wrap

func Wrap(s string, w int) []string

Wrap breaks plain text into lines of at most w cells (components, wrapping): at spaces and on either side of a wide character; inside a word only when it is longer than a line, cut by cells, a wide character never cut in half. Line breaks in s are kept. The text is only shown, so the space it breaks at is not drawn (the textarea and the viewer use foldAt).

Types

type Action

type Action struct {
	// Key is the hotkey as KeyName writes it ("r", "D", "Alt-t", "go",
	// "Enter", "/"); "" for none. It may not take a core key or a movement
	// letter (Rules K1, K12); "Enter" is the panel's Enter action.
	Key string
	// Label is the action's name ("Rename"); the hotkey marker is added
	// where Rules M5 puts it.
	Label string
	// Desc is one line: what it does to what.
	Desc  string
	Scope Scope
	// Hint is the word after the key in the panel's bottom-border hint
	// ("logs" for Enter:logs); "" keeps the action out of the hint.
	Hint string
	// Ask, when set, asks first (Rules F6): every time.
	Ask *Ask
	// AskFor, when set, is asked when the action runs, after Hidden and
	// Disabled, for the question it asks then (the name of the item under the
	// cursor); nil asks nothing (a step that turns out to need no asking). It
	// wins over Ask. Run from a menu, the confirm opens over the menu, so Esc
	// on it goes back to the menu (Rules T1).
	AskFor func() *Ask
	// Hidden reports that its object does not exist now: the row is not
	// shown at all (Rules M6).
	Hidden func() bool
	// Disabled reports that it cannot run now: the row is shown in the
	// disabled colour, the cursor skips it and its hotkey does nothing.
	Disabled func() bool
	// Run from a menu, an action that opens a popup keeps the menu under it,
	// to come back to (Rules T1, F4); one that opens nothing has done the
	// menu's work, and the menu closes. A confirm before it is only a door:
	// Esc on it goes back to the menu, and once it is accepted what the action
	// does decides.
	//
	// Clear makes what it opens clear the menu, and the popups under it, as
	// it opens (T1: the item is gone, a shell, another screen, a drill-down).
	Clear bool
	// Stay keeps the menu it runs from open even when it opens nothing.
	Stay bool
	// Detail marks a row whose Enter opens its full text over the menu (a
	// worklist, Rules F1): the menu stays and its hint is Enter:detail
	// (components/dialog/menu).
	Detail bool
	// EnterHint is the word after Enter in a menu's hint while the cursor is
	// on this row ("cancel" on a work list's transfer, which asks first with
	// the row's full text); "" is run, or detail with Detail
	// (components/dialog/menu, work lists).
	EnterHint string
	// Live, when set, gives the row's label and description each time the
	// menu is drawn: a work list's progress and state change while it is open
	// (components/dialog/menu). The row keeps the width it had when the menu
	// opened.
	Live func() (label, desc string)
	// ToDetails, on a panel's "Enter" action only, says whether Enter on the
	// item under the cursor also sends the focus to the panel's Details: the
	// one place Enter changes panels, whether and on which items being the
	// app's call (Rules K3).
	ToDetails func() bool
	// Do runs it.
	Do func() tea.Cmd
}

Action is something the user can do. One registration gives the Space menu row, the global operation popup row, the key reference row, the hotkey and the hint, so they cannot drift apart (Rules M3).

type App

type App struct {
	// Name is the app's name, for its environment variables (Rules E2).
	Name string
	// Quit is the quit flow (Rules K9): nil leaves at once, an Ask confirms
	// first. A second Ctrl-C during the flow always leaves.
	Quit *Ask
	// QuitDesc is the Quit row's description in the global operation popup.
	QuitDesc string
	// QuitFlow, when set, is the quit flow itself, in place of Quit (Rules
	// K9: the app's): q and Ctrl-C run it, a menu of the app's own to choose
	// how to leave, say. While the popup it opens is open, a second Ctrl-C
	// leaves at once, as in any quit flow.
	QuitFlow func() tea.Cmd
	// Global are the global operations besides switching screens and
	// quitting (Principle P3); their hotkeys work on panels too (Rules M4).
	Global []Action
	// Status is the statusbar for w cells (layout/screen); nil for none. It
	// is spans, in the app's colours (Overlay0 words, a Lavender value...):
	// a value from outside (a path) has its control characters drawn by
	// name (components, control characters). Cut at w.
	Status func(w int) []Span
	// Canvas, when set, is a screen the app draws itself, whole: a full-screen
	// lock screen. The lib draws it instead of its screens, chips and footer,
	// h lines of w cells, and puts its popups and toasts over it, so confirm,
	// toast and every kind of input work there as anywhere (Rules E8).
	Canvas func(w, h int) []string
	// CanvasKey gets every key on the Canvas while no popup is open and
	// reports whether it took it. Of what it leaves only Ctrl-C does
	// something, the quit flow (Rules K9: App.Quit decides); no Space menu,
	// no ?, no q: the screen is the app's.
	CanvasKey func(key string, msg tea.KeyMsg) (bool, tea.Cmd)
	// HideSecretsInPreview is the user's setting hide_secrets_in_preview,
	// read from the app's settings file (tdp E3): a file preview shows only
	// the first line of a private key, and nothing of a file only its owner
	// may read (components/input/file-picker).
	HideSecretsInPreview bool
	// InputTabWidth is the user's setting input_tab_width: how many spaces
	// Tab puts in a textarea, and the spaces a tab in any text is drawn as;
	// 4 when unset (components/input/textarea, Rules E3).
	InputTabWidth int
	// ToastLines caps how many lines a toast has, the last one kept ending in
	// "…"; 0 for no cap (components/dialog/toast: the app's choice).
	ToastLines int
	// Spinning reports that a loading icon of the app's own is on the screen
	// (LoadingFrame in a row), so the lib keeps drawing it turning.
	Spinning func() bool
	// Progress is a long job's progress, 0 to 1, for the bar under the
	// screen chips; ok false when there is none.
	Progress func() (frac float64, ok bool)
	// ShiftTab makes Shift-Tab go to the previous panel (Rules K2: the app's
	// choice).
	ShiftTab bool
	// OnMsg gets the messages the lib does not handle itself.
	OnMsg func(tea.Msg) tea.Cmd
	// OnResize runs when the terminal changes size, after the lib has laid
	// the screen out again: the place to change what depends on the width
	// (which columns a list shows; Rules L1).
	OnResize func(w, h int) tea.Cmd
	// ForceNarrow draws every screen narrow (for showing the narrow layout).
	ForceNarrow bool
	// Start runs when the program starts.
	Start func() tea.Cmd
	// Splash is the family's easter egg, opened with V on a panel (Rules S).
	// Every app has one: Run refuses an app without it.
	Splash Splash
	// MinWidth and MinHeight are the app's least size (Rules L1): 80 × 40 when
	// unset, the most they may be. On a smaller terminal only the app's name
	// and "needs a terminal of at least W×H" are drawn, in the middle; every
	// key is ignored, the PTY's too, but Ctrl-C pressed twice, which leaves.
	// What the app was showing comes back as the terminal grows.
	MinWidth, MinHeight int
	// CoreKeys are core keys of the app's own (Rules K1): each means the same
	// on every panel and popup, but on a typing row, in a PTY and in a mode;
	// no hotkey may take one.
	CoreKeys []CoreKey
	// contains filtered or unexported fields
}

App is the whole terminal UI: screens of panels, the popups over them, the footer. It is a Bubble Tea model.

func New

func New(name string) *App

New makes an app called name.

func (*App) AddScreen

func (a *App) AddScreen(s *Screen) *Screen

AddScreen adds a screen. It panics when a panel registers a key that is not the app's to take (Rules K1, K12): that is a mistake in the program.

func (*App) Checks

func (a *App) Checks(c Checks) tea.Cmd

Checks opens a checkbox popup.

func (*App) CloseAll added in v0.2.0

func (a *App) CloseAll() tea.Cmd

CloseAll closes every popup, the lib's and the app's, each playing its own closing at the same time (Rules F2), as when what was done ends the whole stack (Rules T1; a long session started from a viewer). A confirm or an input closed this way is cancelled (OnCancel).

func (*App) ClosePopup added in v0.1.9

func (a *App) ClosePopup(b PopupBody) tea.Cmd

ClosePopup closes the popup b is the inside of, and every popup over it.

func (*App) CloseTop added in v0.2.0

func (a *App) CloseTop(n int) tea.Cmd

CloseTop closes the n popups on top, the lowest of them animating, as the last step of a flow ends the steps under it (Rules T1); a step that has been answered is not cancelled.

func (*App) Confirm

func (a *App) Confirm(ask Ask, accept func() tea.Cmd) tea.Cmd

Confirm asks ask; accept runs when the user answers Enter. The popup under it stays (Rules T1, a short confirm).

func (*App) Error

func (a *App) Error(title, message string) tea.Cmd

Error opens an error popup over the top popup (components/dialog/note): an action failed.

func (*App) ErrorAbout added in v0.2.0

func (a *App) ErrorAbout(title, subject, message string) tea.Cmd

ErrorAbout is Error naming what failed: subject goes after ": " in the title, kept as it is, RENAME FAILED: notes.md (components/dialog/note).

func (*App) Find

func (a *App) Find(f *Finder) tea.Cmd

Find opens a finder.

func (*App) Focus added in v0.2.0

func (a *App) Focus(p *Panel) tea.Cmd

Focus moves the focus to p, on whichever screen it is, switching to that screen. The app does it where the user expects the focus to move: after what they pressed, never on something that happened in the background (a load done, a job ended; Rules K3). A panel on no screen is left alone.

func (*App) Form

func (a *App) Form(f *Form) tea.Cmd

Form opens a form. It panics on a field with more than five options: those go in a select or a checkbox popup (components/dialog/form); on a form with both Submit and SubmitLong; and on a button with not one of Submit, SubmitLong, Run and Open: mistakes in the program.

func (*App) Frame

func (a *App) Frame() []string

Frame is the screen as lines, each exactly as wide as the terminal (Rules L4).

func (*App) Init

func (a *App) Init() tea.Cmd

Init runs Start.

func (*App) LoadChecks added in v0.2.0

func (a *App) LoadChecks(c *Checks) tea.Cmd

LoadChecks opens a checkbox popup whose options are still coming, as LoadSelect does: keep the pointer and append to Options (and Checked) while Loading is true.

func (*App) LoadSelect added in v0.2.0

func (a *App) LoadSelect(s *Select) tea.Cmd

LoadSelect opens a select whose options are still coming: keep the pointer and append to Options while Loading is true. Meanwhile the title has the loading icon and the popup is as wide as it can be, its width not known when it opens (Rules F7).

func (*App) Log added in v0.4.0

func (a *App) Log(kind ToastKind, text string)

Log writes to the App Log: in kind's level (Info INFO, Success OK, Warning WARN, Error ERR), at the App's time. Every toast and error popup is logged by itself; the app logs the actions it does and their results and what it does in the background (moving its files, defaults it made at the start, the details of what it could not reach). It may be called from any goroutine; the screen shows it with the next frame.

func (*App) Menu

func (a *App) Menu(title string, sections ...MenuSection) tea.Cmd

Menu opens a menu of sections (Rules M8); running a row closes the menu and everything under it.

func (*App) MenuAbout added in v0.2.0

func (a *App) MenuAbout(title, subject string, sections ...MenuSection) tea.Cmd

MenuAbout is Menu for one thing: subject goes after ": " in the title, kept as it is, OPEN WITH: notes.md (components, upper case).

func (*App) MoveAside added in v0.4.0

func (a *App) MoveAside(path string, err error, fresh func() tea.Cmd) tea.Cmd

MoveAside asks what to do with a file of the app's own it cannot read (Rules E3, when it cannot be read): its settings, its state or its data, whatever the reason (a bad format, a broken file, no permission). The confirm, MOVE ASIDE, names the path and why:

  • Enter (move) renames the file to <name>.bak (.bak.1, .bak.2… when that is taken: never over another), logs it and runs fresh, where the app starts a new one in the same place (its defaults for settings, empty state or data) and goes on.
  • Esc (quit) leaves the app: Run returns the path and the reason, for main to write to stderr as it does any error of Run.

A cache that cannot be read is not asked about: throw it away and build it again. Something outside the app that cannot be reached (a cluster, the network) is not the app's own: the app goes on and says so where it shows it (layout/panel, loading and errors).

func (*App) Number

func (a *App) Number(in NumberInput) tea.Cmd

Number opens a number input popup.

func (*App) Open

func (a *App) Open(in Input) tea.Cmd

Open opens an input popup.

func (*App) OpenTerminal

func (a *App) OpenTerminal(t *Terminal) tea.Cmd

OpenTerminal opens a terminal popup with t in it, starting it if it has not started (components/dialog/terminal).

func (*App) Password

func (a *App) Password(in PasswordInput) tea.Cmd

Password opens a password input popup.

func (*App) PickColor

func (a *App) PickColor(c ColorPicker) tea.Cmd

PickColor opens a color picker.

func (*App) PickDate

func (a *App) PickDate(d DatePicker) tea.Cmd

PickDate opens a date (or date and time) picker.

func (*App) PickFile

func (a *App) PickFile(p FilePicker) tea.Cmd

PickFile opens a file-picker.

func (*App) PlaySplash added in v0.2.0

func (a *App) PlaySplash(s Splash) tea.Cmd

PlaySplash plays s over the whole screen now, from its first frame, as V does with the app's own (Rules S): a splash the app shows of its own accord, a mark being designed, say. Any key closes it.

func (*App) Popup added in v0.1.9

func (a *App) Popup(p Popup) tea.Cmd

Popup opens p over what is on top. It panics when p.Body is nil or not comparable: that is a mistake in the program.

func (*App) Reorder added in v0.1.6

func (a *App) Reorder(r Reorder) tea.Cmd

Reorder opens a reorder popup.

func (*App) Run

func (a *App) Run() error

Run asks the terminal how wide an icon is and what its colours are, then runs the app in the alternate screen. When the user leaves on MoveAside's Esc it returns the path that cannot be read and why, for main to write to stderr (Rules E3).

func (*App) Screen

func (a *App) Screen() *Screen

Screen is the current screen.

func (*App) Select

func (a *App) Select(s Select) tea.Cmd

Select opens a select popup.

func (*App) ShowScreen added in v0.2.0

func (a *App) ShowScreen(s *Screen) tea.Cmd

ShowScreen switches to s, as its chip's key does (Rules K3: when the user expects it).

func (*App) Size

func (a *App) Size() (w, h int)

Size is the terminal's size.

func (*App) Text

func (a *App) Text(in TextInput) tea.Cmd

Text opens a text input popup.

func (*App) Textarea

func (a *App) Textarea(t Textarea) tea.Cmd

Textarea opens a textarea.

func (*App) Tick added in v0.3.0

func (a *App) Tick(d time.Duration, fn func(time.Time) tea.Msg) tea.Cmd

Tick is tea.Tick on the App's clock. A timer of the app's own made with it (a debounce, a message that goes after a while) waits, under a test that drives the App, until the test moves the time on (tdptest); one made with tea.Tick runs on the real clock there too.

func (*App) Toast

func (a *App) Toast(kind ToastKind, text string) tea.Cmd

Toast shows a short message from the bottom (components/dialog/toast); a second one replaces the first and starts its time again.

func (*App) Update

func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd)

Update handles a message.

func (*App) View

func (a *App) View() string

View draws the whole screen.

func (*App) Viewer

func (a *App) Viewer(v *Viewer) tea.Cmd

Viewer opens a viewer.

type Ask

type Ask struct {
	Title string // the action's name, after the shield glyph: "Delete"
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject  string
	Question string   // the last line: "Delete web-a8k2?"
	Verb     string   // Enter:<verb> in the hint: "delete"
	Details  []string // what to read before answering, above the question
	Warn     []int    // which lines of Details are warnings (Peach)
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

Ask is a confirm popup (components/dialog/confirm): Enter accepts, Esc cancels.

type Body

type Body interface {
	// Draw returns h lines of exactly w cells: the inside of the frame.
	Draw(w, h int, focused bool) []string
	// Key handles a key the panel did not take; it reports whether it did
	// something with it. Movement keys arrive here.
	Key(key string, msg tea.KeyMsg) (bool, tea.Cmd)
	// HasItem reports whether the cursor is on an item, so that item
	// operations apply (Rules M2).
	HasItem() bool
	// Scroll is the scroll position for the bottom border after the last
	// Draw ("3 of 40", "1-20 of 120"), "" when everything fits.
	Scroll() string
	// MoveKeys are the movement keys that work here, for the key
	// reference's move section (Rules K6).
	MoveKeys() []KeyRow
}

Body is what is inside a panel's frame. The lib has List, Values and Text; an app that draws its own content implements Body itself.

type Capsule

type Capsule struct {
	Text     string
	Selected bool
}

Capsule is one part of a capsule chain: a panel's [N] label or a tab, a screen chip.

type Checks

type Checks struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	Options []string
	Checked []bool
	// Cursor is the option the cursor starts on, scrolled into sight; the
	// app decides (components/input/checkbox). 0, the first, by default.
	Cursor int
	// Unavailable reports an option that is not available: drawn in
	// Surface2, the tick too, but the cursor stops on it and Space flips it
	// like any other; what it means is the app's (components/input/checkbox).
	Unavailable func(i int) bool
	// Loading, for a checkbox popup opened with LoadChecks, is true while
	// Options are still coming; Checked may be shorter than Options.
	Loading bool
	// Done gets the new set. It is not called when nothing changed.
	Done func([]bool) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func([]bool) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

Checks picks several values (components/input/checkbox, the checkbox popup): Space checks a row on screen only, Enter writes the whole set back.

type Color

type Color struct {
	RGB
	Set bool
}

Color is a colour or no colour (the terminal's own).

func DimColor

func DimColor(c Color) Color

DimColor is DimRGB on a Color; no colour stays no colour.

func Hex

func Hex(s string) Color

Hex makes a Color from "#rrggbb".

func LayerColor

func LayerColor(k int) Color

LayerColor is the colour of the popup at layer k, the bottom one being 1; layer 4 and up are Sapphire. A toast is always layer 1.

func (Color) B

func (c Color) B() uint8

func (Color) G

func (c Color) G() uint8

func (Color) R

func (c Color) R() uint8

func (Color) String

func (c Color) String() string

type ColorPicker

type ColorPicker struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Value is the colour now; one not Set is no colour yet (the theme's
	// own): old shows none and no swatch, new starts from #000000, and Choose
	// always counts as a change (components/input/color-picker).
	Value Color
	// Done gets the new colour. It is not called when it did not change.
	Done func(Color) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(Color) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

ColorPicker picks a colour (components/input/color-picker): old and new side by side, the red, green and blue channels each chosen from a select.

type Column

type Column struct {
	// Title is the column's header. A list of one column whose Title is ""
	// has no header.
	Title string
	// Number marks a number column: right-aligned and sorted by value (9
	// before 10). Text columns are left-aligned and sorted as text.
	Number bool
	// Flex did let the column give up room first when the panel is narrow. It
	// does nothing since tdp v0.8.0: mark a long column Shrink.
	Flex bool
	// Shrink marks a column of long values (a name, a URL, a line of text):
	// the shrinkable columns share the width the others leave, cut each its
	// own way when too long, down to MinWidth (12 cells when unset). The
	// other columns, short values (a state, a number, a date), keep the width
	// of their widest value (layout/panel).
	Shrink   bool
	MinWidth int
	// Drop is when the column is taken away, whole and its header with it,
	// when the columns do not fit even with the shrinkable ones at their
	// MinWidth: 1 first, then 2 and on; 0 never (the name). As the panel
	// grows they come back in order, and the shrinkable ones grow back
	// (layout/panel).
	Drop int
	// Path cuts the column's values from the front ("…/.ssh/id_ed25519").
	Path bool
	// Middle cuts the column's values in the middle, for a value whose two
	// ends both matter ("demo-app…66-gm72n").
	Middle bool
}

Column is one column of a List (layout/panel, ordinary lists).

type CoreKey

type CoreKey struct {
	Key, Desc string
	Do        func() tea.Cmd
}

CoreKey is a core key of the app's own (Rules K1).

type DatePicker

type DatePicker struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Value is the value now; zero for none.
	Value time.Time
	// Time adds the hour and the minute (five minutes a step).
	Time bool
	// Min and Max limit the dates; zero for no limit.
	Min, Max time.Time
	// SundayFirst starts a week on Sunday instead of Monday.
	SundayFirst bool
	// Done gets the new value. It is not called when it did not change.
	Done func(time.Time) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(time.Time) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

DatePicker picks a date, or a date and a time (components/input/datetime-picker).

type Failure

type Failure struct {
	Fact, Hint string
}

Failure is what a panel shows when its content could not be had (layout/panel, loading and errors): a fact in Red and a hint naming a key.

type Field

type Field struct {
	Label string
	// Value is the value as shown (components/input/README, how a value is
	// shown); Empty is what an empty value means, shown in grey ("not set").
	// A control character in it, ESC too, is drawn by its name (Escape).
	Value func() string
	// Styled, when set, is how the value is shown in colours of the app's own
	// (a swatch, ColorSpans; a state in Green): each piece's text drawn as
	// Value's is. Value stays what the value is.
	Styled func() []Span
	Empty  string
	// Path cuts a value too long from the front: "…/.ssh/id_ed25519".
	Path bool
	// Options make the field a radio drawn in place: Selected is the chosen
	// one. With Multi it is a group of checkboxes: Checked says which.
	Options  []string
	Selected int
	Multi    bool
	Checked  []bool
	// OptionUnavailable reports a checkbox option that is not available:
	// drawn in Surface2, the tick too, but stopped on and flipped like any
	// other (components/input/checkbox).
	OptionUnavailable func(i int) bool
	Disabled          func() bool
	// Err marks the field's value as wrong: its label turns Red.
	Err func() bool
	// Edit is what Enter does on a field that is not drawn in place: open its
	// input popup.
	Edit func() tea.Cmd
	// OnChange runs when a radio or checkbox changes in place; it takes
	// effect at once (there is no submit).
	OnChange func() tea.Cmd
	// Desc, when set, says what the row means, for the "what each row is"
	// section first in the panel's key reference: the label is a word or
	// two (layout/panel, components/dialog/note). Whether a panel has the
	// section is the app's: it has it when a field has a Desc.
	Desc string
}

Field is one value of a Values panel (layout/panel, one value per row).

type FilePicker

type FilePicker struct {
	// Title is the field's name: the title writes the directory after it
	// ("IDENTITY FILE · ~/.ssh").
	Title, Glyph string
	// Root is the directory the list opens in: a start, not a boundary.
	Root string
	// Value is the file now, if any: the cursor starts on it, in Green.
	Value string
	// Dirs picks a directory (components/input/file-picker, choosing a
	// directory). Open it at the directory now (Root = Value): the cursor
	// starts on ".", in Green.
	Dirs bool
	// Icon is a row's icon and its colour, the app's choice (a file type's
	// colour); a folder or a file glyph in Text when unset.
	Icon func(path string, dir bool) (string, Color)
	// Done gets the chosen file (or directory). It is not called when it did
	// not change.
	Done func(path string) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(path string) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

FilePicker picks a file (components/input/file-picker): a finder over a directory, Enter going into a directory or choosing a file. With Dirs it picks a directory instead: only directories are listed, and Enter on the first row, ". (this directory)", chooses the one listed.

type FilePreview added in v0.2.0

type FilePreview struct {
	Path string
	// Line is a text file's line to start near: a search's hit.
	Line int
	// FS is where it reads: the local disk when nil.
	FS PreviewFS
	// Icon is a tree entry's icon and its colour, as a FilePicker's.
	Icon func(path string, dir bool) (string, Color)
	// Info puts TYPE, SIZE, MODIFIED and MODE under the preview, after a
	// separator.
	Info bool
	// EmptyFact and EmptyHint are what it says with no Path.
	EmptyFact, EmptyHint string
	// contains filtered or unexported fields
}

FilePreview is a panel's body that shows a file or a directory as a file-picker's preview does: a tree, a text file in colour with line numbers, an image, a PDF, hex; with Info, its four facts under it (components/input/file-picker). As a details panel it follows its list's cursor: set Path in the list's OnMove (layout/panel, details). It takes no keys and does not scroll, as a preview does not.

func (*FilePreview) Draw added in v0.2.0

func (p *FilePreview) Draw(w, h int, _ bool) []string

Draw draws the preview in w × h, one cell from the frame on either side.

func (*FilePreview) Empty added in v0.2.0

func (p *FilePreview) Empty() bool

Empty reports whether there is nothing to show, for the panel's loading and failure.

func (*FilePreview) HasItem added in v0.2.0

func (p *FilePreview) HasItem() bool

HasItem is false: a preview has no item of its own.

func (*FilePreview) Key added in v0.2.0

func (p *FilePreview) Key(string, tea.KeyMsg) (bool, tea.Cmd)

Key takes no keys: a preview does not scroll.

func (*FilePreview) MoveKeys added in v0.2.0

func (p *FilePreview) MoveKeys() []KeyRow

MoveKeys are none.

func (*FilePreview) Scroll added in v0.2.0

func (p *FilePreview) Scroll() string

Scroll is "": a preview does not scroll.

type Finder

type Finder struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Verb is what Enter on the list does, for the hint ("open").
	Verb    string
	Items   []FinderItem
	Loading bool
	// Preview is the row under the cursor: a title (its name, or where it
	// is) and the lines to show.
	Preview func(FinderItem) (title string, lines []string)
	// Files says the items are files and directories, their IDs the paths:
	// the preview shows them as a file-picker does, with their facts under
	// it (components/input/file-picker), instead of Preview.
	Files bool
	// FS is where Files reads the paths: the local disk when nil.
	FS PreviewFS
	// Search, when set, is a search the app runs itself (a search tool, a
	// remote host), answering a batch at a time: it gets the typed words each
	// time they change and puts what it finds in Items as it comes, Loading
	// meanwhile; the list is then Items as they are, not filtered again. It
	// must not wait for the search: typing goes on while it runs
	// (components/input/finder).
	Search func(typed string) tea.Cmd
	Done   func(FinderItem) tea.Cmd
}

Finder is a list with a search that goes somewhere (components/input/finder): type to narrow the list, Enter on a row to go there, the row under the cursor previewed beside it. Keep the pointer: rows may keep arriving — append to Items while Loading is true.

type FinderItem

type FinderItem struct {
	ID   string // the app's own key for it
	Text string // shown, and matched unless Match is set
	// Match, when set, is what the typed words are matched against instead
	// of Text: the item's other content, a file's text (components/input/finder:
	// what is matched is the app's choice).
	Match string
	// Icon, when set, goes before the text in IconColor (a file's type).
	Icon      string
	IconColor Color
	// Line, with Files, is a text file's line a search found: the preview
	// starts near it.
	Line int
}

FinderItem is one row of a Finder.

type Form

type Form struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Name is the form in the discard question: "Discard changes to New host?"
	Name   string
	Fields []*FormField
	// Button is the main submit button's text: what the form does ("Save").
	// ButtonSubject is the name it acts on, kept as it is after the verb on
	// the button and in the hint: "Overwrite KBU.svg", "Enter:overwrite
	// KBU.svg" (components/dialog/form, hint).
	Button, ButtonSubject string
	// Buttons are the action area's other buttons, row by row from the top,
	// each row on the right. The main submit goes at the end of the last
	// row, the bottom right; an empty last row puts it on a row of its own.
	Buttons [][]*FormButton
	// Focus, when set, is the field the form opens on.
	Focus *FormField
	// Submit does what the form is for once every Check passes. An error
	// opens an error popup over the form, which stays; otherwise the form
	// closes and the command runs.
	Submit func() (tea.Cmd, error)
	// SubmitLong is a Submit that takes long ("Connect"), in its place: it
	// runs off the update loop, the loading icon after the title and Enter
	// on its button doing nothing until it ends. ctx ends when the form
	// closes first; what it returns then is dropped (components/dialog/form).
	SubmitLong func(ctx context.Context) (tea.Cmd, error)
	// OnCancel runs when it closes without being submitted: Esc, its discard
	// confirm, or the popup it is over closing it, as an Ask's and an input's
	// do (Rules F6).
	OnCancel func() tea.Cmd
}

Form is several values filled in and submitted together (components/dialog/form): Enter on a field opens its input popup, options are chosen on the form, the button submits.

type FormButton added in v0.1.7

type FormButton struct {
	// Label is what it does ("Overwrite"); Subject the name it acts on, kept
	// as it is, as the form's ButtonSubject.
	Label, Subject string
	// Submit makes it another submit ("Save As"): it checks the whole form
	// as the main one does, and the form closes once it is done. SubmitLong
	// is one that takes long, as the form's.
	Submit     func() (tea.Cmd, error)
	SubmitLong func(ctx context.Context) (tea.Cmd, error)
	// Run makes it an action that keeps the form open ("Test"). It checks
	// Needs first, the fields it needs (their Check); then it runs off the
	// update loop, the loading icon after the title and Enter on it doing
	// nothing until it ends. ctx ends when the form closes first; its result
	// is then dropped, Apply too.
	Needs []*FormField
	Run   func(ctx context.Context) FormResult
	// Open makes it an action that opens a popup over the form, on the update
	// loop, the form staying under it: a terminal popup (OpenTerminal) that
	// copies a key to the host, say. Back, when set, runs as the popup it
	// opened closes and the form is on top again: where the app reads what
	// was done there into the fields; true counts as a change to the form.
	Open func() tea.Cmd
	Back func() (changed bool)
}

FormButton is a button of a form's action area besides the main submit (components/dialog/form): another submit, or an action that keeps the form open. It has one of Submit, SubmitLong, Run and Open.

type FormField

type FormField struct {
	Label string
	// Stacked puts the label on a row of its own and the value under it, as
	// wide as the inside: for long values (a URL, a path, a command).
	Stacked bool
	// Value is the value as shown (components/input/README, how a value is
	// shown); Empty is what an empty value means, in grey ("not set").
	Value func() string
	// Styled, when set, is how the value is shown in colours of the app's
	// own (a swatch, ColorSpans): each piece's text drawn as Value's is
	// (Escape). Value stays what the value is.
	Styled func() []Span
	Empty  string
	// Path cuts a value too long from the front: "…/.ssh/id_ed25519".
	Path bool
	// Edit makes the input popup Enter opens, when it opens: it starts from
	// the value then.
	Edit func() Input
	// Options make the field a radio drawn on the form, at most five
	// (Selected is the chosen one); with Multi, a group of checkboxes
	// (Checked says which).
	Options  []string
	Selected int
	Multi    bool
	Checked  []bool
	// OptionDisabled reports an option that cannot be chosen now: drawn in
	// the disabled colour and skipped (components/dialog/form).
	OptionDisabled func(i int) bool
	// OptionUnavailable reports a checkbox option that is not available:
	// drawn in Surface2, the tick too, but stopped on and flipped like any
	// other (components/input/checkbox).
	OptionUnavailable func(i int) bool
	Disabled          func() bool
	// Check is the whole form's rule for this field (required, depends on
	// another field, a name taken), run when the form is submitted: what is
	// wrong, or "".
	Check func() string
}

FormField is one field of a Form (components/dialog/form).

type FormResult added in v0.1.7

type FormResult struct {
	Toast string
	Info  bool
	Lines []string
	Err   error
	// Apply, when set, runs on the update loop as it ends, before the result
	// shows: where the action writes a value into a field, which then counts
	// as changed.
	Apply func()
}

FormResult is what came of a form's action (components/dialog/form): Err opens an error popup, the focus on the button so that Enter tries again; else Lines open a note to read; else Toast says it in one sentence, a Success toast, or Info when Info is set: which one a thing done is, is the app's (components/dialog/toast).

type Input

type Input interface {
	// contains filtered or unexported methods
}

Input is an input popup to open on its own (App.Open) or from a form's field: TextInput, PasswordInput, NumberInput, Select, Checks, and the rest.

type KeyHint

type KeyHint struct {
	Key, Desc string
}

KeyHint is one "key:what" pair of a hint or the footer (Rules M5).

type KeyRow

type KeyRow struct {
	Key, Desc string
	Disabled  bool
}

KeyRow is one row of a key reference.

type KeySection

type KeySection struct {
	Title string
	Rows  []KeyRow
}

KeySection is a titled section of a key reference ("terminal").

type List

type List struct {
	Columns []Column
	// Marks keeps a column for marks in front of every row (Principle P3,
	// batches of marked items).
	Marks bool
	// Draggable keeps that column too, for the swap glyph of the row held in
	// a Drag mode (layout/panel): Hold, MoveHeld, Drop.
	Draggable bool
	// Filterable lets / open a panel filter on the list.
	Filterable bool
	// EmptyFact and EmptyHint are what an empty list says (layout/screen,
	// empty states); keys in the hint written in brackets.
	EmptyFact, EmptyHint string
	// Location, when set, is the list's location row: where the user is (a
	// directory, a remote path, the level drilled into), in Lavender on the
	// panel's first row, cut from the front. It is only shown: the cursor
	// does not stop on it and the scroll position does not count it
	// (layout/panel, location row). Whether a list has one is the app's.
	Location string
	// OnMove runs when the cursor comes to another row, by a key or not, the
	// first time included: before the frame is drawn, so a details panel
	// shows that row in the same frame (layout/panel, details). It does not
	// run while no row is under the cursor.
	OnMove func(Row) tea.Cmd
	// contains filtered or unexported fields
}

List is a panel body with one item per row: a header, a cursor, marks, a panel filter (layout/panel).

func (*List) Actions

func (l *List) Actions() []Action

Actions are the list's own panel operations: the filter.

func (*List) ClearFilter

func (l *List) ClearFilter()

ClearFilter takes the filter away.

func (*List) ClearMarks

func (l *List) ClearMarks()

ClearMarks unmarks every row.

func (*List) Current

func (l *List) Current() (Row, bool)

Current is the row under the cursor.

func (*List) Draw

func (l *List) Draw(w, h int, focused bool) []string

Draw draws the list in w × h: the location row, the filter's typing row, the header, the rows.

func (*List) Drop added in v0.1.6

func (l *List) Drop()

Drop puts the held row down where it is; its own mark shows again.

func (*List) Empty

func (l *List) Empty() bool

Empty reports whether the list has no rows at all.

func (*List) Filtering

func (l *List) Filtering() bool

Filtering reports whether a filter is set.

func (*List) Focus

func (l *List) Focus()

Focus is called when the panel gets the focus: a list with a filter set goes straight back to its typing row.

func (*List) HasItem

func (l *List) HasItem() bool

HasItem reports whether the cursor is on a row.

func (*List) Hold added in v0.1.6

func (l *List) Hold() bool

Hold picks up the row under the cursor in the panel's Drag mode (layout/panel): it is drawn as the cursor in the mode's colour, Yellow, with the swap glyph in its mark column, until Drop. It reports whether there was a row. The Drag mode and its keys are the app's (Panel.EnterMode).

func (*List) Holding added in v0.1.6

func (l *List) Holding() bool

Holding reports a row held.

func (*List) Key

func (l *List) Key(key string, msg tea.KeyMsg) (bool, tea.Cmd)

Key moves the cursor and types in the filter.

func (*List) Marked

func (l *List) Marked() []string

Marked are the IDs of the marked rows.

func (*List) MoveHeld added in v0.1.6

func (l *List) MoveHeld(down bool)

MoveHeld swaps the held row with the next or the previous row shown, the cursor going with it. Past the ends it wraps: the last moved down goes to the top and the others move down one (Rules K12). It moves the rows themselves: a sorted list shows no move, so the app leaves the sort first.

func (*List) MoveKeys

func (l *List) MoveKeys() []KeyRow

MoveKeys are the list's movement keys; none when nothing is shown, nothing to move to (Rules M6).

func (*List) OpenFilter

func (l *List) OpenFilter()

OpenFilter puts a typing row at the top of the list, or goes back to it to type on (layout/panel, panel filter).

func (*List) Rows

func (l *List) Rows() []Row

Rows are all the rows, as set.

func (*List) Scroll

func (l *List) Scroll() string

Scroll is "N of M" when the rows do not all fit.

func (*List) Select

func (l *List) Select(id string) bool

Select puts the cursor on the row with this ID, the next frame scrolling it into sight, and reports whether it is shown. After the app adds an item (New, Copy, Import), it selects it (Rules K3); a row the panel filter hides is not shown, so the app clears the filter first if it wants it seen.

func (*List) SetMarked added in v0.2.0

func (l *List) SetMarked(ids []string)

SetMarked marks the rows with these IDs and unmarks the others, the rows the panel filter hides too.

func (*List) SetRows

func (l *List) SetRows(rows []Row)

SetRows replaces the list's rows, keeping the cursor on the same row when it is still there.

func (*List) SortBy

func (l *List) SortBy(keys ...SortKey)

SortBy sorts the list by keys, first key first: numbers by value, text as text (layout/panel). nil keeps the rows' order. Rows the keys cannot tell apart go by their IDs, so the same rows come out in the same order however they were set: a list must keep one order every time it is opened or fetched again, and data with no order of its own (a kubeconfig's contexts) is sorted by name.

func (*List) Sorting

func (l *List) Sorting() []SortKey

Sorting is what the list is sorted by.

func (*List) ToggleMark

func (l *List) ToggleMark()

ToggleMark marks or unmarks the row under the cursor.

func (*List) Typing

func (l *List) Typing() bool

Typing reports whether the focus is on the filter's typing row.

type MenuSection struct {
	Title   string
	Actions []Action
}

MenuSection is a group of a menu's rows under a title. A menu of one group needs none; in a menu of more, every group has one, the first too (Rules M8).

type Mode

type Mode struct {
	Name string // shown at the right of the top border: one word
	// Keys are the mode's own keys: the only ones that work besides the
	// core keys of K11. They are listed in its key reference and the footer.
	Keys []ModeKey
	// Exit is the verb Esc does in the footer ("cancel" for Esc:cancel).
	Exit string
	// Pause suspends Tab and the digits in the mode; pressing them then says
	// to leave the mode first.
	Pause bool
	// OnExit runs when Esc leaves the mode, to go back to the state before it.
	OnExit func() tea.Cmd
}

Mode is a temporary state of a panel in which some keys mean something of their own and Esc leaves (Rules K11).

type ModeKey

type ModeKey struct {
	// Key is how it is written in the footer and the key reference ("j|k");
	// Keys are the keys it takes, Key itself when empty.
	Key  string
	Keys []string
	Desc string
	// NoFooter keeps it out of the footer (movement keys): it stays in ?.
	NoFooter bool
	// Do runs it, with the key pressed.
	Do func(key string) tea.Cmd
}

ModeKey is one of a mode's keys.

type NumberInput

type NumberInput struct {
	Title, Glyph, Prompt, Verb string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Name is the value's name in its errors ("Port must be 1-65535"); left
	// unset, the title names it.
	Name string
	Old  string
	// Suggest is a value the app proposes when there is no Old, as on a
	// TextInput.
	Suggest  string
	Min, Max float64
	Negative bool // "-" may be typed
	Decimal  bool // "." may be typed
	// Optional lets the value be empty: an empty one is not checked against
	// the range and is written back empty; what it means the form or panel
	// says in grey (components/input/number). Otherwise empty is an error.
	Optional bool
	// Unit is what the number counts, the whole word ("seconds"), in grey on
	// a row of its own under the value (components/input/number).
	Unit  string
	Check func(string) error
	Done  func(string) tea.Cmd
	// Stay keeps it open under what Done opens: a step of a flow (current
	// PIN, new PIN, confirm PIN), each step a popup of its own, Esc on the
	// next one coming back here (Rules F1, F4). The app ends the flow with
	// CloseTop when its last step is done (T1).
	Stay bool
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(string) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
	// Unchanged, when set, runs when Enter takes the value it opened with, so
	// nothing is written back and Done is not called: a step of a flow ends
	// the flow (CloseTop), a page waiting for an answer gets the value it
	// offered (Rules K3, F6).
	Unchanged func() tea.Cmd
}

NumberInput is a number (components/input/number).

type Pager

type Pager struct {
	Follow bool
	// EmptyFact and EmptyHint are what it says with no lines.
	EmptyFact, EmptyHint string
	// contains filtered or unexported fields
}

Pager is a panel body that shows lines with no cursor: a preview, a log. When Follow is set, new lines keep it at the bottom as long as it was there (streaming content). Its lines are reading content, drawn as a terminal draws them (components, control characters): \n starts another line, a \r in one leaves its last state, SGR colours it, a tab is spaces, any other control character is its name in Red.

func (*Pager) Append

func (t *Pager) Append(lines ...string)

Append adds lines at the end.

func (*Pager) Draw

func (t *Pager) Draw(w, h int, focused bool) []string

Draw draws the lines seen from the top, one cell from the frame.

func (*Pager) Empty

func (t *Pager) Empty() bool

Empty reports whether there is nothing to show.

func (*Pager) HasItem

func (t *Pager) HasItem() bool

HasItem is false: a text has no items (Rules K3: Enter works on the whole panel).

func (*Pager) Key

func (t *Pager) Key(key string, _ tea.KeyMsg) (bool, tea.Cmd)

Key scrolls.

func (*Pager) Lines

func (t *Pager) Lines() []string

Lines are the lines.

func (*Pager) MoveKeys

func (t *Pager) MoveKeys() []KeyRow

MoveKeys are the scrolling keys; none when every line fits, nothing to scroll (Rules M6).

func (*Pager) Scroll

func (t *Pager) Scroll() string

Scroll is "N-M of T" when the lines do not all fit.

func (*Pager) SetLines

func (t *Pager) SetLines(lines []string)

SetLines replaces the lines.

type Panel

type Panel struct {
	// Label is the panel's name after its [N]. With tabs it is the first
	// tab's name: a panel has no other name (layout/panel, tabs).
	Label string
	// Name, when set, is the name of the one thing the panel shows, kept as it
	// is after the label, upper case, and ": " ([2] PODS: nginx-7d4f); with no
	// Label it is the whole label ([1] prod-web-01). The Space menu and the
	// key reference write it so too (components, upper case).
	Name string
	// Tabs are the panel's other tabs, after the label's own. Tab is the
	// current one, 0 being the label's; h/l switch tabs and call OnTab.
	Tabs  []string
	Tab   int
	OnTab func(tab int) tea.Cmd
	Body  Body
	// Actions are the panel's item and panel operations; a "Enter" key is
	// what Enter does here (Rules K3).
	Actions []Action
	// Details is the panel that shows the item under this panel's cursor
	// (components/layout/panel, details): the "Enter" action may send the
	// focus there (Action.ToDetails).
	Details *Panel
	// Stream marks streaming content, which is not dimmed when unfocused
	// (Rules T2).
	Stream bool
	// Loading puts the loading icon after the capsule chain; LoadingText is
	// what an empty panel says meanwhile ("loading pods…").
	Loading     bool
	LoadingText string
	// Fail, when set and the panel has nothing to show, is shown in its middle.
	Fail *Failure
	// OnEsc is the app's "one level up" for Esc on this panel (Rules K4); it
	// reports whether there was one.
	OnEsc func() tea.Cmd
	// HintPressableOnly leaves what cannot run now out of the bottom-border
	// hint (Rules M6: the app's choice); otherwise it is listed in the
	// disabled colour, as in the Space menu and ?.
	HintPressableOnly bool
	// OtherKeys are sections of its ? about another surface that has no key
	// reference of its own (Rules M6: a PTY, a grid), after the panel's own.
	OtherKeys []KeySection
	// contains filtered or unexported fields
}

Panel is a region of a screen with its own cursor and content (Principle "Panel"). It is numbered by its place in its screen, from [1].

func (*Panel) EnterMode

func (p *Panel) EnterMode(m *Mode)

EnterMode puts the panel in m; Esc leaves it (Rules K11).

func (*Panel) InMode

func (p *Panel) InMode() *Mode

InMode reports the mode the panel is in, or nil.

func (*Panel) LeaveMode

func (p *Panel) LeaveMode()

LeaveMode leaves the mode without running its OnExit.

type PasswordInput

type PasswordInput struct {
	Title, Glyph, Prompt, Verb string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Name is the value's name in its errors when the title is not
	// ("Password" on a "Log in"); left unset, the title names it.
	Name string
	// Set says a password exists now: eight grey dots stand for it.
	Set bool
	// Unlock is the unlock look: one ● per character, a cell apart, centred
	// (a PIN), and no prompt row: the title says what to type (UNLOCK,
	// CURRENT PIN); otherwise one • per character from the left.
	Unlock bool
	MaxLen int
	Check  func(string) error
	// PausedUntil, when set, is asked when the popup opens and after each
	// Enter: until the time it gives, the popup takes no value, the error row
	// counting down each second ("try again in 27 s") and every key but Esc
	// swallowed (a PIN wrong too many times; components/layout/popup, the
	// error row).
	PausedUntil func() time.Time
	// Done gets the new password; "" with clear true when it was removed.
	Done func(value string, clear bool) tea.Cmd
	// Stay keeps it open under what Done opens: a step of a flow (current
	// PIN, new PIN, confirm PIN), each step a popup of its own, Esc on the
	// next one coming back here (Rules F1, F4). The app ends the flow with
	// CloseTop when its last step is done (T1).
	Stay bool
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(value string, clear bool) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
	// Unchanged, when set, runs when Enter takes the value it opened with, so
	// nothing is written back and Done is not called: a step of a flow ends
	// the flow (CloseTop), a page waiting for an answer gets the value it
	// offered (Rules K3, F6).
	Unchanged func() tea.Cmd
}

PasswordInput is a masked input (components/input/password).

type Popup struct {
	// Title is what the popup is for, upper case on the frame; Subject is
	// the one thing it acts on, kept as it is after ": " ("DRAW: kamonu";
	// components/layout/popup). Glyph goes before the title.
	Title, Glyph, Subject string
	Body                  PopupBody
}

Popup is a popup of the app's own, for what none of the lib's popups holds (a grid to draw on). The app draws what is inside the frame and handles its keys; the lib draws the frame with the title and the hint in the layer colour, dims what is under it, opens and closes it with the animation (Rules F2, F8), and puts its own popups over it: a confirm, an input, an error popup, the key reference.

type PopupBody added in v0.1.9

type PopupBody interface {
	// Size is the inside on a sw × sh screen: w cells from frame line to
	// frame line, h rows between the blank rows above and below. It is asked
	// when the popup opens and when the terminal changes size; the lib makes
	// it wide enough for the title and the hint, and no bigger than the
	// screen allows (Rules F7).
	Size(sw, sh int) (w, h int)
	// Draw returns h lines of w cells; layer is the popup's colour. The cell
	// at either end is the blank margin, which only a full-row cursor covers
	// (components/layout/popup).
	Draw(w, h int, layer Color) []string
	// Key handles a key while the popup is on top and reports whether it
	// did. The lib does the keys it leaves: Esc closes the popup, ? opens its
	// key reference, q starts the quit flow (Rules K1). Ctrl-C never comes
	// here: it is always the quit flow (K9).
	Key(key string, msg tea.KeyMsg) (bool, tea.Cmd)
	// Hint is the hint on the bottom frame line now (Rules M5).
	Hint() []KeyHint
	// Keys is its key reference: only its own keys, Esc among them (Rules K6).
	Keys() []KeySection
}

PopupBody is the inside of a popup of the app's own. It must be comparable, a pointer most often: ClosePopup finds the popup by it.

A body may also have QuitNote() string: what quitting now would lose ("Changes to kamonu are not saved."), "" for nothing. The quit flow's confirm puts it last, in Peach, and asks first even when App.Quit is nil (Rules K9: the quit flow is the app's).

A body that scrolls may also have Scroll() string: its scroll position ("3 of 40", "1-20 of 120"), "" when everything fits. The lib draws it on the right of the bottom frame line in the layer colour and, when the popup opens, makes it wide enough for it and the whole hint (components/layout/popup, when the content does not fit).

type PreviewFS added in v0.2.0

type PreviewFS interface {
	Open(name string) (PreviewFile, error)
	Stat(name string) (fs.FileInfo, error)
	// Lstat does not follow a link, ReadLink says where one points: a link's
	// TYPE is "link → <target>".
	Lstat(name string) (fs.FileInfo, error)
	ReadLink(name string) (string, error)
	ReadDir(name string) ([]fs.FileInfo, error)
}

PreviewFS is where a file preview reads: the local disk when nil, or a file system the app gives, such as a remote host's over SFTP, so its files get the same preview, tree and facts (components/input/file-picker, the preview). Paths are the app's own, slash-separated as SFTP's are.

type PreviewFile added in v0.2.0

type PreviewFile interface {
	io.Reader
	io.ReaderAt
	io.Closer
}

PreviewFile is an open file of a PreviewFS: read from its start, or at an offset (a PDF, a zip).

type ProbeResult

type ProbeResult struct {
	IconCells      int // cells a Nerd Font icon takes
	PowerlineCells int // cells a powerline symbol takes
	// IconFrom and PowerlineFrom say where each came from: the variable's
	// name, "probe", or "default".
	IconFrom, PowerlineFrom string
	Palette                 int // how many of the 16 colours the terminal reported (OSC 4)
}

ProbeResult is what Probe found out about the terminal.

func Probe

func Probe(app string) ProbeResult

Probe finds out, before Bubble Tea takes the terminal, how many cells an icon and a powerline symbol take (tdp E5) and the terminal's 16 colours (components/color, dimming). Each width comes from the app's variable (<APP>__ICON_WIDTH, <APP>__POWERLINE_WIDTH), then the family's (TERMINU__ICON_WIDTH, TERMINU__POWERLINE_WIDTH), then the one brought over ssh (LC_TERMINU_ICON_WIDTH, LC_TERMINU_POWERLINE_WIDTH), then asking the terminal where the cursor ended up after printing one (CPR, unix only). The palette is asked for with OSC 4 at the same time; colours the terminal does not report stay xterm's. Probe sets the lib's widths and palette and reports what it found.

type RGB

type RGB [3]uint8

RGB is a 24-bit colour.

func DimRGB

func DimRGB(c RGB) RGB

DimRGB fades c toward Base. It never lightens: a channel darker than Base keeps its own value.

type Rect

type Rect struct{ X, Y, W, H int }

Rect is a panel's place in the panel area, in cells.

type Reorder added in v0.1.6

type Reorder struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	Options []string
	// Done gets the new order: the options' indexes as they now stand
	// ([2 0 1]: the third first). It is not called when the order did not
	// change.
	Done func(order []int) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(order []int) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

Reorder puts a set of items in an order by hand (components/input/reorder): the panel's Drag mode in a popup. D picks up the item under the cursor, j|k move it, Enter drops it; Enter with nothing held writes the order back.

type Row

type Row struct {
	ID string // what marks are kept by; the row's place when empty
	// Cells are the row's text, one per column: a control character in one,
	// ESC too, is drawn by its name (Escape; layout/panel). The filter and
	// sorting go by them.
	Cells []string
	// Styled, when set for a cell, is how it is drawn in colours of the app's
	// own: each piece's text drawn as a cell's is. The cell's text stays in
	// Cells.
	Styled [][]Span
	// Sort, when set, are the values sorted by instead of the cells (an age
	// shown as "3d" sorted by seconds).
	Sort []string
	// SortGroup keeps rows apart when the list is sorted: the lower group
	// first, sorting only within each (a file manager's directories before its
	// files); no title is drawn between them (layout/panel). The rows of one
	// group keep their order when the list is not sorted.
	SortGroup int
	// Group is the title of the group the row is in (layout/panel, groups).
	// A group's rows stay together, the groups in the order they first
	// appear, sorted within; the rows are drawn two cells further in, under
	// a title the cursor skips. A group with no row shown has no title.
	Group string
}

Row is one item of a List.

type Scope

type Scope int

Scope is what an action works on (Principle P3).

const (
	ItemScope   Scope = iota // the one item under the cursor
	PanelScope               // the focused panel, or its tab, as a whole
	GlobalScope              // the whole app
)

type Screen

type Screen struct {
	// Name is the screen's chip and its row in the global operation popup
	// ("Panels"); Key is its global hotkey ("P"). Desc is that row's
	// description.
	Name, Key, Desc string
	Panels          []*Panel
	// Layout places the panels in the panel area, one Rect each; nil puts
	// them side by side in equal widths. How a screen is laid out is the
	// app's (Rules L1).
	Layout func(w, h int) []Rect
	// NarrowBelow is the width under which only the focused panel is drawn,
	// across the whole width (layout/screen, narrow); at most 80.
	NarrowBelow int
	// contains filtered or unexported fields
}

Screen is a set of panels shown together (Principle "Screen"). A screen's panels are numbered from [1] by their order (Rules K1).

func (*Screen) Focused

func (s *Screen) Focused() *Panel

Focused is the panel with the focus.

type Select

type Select struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	Options []string
	// Keys, when set, are the options' hotkeys (Rules M5); "" for none.
	Keys []string
	// Selected is the value now; -1 for none.
	Selected int
	// Loading, for a select opened with LoadSelect, is true while Options
	// are still coming.
	Loading bool
	// Done gets the chosen option. It is not called when the value did not
	// change.
	Done func(int) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(int) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

Select picks one value from a list (components/input/select).

type Selection

type Selection struct {
	Key     string // "v"
	Name    string // the mode's name on the frame: "Visual"
	CopyKey string // "y" when unset
	// Copy takes the text copied. Unset, it goes on the terminal's
	// clipboard and a Success toast says so (components/dialog/note).
	Copy func(text string) tea.Cmd
}

Selection is a viewer's selection mode: Key brings the cursor out at the start of the first line in sight, Key again starts the selection where it is, the cursor moves through the text (h|j|k|l, w|b|e, 0|$), CopyKey hands the selected text to Copy and leaves; Esc leaves.

type Slider

type Slider struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject     string
	Bottom, Top float64
	// Step is the distance between two numbers: (Top − Bottom) / 10 when 0.
	Step  float64
	Value float64
	// Done gets the new number. It is not called when it did not change.
	Done func(float64) tea.Cmd
}

Slider is a number in a range, shown as a track (components/input/slider). It has no keys of its own: Enter opens a select of the numbers from Bottom to Top, Step apart.

type SortKey

type SortKey struct {
	Col  int
	Desc bool
}

SortKey is one column the list is sorted by.

type Span added in v0.4.0

type Span struct {
	Text  string
	Style Style
}

Span is a piece of text in a style of the app's own, for the places an app colours text it gives the lib (a list's cell, a value, the statusbar): the text is drawn as any name or short text, a control character in it by its name (Escape); the style is the app's.

func ColorSpans added in v0.4.0

func ColorSpans(c Color) []Span

ColorSpans is a colour as a value on a form or a panel shows it, for Styled: a small swatch and its hex (components/input/color-picker).

func SliderSpans added in v0.4.0

func SliderSpans(value, bottom, top float64, width int, color Color) []Span

SliderSpans is a value on a track of width cells in color, then the number (components/input/slider): a heavy line with a dot where the value is. For Styled, the number in Value.

type Splash

type Splash func(frame, w, h int) (lines []string, done bool)

Splash draws frame n (from 0) of the family's easter egg in w × h cells (Rules S). done reports that the reveal is over; the last frame then stays until a key. NewSplash makes the one Rules S5 describes.

func NewSplash

func NewSplash(icon []byte, name, version, intro string) (Splash, error)

NewSplash is the family's splash (Rules S5): the mark in icon, the app's docs/icon.svg, cell for cell, and under it the app's name (its command's), its version as `<app> version` prints it and a one-line intro, then the family's author and the hint. The text is designed with the mark: a name, version or intro wider than the mark (50 columns) is an error.

icon is read as S5 describes it: a 250 × 250 viewBox of 10 × 10 rects, the U frame in #205090 and the letters in #f2b753 within rows 3–21. The letters are revealed one at a time: each XML comment starts the next one, so the svg lists them in the order of the app's name with a comment before each (`<!-- K -->`). Without comments all the gold is one letter.

type Style

type Style struct {
	Fg, Bg    Color
	Bold      bool
	Underline bool
}

Style is what a piece of text is drawn with. The zero Style draws the text as it is.

func Fg

func Fg(c Color) Style

Fg is a Style with a foreground only.

func On

func On(c Color) Style

On is text in Base, bold, on a block of c: a cursor, a capsule, a button.

func (Style) Bolded

func (s Style) Bolded() Style

Bolded returns s with bold on.

func (Style) Render

func (s Style) Render(text string) string

Render draws text in this style, resetting after it. text is one line.

type Terminal

type Terminal struct {
	// Title is what runs, in capitals ("SHELL", "ALTERM"); Subject what it
	// runs on, as it is ("myhost"): the popup's title reads "ALTERM: myhost".
	Title, Subject string
	// Command is the program and its arguments: the user's shell ($SHELL)
	// when empty.
	Command []string
	Dir     string
	// Env is added to the app's environment. TERMINU__ICON_WIDTH and
	// TERMINU__POWERLINE_WIDTH are always set to the app's own (tdp E5).
	Env []string
	// Ends makes the exit key end the program ("Alt-Esc:exit"); otherwise
	// leaving keeps it running ("Alt-Esc:leave").
	Ends bool
	// Keys are the app's own keys in the PTY, shown after the exit key.
	Keys []TerminalKey
	// Interactive marks a terminal the user types commands of their own in
	// (a shell, an ssh session, kubectl exec), not one the app opens to run
	// one program for one thing ($EDITOR, sudo asking for a password). It
	// has the two modes of components/dialog/terminal, which every other PTY
	// leaves to its program, PgUp too:
	//   - History, on PgUp: j|k, u|d, gg|G, PgUp|PgDn scroll back through
	//     what went off the top, Esc goes back to the live screen, any other
	//     key leaves the mode and goes to the program. While the program
	//     fills the whole screen (vim, less, htop: the alternate screen),
	//     PgUp is the program's and the hint leaves PgUp:history out.
	//   - The selection, on Alt-v (the family's v, with Alt to get out of
	//     the program): the screen holds still, what the program prints
	//     meanwhile waiting; the cursor starts at the first line in sight
	//     and goes up into the history too; v starts selecting, y copies,
	//     Esc leaves. Copy takes the text; unset, it goes on the terminal's
	//     clipboard with a Success toast.
	Interactive bool
	// AllKeys, while set, hands every key to the program but the exit key
	// and the app's own Keys (the one that takes them back): the user gave
	// them all away (sshu's locked cell, a family app running inside). The
	// selection is the program's then and PgUp|PgDn go to it, the innermost
	// PTY scrolling its own history (components/dialog/terminal).
	AllKeys bool
	// ExitKey is the key that takes the focus out (Rules K10), Alt-Esc when
	// unset; the app may change it while the program runs, and the hint and
	// the footer show it as it is. Alt-Esc always asks first; another asks
	// when ExitAsks.
	ExitKey  string
	ExitAsks bool
	// Copy takes what the selection copies (Interactive); unset, it goes on
	// the terminal's clipboard with a Success toast.
	Copy func(text string) tea.Cmd
	// NotReady, while it reports true, keeps the ordinary keys from the
	// program (it is still connecting); Ctrl-C and the exit key still work.
	NotReady func() bool
	// OnExit runs when the program ends.
	OnExit func(err error) tea.Cmd
	// OnFirstOutput runs when the program first prints something after
	// Start: an ssh that has connected, to end the panel's loading and
	// NotReady (Rules K10, F7).
	OnFirstOutput func() tea.Cmd
	// contains filtered or unexported fields
}

Terminal is a program running in a PTY (components/dialog/terminal): a shell, an editor, a remote session. Put it in a panel as the panel's Body, or open it in a terminal popup (App.OpenTerminal). While the focus is in it every key goes to the program, but the exit key and the app's own keys (Rules K10).

func (*Terminal) Draw

func (t *Terminal) Draw(w, h int, focused bool) []string

Draw draws the program's screen; its cursor shows while the focus is in it.

func (*Terminal) Empty added in v0.2.1

func (t *Terminal) Empty() bool

Empty reports a program started that has printed nothing yet: the panel's LoadingText shows in its middle meanwhile (layout/panel, loading).

func (*Terminal) Enter

func (t *Terminal) Enter()

Enter puts the focus in a terminal in a panel: keys go to the program until the exit key.

func (*Terminal) Exited

func (t *Terminal) Exited() bool

Exited reports whether the program has ended.

func (*Terminal) HasItem

func (t *Terminal) HasItem() bool

HasItem: a terminal has no items.

func (*Terminal) Hide

func (t *Terminal) Hide(a *App) tea.Cmd

Hide takes the focus out and leaves the program running: its popup closes, a terminal in a panel lets the panel's keys work again. For an app's own exit key, such as one that hides a terminal popup.

func (*Terminal) Inside

func (t *Terminal) Inside() bool

Inside reports whether the focus is in the terminal.

func (*Terminal) Key

func (t *Terminal) Key(string, tea.KeyMsg) (bool, tea.Cmd)

Key does nothing: while the focus is in the terminal the app sends it every key; outside it the panel's own keys apply.

func (*Terminal) Mode added in v0.2.0

func (t *Terminal) Mode() string

Mode is the mode the terminal is in, "" for none.

func (*Terminal) MoveKeys

func (t *Terminal) MoveKeys() []KeyRow

MoveKeys: none outside the terminal.

func (*Terminal) Running

func (t *Terminal) Running() bool

Running reports whether the program has started and not ended.

func (*Terminal) Scroll

func (t *Terminal) Scroll() string

Scroll is the history position when scrolled back.

func (*Terminal) Spoke added in v0.2.0

func (t *Terminal) Spoke() bool

Spoke reports whether the program has printed anything since Start.

func (*Terminal) Start

func (t *Terminal) Start() tea.Cmd

Start runs the program, sized as the terminal was last drawn (80 × 24 before that). The command it returns keeps the screen up to date.

func (*Terminal) Stop

func (t *Terminal) Stop()

Stop ends the program.

type TerminalKey

type TerminalKey struct {
	// Key is how it is written in the footer and the hint ("Alt-←|→|↑|↓");
	// Keys are the keys it takes, Key itself when empty, as a ModeKey's
	// (Rules M5).
	Key  string
	Keys []string
	Desc string
	Do   func() tea.Cmd
	// DoKey, when set, runs instead of Do, with the key pressed.
	DoKey func(key string) tea.Cmd
}

TerminalKey is one of the app's own keys in a PTY (Rules K10).

type TextInput

type TextInput struct {
	Title string // what the popup is for, in its title ("Rename")
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	Glyph   string // its title glyph: the app's choice
	// Prompt is one line saying what to type ("New name for assets").
	Prompt string
	// Verb is what Enter does, for the hint ("rename").
	Verb string
	// Name is the value's name in its errors when the title says what the
	// popup does rather than what the value is called: "Name can't have
	// control characters" on a "Save as" (components/input/README). Left
	// unset, the title names it.
	Name string
	// Old is the value now: offered in grey from the cursor on, Tab takes it
	// (components/input/README, the grey suggestion).
	Old string
	// Suggest, when there is no Old, is a value the app proposes for a new
	// thing ("assets.zip"), offered in grey as Old is; Enter with nothing
	// touched takes it, checked and written back as any value
	// (components/input/README, a suggested value).
	Suggest string
	// MaxLen, when set, is the most characters the value can have; the
	// popup's width is then known when it opens.
	MaxLen int
	// Check is the value's own rules: an error is written on the error row
	// and the value is not written back.
	Check func(string) error
	// Search marks a value that is only searched for, never used: control
	// characters in it are drawn, not held back on Enter, as on a finder's
	// typing row (components/input/README, pasted control characters).
	Search bool
	// Done gets the new value. It is not called when the value did not change.
	Done func(string) tea.Cmd
	// Stay keeps it open under what Done opens: a step of a flow (current
	// PIN, new PIN, confirm PIN), each step a popup of its own, Esc on the
	// next one coming back here (Rules F1, F4). The app ends the flow with
	// CloseTop when its last step is done (T1).
	Stay bool
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(string) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
	// Unchanged, when set, runs when Enter takes the value it opened with, so
	// nothing is written back and Done is not called: a step of a flow ends
	// the flow (CloseTop), a page waiting for an answer gets the value it
	// offered (Rules K3, F6).
	Unchanged func() tea.Cmd
}

TextInput is a one-line text input popup (components/input/text): a prompt, the value, an error row.

type TextMatch added in v0.2.0

type TextMatch struct{ Line, From, To int }

TextMatch is a stretch of a viewer's line: its characters From to To, To not in, counted as the selection mode's cursor counts them.

type Textarea

type Textarea struct {
	Title, Glyph string
	// Subject is the one thing it acts on, kept as it is after ": " in the
	// title: "EDIT: filu" (components/layout/popup).
	Subject string
	// Name is the text's name in its errors when the title is not ("Notes"
	// on an "Edit"); left unset, the title names it (components/input/README).
	Name string
	Old  string
	// Check, when set, judges a changed text on Enter: an error keeps it
	// open and is written in the error row, as on a one-line input (Rules K3).
	Check func(string) error
	// Done gets the new text. It is not called when it did not change.
	Done func(string) tea.Cmd
	// Commit, when set, writes a changed value back when Enter takes it, on
	// the update loop, before Done: an error keeps the popup open with the
	// value in it and opens an error popup over it, "RENAME FAILED"
	// (components/layout/popup, the action failing; Rules F4).
	Commit func(string) error
	// OnCancel runs when it closes with no answer: Esc, or the popup it is
	// over closing it, so whoever waits for the answer (a web page's prompt,
	// ssh's askpass) hears it was cancelled (Rules F6).
	OnCancel func() tea.Cmd
}

Textarea is multi-line text (components/input/textarea): it opens writing, with the value there and the cursor at its end; Esc moves to the moving state, where Enter saves.

type ToastKind

type ToastKind int

ToastKind is what a toast says (components/dialog/toast): which one goes with the meaning of its colour.

const (
	Info    ToastKind = iota // a neutral word, neither good nor bad
	Warning                  // done, or still usable, with something to notice
	Error                    // an action failed
	Success                  // an action is done
)

type Values

type Values struct {
	Fields []*Field
	// Header, when set, is the table header over the labels and the values.
	Header [2]string
	// OnMove runs when the cursor comes to another place, by a key or not,
	// the first time included: the field, and the option in it (-1 for a
	// field not drawn in place). It runs before the frame is drawn, so a
	// details panel follows in the same frame (layout/panel, details).
	OnMove func(f *Field, option int) tea.Cmd
	// EmptyFact and EmptyHint are what it says with no fields, in Overlay0, as
	// a list does (layout/screen, the empty state).
	EmptyFact, EmptyHint string
	// contains filtered or unexported fields
}

Values is a panel body with one value per row: a settings screen (layout/panel). It moves like a form; a value takes effect when it is set.

func (*Values) Actions added in v0.2.0

func (v *Values) Actions() []Action

Actions are the panel's own item operation: what Enter does on the item under the cursor, so the Space menu and ? have it as the hint does (Rules M3, K6); in the disabled colour on a field that cannot be used now (M6).

func (*Values) Current

func (v *Values) Current() *Field

Current is the field under the cursor.

func (*Values) Draw

func (v *Values) Draw(w, h int, focused bool) []string

Draw draws the fields: labels in upper case in Blue, two cells, the values.

func (*Values) Empty

func (v *Values) Empty() bool

Empty reports whether there are no fields.

func (*Values) EnterHint

func (v *Values) EnterHint() string

EnterHint is what Enter does on the item under the cursor, for the panel's hint: edit a value, choose a radio option, toggle a checkbox.

func (*Values) HasItem

func (v *Values) HasItem() bool

HasItem: a value panel's rows are its items.

func (*Values) Key

func (v *Values) Key(key string, _ tea.KeyMsg) (bool, tea.Cmd)

Key moves item by item and does Enter (layout/panel, one value per row).

func (*Values) MoveKeys

func (v *Values) MoveKeys() []KeyRow

MoveKeys are a one-dimensional list's (Rules K12); none with no fields, nothing to move to (Rules M6).

func (*Values) Scroll

func (v *Values) Scroll() string

Scroll is "N of M" when the items do not all fit.

type Viewer

type Viewer struct {
	// Title is what it shows, in capitals ("YAML"); Subject is the thing
	// itself, as it is ("pod/nginx"): the title reads "YAML — pod/nginx".
	Title, Subject string
	Glyph          string
	Lines          []string
	Loading        bool
	// Lang is the language its lines are coloured in, as the file preview
	// colours a text file (components/dialog/note): a chroma lexer's name or
	// alias ("yaml", "json"), a file name to judge by ("deploy.yaml"), or a
	// content type ("application/json"). Unset or not known: plain text, in
	// Text (an App Log).
	Lang string
	// Tabs, when set, follow the title (components/layout/popup); h/l switch
	// them and OnTab fills Lines for the new one.
	Tabs  []string
	Tab   int
	OnTab func(tab int) tea.Cmd
	// Keys are the viewer's own hotkeys, first in its hint.
	Keys []ViewerKey
	// Select is its selection mode (Rules K11): every viewer has one
	// (components/dialog/note). Unset, it is v, Visual, y copying to the
	// terminal's clipboard with a Success toast.
	Select *Selection
	// NoWrap keeps a line too long on its row, cut with "…" at the end, for
	// content that lines up (columns side by side, a table); lines fold by
	// default (components/dialog/note).
	NoWrap bool
	// NoNumbers leaves out the line numbers, for a table the app lays out
	// itself, where they mean nothing (components/dialog/note).
	NoNumbers bool
	// Hits are what a search the app runs found (its own / and n|N): each
	// underlined in its own colours, Hits[Current] drawn as the cursor block,
	// "you are here" (components/dialog/note, the viewer's search; FindText
	// finds them). No new colour: Yellow is the selection's.
	Hits    []TextMatch
	Current int
	// contains filtered or unexported fields
}

Viewer is a note to read (components/dialog/note): a YAML view, a log, a file. Its lines are numbered on the left and a long one folds onto the rows under it. It scrolls, may have keys of its own and a selection mode, and closes with Esc only. Keep the pointer: lines may keep arriving while Loading is true.

func (*Viewer) FindText added in v0.2.0

func (v *Viewer) FindText(q string) []TextMatch

FindText is every place q is in the lines, in either case, for the app's search (Hits).

func (*Viewer) ScrollTo

func (v *Viewer) ScrollTo(i int)

ScrollTo puts line i at the top as far as the lines go; in the selection mode the cursor goes to its start.

func (*Viewer) Top

func (v *Viewer) Top() int

Top is the first line in sight.

type ViewerKey

type ViewerKey struct {
	Key, Desc string
	Do        func() tea.Cmd
}

ViewerKey is one of a viewer's own hotkeys.

Directories

Path Synopsis
examples
minimal command
minimal is the smallest terminu app: one list and one action on it.
minimal is the smallest terminu app: one list and one action on it.
showcase command
Command showcase shows every component of terminu design and every way they combine, on fake data laid out like a real app.
Command showcase shows every component of terminu design and every way they combine, on fake data laid out like a real app.
internal
testhook
Package testhook is how tdptest reaches into the lib: terminu fills these in when it loads.
Package testhook is how tdptest reaches into the lib: terminu fills these in when it loads.
proto
c1 command
Package main is a throwaway prototype for C1 in the blind-spot record: does lipgloss v2's layer compositing keep frames straight when a Nerd Font icon advances the cursor two cells (tdp E5)? It is not the lib.
Package main is a throwaway prototype for C1 in the blind-spot record: does lipgloss v2's layer compositing keep frames straight when a Nerd Font icon advances the cursor two cells (tdp E5)? It is not the lib.
Package tdptest drives a terminu App in a Go test the way a user does — keys, a paste, the terminal changing size, time going by — and checks, after every message, what tdp asks of every screen: each row exactly as wide as the terminal (tdp L4).
Package tdptest drives a terminu App in a Go test the way a user does — keys, a paste, the terminal changing size, time going by — and checks, after every message, what tdp asks of every screen: each row exactly as wide as the terminal (tdp L4).

Jump to

Keyboard shortcuts

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