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

terminu

package module
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Oct 14, 2026 License: GPL-3.0 Imports: 36 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 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: confirm, toast and every kind of input work on their own.

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

The API is not settled yet: the library is still checking the spec against a working implementation. This section will show a minimal app once it is.

To see what 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

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 4 App.InputTabWidth

Which terminu design

terminu-go implements tdp v0.6.5. 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).

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.

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 ColorText

func ColorText(c Color) string

ColorText is a colour on a form or a panel: a small swatch and its hex (components/input/color-picker).

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 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 MarkLabel

func MarkLabel(key, label string) string

MarkLabel writes a hotkey into its label the way Rules M5 says: in place when it is the label's first letter ("[r]ename") or a letter inside it ("UR[L]"), in front otherwise ("[n] New"); a modifier key in place of the letter it ends with ("[Alt-t]erm"), several letters in place ("[go]to"), a digit, a core key or a symbol always in front ("[3] Favorites", "[Enter] Edit", "[/] Search"). The case counts: "[A]dd" is Shift-A. 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 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 SliderTrack

func SliderTrack(value, bottom, top float64, width int, color Color) string

SliderTrack draws 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.

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 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, at spaces where it can and inside a word that is longer than a line. Line breaks in s are kept.

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
	// 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
	// Stay keeps the menu it runs from open (a step of its own over it).
	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
	// 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
	// Global are the global operations besides switching screens and
	// quitting (Principle P3); their hotkeys work on panels too (Rules M4).
	Global []Action
	// Status draws the statusbar in w cells (layout/screen); nil for none.
	Status func(w int) string
	// 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; 4 when unset (components/input/textarea).
	InputTabWidth int
	// 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
	// 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
	// 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) 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) Find

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

Find opens a finder.

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), a mistake 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) 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) 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) 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.

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) 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) 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)
}

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
	// Done gets the new set. It is not called when nothing changed.
	Done func([]bool) 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   Color
	// Done gets the new colour. It is not called when it did not change.
	Done func(Color) 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 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 lets the column give up room first when the panel is narrow.
	Flex bool
	// Path cuts the column's values from the front ("…/.ssh/id_ed25519").
	Path 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): "!" for the app's log.

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
}

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").
	Value func() string
	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
	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
}

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
}

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 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
	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
	// Icon, when set, goes before the text in IconColor (a file's type).
	Icon      string
	IconColor Color
}

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 submit button's text: what the form does ("Save").
	Button string
	// 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)
}

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 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
	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
	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 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
	// 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
	// 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 filter's typing row, the header, the rows.

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) 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) MoveKeys

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

MoveKeys are the list's movement keys.

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)

Select puts the cursor on the row with this ID, if it is shown.

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; nil keeps the rows' order.

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 an optional title (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 field's name in the range error ("Port").
	Name     string
	Old      string
	Min, Max float64
	Negative bool // "-" may be typed
	Decimal  bool // "." may be typed
	Check    func(string) error
	Done     func(string) 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).

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.

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
	// 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
	// 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); otherwise one • per character from the left.
	Unlock bool
	MaxLen int
	Check  func(string) error
	// Done gets the new password; "" with clear true when it was removed.
	Done func(value string, clear bool) tea.Cmd
}

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

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 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 Row

type Row struct {
	ID    string   // what marks are kept by; the row's place when empty
	Cells []string // one per column; may carry the app's own colours
	// Sort, when set, are the values sorted by instead of the cells (an age
	// shown as "3d" sorted by seconds).
	Sort []string
	// 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
	// Done gets the chosen option. It is not called when the value did not
	// change.
	Done func(int) 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    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 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
	// History lets PgUp and PgDn scroll back through what went off the top.
	History bool
	// 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
	// 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) 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) 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) 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, Desc string
	Do        func() 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
	// Old is the value now: offered in grey from the cursor on, Tab takes it
	// (components/input/README, the grey suggestion).
	Old 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
	// Done gets the new value. It is not called when the value did not change.
	Done func(string) tea.Cmd
}

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

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
	Old     string
	// Done gets the new text. It is not called when it did not change.
	Done func(string) 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).

const (
	Info ToastKind = iota
	Warning
	Error
)

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
	// 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) 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).

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, when set, gives it a selection mode (Rules K11).
	Select *Selection
	// 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) 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
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.
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.

Jump to

Keyboard shortcuts

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