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

ochazuke

package module
v0.36.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 35 Imported by: 0

README

ochazuke
Latest release Go Reference Go version License

A Go TUI framework on Bubble Tea v2 where nouns, verbs and keybindings are data, so every action in your app is also a command you can run from the shell.

Ochazuke builds on Charmbracelet Bubble Tea v2 (charm.land/bubbletea/v2, Lip Gloss v2, Bubbles v2) and adds retained pages, default Lip Gloss styling, partial-screen modals, nouns and instances with one stack walk for actions and bindings, modal text editing, a nested command palette, and live CLI control.

Module github.com/unixsuperhero/ochazuke; requires Go 1.26 or newer. It builds on oden for CLI resolution.

go get github.com/unixsuperhero/ochazuke@latest

Explore the architecture

Open the interactive architecture field guide locally:

open docs/architecture.html

No server, install, or internet connection is needed. Search 12 components, inspect their responsibilities and source locations, step through keyboard/CLI/picker/modal journeys, and experiment with retained modal state. The state lab illustrates ownership; it does not connect to a running Go application. The linked architecture map supports node exploration and light/dark themes; its editable specification is architecture-map.json.

The guide links to four generated pages: Actions, Default bindings, Go API, and CLI & socket. Each page has separate navigation links; the action, binding, and Go API pages have their own search. Actions and bindings reflect a fresh demo, not a running application's customized registry. Editor grammar shortcuts remain in the keyboard guide.

The keybinding execution walkthrough follows Tab through input focus, Ctrl-N through modal creation, and a configured g g sequence through timed longest-match resolution (g, g g, g g g). It includes Go excerpts, concrete state snapshots, GitHub source links, and an illustrative sequence stepper.

For a graph beside the code, open the keybinding execution explorer. Hover or focus a function to preview its source; click to pin it. Choose Tab, Ctrl-N, or the sandbox sequence path and follow highlighted source lines. This is a source-backed snapshot, not a live debugger. Rebuild it with python3 docs/keybinding-explorer/generate.py; stale source mappings must be reviewed before regeneration.

Regenerate after changing registrations or Go declarations/comments:

go run ./cmd/docgen

Go's built-in documentation also works directly in the Ochazuke project directory:

go doc
go doc -all .
go doc Runtime.Execute

The HTML API catalog uses the standard go/doc library, with no documentation-server dependency. Edit cmd/docgen/main.go for the layout; do not hand-edit the four generated pages. Keep all four HTML files beside architecture.html when copying the guide.

Run on macOS

Requires Go 1.26 or newer.

go run ./cmd/ochazuke run

For a reusable executable:

go build -o /tmp/ochazuke ./cmd/ochazuke
/tmp/ochazuke run

In another terminal:

/tmp/ochazuke operations list
/tmp/ochazuke page open --json '{"id":"playground"}'
/tmp/ochazuke input undo
/tmp/ochazuke input undo --json '{"steps":3}' --target directory
/tmp/ochazuke state get --json '{"path":"/inputs/directory/text"}'

Global --socket PATH goes before run or NOUN VERB. It selects a specific application instance. The default socket on macOS is ~/Library/Caches/ochazuke/app.sock. run --headless uses the same Bubble Tea event loop without terminal rendering.

Keybinding sandbox

Run ochazuke keybinding (or go run ./cmd/ochazuke keybinding) without starting the demo or socket server. It opens Stage 1: Define bindings:

  • a: add a binding; enter Tea key names and a dummy action identifier in separate fields.
  • e / d: edit / delete the selected binding; arrows select a row.
  • t: edit the ambiguous-binding timeout (Go durations such as 1000ms or 1s; must be positive). The current timeout is shown in setup and in the sandbox.
  • i / o: import / export JSON. Enter a path or press Ctrl-O to browse files and folders. An export folder selection proposes bindings.json; the filename remains editable.
  • Enter: Enter sandbox. In Stage 2, the left column shows the latest key, outcome, and effective bindings; the right column logs messages newest first (last 200 retained).
  • Ctrl-G: return to setup without losing bindings or history. Ctrl-C exits either stage.

Import replaces in-memory bindings only after the whole file validates. Export asks before overwriting an existing file. Changes remain in memory until exported; exiting does not autosave.

Preload setup from a file with ochazuke keybinding --bindings=path/to/bindings.json. The import/export format is:

[
  {"keys": "ctrl+s", "action": "save"},
  {"keys": "g g", "action": "go-top"}
]

Names come directly from Bubble Tea's tea.KeyPressMsg.String() (a paste shows as [text]), quoted in the log to make whitespace visible. Ctrl-C and Ctrl-G are reserved and cannot be bound. Keys that the terminal encodes identically cannot be distinguished by the sandbox.

You can also preload dummy actions with repeatable --bind 'KEYS=ACTION' options. These override matching keys from --bindings, regardless of flag order:

go run ./cmd/ochazuke keybinding \
  --bind 'ctrl+s=save' \
  --bind 'g g=go-top' \
  --bind 'alt+x=alternate'

These are real Ochazuke rows on the app noun dispatched through Runtime.Dispatch and the operation registry. Each dummy action prints ACTION: <identifier> in the sandbox's latest-result area and history, without performing any app operation. The left column lists the effective bindings; history labels unmatched keys, pending sequences, cancelled sequences, and timed-out sequences. A later definition of the same key sequence replaces the earlier one.

Timed ambiguous bindings

When a complete binding is also a prefix of a longer binding, the runtime waits instead of firing at once. With g, g g, and g g g all bound:

  • The first g is complete but ambiguous: it is deferred, and the timer starts.
  • Each valid continuation (g g, then g g g) refreshes the timer. The longest complete match so far is the fallback.
  • When no longer continuation remains possible, the third g runs g g g immediately.
  • On timeout, the fallback (the longest completed binding) runs. The sandbox updates the outcome of the last key row rather than adding a key; if nothing runs, it shows that the sequence timed out.
  • If a different key arrives, the completed fallback (if any) runs and the mismatching key is then dispatched fresh through the whole stack, so it still reaches the editor or any row that binds it.
  • A pure prefix (not itself a binding) times out and clears without running anything.
  • While a sequence waits, the focused instance is in a prefix mode named after the base mode and the keys so far (normal g); bindings.list then lists the continuations.
  • Escape cancels all deferred work. Late timer events from a cancelled or superseded wait do nothing, and context changes cancel pending sequences.

The timeout defaults to 1000ms (Neovim's timeoutlen). Set it with --timeout DURATION (for example --timeout 1s; zero or negative values are rejected) or press t in setup. The timeout is not stored in the bindings JSON, which remains a plain array, so it is not persisted by export. It is kept when bindings are imported, edited, or deleted.

Coalesced ordinary typing is split into keys, like the app's dispatcher, so multi-key sequences work during rapid typing. Bracketed paste remains one logged message and does not trigger bindings.

Full-project example

examples/full-project is a standalone application assembled from separate configuration, action, binding, and page modules. Its JSON files define pages, instances and nouns, inputs, actions, binding rows, modals, modes, and appearance. Go registries supply the handlers and renderers named by that configuration.

go run ./examples/full-project
go run ./examples/full-project --config ./examples/full-project/config
go run ./examples/full-project --smoke

The default configuration is embedded in the executable. --config reads an editable directory instead. The smaller examples/core remains a headless example of action fallback, modal restoration, and isolated undo.

CSV example

examples/csv is the lightweight second consumer from docs/CSV-APP-SKETCH.md: a modal CSV reader and editor built without touching the resolver, the editor spec or the modal stack. It brings its own component, grid, a two-dimensional cursor with one instance for the focused cell parented to a row instance parented to a column instance, so the stack under a cell is [cell, row, column, grid, sheet, app] and dd resolves on row, s on column, i on cell, ctrl+s on sheet.

go run ./examples/csv examples/csv/testdata/people.csv
go run ./examples/csv --smoke examples/csv/testdata/people.csv   # render one frame and exit
go run ./examples/csv bindings list --all > csv-bindings.json # all stored binding tables as config rows

What it shows: motions with counts (3j, 2G, gg) through the grid's move verb; a cell edit, rename, add-column, open and save-as through one prompt modal (enter commits, esc cancels, ctrl+o opens the path picker into the field); a persistent filter field below the grid, the sheet's second focus stop (} or tab from the grid lands on it ready to type, enter filters by the cursor column and returns to the cell, shift+tab, esc and { from the grid move between the two, / on a column focuses it); three visual modes (v, V, ctrl+v) that share one mode table on the grid noun with m/M/ctrl+a marks, and : inside a selection opens the palette over it without dropping it; yank, delete and paste through the shared registers with the yank's shape (V y p pastes rows, ctrl+v y p pastes columns); transform on every noun that resolves to a cell set (cell, row, column, selection) as one verb over a map of named transformations (t u, t l, t t, t i title, t s slug, f for fill-down), whose name parameter takes its pick list from transformations.list (ids with labels; ctrl+n/ctrl+p, type to filter, enter); every edit as one Patch that is also its undo record (u, ctrl+r); per-verb Confirm on dd (Delete row r1?) and D (Delete column c1 (email)?, the header carried by a row the grid puts on the column instance through grid.WithInstanceRows); verb guards, so the palette offers selection.* only during a selection and row.*/column.* only while the focus is in the grid (the grid's selecting and in-grid guards). The one core addition it needed is Runtime.StoreYank/ReadYank (yank_api.go), so a component outside the package can store a yank of a shape the editor does not know; the second is the verb guard below.

Pages and default presentation

run launches the demo in examples/demo (demo.New()), not an application you must adopt; the framework package itself holds no application pages. It starts on Overview. The framework accepts your own page IDs, renderers, state, inputs, and registered actions.

The shell around a page is one chrome, Layout.Shell: a title row with the mode badge right-aligned, a subtitle row (the page tabs, or the page title beside the sidebar), a rule, the body, a rule, the footer and a margin, inside a two-cell gutter. Layout.PageBlock renders through the same method, so a page and the application around it reserve the same rows and Layout.BodySize is the body a renderer receives (less the sidebar). The footer and every modal's hint line are BindingHints: the focused instance's binding rows in resolution order (ctrl+j page.next · : palette.open, each row its key and noun.verb, or the row's Hint; the descriptions are in BindingHelp), the same data bindings list prints, so a hint can never name a key that is not bound and rebinding a key moves its hint. The last key's error replaces the footer until the next key.

? keyboard help is a list table with prefix, key, operation, arguments, and description columns: a key sequence is split into its prefix and final key (SplitKeys; ctrl+a v reads ctrl+a | v, g c x reads g c | x), a prefix's rows sit together sorted by key after the section's single keys, and the prefix is drawn once per group (Column.Group, any table can use it); the keys, operation and description wrap inside their columns. Arguments are compact JSON, resolved against the focus and mode when help opens; values depending on a future pattern key remain $key, $digit, or $text. Long arguments truncate with … rather than wrapping. BindingHelp exposes the full preview in HelpRow.Args, the split key in HelpRow.Prefix/Final, and the request a hinted row runs in HelpRow.Request (its Hint in HelpRow.Hint); opening help never executes the bindings.

Demo page Purpose
Overview Navigation and a short framework tour
Playground Text, path and multiline paths fields, shared yanks, nested drafts, path picker
Commands Live operation catalog, argument schemas, and remembered Playground bindings
Appearance Live theme and density controls: t cycles theme, d toggles density

In normal mode, Ctrl-J advances and Ctrl-K goes back. The demo does not bind Alt-number shortcuts.

These are suggested starter/demo pages. Domain pages such as Agents, Panes, Projects, and application Settings belong to consuming applications; the framework does not require those names or behaviors.

Registering your own pages

A page is an instance of kind page under app. Page carries a title, the instance it starts focused on (Focus, the page itself by default) and a renderer; while another page is open the page remembers its focus and mode (Page.Remembered), and page.open restores both. Pages, lists, inputs and terminals share one constructor family taking the same options (Parent, Kind, Focus): r.AddPage(id, title, render, Focus(..)), r.AddList(id, items, Parent(..)), r.AddInput(in, Parent(..), Kind(..)), r.AddTerminal(id, stream, Parent(..)). RegisterPage(&Page{...}) and NewList(r, ...) are the same registrations for callers that hold the struct or the runtime as a value. Register the page's instances and inputs first when the page starts inside one of them:

r := ochazuke.NewRuntime()
r.Instance("home")                                   // noun "home" is defined with it
r.AddInput(ochazuke.NewInput("title", "", false), ochazuke.Parent("home"))
_, err := r.AddPage("home", "Home", func(r *ochazuke.Runtime, width, height int) ochazuke.Block {
    return ochazuke.Text("Welcome to your application.")
}, ochazuke.Focus("title"))
if err != nil { panic(err) }
if err := r.OpenPage("home"); err != nil { panic(err) }
model := ochazuke.NewModel(r)
if _, err := tea.NewProgram(model).Run(); err != nil { // Model.View sets AltScreen
    panic(err)
}

Registration does not activate a page; OpenPage (or page.open) sets Runtime.Page and restores the page's frame. Switching reuses the page's exact frame and input instances: selection, focus, modes, drafts, undo history, and scrolling survive a round trip. Normal-mode Ctrl-J/K cycles PageList(), app's page children in registration order. Rows bound on a page instance or its noun apply while that page holds the focus. Page tabs have no numeric shortcuts; ' (page.pick) opens a chooser of every page, the pages visited before most recent first, then the never-visited ones in registration order, then the current page last, marked current (so ' then enter goes back to the page last left; type to filter by title, enter opens), or type page in the palette to complete a page ID. When the tabs do not fit, the row windows around the active tab and shows ‹N / N› for the pages hidden on each side.

Ordinary modals block page switching. The palette closes before dispatching its line, so open page alpha works when no other modal is underneath it. Focusing an inactive page's input does not silently navigate; open that page first. Explicitly targeted editing operations may still update an offscreen input without moving focus.

/tmp/ochazuke pages list
/tmp/ochazuke page open --json '{"id":"commands"}'
/tmp/ochazuke page next
/tmp/ochazuke page previous
/tmp/ochazuke state get --json '{"path":"/page"}'
/tmp/ochazuke state get --json '{"path":"/pages/playground/frame/focus"}'
/tmp/ochazuke surface scroll --json '{"lines":8}'

Styling and modal behavior

  • Default styling uses restrained teal/green accents, rounded panels, heavier focused borders, page tabs, a mode badge, and a status line. The status line and modal hints are derived from binding rows (BindingHints), never written by hand; the picker's own help line is derived the same way.
  • Themes: adaptive, dark, light, mono, plus the user's colour schemes (below). Density: comfortable or compact. Adaptive follows the terminal's detected background; NO_COLOR disables theme colors. Focus remains visible through border weight and a tab marker.
  • Modals are centered, content-sized panels, at most about 80% of the terminal width (no fixed column cap) and about 75% of its height; a chooser fills that width. They leave a visible, subdued perimeter of the previous screen whenever the terminal can accommodate it. Nested modals retain the previous modal and page underneath.
  • Resizable panes: a page that draws a list/detail split passes its Layout through Runtime.PaneLayout(page, layout); Layout.Split, SplitBlock and SplitFocusBlock then follow the page's divider (Layout.Shift), and a split drawn by hand divides with layout.Divide(defaultFirst, total, ochazuke.PaneMinWidth) (PaneMinHeight stacked). Catalog pages with a detail (cli.MountPage) and the demo's Library do this already. In normal mode > grows the first pane (the list, left or top) by PaneStep (5%) of the split, < shrinks it, = resets; counts repeat (3>). The shift stops at PaneShiftLimit (30 points) either way and each side keeps PaneMinWidth cells (PaneMinHeight rows stacked). The keys answer, and the footer shows >/< resize, only on a page whose last frame drew a split; pane grow, pane shrink and pane reset (each with an optional page) work from the CLI and palette anywhere. Dividers persist per page id in panes.json in the state directory (Config.PanesPath()), replaced atomically on each change; with no state directory (Config.Disable) they last the session.
  • Tiny terminals use reduced chrome rather than overflowing the screen. Bubbles viewports handle overflowing page and modal content; PgUp/PgDn scroll the active surface.
  • In an input modal, Escape returns insert mode to normal; another Escape closes the modal. A pending normal-mode operator/count is canceled before closing. "Escape pops a modal" is one row: the modal noun's normal-mode esc, which sits inside app and outside the editor and palette tables. Ctrl-G closes demo drafts directly. Palette and picker cancellation retain their own documented behavior.
  • App-specific modal rendering is registered through Model.ModalViews[frameID] = ModalView{Title, Render}. The framework owns positioning and composition; the renderer receives the modal frame and available content dimensions.
  • Render callbacks return a Block{Text, Cursor} and may use trusted Lip Gloss ANSI styling in Text. Sanitize external text before rendering it; built-in input and filesystem labels already do so. Text(s) wraps content that has no cursor.
  • The terminal cursor is real, not painted. InputBlock(in, w, h) reports the cursor cell of the input it renders; compose it only for the focused input and use InputView for the rest. Every composer (Layout.PageBlock/PanelBlock/SplitBlock/SidebarBlock, ComposeModalBlock, the shell) moves the child's cursor by where it placed the child, so Model.View can set tea.View.Cursor: a bar in insert mode, a block in normal mode, an underline in visual modes, hidden when no input is focused or a modal without an input covers it. Wide/combining graphemes, terminal-cell clipping, and visual-selection reverse video share the same renderer; moving the cursor never shifts following text.
/tmp/ochazuke themes list
/tmp/ochazuke appearance get
/tmp/ochazuke appearance set --json '{"theme":"dark","density":"compact"}'

Theme and density changes are atomic and apply immediately to all pages and modals. appearance set changes them for the session. NewStyles(*model.Appearance) supplies the same style vocabulary to application renderers.

Colour schemes

The theme setting selects the colour scheme and persists in settings.json. The framework declares it (Runtime.EnsureConfigSettings, called by Model.Init and the CLI shell) unless the application declared a setting of that name, which it then applies itself. Its choices are the built-in themes followed by the schemes in themes.json, a file beside settings.json (themes path prints it):

{
  "kombu": {
    "background": "dark",
    "accent": "#C9A227",
    "border": "#5C4A1E",
    "error": {"light": "#B3261E", "dark": "#FF6F61"}
  }
}
Role Draws
text Body text and the title
muted Hints, inactive tabs, the dimmed backdrop under a modal, the focus beside the mode (focus-display)
accent The active tab, the badge, the subtitle, focused panel and modal borders
border Unfocused panel borders
surface The background of the active tab and the badge
error Errors. It must differ from accent and muted.

Each role is "#RRGGBB", "#RGB", an ANSI index "0" to "255" (for 16- and 256-colour terminals), or {"light": ..., "dark": ...}. background picks the side: adaptive (the default, the terminal's), light or dark. A role left out keeps the built-in palette's. The selected row stays reverse video in every scheme, so it reads without colour.

A scheme that does not validate is skipped and reported as a notice. That covers an unknown role, a bad colour, background or tone, a built-in's name holding anything but tones, and an error colour equal to the accent or muted one. A theme value that names a skipped scheme reads its default, so the screen falls back to adaptive. After editing the file, themes reload reads it again and withdraws fixed notices. Appearance.Accent still goes on top of any scheme. mono and NO_COLOR draw no colour whatever the scheme. An application can supply its own schemes with model.Appearance.Schemes = ochazuke.NewSchemeSet(...) (ParseSchemes reads the file format).

Tones

A cell, or a run of runes inside one, can carry a named tone instead of colour: ListItem.Tones (and TreeNode.Tones, and "tones" on a catalog row line) hold ToneSpan{Cell, Tone, From, To}, rune offsets into the cell's text, To 0 for its end. The theme decides what a tone looks like, so colour stays configuration:

ochazuke.ListItem{ID: "w1", Noun: "pane", Label: "work/claude",
    Cells: []string{"★ work/claude", "needs input"},
    Tones: []ochazuke.ToneSpan{ochazuke.SpanTone(0, 0, 1, ochazuke.ToneStar), ochazuke.CellTone(1, ochazuke.ToneNeedsInput)}}
Tone Light / dark Attributes
state.working #1F6FB2 / #7CB7E8
state.needs-input #9A6700 / #E3B341 bold
state.done #2E7D32 / #8BC48A
state.errored the scheme's error bold
state.idle the scheme's muted
state.exited the scheme's muted dim
marker.star #B08800 / #F2CC60 bold
marker.queued the scheme's accent

mono and NO_COLOR draw the attributes alone. An unknown tone draws plain. The text never changes, so tones never count toward a width or a cut. The selected row stays one reverse-video bar: inside it a tone keeps its bold and drops its colour and dim. In themes.json a scheme takes "tones", laid over the built-in ones, and an entry named after a built-in theme may hold tones alone:

{
  "dark": {"tones": {"state.working": {"fg": "#5FAFD7", "bold": true}, "build.cached": "muted"}},
  "mono": {"tones": {"state.done": {"bold": true}}}
}

A tone is {"fg": ..., "bold": true, "dim": true} or a colour alone; fg is a colour as a role takes it or a role's name (text, muted, accent, border, surface, error), which follows the scheme. adaptive's entry reaches dark and light too. Styles.Tone(name) is a tone's style for an application's own text.

Keyboard guide

The demo starts on Overview in normal mode. Open Playground with Ctrl-J; Tab/Shift-Tab cycle its directory, notes, and paths fields. Overview, Commands, and Appearance support j/k and up/down scrolling. Demo page shortcuts are normal-mode-only, so insert-mode Ctrl-K still kills to line end. The footer of every screen is the live version of this table for the focused instance and its mode: the rows bindings list prints, innermost first.

Macro recording consumes exactly one register character after q. For qq:, the first q prompts for a register, the second starts recording into q and displays REC @q, and : opens the palette. A normal-mode q stops recording. Invalid register input reports an error but does not trap later commands in register selection.

Context Keys Behavior
Normal input h j k l, arrows Rune/line movement
Normal input w b e, 0 $, gg G Word, line, and document motions
Normal input i a I A Insert here, after cursor, at first nonblank, or at line end
Multiline normal input o O Open a line below/above and insert
Normal input x, u, Ctrl-R Delete, undo, redo
Normal input Numeric prefix Count for motions, delete, undo/redo; Ngg/NG select a line
Normal input v, V, Ctrl-V Character, line, or block visual selection
Normal input yy, p, P Yank a line; paste after/before
Normal app Ctrl-Y Open shared yank history
Normal input "a Select named register a for the next operation
Normal app/input qa … q, @a, @@ Record macro a, replay it, repeat the last macro
Insert input Ctrl-A/E, Ctrl-B/F Line start/end, previous/next rune
Insert input Ctrl-P/N, Alt-B/F Previous/next line, previous/next word
Insert input Ctrl-W/U/K, Ctrl-Y Kill path component/to line start/to line end; paste the app's current yank
Insert input Backspace, Delete/Ctrl-D, Enter Delete backward/forward; newline in multiline inputs
Insert input Escape Return to normal mode
Normal input Escape Cancel a pending operator, count, prefix or register
Normal app/input : Open the palette's command line
Any editing mode Ctrl-Space Open palette without stealing Readline Ctrl-P
Normal Playground Ctrl-O Open the shared picker using the field's filter
Normal path/paths field Enter Open picker using the current value/line
Normal Playground / draft Ctrl-N Open a draft / nest another draft
Draft modal Ctrl-G Close and restore the parent
Normal Playground Ctrl-X Inspect the handling noun
Normal app ? Open the scrollable keyboard guide
Insert (any input) Ctrl-\ Open the keyboard guide (? types itself there)
Normal list / Filter the list's rows (see Filtering a list)
Normal list gg, G, NG/Ngg First row, last row, row N (list.goto; clamps, never spills)
Normal list Ctrl-D/U, Ctrl-F/B Half a page / a page down and up, the window moving with the selection (list.scroll; 3 Ctrl-D is 3 rows, 2 Ctrl-F two pages)
Normal cell table (CellSelect) h/l, 0/^/Home, $/End, N|, gc Move the current column: one left/right (counts), the first, the last, column N, a chooser of titles (list.column, list.choose-column); the view scrolls to show it
Normal cell table y Copy the current cell into the registers (list.yank)
Normal cell table z>, z<, z= Widen / narrow the current column by 2 cells (counts), every column automatic again; remembered per table (list.resize-column, list.reset-columns)
Normal table Y Copy the row's cells joined by tabs into the registers (list.yank {what: row})
Normal table V Visual row selection over the marks: j/k extend it, a row's verb then runs for every selected row (one confirm), esc ends it (list.visual, list.end-visual)
Normal table zl/zh Scroll the columns one right/left; the pinned columns stay (list.scroll-columns)
Normal row table (the default) l/h Scroll the columns one right/left; at the last column / the start move the focus to the next / previous stop (list.scroll-columns, focus.next/focus.previous)
Normal row table (the default) $, ^/0 Scroll to the view with the last column / back to the first (list.scroll-columns)
Normal cell table zp Pin or unpin the current column (list.toggle-pin-column); any table: list.pin-column, unpin-column, unpin-columns from the palette
Normal Appearance t, d Cycle theme / density
Normal demo Ctrl-J/K Next/previous page
Normal page with a list/detail split >, <, = Grow / shrink the list pane by 5% (counts: 3>), reset the divider; remembered per page
App or modal PgUp/PgDn Scroll the active surface
App Ctrl-C Quit (q is reserved for macro recording)
Attached terminal any key Forwarded to the pane; Ctrl-A is the leader (d detach, Ctrl-A literal, bound key runs, other key sends both, timeout sends the leader)

On terminals where Option does not send Meta sequences, configure Option-as-Meta to use Alt-B/F. Ctrl-Space is received as ctrl+space by Bubble Tea v2 (v1 spelled it ctrl+@). Every row above is a registered binding: the demo's on its nouns and instances, the editor's in editor_bindings.json. bindings list prints the rows the focus answers to in its current mode, pending modes included, so 2, d or " followed by bindings list shows the count, operator or register table.

Palette and selectors

  • : opens the palette: an ex-mode command line, one prompt input (insert pinned, the Readline rows edit it: Ctrl-W deletes a word, Ctrl-U the line). The line's text is the command; there are no stages.
  • Words resolve left to right. The first word is a verb or a noun; the second is the complement (a noun that has the verb, or a verb or instance of the noun); then the operation's parameters in order (required first, then by name). Every word may be a unique prefix (tr r up); an ambiguous prefix stays unresolved and the list shows its candidates. A first word that is a prefix of exactly one verb and one noun takes the reading the next words resolve. A value with spaces is quoted (add note "two words").
  • Omit the noun and the verb acts for the nearest instance on the focus path whose noun has it: on a CSV cell transform upper changes the cell, transform row upper the row under the cursor, transform column upper the column, transform selection upper the selection (offered only while one exists; verb guards hide verbs the focus does not answer to). The cursor never moves.
  • The list under the line completes the next word, grouped verbs, nouns, then parameter choices, narrowed by the word being typed. Typing :v, :n or :p as the word restricts the list to verbs, nouns or choices; the code is consumed, not appended, and lapses at the next word.
  • Nothing is highlighted until Ctrl-N/Ctrl-P moves onto the list; typing clears the highlight. Enter or Tab on a highlighted entry appends its word and a space. Enter with nothing highlighted runs the line as typed. A line whose verb still lacks a required word opens the args form: a field per parameter (required first, optional ones marked), the typed words filled in, the first missing one focused; choices and noun-typed words are picked from a type-to-filter chooser. enter runs the verb once every field is valid and otherwise moves to the field that still needs a value; esc (from normal mode) cancels, runs nothing and gives the line back as typed. A key whose binding row leaves a required argument out opens the same form. With the setting palette.missing-opens-form off, an incomplete line stays and the error names its first missing word (missing name (string, pick)). The CLI and the API still fail with NAME is required. Tab with nothing highlighted completes a word that has one completion.
  • Esc clears a non-empty line, then closes; Ctrl-C closes. Up and Down recall earlier lines from the action history (entries with the source palette, which every request a line runs is recorded with).
  • The originating interaction context is captured before the palette opens. undo input edits the originating input, not the palette's line. A failing request reopens the palette with the line and the error.
  • Commands returning a JSON array of objects open a read-only Results modal (for example, operations list or jobs list). This also accepts typed Go slices/arrays and encoded JSON. Columns with fields present in every row come first, followed by less common fields in descending occurrence count; ties sort alphabetically. Missing fields are blank, explicit nulls read null, and nested values are compact JSON. Long cells truncate and rare columns drop first at narrow widths. j/k navigate; Esc closes and restores the originating focus. Empty lists show No rows; non-object results are unchanged. Only palette execution presents results: direct bindings, API/CLI calls, refreshes, and completion sources do not open a table.
  • Pending commands present their results after delivery and their existing Then callback. Failed, cancelled, orphaned jobs, or results arriving while another modal is open do not open a table; an existing modal is never replaced.
  • Instance sources explicitly declare their default verb and ID argument: page playground runs page.open {id: "playground"}, and the instances are also the choices of that argument (open page playground). The full-project example configures these defaults in JSON.
  • Scripts drive the line through the same registry: palette set {text} replaces the line, palette run presses Enter, palette complete {value}, palette move {delta}, palette recall {delta}, palette cancel. Each returns the palette state: the line, its parsed words, the operation, missing word and the completion entries.
// The grammar, one example per form (focus on the cell r1:c1 of row r1)
"transform row upper"   // verb noun arg      -> { key: "row.transform", target: "r1",    args: { name: "upper" } }
"row transform upper"   // noun verb arg      -> same request
"tr r up"               // unique prefixes    -> same request
"transform upper"       // noun omitted       -> { key: "cell.transform", target: "r1:c1", args: { name: "upper" } }
"page playground"       // noun instance      -> { key: "page.open", args: { id: "playground" } }
"t r up"                // "t" is ambiguous: tidy, transform -> stays, list shows both
"transform row"         // incomplete         -> the args form asks for name (setting off: "missing name (string, pick)", line stays)
"transform :p"          // filter code        -> line "transform ", list shows only choices
  • The shared path picker uses an editable absolute path and a Readline-only editor. Ctrl-N/P moves through results without moving the text cursor. Other Readline bindings still edit the path; / and whitespace delimit Ctrl-W kills, while dots and hyphens remain part of a filename.
  • Tab completes the highlighted result into the picker's path input without confirming or closing it. Directories receive a trailing separator for continued browsing; the originating field stays unchanged until confirmation.
  • Escape cancels immediately without changing the originating input. Alt-Up browses the parent directory. Enter opens a directory/selects a file, and Ctrl-S confirms a path. Letters always edit the path; the picker never enters Vim normal mode.
  • Results recursively fuzzy-match the full relative path. Native Git supplies .gitignore semantics, including nested rules, negations, repository excludes, and global excludes; Git must be on PATH. Non-repositories use temporary Git metadata outside the browsed tree. .git and directory symlinks are never traversed.
  • Results are a table (path, size, modified) laid out by the same code as a list with columns (table.go): a header under the Results n/m line, the size right-aligned, … cuts, and modified then size dropping as the results pane narrows; the path takes the rest. Size and modification time are statted lazily: the index walk never stats; only the rows the table draws are looked up, off the loop in the picker's stat slot, cached per path (an LRU of 4096, cleared when a new index starts); a row draws blank cells until its stat lands, and the fixed column widths keep anything from moving. Directories show no size. The selected result keeps the picker's own > marker and is the one reversed row.
  • The picker publishes immediate children first, then builds the recursive index in the background. Listed paths remain selectable and Tab-completable while indexing continues. Query edits search the available index; recursive results preserve the highlighted path when it still matches. Canceled or stale results cannot overwrite newer results. A recursive failure keeps usable shallow results and shows Index incomplete.
  • Leading ~ and ~/ resolve to the home directory. Explicitly opening a directory excluded by an ancestor repository's ignore rule makes that directory a fresh ignore boundary, so a home-repository rule such as proj/ does not hide every project. Root-local, nested, and global ignore rules still apply.
  • Ctrl-S confirms the highlighted result; an empty query includes a current-directory entry for any/dir. The action picker.choose {current:true} explicitly chooses the root. Changing the originating text while the picker is open rejects selection instead of replacing stale offsets.
  • The split view pairs results with the selected entry's metadata and a text or directory preview. Narrow terminals retain the list and selected-entry metadata. Preview work runs outside the UI loop, limits displayed file content to 64 KiB or directory listings to 128 entries, and ignores stale deliveries after selection changes or reopening. Symlinks show their target without following it; binary and special files do not display raw contents.
  • Controls are ordinary rows of the picker noun in insert mode; the path field is a prompt input beneath it. Override or add them with r.Define("picker").Mode("insert").Bind(...) or a binding file row ("request": null removes one). The picker help is derived from bindings.list, so remapped and removed controls are reflected without changing the renderer. No function keys are assigned.
Configuring noun instances

NewPalette supplies live page instances with page.open as their default. Applications can replace that configuration or register sources for their own nouns:

err := model.Palette.RegisterNoun("page", ochazuke.PaletteNoun{
    Source: ochazuke.PagesPaletteSource,
    DefaultVerb: "open",
    Argument: "id",
})
if err != nil { return err }

A PaletteSource receives the runtime and originating context and returns []PaletteInstance, each with an ID and Label. An instance word after the noun passes its ID to the configured argument, then dispatches through the normal operation path. The source is queried live (again after every edit of the line) so newly registered pages appear without rebuilding the palette. After a noun a verb wins over an instance of the same name; the instance stays reachable as the default verb's argument (page open open).

Argument choices

A Parameter with Enum (static values) or Source (an operation key such as transformations.list whose result is []string, []{id, label} or []any of either) completes in the palette: at that parameter's word the list shows the choices (a boolean completes true/false), the word is a unique prefix of a choice's id, and Ctrl-N/P then Enter or Tab appends a highlighted one. A value outside the choices is refused, by the palette and by Execute, unless the parameter sets Free. A source is called at execution to validate; if it fails, validation is skipped rather than blocking the operation. operations.list publishes the fields, and ochazuke NOUN VERB --help prints the choices from the running app.

A Parameter with Noun (and no Source) names one of that noun's things: its choices are the rows of NOUN.list when that verb is registered, asks no confirmation and needs no argument (it then stands in for the Source, off the loop for an application verb, and cli.Catalog and cli.Mount publish it as the source), else the ids of the noun's live instances, labelled by their labels: instances in registration order, each list followed by its rows of the noun (every row, filtered out or not, in list order; the row instance id, never a foreign id in Data). Map rows give their id as id, key or name and their label as label or title; []Operation rows are noun.verb. Enum values come first and an explicit Source wins; a Noun adds no validation at execution. The framework marks page.open id (pages), mode.set/mode.enter mode (modes), confirm.set operation (operations), list.focus id (list, free) and input.focus id/modal.push input (editor, free).

// Parameters of transform.apply
{ name: { type: "string", required: true, source: "transformations.list" },
  case: { type: "string", enum: ["upper", "lower"] },
  tag:  { type: "string", enum: ["draft"], free: true } }
// transformations.list returns ["trim", "dedupe"] or [{ id: "trim", label: "Trim spaces" }]

Readline-only inputs: the prompt noun

Register an input as an instance of the prompt noun:

prompt := ochazuke.NewInput("prompt", "", false)
if err := r.AddInput(prompt, ochazuke.Kind("prompt")); err != nil {
    return err
}

prompt pins insert mode: registration starts the editor in insert mode and SetMode/mode.set keep it there. It binds nothing of its own, so the editor's Readline rows apply and Escape, which only returns insert mode to normal, has no effect; a page, modal or noun on the stack may bind Escape to cancel its own UI, as the picker does. Single-line and multiline editing, path-aware kills, shared yanks, and undo/redo use the existing implementation. Inputs of the default editor kind keep their Vim/Readline behavior.

Path fields

One picker handles files and directories. Field kind determines replacement behavior; its separate filter controls eligible results:

directory := ochazuke.NewInput("directory", "/tmp", false)
directory.Kind = "path"
directory.PathFilter = "dir"

paths := ochazuke.NewInput("paths", "/tmp\n.", true)
paths.Kind = "paths"
paths.PathFilter = "any"
Field kind Picker starting context Selection result
Empty / text Working directory or explicitly supplied path Insert at the cursor captured when opened
path Entire field value Replace the entire value
paths Current line Replace only that captured line, preserving the rest

Filters are file, dir, and any. A paths field is an ordinary multiline editor: motions, o/O, visual selection, registers, yanks, macros, and undo operate on its text. It does not serialize a JSON array or use a separate chip/list widget. Filenames containing a newline cannot be represented as one path per line and are rejected rather than silently splitting the set.

/tmp/ochazuke picker open --json '{"filter":"file","path":"/work"}'
/tmp/ochazuke picker choose
/tmp/ochazuke picker close

Applications constructing their own picker only add its modal renderer; NewPicker registers the picker instance whose Component the runtime drives through the delivery contract:

picker := ochazuke.NewPicker(r)
model.ModalViews["picker"] = ochazuke.ModalView{
    Title: "Choose a path",
    Render: func(_ *ochazuke.Runtime, _ *ochazuke.Instance, w, h int) ochazuke.Block {
        return picker.ViewBlock(w, h)
    },
}

Selectable lists

List reuses the stack walk, binding rows, and bounded row-rendering helpers. The application owns records; each row names a registered noun, and the list turns every row into an instance of that noun under the list:

r.Define("worktree").Verbs("open", "remove").Mode("normal").Bind("o", ochazuke.Do("worktree.open"))
items, err := ochazuke.NewList(r, "sidebar", []ochazuke.ListItem{
    {ID: "repository", Label: "Repository: ochazuke", Noun: "repo"},
    {ID: "main", Label: "Worktree: main", Noun: "worktree"},
}, ochazuke.Parent("workspace"))
if err != nil { return err }
if err := items.Focus(); err != nil { return err }

The id (sidebar here) identifies the list instance (r.List("sidebar") returns it); the list instance answers to the list noun (Kind picks another). Render items.View(width, height) inside an existing page or panel. It keeps the selected row visible as selection or viewport size changes. Default normal-mode j/k, up/down and Space (toggle mark) are rows of the list noun; a row's noun sits inside the list on the stack and can override them, and a row instance may carry its own rows above its noun's.

  • Focus() points the focus at the selected row (or the list when empty). Runtime.SetFocus(id) switches to another instance; the old selection remains, but its rows are no longer active. A list is one focus stop: } and tab leave it for the next stop, and jumping back lands on its selected row (FocusTarget).
  • Move(delta) and Select(itemID) change selection without stealing focus. Selected() returns the selected value and whether one exists.
  • SetItems(rows) copies the records and preserves selection by stable ID. It creates the instance of each new row under the list (Instance{ID: item.ID, Noun: item.Noun, Parent: <list>}) and removes the instances of rows that disappear; a retained row keeps its instance and marks. A removed selection falls back to its previous index, clamped to the remaining rows. An empty list focuses the list itself. An empty or duplicate ID, an unregistered noun, or an ID that is already another instance rejects the whole update.
  • Refreshes also move every focus reference (the focus, a page's remembered focus, a modal's return) that pointed at the list or a removed row to the selected row (RetargetFocus), so returning from another page/modal cannot restore a removed row's rows.
  • ToggleMark(id) and Marked() keep a multi-selection. list.apply {verb, args} runs <row noun>.verb with the row as target and args as its arguments for every marked row (the selected row when none is marked), skipping and reporting rows whose noun lacks the verb; a verb with a confirm asks once for all the rows.
  • Call these methods on the runtime's event loop. Data loading and domain actions remain application-owned. There is no tree expansion or built-in search provider.
Moving through a list

Every list (page lists, tables, trees, a catalog page's lists, the Settings page's, lists in modals such as ? help) answers the Vim paging motions in normal mode, rows of the list noun beside j/k, so a row noun, the list instance or a component that binds the same keys answers first:

Keys Operation Does
g g / G list.goto {position, row} The first / last row; with a count the row of that number (5G, 5gg), clamped
ctrl+d / ctrl+u list.scroll {delta, half} Half a page down / up; a count is rows (3 ctrl+d)
ctrl+f / ctrl+b list.scroll {delta} A page down / up; a count is pages

A page is the rows the list's own window showed in the last frame when the list windows itself, else the height of the page body it is drawn on (a list drawn at its full length on a scrolling page), else the rows it showed (a list in a modal), else 10 before any frame. The window moves with the selection by the rows moved, so the selected row keeps its line (Vim's ctrl+d), and the scrolloff margin then applies as for j/k. None of them spills to the next stop: at the list's edges they stop quietly. They are motions: off the footer, under movement in ? help. pgup/pgdn still scroll the surface without moving the focus. Story: lists/page-motions.

Tables select rows; cell mode is opt-in

A table (a list with columns) selects rows. Only a table added with CellSelect() (an InstanceOption for AddList; for a table one reads cell by cell, such as a JSON or CSV view) has a current column as well as a selected row, the way a spreadsheet (o-csv, on grid) has a current cell. Every other table, trees' lists, the ? help modal, the Settings page and cli catalog tables included, leaves h/l to its row nouns: where the row noun does not bind them, l scrolls the view one column right until the last column shows and then moves the focus to the next stop, h scrolls back to the start and then moves it to the previous stop (the horizontal twin of the j/k spill; a modal, a NoSpill list and the page's first or last stop clamp), $ scrolls to the view with the last column and ^/0 (without a count) back, z l/z h one column (list.scroll-columns). RowSelect() is a deprecated no-op kept for code written against v0.12.0.

Pinned columns draw first, in declared order, and never scroll; the default is the columns declared Column.Pinned, else the first column. list.pin-column {column}, list.unpin-column {column}, list.toggle-pin-column {column} (z p on a cell-mode table) and list.unpin-columns change them on any table (column is a title or a 1-based number; without it a row-select table opens the column chooser), list.reset-columns (z =) puts the default back, and a user's pins are remembered per table in tables.json. When the pins leave no room for a scrolling column the view leaves pins out from the right (the saved pins stay). A table with a header whose columns do not fit reserves 8 cells and marks its edges: ‹N after the pinned columns, N› at the right edge, N the scrolling columns hidden on that side.

On a cell-mode table the row keeps the list's one highlight; the current cell is drawn underlined inside it, the header underlines the current column's title, and an unfocused table fades both with the list. Rows of the list noun in normal mode that answer only on a cell-mode table (table_cells.go), except where the table says every table:

Keys Operation Does
h l left right list.column {delta, steps} One column left / right in drawing order (pins first); a count is columns; on the first / last column focus.previous / focus.next instead (clamped in a modal, on a NoSpill list, at the page's first / last stop)
0 (no count) ^ home / $ end list.column {to} The first / last column
N | list.column {index} Column N (`3
g c list.choose-column A chooser of the column titles
y list.yank {what: cell, register} The cell's text into the registers ("ay names one)
Y list.yank {what: row, register} Every table: the row's cells joined by tabs
z > / z < list.resize-column {delta, steps} Widen / narrow the current column by ColumnWidthStep (2) cells, never below 3
z = list.reset-columns Every column automatic again, the default pins back
z p list.toggle-pin-column Pin or unpin the current column
z l / z h list.scroll-columns {delta, steps} Every table that scrolls: the view one column right / left; on a cell-mode table the current column is carried along when it scrolls off
V, then esc list.visual {on}, list.end-visual Every table with marks: visual row selection (below)

j/k keep the column. Each frame scrolls the column offset as little as shows the current column: the first column stays (it names the row), a column dropped by Priority or past the right edge comes into view as the column reaches it, and the first column brings the unscrolled view back. The movement keys are motions (off the footer, under movement in ? help); y, Y, V and the width keys are hidden from the footer too and listed in ? help. A row noun that binds one of these keys answers first (the walk is inner-first): that table keeps the app's key and loses the cell key. enter is unchanged, the row's default action. The width keys need a current column, so they answer on cell-mode tables only; a row-select table's widths are the application's (SetColumnWidth). A cell-mode table's current column and the widths given with z >/z < are remembered per table id in the state directory's tables.json (Runtime.TableState, SetTableState; in memory without a state directory); every table's pins are remembered there too (TableState.Pins; a row-select table remembers nothing else). API: List.Column, SetColumn, ColumnWidths, SetColumnWidth, ResetColumnWidths, ColumnOffset, PinnedColumns, SetPinnedColumns, PinColumn, UnpinColumn, UnpinColumns, ResetPins.

A host's own h "back" row on a table should take the guard list-first-column (cli's groups rail does): on a cell-mode table h then moves left and goes back only from the first column; on a row-select table it holds, so h goes back at once. An instance row answers before the list noun's, so a host's h back row wins over the table's scroll and spill.

V starts a visual row selection at the selected row on any table (not on a NoMarks one). It is the marks: while it lasts they are the marks V found plus every shown row between the anchor and the selection, so j/k, counts and the paging motions extend it (they clamp instead of spilling), the * gutter draws it and list.apply fans out over it. The mode stays normal, so the rows' keys still answer; a key whose row runs an application verb of the row's noun runs it for every selected row through list.apply, with one confirm when the verb has one, and ends the selection. esc (or V) ends it without running anything. Either way the marks go back to the ones V found; space during it toggles those.

The design is in docs/UI-MODEL.md "Tables". Stories: tables/cell-motion, tables/column-count, tables/column-resize, tables/visual-apply, tables/unfocused-cell-dimmed, tables/column-motions, tables/row-select, tables/scroll-then-focus-{right,left}, tables/cell-edge-focus, tables/nospill-clamps, tables/modal-clamps, tables/row-noun-l-wins, tables/edge-markers{,-start,-fit}, tables/pin-{cell,row-palette,declared,narrow}, tables/unpin-columns, catalog/groups-table-h; an o-story list opts in with cellSelect, a column with pinned.

Filtering a list

Every list filters: / on a focused list (a page list, a table, a catalog page's list, the Settings page's, a list in a modal) opens a query line above its rows, and typing keeps the rows whose label or cells hold every word of the query, ignoring case, in list order:

/idle                                                                 2 of 4
pane                  harness      state    cwd
work/claude           claude-code  idle     ~/proj/matomeru
play/pi               pi           idle     ~/proj/nabe
Keys (mode list_filter) Does
printable keys, space, paste list.filter-type: add to the query
backspace / ctrl+h list.filter-erase; on an empty query list.filter-keep (leaves the line)
up/down, ctrl+p/ctrl+n list.move {clamp}: move among the matches, never spilling
enter list.filter-keep: back to normal mode with the filter kept (the line stays; / edits it again). On a list added with ClearFilterOnEnter() (or a tree), with a match: list.filter-choose, which clears the filter, keeps the selection on the match and goes back to normal mode
esc list.filter-clear: clear the filter and select the row / found
ctrl+u list.filter {query: ""}: empty the query

In normal mode esc on a filtered list clears the filter (list.filter-clear, guarded off on an unfiltered list so esc there stays the outer level's). The / row is an app row guarded by list-filterable and added after the Model's defaults: an application's own / on any level (its row noun, the list, the page) answers first, and the footer's / filter is the hint a narrow footer cuts first.

The filter hides rows from drawing and from motions (j/k, counts, spills) and never removes them: their instances, marks and data stay. SetItems and List.Patch (which patches every row, hidden ones included) while filtered filter the new rows and keep the selection by id; list.apply fans out to the marked rows the filter shows (a mark on a hidden row waits); a command that lands on a hidden row (Select, focus.set, a job's focus) clears the filter. Items() and Len() are every row; Shown() counts the shown ones, Filter() is the query, Filtering() reports the line, SetFilter(q) sets it, and Height() (Len() plus the line) is what a host sizing a list to its rows passes to View. The CLI form is list filter --target LIST --query Q (an empty query clears); the state at /lists/<id> then has filter and total, and its items are the shown rows. Stories: lists/filter-*, catalog/filter-setitems, tables/filter-by-cell, settings/filter.

The operation equivalents are list.focus {id}, list.move {id?, delta, clamp?}, list.goto {id?, position?, row?}, list.scroll {id?, delta, half?, steps?}, list.scroll-columns {id?, to?, delta?, steps?}, list.column {id?, to?, index?, delta?, steps?}, list.choose-column {id?}, list.yank {id?, what?, register?}, list.resize-column {id?, delta, steps?}, list.reset-columns {id?}, list.pin-column {id?, column?}, list.unpin-column {id?, column?}, list.toggle-pin-column {id?, column?}, list.unpin-columns {id?}, list.visual {id?, on?}, list.select {id?, item}, list.mark {id?, item?}, list.apply {id?, verb, args?} and list.filter {id?, query}. Omit id to operate on the list on the focus stack. State is inspectable under /lists/<id> (rows show their noun, marked lists marks) and /instances/<rowId>. The full-project example's Items page mixes repo, worktree and branch rows in one list: each row answers to its noun's verbs and keys, ? lists the focused row's rows, and Space plus list.apply fans one verb out over several rows.

Terminal instances

A terminal is an instance of the terminal noun whose component shows a pane the application streams in. The framework owns no PTY, shell or daemon: AddTerminal(id, stream, Parent(page)) takes a TerminalStream (Next(ctx) blocks for the next TerminalFrame{Lines, Cursor, Shape, Hidden}, Send(keys...) forwards key names as bindings spell them, Paste(text), Resize(cols, rows)), which is the application's daemon client; the examples use an in-process echo stream. A stream that also implements TerminalReleaser (Release() error) is released once when the terminal closes (Pop, SetItems, RemoveInstance), so a daemon size lease is given back without an app override. terminal.detach only moves focus and keeps the pane for a later attach; AddTerminal(id, stream, Parent(page), CloseOnDetach()) makes detach remove the instance too, releasing the stream. Frames arrive through the delivery contract: each read is work owned by the instance (Runtime.Start), its completion is delivered to the component when still current, and a frame that completes after the instance closed or a newer read started is dropped. term.ViewBlock(width, height) renders the pane into the area a page gives it, tells the stream that size when it changes, and reports the pane's cursor; while the terminal is focused the shell places the real terminal cursor there, shaped as the pane reports (bar, underline, else block).

The instance starts in the terminal base mode, where the noun's any and paste rows forward every key and paste to the pane, so nothing on the page or the application answers keys while a pane is attached. The leader ctrl+a is an Enter("leader_pending", Payload{Prefix: "ctrl+a", Request: send ctrl+a}) row: leader_pending is a one-shot exclusive mode, the prefix arms the sequence timer and the stored request is what the timer runs:

Keys Behavior
any key, paste Forwarded to the pane unchanged (Escape and Ctrl-C included)
ctrl+a then d terminal.detach: focus returns to the terminal's parent; the pane keeps running
ctrl+a then ctrl+a Sends one literal ctrl+a
ctrl+a then a bound key Runs that row's request (bind rows into the terminal noun's leader_pending table in code or config)
ctrl+a then an unbound key Sends the leader and the key, in order, in one Send and returns to terminal mode
ctrl+a alone past BindingTimeout Sends the leader alone; the next key starts fresh. The terminal-leader-timeout setting overrides that wait for the leader; 0 waits for the next key however long it takes

Every row under the leader is a Request: d runs terminal.detach, the unbound-key row is terminal.send with keys joined from the prefix and the key (Join("keys", SourcePrefix, SourceKey)), the paste row is terminal.paste {keys: prefix, text}, which forwards the leader through terminal.send before it pastes. The leader is a setting: terminal-leader (TerminalLeaderSetting, default ctrl+a, one ctrl+ or alt+ key) is declared by Runtime.EnsureTerminalLeader() unless the application declared it (an application with terminals calls it at setup; cli.AddCommands does; an application without terminals never shows the setting), read from settings.json and followed on every change (settings set terminal-leader ctrl+b, the Settings page, another process's write). Applying it (Runtime.SetTerminalLeader(key), Runtime.TerminalLeader()) rewrites the rows, which stay data: every row on any noun that enters leader_pending on one key with that key as its prefix (the framework's and an application's own, such as a leader row with a richer hint) moves to the new key with its prefix and timeout request, the leader_pending row that sends the old leader now sends the new one on the new key, hints naming the old leader name the new one, and the application's other leader_pending rows (d, its own keys) stay. The old leader becomes ordinary input that reaches the pane. AddTerminal applies it too, so leader rows defined after Init move. terminal.attach {id} focuses a pane from anywhere on its page; terminal.send {keys} and terminal.paste {text, keys?} are the operations the rows run and are available to the CLI. bindings list in terminal mode prints the terminal table alone (the leader, any, paste); in leader_pending it prints the leader table, so the footer shows exactly what the pane will do with the next key. The leader row carries a hint (Binding.Hinted, config field hint), so the attached footer reads ctrl+a leader: d detaches, ctrl+a sends ctrl+a instead of its mode.enter operation; any row may set one, and bindings list returns it with the row. The full-project example's Console page is a terminal page added from config rows and the public API only.

Markdown and checklists

The markdown package renders Markdown with Charm's Glamour v2 and edits task lists in place. It is a package of its own, built on the public API like grid, so only an application that imports it carries Glamour, goldmark and chroma (the demo binary, cmd/ochazuke, grows from 10.2 MB to 19.9 MB, mostly chroma's lexers; a binary that does not import markdown, such as examples/hello, is unchanged).

  • markdown.Block(text, width, appearance) renders text in Glamour's standard dark or light style, chosen from the appearance's background (Appearance.Background(): an adaptive theme follows Dark), and the plain notty style under the mono theme and NO_COLOR (markdown.StyleOf). Glamour's blank first and last lines are dropped and every line is cut to the width. Renderings are cached by text, width, style and options. markdown.Block(text, w, a, markdown.PreserveNewlines()) keeps every newline inside a paragraph a line break (Glamour's WithPreservedNewLines), for notes written as plain lines; code blocks and tables draw the same either way, and markdown.New(r, id, parent, src, markdown.PreserveNewlines()) gives a Doc the same rendering without changing its items or toggles. For a read-only preview, put it in a detail so J/K scroll it: body, marker := detail.Window(markdown.Block(text, w, *m.Appearance).Text, w, h).
  • markdown.New(r, id, parent, src) is a *markdown.Doc: a rendered document with a line cursor, edited in place, vim style. Draw it with doc.Block(width, height, *m.Appearance) (or View for the text alone; Block carries the terminal cursor while a block is open). The document is a sequence of elements (markdown.Elements(text): heading, paragraph, bullet, numbered, task, table (one per row), code, quote, rule, note), each a range of whole source lines. The cursor sits on a drawn line: the stop is a list child (doc.ListID(), id + "/lines", added NoMarks) with a row per line an element covers (markdown-line; a checklist item's lines are markdown-task rows, a note's markdown-note rows), so j/k, g g/G, ctrl+d/ctrl+u, ctrl+f/ctrl+b, counts and spills are the list's. The cursor's line is the one reverse-video highlight while the Doc holds the focus. With height 0 every line is drawn and the page scrolls; with a height the Doc windows itself and draws a position marker. A drawn document is a stop; an empty read-only one is not (an empty editable one has one blank line to stand on).
  • Motions on the markdown noun in normal mode (all motions, all with counts): }/{ markdown.next-block/previous-block move between components (a heading, a paragraph, a whole list, an item's notes, a table, a code block, a quote); ] ]/[ [ markdown.next-same/previous-same to the next or previous element of the kind under the cursor; ] or [ and a kind letter markdown.next-kind/previous-kind {kind}: h heading, t table (a table counts once), x checklist item, l list item, c code, q quote, n note, p paragraph. At an edge they do nothing. g O markdown.outline is a chooser over the headings.
  • Editing: i a I A (markdown.insert {at: cursor|after|start|end}) open the cursor's element in the ochazuke editor as its raw Markdown, in place: only that element switches to source (markdown-edit input, insert mode), the rest stays rendered; esc (markdown.leave) writes it, tidies it (a table's pipes aligned, an ordered list renumbered) and draws it again. o/O (markdown.open {where}) open a new line below or above that continues the structure: a list item with the same marker and indent (numbered one on, the list renumbered unless numbered 1. throughout; a checklist item unchecked; below an item's whole subtree), an empty table row, a note line on a note, else a paragraph. In the editor: enter on an item ends it at the cursor and starts the next (markdown.continue-item), on an empty item ends the list (markdown.end-list), on a row starts a row (markdown.next-row), after a heading starts a paragraph, in a quote quotes the next line; tab/shift+tab nest or take out a list item (markdown.indent/outdent) and move between table cells (markdown.cell; past the last cell a new row); backspace on an empty marker (markdown.unmark) leaves an indented continuation line; / on an empty line (markdown.insert-block) is a chooser of block kinds (table asks for its columns, checklist, bullets, numbered list, heading 1 to 3, code block, quote, rule). Each insert-mode key is a guarded row, so elsewhere the editor's own key answers. Normal mode: x markdown.toggle (on a checklist item), d d markdown.delete (the cursor's line of a paragraph, quote or code block; an item with its notes and nested items; a heading's line only; a table's header the whole table), y y markdown.yank and p/P markdown.paste (lines, the shared registers), u/ctrl+r markdown.undo/redo (every edit, each written), markdown.reload.
  • Notes: a blockquote nested directly under a list item whose first line has the shape > **author** date: holds notes (any other blockquote is a quote); each **author** line starts a note, the lines after it continue it; ✓ after the date marks it resolved and · sent sent (> **owner** 2026-10-27 ✓ · sent: text). The Doc draws an item's notes in a box of their own (author bold, date faint, the note colour on the border and author in the dark and light styles, none in mono). n markdown.note opens a new note line under the item after its notes, signed by Doc.Author, else the markdown.note-author setting (default owner), and dated today (Doc.Now); an untouched note writes nothing. x on a note row markdown.resolve toggles resolved; z a markdown.fold folds an item's notes to · N notes (the open ones; , M resolved after). Editing a sent note's text clears its sent marker. For a CLI: markdown.Notes(text) (ids ITEM.N: the item's place among list items and the note's under it), AddNote, ReplyNote, SetResolved, SetSent, SetFlags, FindItem, CleanNote (control characters dropped, NoteMax 4 KiB refused, never cut), ItemText, ItemPath, NoteElement.
  • markdown.Source{Load, Save} is the store. markdown.File(path) reads the file and writes it atomically (a temporary file beside it renamed over it, permissions kept, a symlink written through; markdown.WriteFile is the same write). A custom Source, such as a note in a database, gets the whole edited text from Save. Without a Save the Doc is read-only and every edit is refused. Doc.SetSource points a Doc at another source (a buffer named with save as).
  • Commit on change, no save key: typing is written after a pause (Doc.Debounce, default 400 ms, markdown.DebounceMsg) and when the element is left; every other edit at once. An edit rewrites only the bytes of what it changes (a toggle the box byte). Before writing, the Doc loads the source again; when it changed since the Doc read it, the element is found again by its text (and its place among elements with that text) and the edit is made on the new text; when it is gone, changed or ambiguous the edit is refused with an error wrapping markdown.ErrRefused (the footer status in the TUI, a non-zero exit on the CLI) and nothing is written. A write that fails while a block is open leaves the typed text in the unnamed register.
  • markdown.Tasks(text) lists the checklist items as goldmark parses them (GFM, the parser Glamour uses), each with Box (the byte offset of the box character), Checked, Text (its first line) and Line. Every list form (-, *, +, 1., 2)) and nesting count. A - [ ] in a code block, or outside a list item, is not a task.

How rendered lines map to elements: before rendering, each element's text gets a start marker and an end marker, single private-use runes (one per element, so wrapping cannot split one); a blockquote of notes is replaced by one placeholder quote line, which the Doc replaces with its note box. The Doc searches the rendered lines for the markers and strips them, giving a space back at the end of the line. An element covers the lines from its start marker to its end marker; one Glamour draws without text to carry a marker (a rule, an empty row) takes a line from the gap between its neighbours. The demo's Checklist page, the markdown o-story stories and o-md show it working.

Input modes

Every Input has a noun and two field settings, plus behavior all inputs share. Modes are names, not an enum; these are the ones the built-in editor enters.

Setting Value Effect
noun (Kind) editor (default) Vim editing. Starts in normal. i a I A o O enter insert; Escape returns to normal. v V Ctrl-V enter visual, visual-line, visual-block. Counts, operators, g, ", q and @ enter the pending modes count_pending, operator_pending, g_pending, register_pending, macro_pending, replay_pending.
noun (Kind) prompt Insert only. Starts in insert and mode.set keeps it there. Escape has no effect of its own, so a noun on the stack can bind it, as the picker binds cancel.
Kind "" or text Picker opens at the working directory or a supplied path and inserts at the cursor captured when it opened.
Kind path Picker opens at the whole value and replaces it.
Kind paths Forces Multiline. Picker opens at the current line and replaces only that line.
PathFilter any, file, dir Which picker results are eligible. Does not change editing keys.

Shared by every input regardless of these settings:

  • Readline keys in insert mode (Ctrl-A/E/B/F/P/N, Alt-B/F, Ctrl-W/U/K/Y, Ctrl-H/D). They are rows of the editor noun, the fallback of every stack; a row with mode: insert on any noun or instance on the stack, or a binding-file row on editor itself, overrides a key. There is no plain mode without them.
  • Ctrl-W kills one path component, treating / and whitespace as boundaries, in every input, not only path kinds.
  • One undo tree, named registers, shared yanks, and macros.

Mode resolution for key dispatch: the mode is global, like Vim's: Runtime.Mode is {Name, Payload} and mode.set (Runtime.SetMode) writes it. A pending mode is entered on it with a typed Payload (Count, OpCount, Op, Prefix, Request, Shadow) and leaves back to the base mode it replaced (Payload.Return, read as BaseMode()); a key-sequence prefix carries the count, so 3gg reaches line 3. A focused input is in the global mode; Input.Mode is the editing mode (normal, insert or a visual mode) the editor code reads, kept equal to the base mode while the input is focused. A focus change that leaves the current focus stop resets the mode to normal with an empty payload, or to the pinned mode of the newly focused noun (prompt pins insert, terminal pins terminal); a move inside one stop (a grid's cell to the next, a list's row to the next) keeps it, so a visual selection survives cursor moves. Closing a modal and reopening a page restore the mode they left. A component that follows the mode implements ModeObserver (ModeChanged(from, to)), called when the base mode changes while the focus is on or beneath it. A pending mode on a focus without an input still works for what needs no input: 2@a replays a macro from a page.

API and ownership

Every keyboard action dispatches a registered operation. The CLI is a generic NOUN VERB adapter over that same registry; it has no separate list of supported actions. operations list returns the current catalog and argument specifications.

cli.Mount maps every registered noun and operation into oden command groups and dispatch metadata, including parameter schemas. Oden owns contextual help rendering and command resolution: BIN NOUN and BIN NOUN -h/--help show that noun's verbs and schemas; BIN NOUN VERB --help describes only that operation. The adapter only maps the registry and delegates execution to cli.Shell. Help never enters Shell.Prepare, so it does not start a daemon or execute an operation.

r := ochazuke.NewRuntime()
input := ochazuke.NewInput("title", "Example", false)
if err := r.AddInput(input); err != nil { // Parent("app") and Kind("editor") by default
    panic(err)
}
if err := r.SetFocus("title"); err != nil {
    panic(err)
}
result, err := r.Execute(ochazuke.Request{
    Verb: "undo", Noun: "input", Target: "focused-input",
    Args: ochazuke.Arguments{"steps": 1},
})

Runtime is single-owner state and holds exactly: the operation registry (Operations, Modes, guards), the nouns and instances with their parent links, three plain interaction fields (Focus, Mode, Page), the chain of open modals (each modal instance's Payload.Return is the focus it opened over; read it through Modals, TopModal, ModalFocus, BaseFocus), the pending key-sequence state (Pending, BindingTimeout), Quit and the queue of off-loop commands the adapter drains. It holds no component fields: inputs, lists, pages, the picker and the registers are instances whose Component is the typed object, read through r.Input(id), r.List(id), r.Inputs(), r.Lists(), r.PageOf(id) and r.Registers(). In a running application, call it from Bubble Tea's update loop; external goroutines/processes use Call/CallMsg, not concurrent Execute calls. Request.Target names any instance: an input, a list, a list row, a page or a modal body; the request then runs in the context that addresses that instance (its ancestry, plus InputID for an input). focused-input means the current focus. ExecuteAt accepts a captured context for explicit originating-target dispatch. Input.SetMode is the mode transition hook; do not assign Input.Mode directly.

File Responsibility
runtime.go Registry (operation kinds), the global mode (Mode, SetMode, BaseMode, ModeObserver), ExecuteAt through noun actions and confirm, typed lookups, JSON inspection
focus.go Runtime.Focus, the modal chain (Push, OpenModal/WithInput, Pop), the mode rule on focus change, RemoveInstance, RetargetFocus, focus jumps (focus.next/focus.previous/focus.set, JumpFocus)
delivery.go The component delivery contract: Component, Ticket, Completion, Start, Deliver, PendingCommands; application verbs (Ctx, Result, Done/Failed/Pending)
confirm.go The confirm noun, SetConfirm, prompt templates
noun.go, instance.go Define builder, binding rows, key classes and guards; instances, parent links, Payload, targets
resolve.go, bindings.go The stack walk, pending modes, key-sequence prefixes and their timer, bindings.list
bindings/*.json, default_bindings.go, editor_keys.go The default key bindings as data (one file per area: the editor noun's mode tables, the app noun's count, register and macro rows, lists, tables, trees, timelines, forms, the terminal, ...), loaded at startup; LoadDefaultBindings, bindings defaults
configuration.go bindings.load, mode.register, context.select
input.go, editing.go, editor*.go Input-owned undo, the editor handlers (motions, operators, kills, insert, paste), visual geometry
list.go Flat selectable rows as instances of their nouns, marks and fan-out, stable-ID refresh, viewport scrolling, row focus
registers.go, macros.go, yank_history.go The registers component: yanks, named registers, history, active register, key recording/replay, history modal
palette.go, palette_parse.go, palette_editing.go, palette_nouns.go, palette_choices.go The ex-mode command line: its operations and rows, the grammar (verb/noun either order, prefixes, omitted noun, parameters), completions and filter codes, history recall, instance sources and defaults, parameter choices, originating context
palette_results.go Palette-only JSON object-array results: inferred frequency-ranked columns, read-only table modal, empty results
params.go Parameter choices: Choice, ParseChoices, Runtime.Choices, Enum/Source validation at execution
picker.go, picker_search.go Contextual path selection, asynchronous fuzzy indexing, native Git ignores
pages.go Pages as instances of kind page (AddPage, RegisterPage), Runtime.Page, remembered focus and mode, navigation, the page picker (page.pick), and focus isolation
chooser.go The chooser: a palette-shaped modal of rows (OpenChooser, ChooserSpec, ChooserRow), its ranking and its rows on the chooser noun
completion.go, completion_list.go Inline completion: an input's triggers and searches (Completions, NounBuilder.Completions, CompletionTrigger, Suggestion, SearchStrings), the token rule (FindCompletionToken) and the Completer, the suggestion list that answers as the input's inner level while typing keeps editing it
terminal.go The terminal noun: AddTerminal, TerminalStream, frames through the delivery contract, the leader contract, pane cursor
layout.go Layout.Shell, the one page chrome (title row with badge, tabs or subtitle, body, footer), panels, splits, sidebar
panes.go Resizable list/detail splits: the divider per page (Layout.Shift, read through Runtime.PaneLayout(page, layout), applied by Layout.Divide and every Split*), Runtime.PaneShift/SetPaneShift, pane.grow/pane.shrink {steps, page} and pane.reset {page}, the > < = rows guarded by pane-split, panes.json in the state directory
theme.go, surface.go Appearance operations, Lip Gloss styles, bounded modal composition on a Lip Gloss canvas
block.go Block and Pos: rendered text plus the focused input's cursor cell
model.go Bubble Tea adapter (delivers messages and drains commands through the runtime), renders through Layout.Shell, BindingHints, Bubbles viewports, modal renderers, terminal cursor
transport.go, cli.go, cmd/ochazuke Private Unix socket and generic CLI dispatch
examples/demo, examples/core, examples/full-project Runnable demo (demo.New()), noninteractive foundation example, configured application with list, modal, confirm and terminal pages
Nouns and instances

A Noun is the behaviour of one kind of thing: its verbs, a binding table keyed by mode ("" for every base mode), an optional pinned mode, and the action handlers that override an operation's default while the noun is on the stack. An Instance is one addressable thing on screen (ID, Noun, Parent, Payload for a modal body's data, optional Bindings of its own, Unfocusable to opt out of being a focus stop); it holds no mode; walking Parent links from the focused instance outward (Ancestry, InstancePath) is the stack keys resolve through and what Context.Path spells out. Declare with the builder and register instances with options:

r.Define("worktree").
    Verbs("open").
    Verb("remove", removeWorktree).                        // registers worktree.remove, or overrides it on this noun
    Action("selection.inspect", inspectWorktree).          // overrides another operation while a worktree is focused
    Mode("normal").
        Bind("o", ochazuke.Do("worktree.open")).
        Bind("d", ochazuke.Enter("operator_pending", ochazuke.Payload{Op: "d"})).
        Pattern(ochazuke.Digit, ochazuke.Do("count.append").FromPayload("digit", ochazuke.SourceDigit)).
        Bind("enter", ochazuke.Do("worktree.open")).When("self").
        Unbind("x")
r.Instance("sidebar", ochazuke.Parent("home"), ochazuke.Kind("panel"))
r.AddInput(ochazuke.NewInput("path", "", false), ochazuke.Parent("sidebar"), ochazuke.Kind("prompt"))

Runtime.Focus names the focused instance and is the source of truth; Context() is derived from it. Request.Target and context.select {instance} address any instance; ActionFor resolves noun overrides innermost first. A row that matched at an outer level addresses that level's instance when its operation is the level's own (d d → row.delete bound on the row noun runs on the row, and its confirm names it); shared operations a level binds (mode.set, input.*, focus.*) keep addressing the focus. ModeBuilder.Extends(parent) gives a mode table inheritance: Define("grid").Mode("visual-row").Extends("visual-block") reads its own rows, then the parent's, including rows bound on the parent later. state.get exposes /nouns, /instances/<id>/{noun,payload,parent}, /mode, /modeState, /modals, /inputs/<id>, /lists/<id>, /components/<id>, /jobs, /focus and /page.

Operation kinds and application verbs

Every operation has a Kind: interaction (the default; Run executes synchronously on the loop) or application. An application verb is declared with the same builder but an AppHandler:

r.Define("worktree").
    Verb("sync", func(c ochazuke.Ctx) ochazuke.Result {          // c.Instance is the worktree row: ID, Noun, Payload
        id := c.Instance.ID
        return ochazuke.Pending(func(ctx context.Context) (any, error) {
            return git.Fetch(ctx, id)                           // off the loop; ctx is cancelled if the row closes
        }).Then(func(c ochazuke.Ctx, v any, err error) {        // back on the loop, only while the job is current
            c.Runtime.List("worktrees").Select(id)
        })
    }).
    Verb("status", func(c ochazuke.Ctx) ochazuke.Result { return ochazuke.Done("clean") })

Done(v) and Failed(err) answer at once; Pending(work) returns a Ticket{Owner, Job, Slot} to the caller and schedules the work as a tea.Cmd. The framework never blocks the loop on application I/O. The palette, operations.list, the generated docs and the example's ? modal show the kind.

Jobs on one owner accumulate by default: five independent verbs on one row (fetch, commit, push...) all deliver, in finish order, each with its ticket. Pending(work).Replacing("preview") opts into replacement: a newer job in the same slot on the same owner cancels the older one, which is what a preview or a search wants. Pending(work).ReplacingAt(listID, "preview") scopes the slot to an ancestor instead: the newest job in (list, preview) cancels the older whichever row asked, so j p j p keeps one capture per list, while the job still belongs to the row (its completion and Then land there). The scope must be the owner or one of its ancestors; per owner remains the default (Replacing(slot) is ReplacingAt(owner, slot)). A job is dropped only when its owner instance closes or a newer job claims its slot, never because the user did something else meanwhile. A Then that removes its own owner (a kill verb dropping its row) is safe: the job is settled first and the completion is not offered to the gone component.

ticket = { owner: "repo-1", job: 42 }                      // accumulate: delivered whenever it finishes
ticket = { owner: "pane-7", job: 43, slot: "preview" }     // replace: job 44 in slot "preview" on pane-7 cancels 43
job    = { owner: "row-3", job: 45, slot: "preview", scope: "panes" } // ReplacingAt: job 46 from row-5 under panes cancels 45

jobs.list returns the jobs list, [{job, owner, verb, slot?, scope?, started, state, error?}] (scope is the slot's scope: the owner for Replacing, the ancestor for ReplacingAt) with state one of running, done, failed, cancelled; verb is the operation key for an application verb (sync.worktree), read for a terminal frame and the slot name for the picker. Finished entries stay in the list until a jobs.list read returns them (bounded to the last 32 meanwhile), so a status rail or toast reads each outcome once; state.get /jobs shows the running jobs only.

History eviction does not suppress completion delivery: a slow job still runs its Then callback and reaches its owning component even when newer jobs fill the retained history before it finishes.

Delivery contract

Work that completes later reports through one contract. A component is any Instance.Component implementing

type Component interface {
    HandleMessage(tea.Msg) bool // apply a result on the loop; true when it was for this component
    PendingCommand() tea.Cmd    // the work scheduled since the last drain
    Close()                     // cancel outstanding work, release what is held
}

Results carry their owner instance id, job id and slot (Completion{Ticket, Value, Err}); Runtime.Launch(owner, verb, result, ctx) starts a Pending result as a job and queues its tea.Cmd (Start(owner, work, then, ctx) is the accumulating shorthand), Runtime.Deliver(msg) settles a running job's completion and hands it to its owner, dropping a stale (cancelled) or orphaned one, and Runtime.PendingCommands() batches every component's pending work. A component that runs its own commands uses the ticket-level form: Issue(owner, verb, slot, ctx) returns the ticket and the work context, Accepts(ticket) asks whether the job is still running and Settle(ticket, err) records its outcome. Closing an instance (Pop, a row dropped by SetItems, removeInstance) cancels its jobs and calls Close. The Model drives components from the instance registry, so an application registers one by setting Instance.Component and nothing else. The picker is a component whose index, search and preview are three replacing slots on the picker instance; the registers are one too (its only cancellable work is a recording in progress); a terminal's frame reads are one replacing read slot.

Per-verb confirm

Confirmation is a property of a verb, declared once rather than at each call site:

r.Define("worktree").Verb("remove", removeWorktree).Confirm("Remove {{id}}?")

It compiles to the framework confirm noun: running worktree.remove opens a confirm modal instead, a modal instance parented at the focus with Payload{Prompt, Request}; y (or Enter) runs the stored request in its original context, n or esc pops, every other key is swallowed. Templates expand {{id}}, {{noun}}, {{label}} (a label argument on the request, else the instance's Label, a list row's label, else its id) and {{<arg>}}. A request that addresses no instance of the verb's noun (no target, none on the focus path: worktree remove /src/a on the command line) reads the request instead of naming app: {{noun}} is the verb's noun, {{id}}, {{target}} and {{label}} the argument of that name, else the first argument in parameter order (Operation.ParameterOrder, positional first). A request without a target addresses the nearest instance of its operation's noun on the focus path (row.delete chosen in the palette while a cell is focused acts for the row, so its prompt names the row); a binding row's own request keeps the row rule (the level it matched at, else the focus). A verb guarded off where it would act is refused (<noun.verb> is not applicable now) from every caller, and its rows drop out of bindings list and the hints. confirm.set {operation, prompt} adds or removes a prompt at run time (an empty prompt removes it; the full-project example sets one from actions.json), and a request with "confirmed": true skips the prompt for scripts.

Local checks:

go test ./...
go test -race ./...
go run ./examples/core

The integrated implementation was checked locally on macOS with the test suite and live terminal/CLI sessions. Checks covered retained page edits and scroll, page-specific bindings, blocked navigation under modals, nested restoration, palette-driven page navigation, live appearance changes, and a narrow 56×18 modal. Regression tests and a live rapid-input check cover Bubble Tea's coalesced typing events without treating bracketed paste as commands.

The editor/picker expansion also passed the full suite and race detector. Live terminal/CLI checks exercised all three visual shapes, cross-field block paste and undo, named registers, counted macro replay, history selection without insertion, o/O, slash-delimited kills, contextual path replacement, recursive fuzzy ignore-aware picking, and a 48×15 layout.

Requirements

  • General UI infrastructure with a stack of stateful modals.
  • First-class pages retaining their interaction state across navigation.
  • Styled partial-screen modal overlays that leave the preceding screen visible.
  • Pushing a modal preserves the previous modal's state; popping restores the previous interaction exactly.
  • Vim-style interaction with arbitrary, application-defined modes.
  • Customizable keybindings scoped to modes, components, or the application, plus global bindings.
  • Public operations available through an API and CLI tools, including a central CLI with subcommands.
  • JSON export of application state and individually addressable public values.
  • Ex-mode command line over the verbs and nouns: either word first, prefixes, completion of the next word.

Stateful modal stack

Preserve modal instances rather than destroying and reconstructing them. Each frame owns its interaction state: mode, focused field, draft values, cursor, selection, and scroll position.

Illustrative state (JavaScript notation, not a Go API contract):

{
  id: "edit-task",
  component: "task-editor",
  mode: "insert",
  focusedField: "description",
  values: {
    title: "Build UI framework",
    description: "Preserve nested modal state"
  },
  cursor: { row: 1, column: 12 },
  selection: null,
  scroll: { x: 0, y: 3 }
}

Opening a directory picker opens another modal. Closing it restores the editor's interaction state. A child can return a result to its parent, such as a selected directory.

In the Go API there are no frames: Runtime.Focus is the one focus and an open modal is an instance on the modal chain. Push(id, focus) opens modal id focused on focus and stores the focus (and mode) it opened over in the modal's Payload.Return; Pop restores both, falling back to the nearest surviving ancestor (ending at the page) when that focus was removed. An id that is not already an instance becomes one of the modal noun for as long as it is open, so "Escape pops a modal" is that noun's normal-mode esc row and a modal body may carry rows of its own. OpenModal(kind, Payload{...}, opts...) is the payload-carrying form: it creates a body instance of noun kind parented at the current focus, with the payload (Prompt, a stored Request, ...) the kind's rows read through FromPayload and request.run, and removes it (with what it holds) on Pop after closing its component. WithInput(in, Kind("prompt")) puts an input inside the body and focuses it, so a one-field prompt is one call; the *Input keeps its text for the next open. The confirm noun is built this way. RemoveInstance(id) closes and drops an instance with its descendants and moves every focus reference into it to the nearest surviving ancestor. Cursor, drafts, selection and undo live on the instances; the mode is global; scroll offsets live on the Model; application data lives with the application.

Focus moves between focus stops: instances of nouns declared Focusable() (list, grid, palette, terminal) and every input, unless the instance sets Unfocusable. focus.next and focus.previous {steps, scope} walk the top modal (else the page) in drawing order (the order the page's last frame drew its stops: List.View, InputBlock, Terminal.ViewBlock, grid.View and Runtime.Placed(id) record it; stops no frame drew follow in tree order), without descending into a stop (a grid is one stop, not its cells), wrap, and land on the stop's remembered child (its FocusTarget, such as the list's selected row, else the focus last held inside). Scope region jumps between instances of nouns declared Region() (panels, never stops themselves). The app noun binds } { and tab shift+tab to stops and ) ( to regions in normal mode, all taking a count; an inner level (a multiline editor's insert table, a palette field) binds its own. focus.set {id} is how handlers move focus. On a page, list.move (j k down up) past a list's last row moves the focus to the next stop in drawing order (a list on its first row), before its first row to the previous stop (on its last row), and stays put at the page's ends; AddList(..., NoSpill()) keeps a list's motions inside it, and lists in modals never spill. Focus never rests on what the last frame did not draw: after the Model draws the page, a focus whose stop the frame did not place (a tab switched away from it, a collapsed panel; a list row counts as drawn when its list is) moves to its nearest ancestor below the page that was drawn, else to the page's first stop in drawing order, else to the page, and the page is drawn again before the footer, so the hints and the next key are what is on screen. It is a plain focus change (the mode rule applies; } goes back once the stop is drawn again) and waits while a modal is open, before the first frame and after a frame that placed no stop (UX-RULES R3.13).

Invariants:

  • Only the focus (inside the top modal while one is open) receives keyboard input.
  • Covered modals retain local editing state.
  • Background results target the originating instance, not whichever modal is currently on top.
  • Shared application data is separate from modal-local state. Pop restores an interaction, not a stale copy of the entire database.
  • In-memory preservation and persistence across process restarts are separate capabilities. Restart persistence is not yet specified.

Extensible interaction modes

Modal windows and editing modes are different concepts. A modal is an instance on the modal chain; the mode is one global value that determines input behavior.

{
  modes: ["normal", "insert", "visual", "command", "directory-search"],
  focus: "edit-task-field",
  mode: { name: "insert", payload: {} },
  modals: ["edit-task"]           // edit-task.payload.return = "task-list"
}

Applications and extensions register mode names (mode.register {name, pending?} or DefineMode); the framework does not restrict them to a fixed enum. A base mode reads a noun's mode table and its any-mode table; a pending mode reads only its own table and is left by the key that answers it (see Scoped, customizable keybindings). Rows can be bound inside any mode, pending ones included, on any noun or instance.

Text fields, single-line text inputs, and multiline text areas support the basic Vim motions and Readline bindings listed above. Normal mode interprets Vim keys; insert mode interprets Readline keys. Full Vim compatibility is not implied.

Input-local editing and undo

Each text input owns its editing state and undo tree. Histories are independent between fields and remain part of preserved modal-local state. An insert session is one undo change, including kills/yanks and open-line creation. Editing after undo creates a new branch without deleting alternatives. input.redo accepts optional steps and a zero-based branch index for its first step; omitted branch selects the newest child. API edits outside insert mode create separate changes.

Inputs expose editing verbs through the shared operation registry. In particular, undo accepts an optional number of steps, defaulting to one change. The input handles its own history; outside callers do not manipulate the tree.

The application supports dispatching an editing operation to the focused input without requiring callers to know its identity or implementation. JSON requests:

[
  { target: "focused-input", verb: "undo", noun: "input" },
  { target: "focused-input", verb: "undo", noun: "input", args: { steps: 3 } }
]

The first request undoes the focused input's last change; the second requests three undo steps from that same input's history. These operations are available through the shared API and CLI as well as UI bindings. Normal/insert modes determine keyboard interpretation, not whether an external caller can invoke the input's editing verbs.

Shared yanks, registers, and macros

Selection and undo belong to each input. Yanks, history, registers, the active register and macro state belong to the registers component, one instance under app (r.Registers() returns it: Named, History, Active, Macro), so a yank from notes can be pasted into paths or a modal editor. It is reached through the register, macro and yank-history operations, never through Runtime fields.

{
  registers: {
    '"': { yank: { text: "ab\nef", shape: "block", width: 2 } },
    a: { yank: { text: "alpha", shape: "char" } }
  },
  yankHistory: [
    { text: "ab\nef", shape: "block", width: 2 },
    { text: "alpha", shape: "char" }
  ]
}
  • v, V, and Ctrl-V select characters, whole lines, and display-cell rectangles. Motions extend selections; y/d/c/p yank/delete/change/replace. Escape cancels selection. Normal yy/dd/cc act on whole lines; motion operators and counts work too.
  • p/P preserve the yank's shape. Block operations handle ragged rows and wide glyphs; block change repeats a contiguous first-row insertion across selected rows when leaving insert mode. Each block edit is one undo step. Single-line inputs reject line/block-shaped pastes rather than flattening them.
  • "a selects register a for the next operation; "A appends. The unnamed register is shared; 0 holds the last yank, 1–9 hold line/multiline deletions, and - holds small deletions. "_ discards without changing shared yanks or history.
  • Normal Ctrl-Y or yank-history.open opens history. j/k or arrows highlight an entry; Enter makes it the unnamed yank and closes without inserting. Then use p/P or insert-mode Ctrl-Y to paste. Escape cancels. History keeps the newest 100 entries, including duplicates.
  • qa … q records into register a; qA appends. @a replays, 3@a repeats three times, and @@ repeats the last macro. Mode changes, bindings, focus changes, and modal/palette interactions use the same dispatcher as live keys. Register yanks and macro recordings share storage: replacing one replaces the other.
  • Macro replay is bounded to 16 nested calls and 10,000 dispatched keys. Errors are reported, not swallowed; earlier edits remain undoable rather than being rolled back as a transaction. Recording is the macro_recording shadow mode on the app noun: each key is appended, then dispatched again in the mode it shadowed; replay runs at depth and is not re-recorded. The q, @, " and count rows are app rows too (registers_bindings.json), so a page or list focus records, replays and counts without an input.
/tmp/ochazuke yank-history open
/tmp/ochazuke yank-history select --json '{"index":0}'
/tmp/ochazuke register get --json '{"name":"a"}'
/tmp/ochazuke register set --json '{"name":"a","text":"shared","shape":"char"}'
/tmp/ochazuke input paste --json '{"register":"a"}'
/tmp/ochazuke macro replay --json '{"register":"a","steps":2}'

input.yank captures a selection/current lines; input.paste inserts a stored yank. Insert Ctrl-W/U/K publish shared kills and Ctrl-Y pastes the unnamed text literally. These operations use the same registry as the keyboard, palette, and CLI. All this state is in memory.

Scoped, customizable keybindings

Bindings are rows of mode tables. A row names the noun (or one instance) it belongs to, the mode table, an exact key or key sequence or a key class, and the request it runs; the shape is identical in Go, in bindings list output and in a binding file:

[
  { noun: "directory", mode: "normal", keys: "U",
    request: { verb: "undo", noun: "input", args: { steps: 3 } } },
  { noun: "editor", mode: "insert", keys: "esc", request: null },          // unbind a default
  { instance: "notes", mode: "normal", keys: "g s",
    request: { verb: "summarize", noun: "workspace" } },
  { noun: "editor", mode: "macro_pending", pattern: "alpha",
    request: { verb: "start", noun: "macro" }, from: { register: "key" } },
  { noun: "editor", mode: "insert", keys: "enter", when: "multiline",
    request: { verb: "insert", noun: "input", args: { text: "\n" } } }
]

bindings list --all returns every stored noun and instance binding row across every mode, including inactive contexts, guarded rows, and bindings shadowed at the current focus. Noun names, their mode tables, and instance IDs have deterministic order; row order within each table is preserved. The result is a JSON array in the format accepted by bindings load, not an operation catalog or a full application snapshot. Operations, guards, instances, mode definitions, and mode inheritance must already exist in the receiving application. Without --all, the operation still lists only the rows the current focus and mode answer to. The socket CLI accepts ochazuke bindings list --all or --json '{"all":true}'.

bindings stops is the app-wide effective keyboard: one entry per focus stop, {page, stop, focus, mode, bindings}, the rows bindings list would print with the focus on that stop. The order is fixed: the pages in tab order, each page's stops in drawing order (as } walks them), the open modals' stops, then app in normal mode. Each stop is read at the focus a jump lands on and in the mode the focus would have there (the current mode at the current focus, else the stop's pinned mode, else normal); nothing moves. The two differ in kind: --all exports what is stored (every table, every mode, shadowed and inactive rows included, loadable with bindings load), stops reports what answers (after shadowing, guards and the modal boundary), per place.

Patterns are digit, alpha, printable, paste, any, a class minus keys (alpha-q) or a set (one-of:h j k l). from fills an argument from the key (key, digit, text) or the mode payload (count, line, steps, op, opcount, prefix); several sources separated by spaces ("keys": "prefix key", Join in Go) join their texts with a space. when names a guard (input, no-input, multiline, count, no-count, self, shadow-idle, op:<key>, or one the application registers with DefineGuard). A verb takes a guard too: Define(noun).Verb(verb, h).When(guard) (Noun.Guards, Runtime.VerbGuard, Runtime.VerbShown) keeps the palette from offering the verb while the guard is false at the focus and mode the palette opened over; a palette noun's default verb is never guarded (RegisterNoun refuses one). The palette lists the nouns of that focus stack first, the focused noun first. A row's identity is its noun or instance, mode, keys or pattern, and when: a row with the same identity replaces the default in place, a new one adds (a new guarded row goes before the unguarded row on its keys, so it answers while its guard holds), request: null unbinds the row with the same identity. A row without when therefore never replaces or unbinds a guarded row on the same keys: give it the guarded row's when (bindings list --all shows every row's guard); the bindings.json loader reports a row a guarded default still answers before as a notice. The input guard the framework adds to the editor's input.* rows (ImplicitGuard) is not part of the identity, so {noun: "editor", mode: "normal", keys: "x", request: null} unbinds x, and an editor row of the user's that runs an input.* operation gets the same guard. ModeBuilder.Unbind removes the unguarded row, UnbindGuarded(keys, guard) the guarded one. Load a file of rows with bindings.load {path} or Runtime.LoadBindings; Runtime.Bind installs rows from Go and the Define(...).Mode(...).Bind/Pattern/Unbind builder writes the same tables.

The resolver is one stack walk. For each level from the focused instance outward to app (the modal noun's level sits just inside app while a modal is open) and then the editor noun as the fallback every stack ends with:

for (inst of stack(focus)) {
  table = rows(inst) + nouns[inst.noun].bindings[mode] + nouns[inst.noun].bindings[""]   // the last only in a base mode
  hit = table[key] ?? table[classOf(key)] ?? table.any
  if (hit) return hit(key, mode.payload, inst)
}

Within one level the instance's own rows sit above its noun's, the mode table above the any-mode table, and an exact key beats a class beats any; between levels the inner one wins. Key sequences are the exception (Vim typeahead): a sequence extending the keys typed so far at any level, inner or outer, makes the walk wait, with the innermost exact match as the fallback (inner x with outer x x: x x runs the outer row, x alone runs the inner one after the timeout); a level answering with a key class (a terminal's any) ends that scan. A row whose verb is guarded off at the focus is skipped and the walk goes on. Application rows therefore beat the editor's defaults for a fresh key, while a pending mode (an operator, a count, a register, a macro prefix, a key sequence) is exclusive: only that mode's tables answer, so dw reaches the editor even when the app binds w. A pending mode is one-shot: the key that answers it consumes it before the request runs, so d then w leaves operator_pending and the operator's own request decides the next mode; a row whose request is mode.enter or count.append stays or moves on. Every pending condition is such a mode with a small typed payload:

normal:           { i: mode.set insert, d: enter operator_pending {op: "d"}, g: enter g_pending, "<digit-0>": enter count_pending {count: digit}, q: enter macro_pending, '"': enter register_pending }
count_pending:    { "<digit>": count.append, ...normal rows with steps from the count, esc: cancel, any: leave and re-dispatch }
operator_pending: { w: operator {motion: "word-forward", steps}, d: operator {motion: "line"} when op:d, g: enter operator_g_pending, any: cancel }
g_pending:        { g: move first-line {line: count}, any: cancel }
macro_pending:    { any: macro.start {register: key}, esc: cancel }
macro_recording:  { q: macro.stop when shadow-idle, any | paste: macro.record {key} = macro.append then key.dispatch {mode: "shadow"} }
"normal g":       { "g g": … the continuations of the sequence; a timer leaves the mode and runs the completed match }

The editor noun's tables (Readline rows in insert, Vim rows in normal and the visual modes, the pending modes above) are bindings/editor.json, one of the embedded default files (see Default bindings are data). bindings list prints the rows the focus answers to in its current mode, so every Readline and Vim key appears there for its mode, and the help modal of the full-project example is built from it. Multi-key sequences such as g g wait in a prefix mode named "<mode> <keys so far>" on the focused instance for up to a configurable timeout (default 1000ms, Neovim's timeoutlen); the timer and a mismatching key leave the mode, run the longest completed match, and the mismatching key is then dispatched fresh. Escape cancels the sequence through a row like any other: every table that binds a sequence keeps a generated "<prefix> esc" → mode.leave row for each prefix (bindings list shows it in the prefix mode), so the resolver itself never names a key. A paste is one event that matches only paste rows, so insert mode inserts it and normal mode ignores it.

Capture rows

The walk above bubbles: the inner level wins. A row may set capture: true (Captured() in Go) to be consulted in a capture pass before it, outer-first (app first, the editor fallback last), for the few app-level keys an inner component must not swallow. The first capture row that answers wins; bindings list shows capture rows first.

{ noun: "app", mode: "", keys: "ctrl+c", capture: true, when: "no-modal", request: { verb: "quit", noun: "app" } }

That row is the only default capture row: NewModel installs it (with the bubbling ctrl+c row it always had) so an inner any row cannot swallow quit. The no-modal guard stands it aside while a modal is open, so the palette's own ctrl+c still closes the palette. A passthrough mode (DefinePassthrough, the terminal mode) has no capture pass at all: while it is the base mode, ctrl+c goes to the attached pane, and quitting means detaching first (ctrl+a d) or a leader row bound to app.quit. To rebind ctrl+c, bind it on app before NewModel (then neither row is installed), or override or unbind each row on its own: an unbind or rebind without when touches the bubbling row only (the loader's notice says the capture row still answers first), and {noun: "app", keys: "ctrl+c", when: "no-modal", request: null} removes the capture row.

Default bindings are data

Every default key is written once, in an embedded file in the grouped bindings format (see The bindings file format): the framework's in bindings/*.json, one per area (editor.json, registers.json, list.json, table.json, list-modes.json, tree.json, timeline.json, form.json, terminal.json, app.json, palette.json, picker.json, ...), the markdown, grid and Commands nouns' in markdown/, grid/ and cli/bindings.json. A group may also carry "fallback": true: its rows are skipped on the keys its table bound before the file loaded (the app noun's :, ?, ctrl+c: an application's row bound before NewModel keeps its key).

An application ships its defaults the same way:

//go:embed bindings/*.json
var defaultBindings embed.FS

// after the verbs and guards the files name are registered, before Model.Init
if err := r.LoadDefaultBindingsFS(defaultBindings, "bindings/*.json"); err != nil {
    return err
}

The rows go into the default layer exactly where the same rows bound in Go would sit (a row replaces the row with its identity, a guarded row goes before the unguarded row on its keys, sequences get their prefix cancels), below the user's bindings.json. Unlike a user file, an embedded file is strict: an unknown operation, argument, source, guard, mode or instance is an error naming the file, the table and the key (MustLoadDefaultBindings panics), so a test that builds the app catches it. Runtime.CheckDefaultBindings(source, data) is the rule check for a test: every name exists, every mode.enter goes into a pending mode, no two rows of a table share a key and guard.

BIN bindings defaults (ochazuke.bindings.defaults, Runtime.DefaultBindings) prints the default layer as a bindings file, without the user's rows and with the rows they replaced back in place; every group carries "source": the embedded file (ochazuke/bindings/list.json, an app's own path), go for a row bound in Go (an instance's rows, a row computed from a spec) or generated for a sequence's prefix cancel. Go Bind stays for rows that are computed at runtime.

Atomic bindings

A binding row is atomic. A row is a Request (one registered operation, its arguments from the row, the key or the mode payload), a mode transition (Enter, Leave) or null (an unbind). There is no row that runs Go code with the key: Binding has no such field, and TestBindingRowsAreAtomic validates every table of the framework, the model, the picker, the demo and the full-project example (Runtime.ValidateBindings) and parses the resolver to check it names no key. The only code that compares key names is the key classifier (printableKey) and one key-name-to-text table (keyText).

The rule is recommended to any application built on the framework that wants to stay extensible and scriptable. Because every row is a Request, every key the user can press is also a CLI command (NOUN VERB --json), a palette entry, a macro step, a config override (the same row shape in a binding file replaces or unbinds it) and a help line (bindings list, the footer, the help modal), with no hidden branch in Go that a script cannot reach. Composite behavior is an operation that calls operations, so a script or an agent can call the pieces on their own; a condition is a named guard, so a config row can carry it ("when": "shadow-idle") and bindings list can show it.

The macro recorder's q row, before and after:

// Before: a Func row that reads the mode and the key in Go. Not a CLI command,
// not a palette entry, invisible to bindings list and to a binding file.
Bind("q", Func(func(r *Runtime, ev KeyEvent) error {
	shadow := ev.Mode.Payload.Shadow
	if shadow.Name == "normal" && shadow.Payload.empty() {
		_, err := r.Execute(Request{Verb: "stop", Noun: "macro"})
		return err
	}
	return record(r, ev)
}))

// After: two atomic rows. q stops under a named guard and falls through to
// the any row otherwise; recording is an operation that calls two operations.
Bind("q", Do("macro.stop")).When("shadow-idle").
Pattern(Any, Do("macro.record").FromKey("key"))      // macro.append {key}, then key.dispatch {key, mode: "shadow"}

The same row in a binding file is {"noun": "app", "mode": "macro_recording", "keys": "q", "when": "shadow-idle", "request": {"verb": "stop", "noun": "macro"}}, and ochazuke macro record --json '{"key": "j"}' is the recorder's step from the command line.

Verb/noun command line

The palette's vocabulary is the registry's verbs and nouns, not a flat list of complete commands. One command line accepts either order:

  • A verb first, then a noun that supports it (or no noun: the focus path supplies it).
  • A noun first, then one of its verbs (or one of its instances, for a noun with a source).
  • Both orders name the same registered operation; the order does not change its meaning.

Illustrative relationships (JavaScript notation, not a Go API contract):

{
  operations: [
    { verb: "create", noun: "task" },
    { verb: "edit", noun: "task" },
    { verb: "edit", noun: "project" }
  ],
  firstWord: { verbs: ["create", "edit"], nouns: ["task", "project"] },
  "edit ":    { nextNouns: ["task", "project"] },   // the complement of the verb
  "task ":    { nextVerbs: ["create", "edit"] },    // the complement of the noun
  "edit task": { key: "task.edit" },
  "task edit": { key: "task.edit" }
}

Both completions derive from the shared operation registry, so they expose the same supported verb/noun pairs.

Shared command and query API

One registry describes supported operations, validated argument types/requiredness, and result descriptions. Commands mutate state; queries inspect it. Unknown operations, unknown arguments, and invalid argument types are rejected before handler execution.

An operation key is noun.verb (input.undo, worktree.remove): the same string the CLI spells NOUN VERB, the same order as Define(noun).Verb(verb), and ownership reads as a path, so a catalog sorted by key groups by noun. Rows written in the earlier verb.noun spelling are rewritten on load for one release and reported by ValidateBindings (two-word keys only: a longer key is never swapped).

Noun namespaces

A noun may be dotted (ochazuke.settings, ai.skills), and so may a verb (dirs.base.add): the action table is flat, a key is a name looked up whole, and a registered action carries its own noun and verb. A name nothing is registered under splits on its last dot (ochazuke.SplitKey: ochazuke.settings.set is noun ochazuke.settings, verb set). See "Action aliases" below. The framework's own nouns live in the ochazuke namespace (FrameworkNamespace): everything NewRuntime, NewModel and the framework constructors register (inside Runtime.Builtin) is ochazuke.<noun> (ochazuke.list, ochazuke.settings, ochazuke.page; app itself stays app, so the app noun's verbs (app.quit, an application's own app.state) never collide with a framework noun such as ochazuke.state), with the short name as an alias (Runtime.Alias, Runtime.Aliases). So:

  • Input accepts either name. Do("list.move"), Request{Noun: "settings"}, a bindings.json or actions.json row naming list, a palette word (settings names or ochazuke.settings names, one word each) and the CLI (bin settings list, bin ochazuke settings list, bin ochazuke.settings list) all reach ochazuke.list / ochazuke.settings (Runtime.CanonicalNoun, CanonicalKey, Operation(key), NounWords).
  • Output is canonical. History (and history.jsonl, which reads old short-named lines as canonical), bindings.list, operations.list, the snapshot and the catalog print ochazuke.settings.set; the footer's hints, the ? help, the jobs ticker and confirm prompts draw the shortest name that reaches the noun (DisplayNoun, DisplayKey: list.move).
  • Define extends, DefineOwn claims. Define("terminal").Verb("copy", …) (or Register with noun terminal, or Kind("prompt")) adds to the framework's ochazuke.terminal. An application with a noun of its own under a framework name declares it with DefineOwn("settings"): from then on settings (keys, files, palette, CLI) is the application's noun and the framework's stays reachable as ochazuke.settings, its verbs intact; nothing is deleted to make room. A noun the application registers under the short name before the framework registers its alias (an instance made before NewModel) owns the name the same way.
  • The v0.16.0 spelling still loads, for one release. v0.16.0 named the framework's nouns app.<noun>; app.settings, app.list.move, bin app settings list and binding, actions and history files holding them are read as ochazuke.<noun> (deprecated) unless a noun of that exact name exists or app has a verb of that name (an application's app.state verb keeps app.state). History reads them back canonical.
  • No overlap. A noun x and a noun x.y coexist, but a verb y on x beside a noun x.y (or the reverse) is refused when registered.
  • Extensions in a host. A host mounting an extension's catalog names its nouns under the extension (cli.Namespace("ai") as PageConfig.Noun: skills is the host's ai.skills; cli.Local reads it back), the namespace taken from the binary with cli.ExtensionNamespace(bin) (its base name lower-cased without a leading o-: o-ai is ai, o-qbt is qbt, mtm-mermaid stays; an error when that is not one noun word or is ochazuke, app or a framework noun's short name), so every host mounts an extension under the same name and two extensions and the host never clash; the extension's own binary keeps its short names. The catalog publishes the active aliases (CatalogDoc.Aliases, CatalogDoc.CanonicalNoun).
  • A noun's row source. A host's list of a catalog noun with no source loads the noun's row source (cli.CatalogDoc.RowSource): its list verb when that declares the noun's own rows (NounBuilder.Rows), else another verb declaring the noun's own rows, else a list declaring no rows (its answer is the noun's own rows), and only then a verb declaring another noun's rows (mtm's store.tables); inside a step, sorted order. A verb listing something else (slack-channel.history, the channel's messages) can declare its rows without taking the noun's list over.

Public user actions and registered state are the compatibility surface. Implementation helpers and handler functions are not remotely callable code.

The same registered operation is accessible through:

  • The Go API.
  • Keybindings.
  • The command palette.
  • A central CLI with subcommands.
  • Optional individual CLI executables delegating to the same interface.

Live CLI examples:

/tmp/ochazuke modal push --json '{"id":"draft","input":"dialog"}'
/tmp/ochazuke modal pop
/tmp/ochazuke mode set --json '{"mode":"insert"}'
/tmp/ochazuke bindings list
/tmp/ochazuke state get --json '{"path":"/modals"}'
/tmp/ochazuke state get --json '{"path":"/inputs/directory/text"}'
/tmp/ochazuke mode register --json '{"name":"directory-search"}'

JSON inspection covers registered pages (page, pages), inputs and histories, the focus and the open modals (focus, modals, each modal instance's payload.return), the global mode (mode, modeState) and the registered modes (pending modes included), nouns with their tables, instances with their payload, and the rows the focus answers to (bindings). state.get accepts a JSON Pointer ("" for the full state; ~1 escapes / and ~0 escapes ~ in a key). Appearance has its own appearance.get query. All registered inputs are public state: do not register secrets without adding an explicit export policy. Handlers, renderers, and terminal handles are not serialized.

Telling the user: notices and status

A handler says what to tell the user and the host decides how it looks. There are three things to say:

  • ctx.Notify(text) posts a persistent notice and returns its NoticeID; ctx.ClearNotice(id) takes it back. It stays until cleared.
  • ctx.Info(text) sets the status line to a plain message ("copied 3 rows", "queued"): drawn before the hints until the next key, a status: line on the CLI.
  • ctx.Error(text) sets the status line to an error: Error: TEXT in the error style in place of the hints until the next key, an error: line on the CLI. A verb that fails returns Failed(err) (drawn the same way); Error is for what fails beside the result (a job's Then, a refresh, a daemon event).

The status line is one slot: Info and Error replace each other and "" clears it. Choose by what happened, never by habit: a failure is Error, anything else (progress, success, "nothing to do", a hint) is Info. ctx.Status(text), the older spelling of Error, is deprecated: the name read as "any status", so plain messages posted through it were drawn Error: …. It works for one release; strictcheck.Check(t) (below) fails an app's tests on any use. Code without a Ctx (setup, a daemon event) posts the same through r.UI(): Notify, ClearNotice, Info, Error. A job's Then already gets a Ctx.

r.Define("worktree").Verb("sync", func(ctx ochazuke.Ctx) ochazuke.Result {
	id := ctx.Notify("syncing " + ctx.Instance.Label)
	return ochazuke.Pending(sync).Then(func(ctx ochazuke.Ctx, _ any, err error) {
		ctx.ClearNotice(id)
		if err != nil {
			ctx.Error("sync failed: " + err.Error())
			return
		}
		ctx.Info("synced")
	})
}, "Sync the worktree")

Each host attaches a sink (ochazuke.UISink: Notify(id, text), ClearNotice(id), Status(text) for the error tone, Info(text) for the plain one; every sink has both, so no message falls back to the error tone, and a sink that wraps another forwards each message with UIMessage.Apply(next); InfoSink is deprecated) with r.UI().Attach(sink):

Host Shows
NewModel notices in the footer (Model.Notices, notice.list, notice.clear); the status line: an error (Model.Status) as Error: TEXT in place of the hints, a plain message (Model.Info) before the hints cut to half the width; the next key clears either, what a key's handler sets stands
cli.Shell notice: TEXT, error: TEXT (Error) and status: TEXT (Info) lines on stderr beside the warning: lines, while the command runs (a job's Then included); UIMessage.Line
A socket or API call (HandleCall) the call's messages in Response.Messages ([{kind, id, text}], kinds notice, clear, status (UIError, the error tone; UIStatus is its deprecated name) and info, omitted when empty) as well as the runtime's sink; CallResponse returns them and ochazuke NOUN VERB prints them on stderr as the shell does
Tests ochazuke.RecordingSink (Messages, Notices, LastError, LastInfo; LastStatus is deprecated)

Deprecated names and the strict check

Every name ochazuke deprecates lives in deprecated.go, built only without the ochazuke_strict tag: it keeps working for one release, then goes. An application adds one test and its suite fails on any use, at the line, with the compiler's message (type-checked, so a Status method of the app's own types is never flagged):

import "github.com/unixsuperhero/ochazuke/strictcheck"

func TestNoDeprecatedOchazuke(t *testing.T) { strictcheck.Check(t) }

Check runs go vet -tags ochazuke_strict ./... from the module root (pass patterns to narrow it), so a vet finding fails it too. Deprecated now: Ctx.Status and UI.Status (use Error for a failure, Info for anything else), UIStatus (UIError), InfoSink (Info is on UISink), RecordingSink.LastStatus (LastError).

With no sink attached messages are discarded, except that until the first sink attaches the UI keeps the last 32 (the startup ring) and replays them, in order, into that sink, then drops them; memory stays bounded whether or not a sink ever attaches. The UI is a component on an instance under app; Runtime has no field for it. Model.Notify(text) still works and posts through the same path.

Tree queries

The instance tree is queried with predicate values, never a selector string. A query is a list of predicates, ANDed; the result is in tree order (app first, depth-first, siblings in the order they were added).

// Go: r.Query(ByNoun("worktree"), Within("sidebar"), Not(Where("marked", "")))
instances.query({ where: [["noun", "worktree"], ["within", "sidebar"], ["not", [["marked"]]]] })
// → [{ id: "wt1", noun: "worktree", parent: "sidebar" }, ...]

Predicates: noun, id, kind (the component an instance fronts, lowercased: input, list, page, terminal; row for a list row, modal for a modal body, else instance), within (any ancestor), child-of (the direct parent), focusable, region, state (the global mode), and the combinators any, all, not over nested lists. A predicate is Predicate{Name, Arg, Of}; an application adds its own with RegisterPredicate(name, func(inst, r, arg) bool) and the CLI can use it at once.

Action history and dot-repeat

Every executed request is one entry in the action history, with a kind:

{ seq: 42, at: "2026-10-24T10:00:00Z", kind: "change",                          // action, change or fetch
  request: { verb: "operator", noun: "input", args: { operator: "d", motion: "word-forward", steps: 1 } },
  context: { focus: "editor", page: "main", mode: "normal", source: "key:w" },   // or "macro:a", "palette", "cli", "api"
  result: { ok: true },                                                         // or { ok: false, error: "..." }
  moved: { from: { focus: "row-a", page: "main", mode: "normal" },              // when the request moved the focus, page or mode
           to:   { focus: "row-b", page: "main", mode: "normal" } },
  job: 7 }                                                                      // when an application verb launched work

An entry is the user action, not the data: the value the operation returned stays with the caller (the CLI prints it) and is never encoded into the history, so j over a list of ten thousand rows records the same small entry as over ten. A move is the ids it went between (moved).

Kind What Examples
action The user's interaction: motions, focus, page and modal moves, modes, the palette's own keys list.move, focus.next, page.open, modal.pop, mode.enter
change Edits and verbs that change something; dot-repeat reads these input.delete, settings.set, an application verb (worktree.remove)
fetch Requests for data: state, list and show verbs, reloads, a host calling an extension state.get, jobs.list, bindings.list, a verb declaring Rows or State
system Reserved for what the runtime does on its own (a resize, a finished job); nothing records it yet

The kind comes from the operation (Operation.HistoryKindOf): its HistoryKind when set (NounBuilder.Fetch() sets fetch; actions.json and the catalog carry it as history), else fetch for a verb declaring Rows or State, change for a Repeatable one, action for a Motion, change for any other application verb (an extension verb that declares nothing may change something, so it is kept with the user's entries), and action for any other interaction verb. A host that mounts another CLI's verbs copies each catalog verb's history to its operation.

The ring keeps the last 500 actions and changes (history-limit) and, apart, the last 100 fetches (HistoryFetchLimit), so a host polling a state verb never pushes the user's entries out. A fetch typed into the palette is the user's line: it stays with the actions and changes, and palette Up/Down recalls it.

Read it with history.list {last, kind} (default the last 50 actions and changes; kind is a comma list or all) or state get /history (the last 50 actions and changes); Runtime.History(n) returns every kind and Runtime.HistoryOf(n, kinds...) the ones named. The resolver records key:<keys>, a macro replay macro:<register>, the transport cli, everything else api; a caller sets its own with Runtime.Sourced(source) (outermost caller wins, so keys inside a replay stay macro:a).

Dot-repeat rides on it. . on app in normal mode runs history.repeat {steps}: the last successful change whose operation is Repeatable runs again with its arguments kept, at the current focus (the recorded target is dropped unless the request came from the CLI and the instance still exists), with a count on . replacing the recorded steps (3.). A confirmed request repeats without asking again (confirmed: true is in the record). The repeatable operations are the editor's changes: input.insert, input.delete, input.kill, input.paste, input.operator, input.replace; motions, focus, pages, modals, lists and the history are not. An application marks its own with Operation.Repeatable on Register. Dot repeats one request, so after typing hello in insert mode . inserts the last o, not the whole insert; macros stay key-based.

Persisted history

A TUI's history survives restarts. Model.Init calls Runtime.EnsureHistory, which reads history.jsonl from the application's state directory and keeps appending to it:

File $XDG_STATE_HOME/<app>/history.jsonl, else ~/.local/state/<app>/history.jsonl (Config.StateDir overrides the directory; history path prints it)
Format One entry per line, in the shape above.
Kept Palette lines: entries with the source palette, the commands the user ran, failed ones included. Other operations that completed, such as page.open, list.select, settings.set and an application's verbs. Fetches only while the history-fetches setting is on (default off: a host polling would fill the file, and nothing reads a fetch back after a restart).
Never kept Text editing and its plumbing: every operation of the input, editor, key, count, register, macro, yank-history, mode, palette and terminal nouns, so nothing typed or pasted into an input, a prompt, the palette line or a terminal reaches the file. Failed operations that did not come from the palette.
Arguments An argument whose name contains text, value, keys, password, secret or token is left out of the line. An operation keeps them only when it sets Operation.PersistArgs (NounBuilder.PersistArgs()). settings.set does, so set settings theme kombu comes back whole.
Bound The history-limit setting (integer, default 500, 0 keeps nothing). The framework declares it when the history is persisted, unless the application declared a setting of that name. It also bounds the ring.
Restore The last history-limit entries go into the ring before the session's own, marked restored: true. Palette Up/Down recalls their lines; dot-repeat skips them.
Writes Each entry is marshalled on the loop and appended by one writer goroutine, never on the loop. When the file reaches twice the limit, and at load, it is rewritten with the last history-limit lines through a temp file and a rename. Config.Flush (the Model calls it on quit) drains the writer.
Problems Lines that do not parse are skipped. The file gets one notice and is rewritten without them. A write error is one notice, and later entries are not saved.
Off No file without a state directory: Config.Disable() (o-story sessions), a test binary, or an application that sets Config.NoHistoryFile. The CLI shell neither reads nor writes the file.

The in-memory ring still records every kind for history.list and dot-repeat. Only the file is filtered. NoHistoryFile turns the file off entirely.

User actions (actions.json)

actions.json sits beside bindings.json in the config directory (actions path prints it). Each entry adds a verb to a noun the application already defines; the verb runs an external command. It is an ordinary application verb (noun.verb), so a bindings.json row, the palette, the CLI (APP NOUN VERB), ? help and the catalog all see it. Model.Init, cli.Mount and the CLI shell install the file before the user bindings, so a row can name an action.

[
  {"noun": "worktree", "verb": "open", "description": "Open in the editor",
   "command": ["code", "--goto", "{{data.path}}"], "label": "{{label}}"},
  {"noun": "worktree", "verb": "prune", "base": "wt", "target": "{{data}}",
   "confirm": "Prune {{label}}?", "parameters": {"force": {"type": "boolean"}}, "timeout": "30s"},
  {"noun": "app", "verb": "deploy", "command": ["deploy", "{{env}}"], "json": true,
   "parameters": {"env": {"type": "string", "required": true, "enum": ["staging", "prod"]}},
   "positional": ["env"], "confirm": "Deploy to {{env}}?"}
]
Field Meaning
noun, verb The existing noun and the new verb (one word). A verb the noun already has is refused
command An argv template, run directly, never through a shell. A string is split on spaces
base The JSON convention instead of command: BASE NOUN VERB --json ARGS [--target TARGET], the shape every ochazuke CLI accepts. call ("noun verb", the noun's words first: "app settings list") names another noun and verb, target is a template for --target (left out when it expands to nothing)
json With command: append --json ARGS
parameters, positional Parameters in the Parameter shape (string, integer or boolean; required, enum, source, free, description) and the order bare words fill them, so the palette, the CLI and help show them
confirm A confirm prompt, as NounBuilder.Confirm takes ({{label}}, {{target}}, {{<arg>}})
label The job's label on the ticker and in jobs list (default the program's name)
dir, timeout, quiet, description Working directory, a duration after which the command is killed, keep the job off the ticker, the help line

Templates ({{name}}) in command, base, target, label and dir expand to target or id (the instance the verb runs on), label (its label, else its id), noun, verb, data (the instance's Data: a string as is, else JSON), data.KEY (one field of Data as a JSON object), args (the arguments as one JSON object) or a declared parameter. An unknown placeholder makes the entry malformed. An argv word that is one placeholder and expands to nothing is left out, so an optional parameter disappears instead of passing "". The command also gets OCHAZUKE_ACTION, OCHAZUKE_TARGET, OCHAZUKE_LABEL and OCHAZUKE_ARGS in its environment and no standard input.

The work runs off the loop as a job (Pending(...).Label(label)): the ticker shows noun.verb label running, the context is cancelled when the owning instance closes, and the command's standard output is the job's result: one JSON document, a list when every line is JSON (JSON Lines), else the trimmed text. A non-zero exit fails the job with the last line of standard error. The CLI shell prints the result as it prints any job's.

A malformed entry (an unknown field, noun or placeholder, a bad type, command and base together) is skipped and is a persistent notice (a CLI warning:); the rest still install. actions reload reads the file again (the last load's verbs go first) and reapplies the user bindings; actions list prints the installed entries.

Security: commands come only from the user's own actions.json in the config directory. A catalog, a binding row, a socket call or an application's data can name an action's verb, as they can any verb, but never define or change what it runs: there is no operation that loads actions from another path, the catalog publishes the verb's parameters and never its command, and a sandbox (Config.Disable, o-story, a test binary) reads no file. Values reach the command as argv words, not shell text; if an entry runs sh -c itself, pass values as positional arguments ("sh", "-c", "git log \"$1\"", "sh", "{{data.path}}") rather than inside the script.

Action aliases

The action table is flat: Runtime.Operations maps a name (noun.verb) to its action, and an alias is one more name for the same action. Nothing is generated or duplicated, so the names cannot drift.

r.Define("pane").
	Verb("kill", app.Kill, "Kill the pane").Alias("forget").          // pane.forget, permanent
	Verb("associate", app.File, "File the pane").AliasOn("project", "associate"). // the same operation under project
	Verb("add-base", app.AddBase).Renamed("base.add", "v0.27.0", "v0.28.0")        // an old name, kept one release
r.AliasAction("pane.zap", "pane.kill")                                // the same, by keys
  • Running. A request, binding row, palette line or CLI call named by an alias runs the canonical action. canonicalRequest rewrites it (Request.Via keeps the alias, and history shows it); the handler reads Ctx.Invoked (application verbs) or Runtime.Invoked() (interaction handlers): Invocation{Name, Noun, Verb, Alias}, where Noun is the alias's noun, the subject side (project for project.associate).
  • Listing. An alias is never an entry of its own. operations.list and the catalog carry aliases on the canonical verb (CatalogVerb.Aliases, ActionAlias objects); the CLI makes a same-noun alias an oden alias of the verb's command (show, display in help) and an alias under another noun a command of that noun's group that says alias of pane.associate; the palette lists the canonical verb with · also NAME and a typed alias narrows to it; ? help names the canonical operation and ends its description with · also: NAME; bindings list rows carry aliases. actions aliases lists every alias with its action and source.
  • Old names. Renamed(old, since, removeAfter) is a deprecated alias (Keep actions: an old name stays one release, recorded in the CHANGELOG). Its first use in a runtime posts one notice, OLD is an old name of NEW (renamed in SINCE; removed after AFTER) (a CLI notice: line).
  • Rules. An alias names a registered noun and a verb (the longest registered noun prefix is the noun, so dirs.base.add is dirs and base.add); it may not be the name of an action (would shadow the action) or of another alias for a different action. An action registered later under an alias's name takes the name: the alias is dropped and the drop recorded (ActionAliasProblems), never a panic, so a host mounting a newer catalog keeps starting.

Alias rows in actions.json

A user adds names without code: an actions.json entry with an alias field is an alias row.

[
  {"alias": "pane.k", "action": "pane.kill", "note": "my short name"},
  {"alias": "list.walk", "action": "list.move"}
]
Field Meaning
alias The new name, noun.verb on a registered noun (a short framework noun name such as list works)
action The action it runs: a registered key, or another alias (which resolves to its action)
note Free text, listed with it in actions aliases

Alias rows install after the file's actions, so a row may name a verb the same file adds; bindings.json rows may use the alias. A row that would shadow an action or another alias (the framework's, the application's or an earlier row's), names an unknown action or noun, or has an unknown field is skipped and reported like a bad bindings row: a persistent notice at load (a CLI warning:), the result of actions reload, and the lines of actions check and bindings check. bindings check also accepts a bindings.json row spelled by an alias and says what it resolves to (bindings.json: row 2 C: pane.close resolves to pane.kill (an alias, app), plus the new name for an old one).

Writes are never filled silently

A request whose required parameter is missing fails naming it (NAME is required, from the API and the CLI) and the palette asks for it (missing NAME) instead of running. Nothing fills a write's slot from the focus or a guess; a binding row that fills one declares it (FromRow).

Settings and the Settings page

An application declares its settings (Runtime.DeclareSetting(Setting{Name, Type, Default, Description, Enum, Section})); their values live in settings.json beside bindings.json, and the settings noun reads and writes them from the CLI and the palette (settings list|get|set|reset|reload|path). Each change is checked against the type and the allowed values and written at once. The types are string (with Enum, one of a set), integer, number, boolean, duration ("400ms") and list (SettingList: strings, a JSON array in the file; settings set paths '["~/proj","~/hub"]'; Settings.Strings reads a copy).

settings.edit opens the Settings page, or switches to it when it is already a tab. There is no default key: an application binds it (matomeru binds ,, the demo s on Appearance), and pressing it on the Settings page does nothing.

 › General  ext.ai  ext.git
╭━ General (6) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╮
┃ scrolloff •  ‹ 3 ›                                                       ┃
┃ theme        ‹ dark ›                                                    ┃
┃ wrap         ‹ false ›                                                   ┃
┃ paths        2 items                                                     ┃
┃              ~/proj                                                      ┃
┃              ~/hub                                                       ┃
┃ ignore       (none)                                                      ┃
┃ editor       vi                                                          ┃
╰━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╯
╭─ theme ──────────────────────────────────────────────────────────────────╮
│ Colour theme                                                             │
│                                                                          │
│ Default is dark. Not in settings.json, so the default applies.           │
│ Takes one of dark, light, mono.                                          │
╰──────────────────────────────────────────────────────────────────────────╯
Part What it shows
Tabs One tab per section: the settings without a section first, as General, then each section in the order its first setting was declared. A setting's section is its Section, else its name before the last dot (ext.ai.poll-every is in ext.ai), else General (Setting.SectionName). tab and ] show the next section, shift+tab and [ the previous, wrapping (settings.section {step}, or {name} from the palette); each section keeps its selected row. With one section there is no tab row
Rows One row per setting, no header: the name on the left (without the section's prefix; a • after it when the value is not the default), the value on the right. ‹ value › marks a value h/l steps (boolean, enum, number); text and durations show as they are, empty text as (empty). A list setting's row shows its item count (2 items, (none) when empty), and each item is a row directly beneath it with the name column blank
Description The selected setting's description, then Default is X. and where the value comes from: Not in settings.json, so the default applies., Set in settings.json., or settings.json holds an invalid value (why), so the default applies.; then what it takes. On an item row it starts with Item 2 of 3. J/K scroll it when it overflows, and >/< move the divider

Where the focus is: focus-display

The framework declares focus-display (FocusDisplaySetting, with theme and the dims in Runtime.EnsureConfigSettings, unless the application declared a setting of that name). It shows where the focus is beside the mode badge on the title row, dim (the muted role; faint in mono), so it is clear where a motion landed. The text alone carries the meaning under mono and NO_COLOR.

Value Title row
off (default) NORMAL, as before
noun NORMAL · pane: the focused instance's noun as DisplayNoun (list, not ochazuke.list)
stack NORMAL · panes › pane-list › pane: the focus path from the outermost level to the innermost, app left out, each level its label when it has one of at most FocusLabelMax (16) cells (a page's is its title), else its noun. Open modals are levels too, after the focus they opened over (work › list › a2 › confirm)

When the path is wider than the room the badge and the title's first quarter leave, it is cut from the left a level at a time behind … › (FitFocusPath), so the innermost level stays; with less room still the innermost level alone follows …. The row is redrawn on every frame, so a focus change, a modal, a mode change or a spill shows at once, and so does a new value: settings set focus-display stack, the Settings page, or the verb focus.display {mode} (off, noun or stack; with no mode, the next in that order). The verb has no default key; it is in the palette (display focus, display focus stack) and the CLI (BIN focus display), and a user binds a key in bindings.json:

[{"noun": "app", "mode": "normal", "keys": "g f", "request": {"noun": "focus", "verb": "display"}}]

Stories chrome/focus-*.

Editing saves each change as it is made; there is no save key.

Keys On a setting row On a list item row
enter flip a boolean, take an enum's next value, add an item to a list, else open a one-line prompt prefilled with the value edit the item (i too)
l right space / h left step a boolean, an enum or a number forward / back (one h/l setting.cycle hint)
n on a list, add an item at the end add an item after this one
d d delete the item
r reset to the default (backspace, delete too) reset the whole list
u take back the last change made on the page (ctrl+z too; offered only when there is one) the same

The prompt (setting-prompt): enter sets the value; one that does not validate (or an empty item) keeps the prompt open with the error in the status; esc closes it unchanged. A value another process writes (the CLI's settings set) shows within a second.

q or esc (settings.close) goes back to the page the Settings page was opened from, with the focus and mode it was left in; the next settings.edit reopens on the section and row it was left on. settings.edit registers the page (SettingsPageID, title Settings) when it opens and settings.close removes it, so it is a tab, in the page picker and on page.next only while it is open. An application that wants a permanent Settings tab calls Runtime.AddSettingsPage() where it registers its pages; settings.close then goes back without removing it.

Status segments

Live items (CPU, memory, disk free, a build state) ride the rule under the header, right-aligned, and take no row. Each is a data row:

sysstat.Register(r, sysstat.Options{Enabled: true, DiskPath: dataDir}) // cpu, ram, disk
r.SegmentProvider("build", func(ctx context.Context, s ochazuke.Segment) (ochazuke.SegmentValue, error) {
	return ochazuke.SegmentValue{Text: "green", Tone: ochazuke.ToneDone}, nil
})
r.AddSegment(ochazuke.Segment{ID: "build", Label: "ci", Provider: "build", Every: 10 * time.Second})
r.AddSegment(ochazuke.Segment{ID: "cpu", Every: 5 * time.Second}) // change a row by id

A user adds or changes rows in segments.json beside settings.json (a command runs only from here or from Go, never from a catalog):

[
  {"id": "branch", "label": "git", "command": ["git", "branch", "--show-current"], "every": "10s", "timeout": "1s"},
  {"id": "cpu", "label": "CPU", "order": 1}
]

Sources are provider (Go), command (first line of its output, killed at timeout) or verb (NOUN.VERB, run outside the history); an answer may be {"text": ..., "tone": ...}. Refreshes run off the loop, one per segment at a time, so a slow source never blocks a key. Every part can be turned off: status.segments (all), status.segments.<id> (one), and status.segment-order holds the order. The segment noun lists, shows, enables, disables, toggles, moves and refreshes them from the palette and the CLI. An extension declares verb-sourced segments in its catalog (segments); a host mounts them with cli.CatalogSegments.

Configuration reference (config export)

Every application has a config noun. config path prints where the user files live; config export [DIR] writes a complete reference of the user configuration to copy from; config reload reads every config file again:

APP config path                  # {"config": "~/.config/APP", "state": "~/.local/state/APP", "reference": "~/.config/APP/reference"}
APP config reload                # themes, settings, actions, bindings, then the app's own files: the problems
APP config export                # writes ~/.config/APP/reference/
APP config export ./ref          # anywhere else
APP config export ~/.config/APP --force   # into the live directory: only with --force
File written What it holds
README.md What each file is, where the live files are (Config.Directory()), and the warning below
settings.json Every declared setting at its current value, in declaration order; valid to copy in as is
settings.reference.json Per setting: name, type, default, enum, description, value, set (the settings list shape), one setting per line
bindings.reference.json Every stored key row of every noun, instance and mode in the bindings.load row shape (bindings list --all), in the grouped bindings format below (bindings export), to copy into bindings.json
actions.example.json Two actions on the app noun (example-run in the command form, example-call in the base form) using every field, one action per line; installs with no notice
themes.example.json example (the built-in palette, every role as {light, dark}) and example-dark (one colour per role); installs with no notice

The framework's settings (theme, focus-display, history-limit) are declared before the export, so they are in the files. Each file is written atomically (a temp file renamed over it), and a file that would land on a live config file (bindings.json, settings.json, actions.json, themes.json in the config directory, through a symbolic link too) is refused before anything is written unless --force. The result is {dir, files, config, state}. Without a config directory (Config.Disable, a test binary) the export needs a DIR. The README lists the action placeholders and the theme roles.

Config files are JSONC

Every config file ochazuke reads (bindings.json, actions.json, settings.json, themes.json, a file given to bindings load, the embedded default specs) is JSONC: JSON with // line comments, /* */ block comments and trailing commas (read with github.com/tailscale/hujson). The files keep their .json names. A file without a / that is valid JSON skips the JSONC parse (about 0.1 ms against 2.5 ms for 100 KB). An error names the line and column in the file as written, comments counted. settings set edits an existing settings.json in place: the comments, layout and member order stay; a changed value is replaced where it stands, a new one appended at the file's indent. Go: StandardizeJSONC(data), DecodeJSONC(data, &v).

The bindings file format

bindings.json is a grouped table, one group per mode table, one row per line (config export's bindings.reference.json, bindings export PATH, bindings defaults and the cmd/ochazuke keybinding tool write it; FormatBindings):

// ochazuke bindings (JSONC): groups by "noun/mode" context; one row per line, ...
{
  "modes": {
    "leader_pending": {"pending": true},
    "terminal": {"passthrough": true}
  },
  "bindings": [
    {"context": "ochazuke.timeline/normal", "bindings": [
      {"key": "l", "action": "ochazuke.timeline.open"},
      {"key": "l", "action": "ochazuke.timeline.enter"},
      {"key": "enter", "action": "ochazuke.focus.next", "hint": "edit"}
    ]},
    {"context": "ochazuke.editor/visual_count_pending", "extends": "visual", "bindings": [
      {"key": "digit", "action": "ochazuke.count.append", "from": {"digit": "digit"}}
    ]},
    {"context": "pane/normal", "bindings": [
      {"key": "]", "action": "panes.tab", "args": {"delta": 1}},
      {"key": "x", "unbind": true},
      {"key": "u", "when": "busy", "unbind": true}
    ]},
    {"instance": "work/agent", "mode": "normal", "bindings": [
      {"key": "ctrl+r", "action": "pane.restart"}
    ]}
  ]
}
  • Group: "context": "noun/mode", split at the last /; a bare "noun" is the noun's any-mode table (mode ""). An instance's table is {"instance": "ID", "mode": "MODE"}. "extends": "MODE" on a noun group says its table reads that mode's rows after its own.
  • Row: one small object per line, its fields in the order key, action, args, then hint, when, from, hidden, help, replay, capture, target, confirmed, unbind. key is a key or a sequence as everywhere else ("g c", "ctrl+a v"); a key that names a class (digit, alpha-q, one-of:h j k l, paste, any, upper, ctrl+alpha) is a pattern row. action is noun.verb, split at the last . (an alias works, as in a row). {"key": "x", "unbind": true} unbinds the default (with when for a guarded one).
  • A key may repeat. A table's rows are in line order, and on a keypress, at each level of the focus path (innermost first), the first row on the key whose action is available answers (Runtime.Available: the action's own availability, such as timeline.open on a closed card only); when none is, the key goes on to the next level out. A container that must keep a key binds it to the always-available noop (ochazuke.key.ignore, NoopAction): the key is consumed there. A row needs no condition: when (a guard on the row) still works, and bindings check lists such rows as debts.
  • Replacing: a file is one batch. Its first row on a table, mode, key and when replaces every row the defaults had there (so {"key": "l", "action": "my.verb"} on ochazuke.timeline/normal replaces both default l rows), its later rows on the same key are alternatives after it.
  • modes: properties global to a mode, {"pending": true} or {"passthrough": true}. The export writes the application's own; in a user file a mode or an extends that differs from the application's is reported and ignored (a user file cannot define a mode).
  • Writing: one header comment line, then modes (names sorted), then one group per table: nouns by name, each noun's modes by name (the any-mode table first), then instances by ID and mode, the rows in exact table order. A row spelled by an alias is written as spelled.
  • Reading: the grouped format, the v0.33 form of a group ("bindings": {"KEY": "noun.verb" | {action, args, hint, when, ...} | null | [those]}, a key once per object) and the older array of rows ([{"noun", "mode", "keys", "request": {"noun", "verb", "args"}, ...}], any field order) all load; the top level tells rows from groups, each group's bindings tells lines from a key map. A bad group or row is skipped and reported (bindings.json: pane/normal q: ...), the rest loads. bindings check names the format it read on the status line (status: bindings.json: grouped format). A hand-written file is never rewritten: only files the tools write follow this layout.
  • Checking: bindings check also warns (warning: lines) about each guard row of the file (a debt) and each merged row that can never answer, because an earlier row on its key in its table is available whenever it is (two always-available actions on one key: the second is dead). bindings check --debts lists every table's guard rows and their count. The default files' rule check (CheckDefaultBindings) reports a dead row as an error.
  • Rows elsewhere: bindings list --json still answers the array of rows (an API result); settings.reference.json and actions.example.json are arrays written one row per line (FormatRows).

Go: ParseBindings(data) (BindingsDoc, format, error), FormatBindings(doc), Runtime.BindingsDoc() (every stored table), GroupBindings(rows), BindingsDoc{Modes, Groups}, BindingGroup{Noun, Instance, Mode, Extends, Rows}, ModeSpec{Pending, Passthrough}, Config.BindingsFormat(), constants BindingsGrouped, BindingsRows, BindingsHeader; availability: Runtime.Available(request, focus, mode), Runtime.SetAvailability(key, guard), NounBuilder.When, KeyEvent.Args; outcomes: Runtime.DispatchOutcome (KeyHandled, KeyUnhandled, KeyConsumed), NoopAction; checks: Runtime.UnreachableBindings(), Runtime.BindingDebts().

Copying a default into a live file pins it: a later version that changes the default does not reach that user. Copy only what you change. A row's guard is part of its identity: a bindings.json row replaces only the rows with the same noun or instance, mode, keys (or pattern) and when, so a guarded row and the unguarded row on the same keys (such as 0 in operator_pending: the count digit with when: count and the line-start motion) are changed one at a time, and the reference loads back unchanged.

config reload reads, in order, themes.json (so the theme setting's choices are current), settings.json (observers see each changed value), actions.json, bindings.json (once; a row can name an action's verb), each only when the application uses it, then the files the application joined with Runtime.OnConfigReload(name, fn). A bad file or entry is skipped and is a notice, never fatal; each file's previous notices are withdrawn first. The result is every problem, each naming its file; without a config directory it is an error. The single-file reloads (themes reload, settings reload, actions reload, bindings reload) stay.

r.OnConfigReload("directories.json", func() []string {
	return app.LoadDirectories() // problems, each prefixed "directories.json: " unless it already is
})

Go: Runtime.ExportConfig(dir, force) (ConfigExport, error), Config.Paths() ConfigPaths, Runtime.ReloadConfig() ([]string, error), Runtime.OnConfigReload(name, fn); constants ReferenceDir, ReferenceReadme, SettingsReferenceFile, BindingsReferenceFile, ActionsExampleFile, ThemesExampleFile.

Catalog inspector (catalog inspect)

Every app mounted with cli.Mount has BIN catalog inspect: one self-contained HTML page for browsing the app's nouns, verbs, key rows, user actions and the framework nouns, plus any other tools' catalogs. It reads only; it never writes the live config.

APP catalog inspect                      # writes <state dir>/catalog-inspect.html (the temp dir without one), prints the path
APP catalog inspect --out page.html --open
APP catalog inspect --bin o-ai --bin ~/bin/o-git --catalog saved.json    # merge other tools' catalogs
APP catalog inspect --annotations notes.json                            # fill the Gaps view
APP catalog inspect --audit ~/hub/o-plans/lists/verb-audit.md           # the family verb-audit.md format
Flag What it adds
--out FILE Where to write the page (default <state dir>/catalog-inspect.html)
--open Open the page once written (open, xdg-open)
--bin PATH Another tool: runs PATH catalog, with the ochazuke version from its build info (repeatable). A tool already read (by its executable or its catalog's name) is skipped; a failing one shows as a failed row
--catalog FILE.json A saved BIN catalog answer (repeatable; read before --bin, so a file pins its catalog name)
--namespace PREFIX A prefix the app mirrors a tool's nouns under (ai for ai-agent) when that tool is not read; a read tool's short name is one already (repeatable)
--annotations FILE.json Notes for the Gaps view: per noun, suggested verbs (verb, params, maybe, note, group) and a dated source (inspect.Annotations documents the JSON)
--audit FILE.md The same from the family's verb-audit.md format (inspect.AuditAnnotations); not with --annotations
--json FILE Also write the merged data as JSON
--timeout D Per --bin catalog (default 20s)

The app's own key rows are its effective rows, what config export writes to bindings.reference.json (the live bindings.json included); every other tool's are its catalog's defaults. Its actions.json verbs are marked as user actions. Framework nouns are kept once under an ochazuke pseudo-tool whichever catalogs carry them, the newest ochazuke's definition winning (an older catalog's bare list folds into ochazuke.list); each verb lists the tools that carry it. A noun named with another read tool's short name and a dash or dot (ai-agent, ai.agent) groups as that extension's mirror and links to its noun. The page has the data inlined, plain JS and no network, works from file://, and has four views (1 By noun, 2 By key with duplicate and shadowing checks, 3 Relations with missing mirrors, 4 Gaps, shown only with annotations); ? lists its keys.

The family-wide page (mtm, every o-* tool mtm extension available finds, nabe, and the verb audit):

mtm catalog inspect $(mtm extension available | jq -r '.[] | select(.status != "missing" and .bin != "") | "--bin=" + .bin') \
  --bin nabe --audit ~/hub/o-plans/lists/verb-audit.md --out ~/hub/o-plans/inspector/index.html

Go: package cli/inspect (standard library only): ParseCatalog, Collector (Add, AddBin, AddFile, SeeBin), Namespaces, Merge(sources, namespaces, actions, host) *Data, Write(data, w), Annotations (ReadAnnotations, Normalize, Match), AuditAnnotations(markdown). node cli/inspect/smoke.mjs PAGE.html [DIR] is the page's headless Chrome check; OCHAZUKE_INSPECT_OUT=/tmp/demo.html go test ./examples/demo -run TestCatalogInspect writes a page to run it on.

Live CLI control

The live transport is a local Unix socket. Its parent directory must be owned by the current user and have no group/other access; the socket is mode 0600. Startup refuses to overwrite an existing socket. Use different --socket paths for different instances. After an unclean exit, verify the old instance is stopped before removing a stale socket.

CLI commands enter Bubble Tea's message loop. The event loop remains the sole owner of UI state; transport handlers do not mutate models concurrently. Queries must also obtain a consistent view through the runtime.

Keyboard, Go API, palette, and CLI use the same command semantics. Queries run through the same event loop and return detached JSON values. The wire protocol accepts one newline-terminated request per connection and returns { "result": ... } or { "error": "..." }, with "messages" when the call posted notices or a status (see Telling the user). The CLI prints the result as JSON and returns a nonzero exit code for errors.

Output formats

Every command of an importable shell (cli.Shell, an app built with cli.Mount) takes --format json|table|markdown|toon (cli.Formats). json is the default and prints exactly what the shell always printed. For a list or state verb's rows (an array of objects, or Lines of Row) the other formats print one table per noun: the noun's declared columns over the rows' cells, else the records' fields with id and label first. table cuts cells with … and drops columns by Priority only when stdout is a terminal (Shell.Width overrides); markdown is a GitHub table; toon is NOUN[N]{col,...}: and one line per row, quoting only values with a comma, a quote or a line break. A result that is not tabular prints JSON with a note on stderr. --format combines with --json '{...}'; a verb with its own format parameter keeps --format as that argument. ochazuke.TableText(cols, rows, width) is the same layout for Go callers.

A TUI for an oden tool

cli.TUI(app) as an oden Default gives any oden tool a Commands page over its catalog (inline commands and <bin>-<name> executables on PATH): command run opens the command in a terminal under its row, attached (ctrl+a d detaches), command help shows its help in the detail beside the list. The terminal is a local pseudo-terminal (cli.StartPty). examples/hello is the smallest such tool (go run ./examples/hello).

Scope and limits

  • Standalone Go module and example application; no gmtm/Matomeru integration.
  • Vim-style editing, not a complete Vim implementation; no promise of text objects, dot-repeat, or full Vim scripting compatibility.
  • Cursor and edit offsets count Unicode runes, not grapheme clusters. Rendering accounts for terminal cell width.
  • Undo trees, yank history, registers, and macros are in memory; they do not persist across restarts.
  • Custom modes are registered names. Applications supply their mode-specific bindings; unknown modes are not silently interpreted as normal or insert.
  • Modals preserve their own state and existing input instances. Closing a modal restores interaction state, not a stale copy of shared application data.
  • Handler registration and widget construction are Go APIs. User-facing operations, including configuration, palette navigation, field editing, and picker actions, are registered and available through the CLI.
  • Terminals are display and input ownership only: the framework ships no PTY, shell, emulator or daemon client. A TerminalStream is the application's; frames are rows of trusted ANSI with a cursor cell, not a cell buffer the framework interprets. Resize, reconnect and session lifetime belong to the stream.
  • The framework package holds no application pages; the demo lives in examples/demo and every example is built on the public API.

Documentation

Index

Examples

Constants

View Source
const (
	AliasFramework = "framework" // the framework's own old names (registered inside Builtin)
	AliasApp       = "app"       // an application's NounBuilder.Alias, Renamed or Runtime.AliasAction
	AliasConfig    = "config"    // a row of the user's actions.json
)

Alias sources: who declared an alias.

View Source
const (
	BindingsGrouped = "grouped"
	BindingsRows    = "rows"
)

Bindings file formats, as ParseBindings and bindings check report them.

View Source
const (
	SettingsReferenceFile = "settings.reference.json"
	BindingsReferenceFile = "bindings.reference.json"
	ActionsExampleFile    = "actions.example.json"
	ThemesExampleFile     = "themes.example.json"
	ReferenceReadme       = "README.md"
)

The files config.export writes.

View Source
const (
	BindingsFile = "bindings.json"
	SettingsFile = "settings.json"
)

BindingsFile and SettingsFile are the file names in the config directory.

View Source
const (
	// SourceGo is a default row bound in Go (Bind, ModeBuilder.Bind): an
	// application's own, or one computed at runtime (an instance's rows).
	SourceGo = "go"
	// SourceGenerated is a key sequence's prefix cancel ("g esc"), which
	// the framework adds to every table with a sequence.
	SourceGenerated = "generated"
)

Source labels of bindings defaults for rows no file declared.

View Source
const (
	JobRunning   = "running"
	JobDone      = "done"
	JobFailed    = "failed"
	JobCancelled = "cancelled"
)

Job states as jobs.list and the snapshot report them.

View Source
const (
	KindInteraction = "interaction" // runs synchronously on the loop (default)
	KindApplication = "application" // an AppHandler; pending work runs off the loop
)

Operation kinds.

View Source
const (
	FocusDisplayOff   = "off"
	FocusDisplayNoun  = "noun"
	FocusDisplayStack = "stack"
)

The focus-display values, in the order focus.display cycles them.

View Source
const (
	HelpHere  = "here"
	HelpPick  = "pick"
	HelpTable = "table"
)

The ? help's views. Here is what the focus answers to now (BindingHelp, the default). Pick is the same table for a chosen noun and mode, as if the focus were on that noun in that mode (NounHelp). Table is a chosen noun's keys by mode: a row per key or key class, a column per mode with rows for the noun or app, each cell the operation that answers there. S (bindings-help.view) cycles the views the settings leave on; n (bindings-help.noun) chooses the noun of pick and table, m (bindings-help.mode) the mode of pick, p (bindings-help.place) the place of pick and table: one of the noun's recorded places (NounPlaces), whose chain gives the walk the levels between the noun and app, or no place. All four are rows on the help modal's noun, guarded so they answer, and are hinted, only where they do something.

View Source
const (
	HelpPickSetting    = "help.pick-view"
	HelpTableSetting   = "help.table-view"
	HelpPendingSetting = "help.pending-modes"
)

The help's settings, declared by EnsureConfigSettings (section help): whether pick and table are in S's cycle (both default on; with both off S does nothing and is not offered) and whether the pending modes (counts, operators, registers, the terminal leader, key-sequence prefixes) are pick's mode choices and table's columns (default off).

View Source
const (
	HistoryAction = "action"
	HistoryChange = "change"
	HistoryFetch  = "fetch"
	HistorySystem = "system"
)

The kinds of history entry (HistoryEntry.Kind, Operation.HistoryKind). An action is the user's interaction (a motion, a focus, page or modal move, a mode, the palette's line), a change is an edit or a mutating verb (what dot-repeat reads), a fetch asks for data (a state, list or show verb, a reload, a host calling an extension). HistorySystem is reserved for what the runtime does on its own (a resize, a job that finished); no entry has it yet.

View Source
const (
	PaneStep       = 5
	PaneShiftLimit = 30
	PaneMinWidth   = 16
	PaneMinHeight  = 3
)

The pane limits: PaneStep is the percent points one pane.grow or pane.shrink moves a divider; PaneShiftLimit bounds Layout.Shift either way; Divide keeps PaneMinWidth cells on each side of a side-by-side split and PaneMinHeight rows on each side of a stacked one, so neither panel collapses.

View Source
const (
	SourceKey     = "key"
	SourceDigit   = "digit"
	SourceText    = "text"
	SourceCount   = "count"
	SourceLine    = "line"
	SourceSteps   = "steps"
	SourceOp      = "op"
	SourceOpCount = "opcount"
	SourcePrefix  = "prefix"
	SourceLetter  = "letter"
	SourceRow     = "row"
)

Sources a From entry may name: "key" (the key name), "digit" (the key as an integer), "text" (pasted or typed text), "count" (payload count, at least 1), "line" (payload count, 0 when absent, else the operator count), "steps" (count times operator count), "op" (payload operator), "opcount", "prefix" (the key sequence the pending mode holds), "letter" (the key's letter, lowercased, without ctrl+ or shift+: f, F, shift+f and ctrl+f all read f; "" for a key that is not one letter), "row" (the row's Data, see FromRow) and "row:<field>" (one field of it). A From value may name several sources separated by spaces ("prefix key"): their texts are joined with a space, the way terminal.send reads a key list.

View Source
const (
	Digit     = "digit"
	Alpha     = "alpha"
	Printable = "printable"
	// Upper is one uppercase letter (A to Z, any script's capital): it
	// answers before Alpha, which an uppercase letter also belongs to.
	Upper = "upper"
	// CtrlAlpha is one ctrl+letter, ctrl+a to ctrl+z: one row for every
	// control letter, the letter read with the letter source. A legacy
	// terminal sends ctrl+h, ctrl+i, ctrl+j and ctrl+m as backspace, tab
	// and enter, and ctrl+[ as esc, so those never reach the row there;
	// labels a row reads this way should avoid h, i, j and m.
	CtrlAlpha = "ctrl+alpha"
	Paste     = "paste"
	Any       = "any"
)

Pattern names.

View Source
const (
	FormNoun       = "ochazuke.form"
	FormFieldNoun  = "ochazuke.form-field"
	FormTextNoun   = "ochazuke.form-text"
	FormChoiceNoun = "ochazuke.form-choice"
	FormOptionNoun = "ochazuke.form-option"
	// FormSuggestNoun is a free text field with suggestions: an input
	// whose enter (normal) and ctrl+space open the suggestions chooser.
	FormSuggestNoun = "ochazuke.form-suggest"

	// FormSuggestionsKind is the suggestions chooser's modal kind
	// (Runtime.Chooser(FormSuggestionsKind) while it is open).
	FormSuggestionsKind = "form-suggestions"
)

Form nouns: the editor, its text fields (single- and multi-line), its choice fields, and the chooser's rows. ("form", not "record", so the palette's words do not collide with the macro verb record.)

View Source
const (
	FormRunnableGuard = "form-runnable"
	FormMissingGuard  = "form-missing"
)

Form availability: form.run is available on a form that runs once (Submit) whose every field is valid (form-runnable), form.next-missing on one with a field still not valid (form-missing). On such a form one of the two always is, so enter's rows (run, next-missing, the field's own action) never fall through to the field's own; on an editor that writes each change neither is, and enter keeps its old action.

View Source
const (
	// SegmentsSetting shows (true, the default) or hides the whole
	// segments area.
	SegmentsSetting = "status.segments"
	// SegmentOrderSetting is the order the user gave the segments
	// (segment.move): ids first to last; segments it leaves out follow in
	// their rows' order.
	SegmentOrderSetting = "status.segment-order"
	// SegmentsFile is the user's segments file in the config directory:
	// a JSON array of Segment rows. A row naming an existing id changes
	// that segment's fields; a new id adds a segment.
	SegmentsFile = "segments.json"
)

The segment settings.

View Source
const (
	DefaultSegmentEvery   = 5 * time.Second
	MinSegmentEvery       = 250 * time.Millisecond
	DefaultSegmentTimeout = 2 * time.Second
	// SegmentMaxWidth is the most cells one segment's text takes.
	SegmentMaxWidth = 32
)

Segment defaults and bounds.

View Source
const (
	SettingString   = "string"   // string; Enum limits it
	SettingInteger  = "integer"  // int
	SettingNumber   = "number"   // float64
	SettingBoolean  = "boolean"  // bool
	SettingDuration = "duration" // time.Duration, written as "400ms"
	SettingList     = "list"     // []string, written as a JSON array
)

Setting types: the value a setting holds, as its getter returns it.

View Source
const (
	// SettingsPageID is the Settings page's instance id.
	SettingsPageID = "__settings"
	// SettingsGeneral titles the section of the settings without one.
	SettingsGeneral = "General"
)

The Settings page: settings.edit opens (or switches to) a framework page of the declared settings. Its sections are tabs across the top (the settings without a section first, under SettingsGeneral, then each section in the order its first setting was declared; tab, shift+tab, ] and [ step through them, wrapping). Below the tabs the shown section is one list of rows, a setting a row: its name on the left (a • after a name whose value is not the default), its value on the right (‹ value › for one h/l steps). A list setting's row reads its item count, and each item is a row beneath it with the name column blank. Under the list the selected setting's description says what its default is, where its value comes from (the default, settings.json, or the default because settings.json holds an invalid value) and what it takes; J/K scroll it.

Every change is saved as it is made; there is no save key. On a setting row enter edits (a boolean flips, an enum takes its next value, a list takes a new item, any other type opens a one-line prompt prefilled with the value), l/h and space step a boolean, an enum or a number, n adds an item to a list, r (or backspace, delete) resets to the default. On an item row enter or i edits the item, n adds one after it, d d deletes it, r resets the whole list. u (or ctrl+z) takes back the last change made on the page. q or esc (settings.close) goes back to the page the Settings page was opened from, with its remembered focus and mode; the page reopens on the section and row it was left on. A value another process sets (the poll) redraws the open lists.

settings.edit registers the page on first use and settings.close removes it again, so it is a tab only while it is open; an application that wants it as a permanent tab calls AddSettingsPage.

View Source
const (
	TerminalLeaderSetting = "terminal-leader"
	DefaultTerminalLeader = "ctrl+a"
)

The terminal leader setting: the key that enters leader_pending.

View Source
const (
	DefaultModalDim     = 35 // about the scheme's muted colour, as the backdrop always was
	DefaultDimUnfocused = 20
	MaxDim              = 90
)

Dim defaults, in percent toward the background (the settings modal-dim and dim-unfocused, Model.Styles).

View Source
const (
	ModalDimSetting     = "modal-dim"
	DimUnfocusedSetting = "dim-unfocused"
)

The dim settings, declared by EnsureConfigSettings.

View Source
const (
	ToneWorking    = "state.working"
	ToneNeedsInput = "state.needs-input"
	ToneDone       = "state.done"
	ToneErrored    = "state.errored"
	ToneIdle       = "state.idle"
	ToneExited     = "state.exited"
	ToneStar       = "marker.star"
	ToneQueued     = "marker.queued"
)

The tones every built-in theme draws (defaultTones); applications and themes.json add their own names.

View Source
const (
	TreeAdd    = "add"
	TreeRename = "rename"
	TreeMove   = "move"
	TreeDelete = "delete"
)

Tree change operations.

View Source
const (
	UINotice      = "notice"
	UIClearNotice = "clear"
	UIError       = "status"
	UIInfo        = "info"
)

UI message kinds. The status line is one line with two tones: UIError is the error tone (Ctx.Error; the TUI draws "Error: TEXT" in the error style, the CLI prints "error: TEXT") and UIInfo the neutral one (Ctx.Info: plain text before the hints, "status: TEXT" on the CLI). Either replaces the other. The kind strings are wire values (Response.Messages), so UIError keeps its old wire value "status".

View Source
const ActionsFile = "actions.json"

ActionsFile is the user actions file in the config directory, beside bindings.json.

View Source
const AppNoun = "app"

AppNoun is the noun of the application as a whole (app.quit). It is never namespaced.

View Source
const ArgsFormID = "args-form"

ArgsFormID is the args form's modal instance (one at a time).

View Source
const BindingsHeader = `` /* 199-byte string literal not displayed */

BindingsHeader is the one comment line FormatBindings starts a file with.

View Source
const CancelKey = "esc"

CancelKey is the key every key-sequence prefix answers with mode.leave: a default row "<prefix> esc" is kept in each table that binds a sequence, so Escape cancels a pending prefix without running anything and the resolver never names a key.

View Source
const ChooserPreviewMin = 60

ChooserPreviewMin is the width from which a chooser with a Preview draws it beside the rows.

View Source
const ColumnWidthStep = 2

ColumnWidthStep is the cells z > and z < add to or take from a column.

View Source
const CompletionNoun = "ochazuke.completion"

CompletionNoun is the noun whose rows answer while suggestions are open.

View Source
const DefaultBindingTimeout = 1000 * time.Millisecond

DefaultBindingTimeout is how long a pending multi-key binding waits for its next key.

View Source
const DefaultCompletionDebounce = 150 * time.Millisecond

DefaultCompletionDebounce is the pause after the last edit before an input's search is asked.

View Source
const DefaultRecordDebounce = DefaultTreeDebounce

DefaultRecordDebounce is how long typing in a field pauses before the record is written.

View Source
const DefaultTreeDebounce = 400 * time.Millisecond

DefaultTreeDebounce is how long typing in an outline line pauses before the line is written.

View Source
const FocusDisplaySetting = "focus-display"

FocusDisplaySetting is the framework setting that shows where the focus is beside the mode in the header: off (the default) leaves the chrome as it was, noun shows the focused instance's noun (NORMAL · pane), stack the focus path from the outermost level to the innermost, open modals included (NORMAL · panes › pane-list › pane). Declared by EnsureConfigSettings unless the application declared one first; read at every frame, so a change (settings set, the Settings page, the focus.display verb, another process's write) shows on the next one.

View Source
const FocusLabelMax = 16

FocusLabelMax is the widest label (in cells) the stack shows for a level; a level with a longer label, or none, shows its noun.

View Source
const FrameworkBindingsSource = "ochazuke/"

FrameworkBindingsSource is the source prefix of the framework's embedded files in bindings defaults ("ochazuke/bindings/list.json").

View Source
const FrameworkNamespace = "ochazuke"

FrameworkNamespace is the namespace of the framework's own nouns: every noun the framework registers (inside Runtime.Builtin) is named ochazuke.<noun> (ochazuke.settings, ochazuke.list, ochazuke.page), with the short name (settings) as an alias. The app noun itself keeps its name (AppNoun), so its verbs (app.quit, an application's app.state) never collide with a framework noun.

View Source
const HelpInsertKey = "ctrl+\\"

HelpInsertKey opens the bindings help in insert mode, where ? types itself: an app row NewModel adds beside ? in normal mode. ctrl+\ is bound by no Readline default, no editor, palette, chooser, completion, filter, form or outline row, and the terminal is in raw mode, so it sends no SIGQUIT.

View Source
const HistoryFetchLimit = 100

HistoryFetchLimit bounds the fetches the ring keeps apart from the user's actions and changes, so a host polling a state verb never pushes those out.

View Source
const HistoryFetchesSetting = "history-fetches"

HistoryFetchesSetting is the declared boolean setting that lets the history file keep fetches (entries of kind fetch the user did not type into the palette). Off by default: a host polling a state verb would fill the file and push the user's own entries out of it, and nothing reads a fetch back after a restart.

View Source
const HistoryFile = "history.jsonl"

HistoryFile is the action history file in the state directory.

View Source
const HistoryLimit = 500

HistoryLimit bounds the action history ring.

View Source
const HistorySetting = "history-limit"

HistorySetting is the declared setting that bounds the history file (and the ring while it is persisted): the last N entries are kept; 0 writes and reads no file.

View Source
const ImplicitGuard = "input"

ImplicitGuard is the guard the framework puts on every editor noun row that runs an input operation (input.cancel aside): such a row answers only while the focus is an input itself, not a non-input instance an application parented under one. It is the framework's, not the user's: it is not part of a row's identity, so a row without when replaces or unbinds the default it shadows, and a user row that runs an input operation on the editor noun gets it too, as the default it replaces had.

View Source
const JobLinger = 4 * time.Second

JobLinger is how long a finished job stays on the ticker after the frame that first saw it finished. The Model schedules one JobLingerMsg for the earliest remembered finish (Model.lingerCommand), so the row goes on an idle screen; any earlier frame past the deadline drops it too, and a newer job can push it off sooner.

View Source
const JobTickerRows = 2

JobTickerRows is the most entries the jobs ticker shows; they ride the rule line above the footer (Layout.TickerRule), so the ticker takes no row of its own.

View Source
const ListFilterMode = "list_filter"

ListFilterMode is the mode the filter line is typed in.

View Source
const MissingOpensFormSetting = "palette.missing-opens-form"

MissingOpensFormSetting says whether a request missing a required argument, run from the palette's enter or from a key, opens the args form (on, the default) instead of naming the first missing word.

View Source
const MovementSection = "movement"

MovementSection is the help section that holds every motion, after the app's rows.

View Source
const NoopAction = "ochazuke.key.ignore"

NoopAction is the always-available action that does nothing: a row bound to it consumes its key (KeyConsumed), which is how a container clamps a key its children leave unhandled at an edge.

View Source
const NounPlaceLimit = 2000

NounPlaceLimit bounds the placements kept, in memory and in the file: past it the least recently seen one is dropped.

View Source
const NounPlaceWriteEvery = 2 * time.Second

NounPlaceWriteEvery is the least time between two writes of the placements file while the Model runs; Config.Flush (on quit) writes what is left.

View Source
const NounPlacesFile = "noun-places.json"

NounPlacesFile is the record of where each noun was seen, in the state directory: a JSON array of NounPlace, most recently seen first.

View Source
const PanesFile = "panes.json"

PanesFile is the remembered pane dividers in the state directory: an object of page id to Layout.Shift.

View Source
const ReferenceDir = "reference"

ReferenceDir is the directory config.export writes to by default, inside the config directory.

View Source
const SettingsPoll = time.Second

SettingsPoll is how often a Model checks settings.json for a change another process made (the CLI's settings set).

View Source
const TablesFile = "tables.json"

TablesFile is the remembered table views in the state directory: an object of table id (a list's, a grid's) to TableState.

View Source
const TerminalLeaderTimeoutSetting = "terminal-leader-timeout"

TerminalLeaderTimeoutSetting is how long leader_pending waits for the next key before it sends the leader alone: a duration, 0 waits until a key comes (tmux's prefix). Declared by EnsureTerminalLeader unless the application declared it first with its own default; the framework's declaration reads DefaultBindingTimeout and, until settings.json holds a value, the leader follows BindingTimeout as every other sequence does.

View Source
const ThemeSetting = "theme"

ThemeSetting is the declared setting that selects the colour scheme.

View Source
const ThemesFile = "themes.json"

ThemesFile is the user colour schemes file in the config directory.

View Source
const TimelineBodyNoun = "ochazuke.timeline-body"

TimelineBodyNoun is the noun of a timeline's body: the place inside the selected card while the focus walks its steps.

View Source
const TimelineCardNoun = "ochazuke.timeline-card"

TimelineCardNoun is the noun of a card that names none.

View Source
const TreeRowNoun = "ochazuke.outline-row"

TreeRowNoun is the noun of a node that names none: enter toggles it.

View Source
const UIStatus = UIError

UIStatus is the error tone's message kind.

Deprecated: use UIError (the same wire value, "status").

Variables

View Source
var BuiltinThemes = []string{"adaptive", "dark", "light", "mono"}

BuiltinThemes are the schemes every application has: adaptive follows the terminal's background, dark and light fix it, mono draws no colour.

FocusDisplayModes are focus-display's values, in cycle order.

Functions

func BindingHints

func BindingHints(r *Runtime, focus string) string

BindingHints renders the rows a focus answers to in its current mode as a hint line, from the innermost level outward (the focused instance's rows, its noun's, then its parents' up to the page and app): "enter demo.open · : palette.open · ? bindings.help". Motions (Operation.Motion) and hidden rows (counts, registers, macros, ctrl+c) are left to ? help, except a motion row with its own Hint (J reads "J/K detail"; a hint that starts with the row's key and a slash is shown alone), and of rows that run the same request only the first (innermost) key is shown. It is the data bindings.list prints, so a hint never names a key that is not bound; the descriptions are in BindingHelp (the ? modal).

func Call

func Call(ctx context.Context, path string, q Request) (any, error)

Call runs q in the application listening on the socket at path and returns its result (CallResponse keeps the messages too).

func ComposeModal

func ComposeModal(background, title, body, footer string, width, height int, styles Styles) string

ComposeModal centers a content-sized panel over the existing page, not a blank screen. Body and backdrop may contain trusted ANSI; title and footer are raw labels.

func CutKey added in v0.16.0

func CutKey(key string) (noun, verb string, ok bool)

CutKey is SplitKey with ok reporting whether the key held a dot.

func DecodeJSONC added in v0.33.0

func DecodeJSONC(data []byte, v any) error

DecodeJSONC decodes a JSONC document into v. A syntax error or a value of the wrong type names its line and column in data.

func DefaultTones added in v0.26.0

func DefaultTones() map[string]ToneStyle

DefaultTones are the built-in tones and how the built-in themes draw them, a copy.

func FitFocusPath added in v0.20.0

func FitFocusPath(levels []string, width int) string

FitFocusPath joins levels with " › " in at most width cells. When they do not fit, it cuts from the left a whole level at a time behind "… › ", keeping the innermost level; with less room the innermost level alone follows "…", cut from its left when it is wider still. A cut path always starts with "…". Width under 1 is "".

func FormatBindings added in v0.33.0

func FormatBindings(doc BindingsDoc) ([]byte, error)

FormatBindings writes doc in the grouped format: the BindingsHeader line, then "modes" (names sorted, one per line) when there are any, then "bindings" with one group per table in doc order, its rows one per line in table order (a key repeats where the table repeats it). A row is an object with its fields in the order key, action, args, hint, when, from, hidden, help, replay, capture, target, confirmed, unbind.

func FormatRows added in v0.33.0

func FormatRows(rows any) ([]byte, error)

FormatRows writes any JSON array of rows (a slice of structs or objects) one row per line, as FormatBindingRows does, each row's fields in their marshalled order (a struct's declaration order).

func FormatRowsJSON added in v0.33.0

func FormatRowsJSON(data []byte) ([]byte, error)

FormatRowsJSON rewrites a JSON array one element per line, keeping each element's key order; an error when data is not a JSON array.

func InputView

func InputView(in *Input, width, height int) string

InputView renders an input's visible text without a cursor, for inputs that are not focused. See InputBlock.

func IntArg

func IntArg(a Arguments, name string, fallback int) int

func JSONPointer

func JSONPointer(value any, path string) (any, error)

func JobsTicker

func JobsTicker(jobs []JobInfo, width int) []string

JobsTicker renders the ticker's entries: one message per job, "noun.verb state" ("noun.verb label state" when the job has a Label), with ": error" on a failure, each truncated to width (Layout.TickerRule joins them into the rule line).

func ModalSize

func ModalSize(width, height int) (contentWidth, contentHeight int)

ModalSize returns safe body bounds, reserving the same chrome as ComposeModal. Modal chrome uses one horizontal padding cell regardless of page density.

func MotionScroll added in v0.32.0

func MotionScroll(want, page int) int

MotionScroll is the scroll a motion makes in a view page lines tall when reaching its target would scroll want lines (down when positive): want itself when it is at most half the page, else half the page (at least one line) toward it.

func Namespaced added in v0.16.0

func Namespaced(namespace, name string) string

Namespaced is name under namespace: "ai" and "skills" give "ai.skills"; an empty namespace is name unchanged.

func On added in v0.36.0

func On(b bool) *bool

On returns a pointer to b, for Segment.Enabled.

func PanelContentSize

func PanelContentSize(width, height int) (int, int)

PanelContentSize returns the body cells available inside a Panel, excluding its border and horizontal padding. Tiny panels omit their chrome.

func ParseSchemes added in v0.2.0

func ParseSchemes(data []byte) (map[string]Scheme, []string)

ParseSchemes reads colour schemes in the themes.json shape, an object of schemes by name:

{"kombu": {"background": "dark", "accent": "#C9A227",
           "error": {"light": "#B3261E", "dark": "#FF8A80"},
           "tones": {"state.working": {"fg": "#5FAFD7", "bold": true}}},
 "dark":  {"tones": {"build.cached": "muted"}}}

A role left out takes the built-in palette's; "tones" (ToneStyle by tone name) are laid over the built-in tones. An entry named after a built-in theme holds only "tones": they override and add to that theme's (adaptive's reach dark and light too). A scheme that does not validate (an unknown role, a bad colour, background or tone, a built-in's name with anything but tones, an error colour equal to the accent or muted one) is skipped and reported; a file that is not a JSON object yields no schemes.

func RegisterInputOperations

func RegisterInputOperations(r *Runtime)

func SegmentSetting added in v0.36.0

func SegmentSetting(id string) string

SegmentSetting is the setting that shows or hides one segment.

func SortBindings added in v0.15.0

func SortBindings(rows []Binding)

SortBindings orders rows the way the full export (bindings.list --all, bindings.reference.json) prints them: by mode (the any-mode rows, mode "", first), then by noun, a noun's rows before an instance's of the same name, then by the request's verb (a row with no verb, an unbind, after every verb), then by keys, a pattern row after the key rows; stable, so rows equal on all of these keep their stored order.

func SpanInView added in v0.32.0

func SpanInView(from, to, page int) bool

SpanInView reports whether any of lines from through to (relative to the view's first line) is inside a view page lines tall.

func SplitKey added in v0.16.0

func SplitKey(key string) (noun, verb string)

SplitKey splits an operation key on its last dot: the noun before it (a noun may be dotted: ochazuke.settings, ai.skills) and the verb after it. It is the reading of a name nothing is registered under; a registered name splits where it was declared (a verb may hold a dot: dirs.base.add), which Runtime.CanonicalKey and the request lookup read from the table. A key with no dot is all noun and no verb.

func SplitKeys added in v0.27.0

func SplitKeys(keys string) (prefix, final string)

SplitKeys splits a key sequence into its prefix and its final key: "z l" is "z" and "l", "ctrl+a v" is "ctrl+a" and "v", "g c x" is "g c" and "x"; a single key (or a pattern, "<digit>") has no prefix.

func StandardizeJSONC added in v0.33.0

func StandardizeJSONC(data []byte) ([]byte, error)

StandardizeJSONC is data as standard JSON: each comment and trailing comma becomes spaces, so every byte offset (and so every line and column) is the original file's. Plain JSON is returned as is, without a copy. A file that is not JSONC is an error naming its line and column.

func StringArg

func StringArg(a Arguments, name, fallback string) string

func TableText added in v0.2.0

func TableText(cols []Column, rows [][]string, width int) []string

TableText lays a table out as plain text lines, the header line first, with the layout a list with columns draws (List.View) and no styling, gutter or highlight: a printer outside the TUI (the command line's --format table) reads the same column declarations. At width > 0 the columns that do not fit drop by Priority and cells cut with "…" at their width; at width <= 0 nothing drops or cuts, every column is as wide as its widest cell or title (Width and Min are ignored). A row with fewer cells than columns has empty ones; control characters in a cell become spaces; trailing spaces are trimmed. No columns is no lines.

func TextBody added in v0.17.0

func TextBody(text string) func(int) string

TextBody is a Body that is always text (wrapped by the timeline).

Types

type Action added in v0.2.0

type Action struct {
	Noun        string               `json:"noun"`
	Verb        string               `json:"verb"`
	Description string               `json:"description,omitempty"`
	Command     Argv                 `json:"command,omitempty"`
	Base        Argv                 `json:"base,omitempty"`
	Call        string               `json:"call,omitempty"`
	Target      string               `json:"target,omitempty"`
	JSON        bool                 `json:"json,omitempty"`
	Label       string               `json:"label,omitempty"`
	Confirm     string               `json:"confirm,omitempty"`
	Parameters  map[string]Parameter `json:"parameters,omitempty"`
	Positional  []string             `json:"positional,omitempty"`
	Dir         string               `json:"dir,omitempty"`
	Timeout     string               `json:"timeout,omitempty"`
	Quiet       bool                 `json:"quiet,omitempty"`
	History     string               `json:"history,omitempty"`
	// contains filtered or unexported fields
}

Action is one entry of actions.json: a verb on an existing noun that runs an external command. It becomes an application operation (noun.verb) like any other, so binding rows, the palette, the CLI, ? help and the catalog see it; its work runs off the loop as a job labelled Label, and the command's standard output is the job's result (JSON when it parses, a JSON value per line when every line does, else the text).

The command is either Command, an argv template run directly (never through a shell), or Base, the convention for a CLI with a JSON form: BASE NOUN VERB --json ARGS [--target TARGET], where NOUN VERB is Call ("noun verb", default this action's noun and verb), ARGS the action's arguments as one JSON object and TARGET the Target template when it is not empty. JSON appends --json ARGS to a Command too.

Templates ({{name}}) in Command, Base, Label, Dir and Target expand to: target or id (the instance the verb runs on), label (its label, else its id), noun and verb (the action's), data (the instance's Data: a string as is, else JSON), data.KEY (one field of Data as a JSON object), args (the arguments as JSON) or a declared parameter's value. An argv element that is exactly one placeholder and expands to nothing is left out, so an optional parameter disappears instead of passing "". Confirm is a confirm prompt template, as NounBuilder.Confirm takes. History is the kind of history entry it records (Operation.HistoryKind): "fetch" for a command that only reads; empty is a change, as for any application verb.

Actions are read only from the user's own actions.json (Config.Path): never from a catalog, a binding row, a socket call or an application's data, so nothing but the user's file decides what runs.

An entry with an "alias" field is an alias row instead (AliasRow): one more name for an existing action, which runs nothing of its own.

func (Action) Key added in v0.2.0

func (a Action) Key() string

Key is the action's operation key, noun.verb.

type ActionAlias added in v0.28.0

type ActionAlias struct {
	Alias       string `json:"alias"`
	Action      string `json:"action"`
	Noun        string `json:"noun,omitempty"`
	Verb        string `json:"verb,omitempty"`
	Since       string `json:"since,omitempty"`
	RemoveAfter string `json:"removeAfter,omitempty"`
	Note        string `json:"note,omitempty"`
	Source      string `json:"source,omitempty"`
}

ActionAlias is one extra name of a registered action. Alias is the name (noun.verb, its noun canonical), Action the canonical key it runs. Noun and Verb are the alias's own noun and verb: the subject side the handler is told (Invocation). Since and RemoveAfter mark a deprecated alias, an old name kept for one release after a rename (Keep actions): its first use in a runtime posts a one-time notice naming the new name. Note is free text shown beside it.

func (ActionAlias) Deprecated added in v0.28.0

func (a ActionAlias) Deprecated() bool

Deprecated reports whether the alias is an old name on its way out.

type AliasRow added in v0.28.0

type AliasRow struct {
	Alias  string `json:"alias"`
	Action string `json:"action"`
	Note   string `json:"note,omitempty"`
}

AliasRow is an alias entry of actions.json: {"alias": "pane.k", "action": "pane.kill"} makes pane.k another name of pane.kill, for keys (bindings.json rows), the palette, the CLI and the API. Alias is noun.verb on a registered noun (a short noun name such as list works); Action is a registered action or alias. An alias may not take the name of an action or of another alias; such a row is skipped and reported, as bindings check reports a bad row. Note is free text listed with it.

type Align

type Align string

Align is a column's alignment: AlignLeft (the zero value) or AlignRight.

const (
	AlignLeft  Align = ""
	AlignRight Align = "right"
)

Alignments.

type AppHandler

type AppHandler func(Ctx) Result

AppHandler is the signature of an application verb: it reads the Ctx and returns Done, Failed or Pending work. Pending work runs off the loop; its completion is delivered to the owning instance through the contract, so a handler never blocks key handling.

type Appearance

type Appearance struct {
	Theme   string      `json:"theme"`
	Density string      `json:"density"`
	Dark    bool        `json:"dark"`
	Accent  color.Color `json:"-"`
	// Schemes are the colour schemes beyond the built-in themes, by name:
	// the user's themes.json (Model.Init, themes.reload and config.reload set it) or an
	// application's own (NewSchemeSet). A pointer keeps Appearance
	// comparable.
	Schemes *SchemeSet `json:"-"`
}

Appearance is event-loop-owned configuration. Operation results are value snapshots. Dark is the terminal background reported by tea.BackgroundColorMsg; the adaptive theme assumes a dark background until that message arrives.

Accent replaces the theme's accent colour when it is not nil: the title, panel and modal borders, the active tab, the badge and every highlight drawn in the accent take it, so an application can say "you are looking at everything" by colour alone (a pin state). Semantic colours (errors) and the muted text keep their meaning. The mono theme and NO_COLOR stay colourless. Applications set it in code; the appearance verbs leave it.

func NewAppearance

func NewAppearance(r *Runtime) *Appearance

NewAppearance registers appearance operations once per runtime.

func (Appearance) Background added in v0.4.0

func (a Appearance) Background() (background string, plain bool)

Background is the background the appearance draws on, "light" or "dark" (an adaptive scheme follows Dark), and whether it draws no colour at all (the mono theme or NO_COLOR). A renderer with its own palette, such as the markdown package's Glamour styles, picks from it.

func (Appearance) ThemeNames added in v0.2.0

func (a Appearance) ThemeNames() []string

ThemeNames are the schemes Theme can name: the built-in themes, then the application's or user's (Schemes) by name.

type ArgumentError added in v0.1.1

type ArgumentError struct{ Err error }

ArgumentError is a request refused for its arguments (a missing required one, a wrong type, a value outside the enum or the source's choices) before any instance was chosen, so a caller never blames the target for it (the CLI adds no "pass --target" hint).

func (*ArgumentError) Error added in v0.1.1

func (e *ArgumentError) Error() string

func (*ArgumentError) Unwrap added in v0.1.1

func (e *ArgumentError) Unwrap() error

type Arguments

type Arguments map[string]any

Arguments are a request's named arguments, keyed by parameter name.

func ParseArguments

func ParseArguments(raw string) (Arguments, error)

ParseArguments parses the same JSON object used by the wire API.

type Argv added in v0.2.0

type Argv []string

Argv is a command line: a JSON array of words, or a string split on spaces (use the array for a word holding a space).

func (*Argv) UnmarshalJSON added in v0.2.0

func (v *Argv) UnmarshalJSON(data []byte) error

UnmarshalJSON reads an array of strings or one string split on spaces.

type Binding

type Binding struct {
	// The field order is the bindings file's (BindingFieldOrder): mode,
	// the noun or instance, the keys or pattern, the request, then the
	// rest. FormatBindingRows writes a file in that order (and the
	// request noun before verb); plain json.Marshal follows the
	// declarations below.
	Mode     string            `json:"mode,omitempty"`
	Noun     string            `json:"noun,omitempty"`
	Instance string            `json:"instance,omitempty"`
	Keys     string            `json:"keys,omitempty"`
	Pattern  string            `json:"pattern,omitempty"`
	Request  Request           `json:"request"`
	When     string            `json:"when,omitempty"`
	Unbind   bool              `json:"unbind,omitempty"`
	From     map[string]string `json:"from,omitempty"`
	Replay   bool              `json:"replay,omitempty"`
	Capture  bool              `json:"capture,omitempty"`
	// Hint replaces "noun.verb: description" in the footer and the help
	// for a row whose operation says nothing useful on its own (the
	// terminal leader's mode.enter); bindings list carries it too.
	Hint string `json:"hint,omitempty"`
	// Hidden keeps the row off the footer's hint line (counts, registers,
	// macros, ctrl+c); ? help and bindings list still show it.
	Hidden bool `json:"hidden,omitempty"`
	// Help replaces the operation's description in ? help and bindings
	// list for a row whose operation is generic (mode.enter into a
	// register, macro or count mode).
	Help string `json:"help,omitempty"`
	// Aliases are the other names of the row's operation, which bindings
	// list adds so a row names its canonical operation and its aliases
	// once; a stored row never holds them (a file's value is dropped).
	Aliases []string `json:"aliases,omitempty"`
}

Binding is one row of a mode table, identical in code and config: {noun | instance, mode, keys | pattern, request | null, from, when}. Keys is an exact key or a space-separated sequence ("g g"); Pattern is a key class: digit, alpha, printable, paste, any, a class minus keys ("alpha-q"), or a set ("one-of:h j k l"); upper is one uppercase letter and ctrl+alpha one ctrl+letter (ctrl+a to ctrl+z). From fills request arguments from the key or the mode payload (see Sources). When names a registered guard. A row's identity is its table (noun or instance), mode, keys or pattern and guard: a row replaces only the row with the same identity, so a row with When and the unguarded row on the same keys coexist, and Unbind removes only the row with the same identity (an unbind row without When removes the unguarded row, never a guarded one). The editor noun's implicit "input" guard is not part of the identity (see withImplicitGuard). Replay dispatches the key again, fresh, after the row ran (a pending mode that hands an unknown key back to the stack). Capture rows are consulted outer-first (app first) before the inner-first walk (see resolve.go).

A row is atomic: a Request (one registered operation, its arguments from the row, the key or the payload), a mode transition (Enter, Leave) or an unbind. There is no row that runs Go code with the key. Composite behaviour is an operation whose handler calls other operations through Execute (macro.record, terminal.paste under the leader); a condition is a named guard. TestBindingRowsAreAtomic pins this for every table.

func Do

func Do(operation string, args ...Arguments) Binding

Do builds a row that runs "noun.verb" with optional arguments.

func Enter

func Enter(mode string, payload ...Payload) Binding

Enter builds a row that enters a pending mode with a typed payload: the operator fields (Op, Count, OpCount) and, for a leader, Prefix (the key sequence the mode holds, which arms the sequence timer) and Request (what the timer runs when no second key arrives).

func HintBindings added in v0.16.0

func HintBindings(r *Runtime, focus string) []Binding

HintBindings is the rows BindingHints shows, in its order: what a check of the footer presses to prove every hint answers.

func Leave

func Leave() Binding

Leave builds a row that returns from a pending mode to the mode it shadowed.

func (Binding) Captured

func (b Binding) Captured() Binding

Captured marks a row as a capture row: consulted outer-first before the inner-first walk (config field "capture").

func (Binding) FromKey

func (b Binding) FromKey(arg string) Binding

FromKey fills an argument from the pressed key name.

func (Binding) FromPayload

func (b Binding) FromPayload(arg, source string) Binding

FromPayload fills an argument from the mode payload (a Source name).

func (Binding) FromRow added in v0.3.0

func (b Binding) FromRow(arg, field string) Binding

FromRow fills an argument from the Data of the row the key acted on: the innermost instance carrying Data from the focus out to the level the row answered at (a list row's ListItem.Data, also when the row is bound on the list). field names a key of that value (a map's key, a struct's JSON field name; dots walk nested values, "repo.path"); "" passes the whole value. A struct is read through its JSON encoding, so the value is what the CLI would send (numbers float64, the whole row an object). A row without Data, or without the field, leaves the argument out. The source is "row:<field>" (or "row") in a binding file.

func (Binding) Guard

func (b Binding) Guard(name string) Binding

Guard names the registered guard the row requires.

func (Binding) Hide

func (b Binding) Hide() Binding

Hide keeps the row off the footer (config field "hidden"); ? help and bindings list still show it.

func (Binding) Hinted

func (b Binding) Hinted(text string) Binding

Hinted sets the row's hint (config field "hint"): the text shown after its key in place of the operation and its description.

func (Binding) Join

func (b Binding) Join(arg string, sources ...string) Binding

Join fills an argument from several sources joined with a space: the leader's any row sends Join("keys", SourcePrefix, SourceKey).

type BindingGroup added in v0.33.0

type BindingGroup struct {
	Noun     string
	Instance string
	Mode     string
	Extends  string
	// Fallback ("fallback": true) keeps a default file's group to the keys
	// its table did not bind before the file loaded (LoadDefaultBindings):
	// an application's row bound earlier keeps its key.
	Fallback bool
	// Source ("source") is where the group's rows are declared, as
	// bindings defaults writes it; a reader keeps it and nothing acts on it.
	Source string
	Rows   []Binding
}

BindingGroup is one table of a grouped bindings file: a noun's or an instance's mode table and its rows in table order. Every row carries the group's Noun or Instance and Mode.

func GroupBindings added in v0.33.0

func GroupBindings(rows []Binding) []BindingGroup

GroupBindings groups rows into tables: nouns by name, then each noun's modes by name ("" first), then instances by ID and mode, each group's rows in their given order.

func (BindingGroup) Context added in v0.33.0

func (g BindingGroup) Context() string

Context is the group's "context" spelling, "noun/mode" or a bare noun; "" for an instance group.

type BindingTimeoutMsg

type BindingTimeoutMsg struct {
	// contains filtered or unexported fields
}

BindingTimeoutMsg resolves a pending key sequence on the owning event loop. Messages from an older sequence or another runtime are ignored.

type BindingsDoc added in v0.33.0

type BindingsDoc struct {
	Modes  map[string]ModeSpec
	Groups []BindingGroup
}

BindingsDoc is a parsed bindings file: its modes and its groups, in file order. A rows-format file is one group per row run of the same table.

func ParseBindings added in v0.33.0

func ParseBindings(data []byte) (BindingsDoc, string, error)

ParseBindings reads a bindings file in either format (JSONC), with the format it found. A malformed file is an error with its line and column; a bad group or value is an error naming the group and key.

func (BindingsDoc) Rows added in v0.33.0

func (d BindingsDoc) Rows() []Binding

Rows are the doc's rows, group by group in file order.

type Block

type Block struct {
	Text   string
	Cursor *Pos
}

Block is the result of rendering. Text is trusted ANSI. Cursor is where the focused input's cursor sits inside Text, or nil when the block holds no focused input. Only the innermost focused component supplies a cursor; composers move it by where they placed the child and never invent one.

func ComposeModalBlock

func ComposeModalBlock(background, title string, body Block, footer string, width, height int, styles Styles) Block

ComposeModalBlock is ComposeModal for a body that may carry a cursor. The result's cursor is the body's, moved to where the panel placed it; the backdrop never contributes one because a modal covers whatever it held.

func InputBlock

func InputBlock(in *Input, width, height int) Block

InputBlock renders width x height cells of an input around its cursor and reports where the cursor sits, without changing text or editing state. Visual selections are painted in reverse video; the cursor itself is left to the terminal, so compose this block only for the focused input.

func Text

func Text(s string) Block

Text wraps rendered text that carries no cursor.

func (Block) Clip

func (b Block) Clip(width, height int) Block

Clip drops a cursor that fell outside a width x height cell area whose top-left corner is the block's origin, as when a composer clipped the child.

func (Block) Offset

func (b Block) Offset(dx, dy int) Block

Offset returns the block with its cursor moved by dx, dy. Text is unchanged.

func (Block) WithText

func (b Block) WithText(text string) Block

WithText replaces the text and keeps the cursor, for composers that rewrite the child's cells without moving them.

type CallMsg

type CallMsg struct {
	Request Request
	Reply   chan Response
}

CallMsg is delivered to the UI loop; only that loop may call Runtime.Execute.

type Card added in v0.17.0

type Card struct {
	ID       string                 `json:"id"`
	Noun     string                 `json:"noun,omitempty"`
	Label    string                 `json:"label,omitempty"`
	Data     any                    `json:"data,omitempty"`
	Header   string                 `json:"header"`
	Meta     string                 `json:"meta,omitempty"`
	Body     func(width int) string `json:"-"`
	Sections []CardSection          `json:"sections,omitempty"`
	Steps    []CardStep             `json:"steps,omitempty"`
	Open     bool                   `json:"open,omitempty"`
	Active   bool                   `json:"active,omitempty"`
	Muted    bool                   `json:"muted,omitempty"`
	Group    string                 `json:"group,omitempty"`
	Revision uint64                 `json:"revision,omitempty"`
	// Event draws the card as something that happened rather than
	// something said: indented one level (eventIndent cells), its border
	// dashed and muted until it is selected. A task's notification is one.
	Event bool `json:"event,omitempty"`
}

Card is one entry of a timeline: a box with a header that always shows (wrapped onto as many lines as it needs) and a meta line under it, and a body shown only while the card is open. Like a ListItem, a card is an instance of its noun (Noun, default TimelineCardNoun) under the timeline's list, so the app's verbs and rows on that noun answer on it and Data is what a verb reads as Ctx.Instance.Data. Label is the instance's label (palette, {{label}}, the / filter), default the header on one line.

Body and every section's Body are asked lazily, with the inner width, only while the card is open and drawn (or scrolled across); the result is kept until the width, the folds or Revision change, so an app bumps Revision whenever a body's text changes (a streaming answer). Open is the card's state the first time the timeline sees its id; after that the user's open and close follow the id. Active draws the card brighter (its header bold, a "●" in its top border, which reads in mono too); Muted draws its header and meta in the muted style (an older card). A card whose Group differs from the card above it is preceded by a full-width rule with the group as its label (a session boundary).

Steps are the places inside an open card that l steps onto and j/k walk (timeline_steps.go). A card with Steps draws its body from them, and Body and Sections are not drawn; a card without them steps through its Body's paragraphs (blank-line separated, a fenced code block one paragraph) and then each section, title and text, as one step.

type CardRole added in v0.17.0

type CardRole string

CardRole is the style a section's text draws in.

const (
	CardRoleText   CardRole = ""
	CardRoleMuted  CardRole = "muted"
	CardRoleAccent CardRole = "accent"
	CardRoleError  CardRole = "error"
)

Card roles: the theme's text, muted (thinking), accent, error.

type CardSection added in v0.17.0

type CardSection struct {
	Name    string                 `json:"name,omitempty"`
	Title   string                 `json:"title"`
	Summary string                 `json:"summary,omitempty"`
	Role    CardRole               `json:"role,omitempty"`
	Folded  bool                   `json:"folded,omitempty"`
	Body    func(width int) string `json:"-"`
}

CardSection is one foldable part of an open card's body, drawn after Body: a title line ("▾ 2 thinking · Summary") and, unfolded, its text indented in its Role's style. Name keys its fold state (default the title, else its number); Folded is its state until the user folds it.

type CardStep added in v0.30.0

type CardStep struct {
	ID         string                 `json:"id,omitempty"`
	Title      string                 `json:"title,omitempty"`
	Summary    string                 `json:"summary,omitempty"`
	Role       CardRole               `json:"role,omitempty"`
	Folded     bool                   `json:"folded,omitempty"`
	Header     bool                   `json:"header,omitempty"`
	Paragraphs bool                   `json:"paragraphs,omitempty"`
	Body       func(width int) string `json:"-"`
}

CardStep is one place inside an open card (Card.Steps), in order.

  • Header: the step is the card's header (a prompt): it draws nothing of its own, its lines are the header's and its text is Header.
  • A Title draws a bold title line; with a Body the step folds like a section ("▾ 2 tool Bash · summary", z 2, timeline.fold {section} by ID, title or number), Folded its first state, and its body is indented under the title. Folded, the title is the whole step; l inside opens it.
  • Paragraphs splits the Body into steps of its own, one per paragraph (blank-line separated, a fenced code block one paragraph); under a Title the title line belongs to the first.
  • Without a Title the Body draws at the full width in Role.

Steps follow each other with a blank line between them, except two titled steps in a row (they stack as sections do). ID keys the fold.

type Choice

type Choice struct {
	ID    string `json:"id"`
	Label string `json:"label"`
}

Choice is one value a parameter accepts: ID is the argument value, Label what a pick list shows.

func ParseChoices

func ParseChoices(result any) ([]Choice, error)

ParseChoices reads a source operation's result: []string, []Choice, []PaletteInstance, []Operation (the id is noun.verb), []map whose id is "id", else "key", else "name" and whose label is "label", else "title", or []any of strings or such maps (the JSON-decoded form).

type Chooser added in v0.2.0

type Chooser struct {
	// contains filtered or unexported fields
}

Chooser is one open chooser, the Component of its modal instance.

func (*Chooser) Close added in v0.2.0

func (c *Chooser) Close()

func (*Chooser) HandleMessage added in v0.2.0

func (c *Chooser) HandleMessage(tea.Msg) bool

The delivery contract: a chooser runs no jobs of its own (Choose and Create return their own results). Pending work they return belongs to the focus the chooser opened over (runApplication), since the chooser and its input are closed before the callback runs.

func (*Chooser) Highlighted added in v0.2.0

func (c *Chooser) Highlighted() (ChooserRow, bool)

Highlighted is the row Enter takes: the moved-to row, else the top match; none means Enter creates.

func (*Chooser) Kind added in v0.2.0

func (c *Chooser) Kind() string

Kind is the chooser's modal kind.

func (*Chooser) Matches added in v0.2.0

func (c *Chooser) Matches() []ChooserRow

Matches are the rows the query keeps: matches first, then trailing rows, then the create row, then the fixed rows.

func (*Chooser) ModalBlock added in v0.2.0

func (c *Chooser) ModalBlock(_ Styles, width, height int) Block

func (*Chooser) ModalTitle added in v0.2.0

func (c *Chooser) ModalTitle() string

ModalTitle and ModalBlock make a chooser a self-drawing modal body (ModalBody), drawn like a record editor and its chooser.

func (*Chooser) Moved added in v0.2.0

func (c *Chooser) Moved() bool

Moved reports whether the user moved the highlight since the last edit.

func (*Chooser) PendingCommand added in v0.2.0

func (c *Chooser) PendingCommand() tea.Cmd

func (*Chooser) Query added in v0.2.0

func (c *Chooser) Query() string

Query is the search input's text.

func (*Chooser) SetRows added in v0.2.0

func (c *Chooser) SetRows(rows []ChooserRow)

SetRows replaces the rows (a background load landing); the highlight stays on the same row id when the user had moved it.

func (*Chooser) Title added in v0.2.0

func (c *Chooser) Title() string

Title is the chooser modal's title.

func (*Chooser) View added in v0.2.0

func (c *Chooser) View(width, height int) Block

View draws the search line, then the rows with the highlighted one in reverse video, with what Enter would create when nothing is highlighted after the matches (before the fixed rows); with a Preview and the width for it, the highlighted row's preview on the right. Its cursor is the search input's.

type ChooserRow added in v0.2.0

type ChooserRow struct {
	ID       string
	Label    string
	Data     any
	Trailing bool
	Fixed    bool
	// Cells, when set, draw the row as aligned columns (each column as
	// wide as its widest cell among the rows); Label stays what the
	// filter reads.
	Cells []string
}

ChooserRow is one choice. Trailing rows (a "browse…" entry) follow the matches. Enter takes one without moving onto it only when the query is not empty and no ordinary row matches, so their filter should be strict (a substring of the label); an empty query with no ordinary rows creates. Fixed rows (a "no pin" entry) sit at the bottom whatever the query: the filter never sees them and Enter takes one only once the highlight is moved onto it, or when the query spells it.

type ChooserSpec added in v0.2.0

type ChooserSpec struct {
	Kind        string
	Title       string
	Rows        []ChooserRow
	Filter      func(query string, row ChooserRow) bool
	Choose      func(ChooserRow) Result
	Create      func(query string) Result
	CreateLabel func(query string) string
	CreateRow   bool
	Empty       string
	// Keep names the columns (indexes into ChooserRow.Cells) cut last when
	// the cells do not fit (a directory chooser's path): the others narrow
	// to their minimum first, then the kept ones, widest first.
	Keep []int
	// Preview, when set, draws the highlighted row's text beside the rows
	// (wrapped, cut at the bottom) once the chooser is ChooserPreviewMin
	// cells wide; narrower, the rows take the whole width.
	Preview func(ChooserRow) string
}

ChooserSpec opens a chooser. Kind is the modal's kind (one open chooser per kind). Title is the modal's title. Filter defaults to a fuzzy match on Label; matches whose label holds the query whole come first, otherwise in Rows order. Create is optional; without it Enter on no match fails. CreateLabel names what Enter would create from the query. CreateRow shows that as a "+ <label>" row while the query is not empty, after the matches and trailing rows and before the fixed rows; typing never matches or ranks it, it is the default only when nothing else matches, and Enter on it creates. Empty is the line shown while there are no rows at all ("loading…"). Keep names the cell columns cut last. Preview draws a preview of the highlighted row beside the rows when there is room.

type Column

type Column struct {
	Title    string `json:"title"`
	Width    int    `json:"width,omitempty"`
	Min      int    `json:"min,omitempty"`
	Max      int    `json:"max,omitempty"`
	Align    Align  `json:"align,omitempty"`
	Priority int    `json:"priority,omitempty"`
	Wrap     bool   `json:"wrap,omitempty"`
	Pinned   bool   `json:"pinned,omitempty"`
	Group    bool   `json:"group,omitempty"`
}

Column is one column of a table. Width fixes the column's cells (0: a flexible column, as wide as its widest cell or title when there is room); Min is a flexible column's least width before columns drop (default: 10 cells or its title's width when wider, at most its widest cell or title). Priority orders dropping when the width is short: the column with the largest Priority drops first, ties the rightmost first; 0, the default, is the most important, and the last column left never drops (it is cut instead). Max caps a flexible column's growth (0: up to its widest cell). Wrap makes List.View wrap a cell longer than its column onto further lines of the row instead of cutting it: the row is as tall as its tallest wrapped cell, the other cells sit on its first line (TableText still cuts). Group makes List.View draw a cell blank when it repeats the cell above it in the same section (and every Group column left of it does too), so a run of rows sharing a value (the help's key prefix) shows it once; the first row a frame draws always shows its cells, and the rows' Cells keep the value (TableText does not blank).

type Completer added in v0.5.0

type Completer struct {

	// Debounce is the pause before a search is asked
	// (DefaultCompletionDebounce when 0).
	Debounce time.Duration
	// contains filtered or unexported fields
}

Completer is the inline completion component: which input it follows, the token whose suggestions are wanted and the answer on screen.

func (*Completer) Close added in v0.5.0

func (c *Completer) Close()

Close hides the list and cancels its searches.

func (*Completer) DebounceCurrent added in v0.5.0

func (c *Completer) DebounceCurrent(msg CompletionDebounceMsg) bool

DebounceCurrent reports whether a pause timer is for the latest request, whose search has not started yet.

func (*Completer) DebounceTick added in v0.5.0

func (c *Completer) DebounceTick() (CompletionDebounceMsg, bool)

DebounceTick is the pause timer in flight, for a host that delivers it at once (tests, o-story): false when no search is waiting on one.

func (*Completer) Err added in v0.5.0

func (c *Completer) Err() error

Err is the search error on screen, nil for none.

func (*Completer) HandleMessage added in v0.5.0

func (c *Completer) HandleMessage(msg tea.Msg) bool

HandleMessage starts a search when its pause ends and takes a search's answer.

func (*Completer) Highlighted added in v0.5.0

func (c *Completer) Highlighted() int

Highlighted is the highlighted suggestion's index, -1 for none.

func (*Completer) Input added in v0.5.0

func (c *Completer) Input() string

Input is the input the list belongs to, "" when none is followed.

func (*Completer) Open added in v0.5.0

func (c *Completer) Open() bool

Open reports whether the suggestion list is shown (its rows answer).

func (*Completer) PendingCommand added in v0.5.0

func (c *Completer) PendingCommand() tea.Cmd

PendingCommand looks at the focused input and schedules the pause timer of a new request.

func (*Completer) Searching added in v0.5.0

func (c *Completer) Searching() bool

Searching reports whether a search newer than the answer on screen runs.

func (*Completer) State added in v0.5.0

func (c *Completer) State() map[string]any

State is what the completion verbs return.

func (*Completer) Suggestions added in v0.5.0

func (c *Completer) Suggestions() []Suggestion

Suggestions are the answer on screen.

func (*Completer) Token added in v0.5.0

func (c *Completer) Token() (CompletionToken, bool)

Token is the token whose suggestions are wanted, and whether there is one.

type Completion

type Completion struct {
	Ticket
	Value any
	Err   error
}

Completion is the message off-loop work sends back. It is delivered to the owning instance only while its ticket is current.

type CompletionDebounceMsg added in v0.5.0

type CompletionDebounceMsg struct {
	Seq uint64
}

CompletionDebounceMsg is the pause timer of one request: when it is the latest (Completer.DebounceCurrent) the search is started.

type CompletionSearch added in v0.5.0

type CompletionSearch func(ctx context.Context, query string) ([]Suggestion, error)

CompletionSearch answers a query, the token after its trigger, with suggestions. It runs off the loop, as a job; its context is cancelled when the answer can no longer be shown (the suggestions were dismissed, or a newer answer is already on screen). An error is shown in the list.

func SearchStrings added in v0.5.0

func SearchStrings(search func(ctx context.Context, query string) ([]string, error)) CompletionSearch

SearchStrings adapts a search that answers plain strings, for the usual case where what the list shows is what is inserted.

type CompletionToken added in v0.5.0

type CompletionToken struct {
	Start   int    `json:"start"`
	End     int    `json:"end"`
	Trigger string `json:"trigger"`
	Query   string `json:"query"`
}

CompletionToken is the trigger token under an input's cursor: the rune range [Start, End) it covers, its trigger and its query (the token after the trigger).

func FindCompletionToken added in v0.5.0

func FindCompletionToken(text []rune, cursor int, triggers []string) (CompletionToken, int, bool)

FindCompletionToken finds the token holding the cursor in text and the index of the trigger it starts with. A token is a run of non-whitespace; it counts only when it starts with a trigger (the longest one, when triggers overlap) and the cursor is past that trigger, at the token's end or anywhere inside it.

type CompletionTrigger added in v0.5.0

type CompletionTrigger struct {
	Trigger string
	Search  CompletionSearch
}

CompletionTrigger is one trigger of an input: the characters that start a completion token and the search that answers its query.

type Component

type Component interface {
	HandleMessage(tea.Msg) bool
	PendingCommand() tea.Cmd
	Close()
}

Component is the delivery contract every instance that works off the loop implements through Instance.Component. HandleMessage applies a result on the loop and reports whether the message was for this component; PendingCommand hands the Bubble Tea adapter the work scheduled since the last drain; Close cancels outstanding work and releases anything held. The picker and the registers component are the framework's own; an application adds its own by setting Instance.Component.

type Config added in v0.2.0

type Config struct {
	App string
	Dir string
	// Report, when set, receives every problem as it is found: a bindings
	// row that was skipped, a file that does not parse, an unknown or
	// invalid setting. NewModel sets it to Model.Notify, so problems are
	// persistent footer notices.
	Report func(problem string)
	// Withdraw, when set, takes back a problem Report sent once it no
	// longer holds (bindings.reload of a fixed file); NewModel sets it to
	// drop the notice.
	Withdraw func(problem string)
	// StateDir, when set, is the state directory (the history file);
	// else $XDG_STATE_HOME/<App>, else ~/.local/state/<App>.
	StateDir string
	// NoHistoryFile keeps the action history in memory only: an
	// application opts out of history.jsonl.
	NoHistoryFile bool
	// contains filtered or unexported fields
}

Config is the application's per-user configuration directory and what the framework loaded from it: the user bindings file (bindings.json, rows in the bindings.load shape that replace or unbind the defaults by noun, mode and keys) and the declared settings (settings.json). It is one instance under app, reached through Runtime.Config(), never a Runtime field.

The directory is Dir when set, else $XDG_CONFIG_HOME/<App>, else ~/.config/<App>. App defaults to the binary's name (a test binary, NAME.test, has none), so every application gets the files without code; an application whose directory is named otherwise sets App before the Model starts (mtm sets "matomeru"). With App and Dir both empty there is no directory: nothing is read or written.

func (*Config) ActionProblems added in v0.2.0

func (c *Config) ActionProblems() []string

ActionProblems are the problems the last read of actions.json found.

func (*Config) BindingProblems added in v0.2.0

func (c *Config) BindingProblems() []string

BindingProblems are the problems the last load of the user bindings found (rows skipped, a file that does not parse).

func (*Config) BindingsFormat added in v0.33.0

func (c *Config) BindingsFormat() string

BindingsFormat is the format the last read of bindings.json found: BindingsGrouped, BindingsRows, or "" when there was no file.

func (*Config) Close added in v0.2.0

func (c *Config) Close()

Close keeps everything: the config lives as long as the runtime.

func (*Config) Directory added in v0.2.0

func (c *Config) Directory() string

Directory is the config directory, "" when there is none.

func (*Config) Disable added in v0.2.0

func (c *Config) Disable()

Disable gives the runtime no config directory (App and Dir empty): a sandbox such as an o-story session reads and writes no user files, and keeps no history file.

func (*Config) Flush added in v0.2.0

func (c *Config) Flush() error

Flush stops the history writer once every line sent to it is written, and writes the placements record (NounPlacesFile) and stops its writer. The Model calls it on quit; later requests are not persisted.

func (*Config) FrameworkTheme added in v0.2.0

func (c *Config) FrameworkTheme() bool

FrameworkTheme reports whether the theme setting is the framework's (the Model applies it to Appearance.Theme) rather than the application's own.

func (*Config) HandleMessage added in v0.2.0

func (c *Config) HandleMessage(tea.Msg) bool

HandleMessage: the config does no off-loop work.

func (*Config) HistoryPath added in v0.2.0

func (c *Config) HistoryPath() string

HistoryPath is the history file, "" when the history is not persisted (no state directory, or the application set NoHistoryFile).

func (*Config) NounPlacesPath added in v0.15.0

func (c *Config) NounPlacesPath() string

NounPlacesPath is the placements file, "" when placements are not persisted (no state directory: Config.Disable, a test binary).

func (*Config) PanesPath added in v0.4.0

func (c *Config) PanesPath() string

PanesPath is the panes file, "" when dividers are not persisted (no state directory: Config.Disable, a test binary).

func (*Config) Path added in v0.2.0

func (c *Config) Path(name string) string

Path is a file in the config directory, "" when there is none.

func (*Config) Paths added in v0.6.0

func (c *Config) Paths() ConfigPaths

Paths are the config, state and default reference directories.

func (*Config) PendingCommand added in v0.2.0

func (c *Config) PendingCommand() tea.Cmd

PendingCommand: nothing is ever scheduled.

func (*Config) Schemes added in v0.2.0

func (c *Config) Schemes() map[string]Scheme

Schemes are the user's colour schemes from themes.json, by name, as the last read found them (EnsureConfigSettings, themes.reload).

func (*Config) StateDirectory added in v0.2.0

func (c *Config) StateDirectory() string

StateDirectory is the application's state directory, "" when there is none: StateDir when set, else $XDG_STATE_HOME/<App>, else ~/.local/state/<App>. Disable leaves none.

func (*Config) TablesPath added in v0.12.0

func (c *Config) TablesPath() string

TablesPath is the tables file, "" when table views are not persisted (no state directory: Config.Disable, a test binary).

type ConfigExport added in v0.6.0

type ConfigExport struct {
	Dir    string   `json:"dir"`
	Files  []string `json:"files"`
	Config string   `json:"config"`
	State  string   `json:"state"`
}

ConfigExport is what config.export wrote: the directory, the files in it (absolute paths, in the order written) and where the live files are.

type ConfigPaths added in v0.6.0

type ConfigPaths struct {
	Config    string `json:"config"`
	State     string `json:"state"`
	Reference string `json:"reference"`
}

ConfigPaths are where the application's user files live: Config is the config directory (bindings.json, settings.json, actions.json, themes.json), State the state directory (history.jsonl, panes.json) and Reference the directory config.export writes to by default. A directory the application has none of is "".

type Context

type Context struct {
	Path    []string `json:"path"`
	InputID string   `json:"inputId,omitempty"`
}

Context is the derived view of a focus: the instance ids from app down to the focused instance (Path), and the focused input when there is one.

func (Context) Clone

func (c Context) Clone() Context

Clone returns a copy whose Path does not share the original's backing array.

type Ctx

type Ctx struct {
	Runtime  *Runtime
	Context  Context
	Args     Arguments
	Instance *Instance
	// Subjects are the instances the request acts on: Instance alone,
	// or, for a verb that takes many (Operation.Many) run over a visual
	// selection, every selected row in display order (Instance among them).
	Subjects []*Instance
	// Invoked is the name the request ran the verb by: its own key, or an
	// alias (an old name, a user's actions.json alias, the other noun of
	// an operation reachable under two nouns), with that name's noun as
	// the subject side.
	Invoked Invocation
}

Ctx is what an application handler receives: the runtime, the context the request runs in, its arguments and the instance it addresses (id, noun, payload).

func (Ctx) ClearNotice added in v0.4.0

func (c Ctx) ClearNotice(id NoticeID)

ClearNotice takes back a notice Notify posted.

func (Ctx) Each added in v0.36.0

func (ctx Ctx) Each(one AppHandler) Result

Each is how a verb that takes many subjects (Operation.Many) makes its change once it knows what to do (an argument given, a chooser answered): one runs for every subject in order, each with a Ctx narrowed to that subject (Instance, Context and Subjects), so the one-subject body stays as it was. A run over several posts the notice a per-row run would (subjectsNotice): one info, or one error counting the done and the failed. A body must answer synchronously (Done or Failed); pending work counts as a failure. On one subject Each is that subject's Result.

func (Ctx) Error added in v0.31.0

func (c Ctx) Error(text string)

Error sets the status line to an error through the runtime's UI (the TUI's "Error: TEXT" until the next key, an error: line on the CLI); "" clears it. A verb that fails returns Failed instead; Error is for what fails beside the result (a job's Then, a refresh).

func (Ctx) Info added in v0.31.0

func (c Ctx) Info(text string)

Info sets the status line to a plain message through the runtime's UI (before the hints until the next key, a status: line on the CLI); "" clears it. Notify is the notice that stays.

func (Ctx) Notify added in v0.4.0

func (c Ctx) Notify(text string) NoticeID

Notify posts a persistent notice through the runtime's UI (the footer in the TUI, a notice: line on the CLI) and returns its id.

func (Ctx) Status deprecated added in v0.4.0

func (c Ctx) Status(text string)

Status is Error: the status line in its error tone.

Deprecated: use Error for what failed, Info for a plain message.

func (Ctx) SubjectIDs added in v0.36.0

func (ctx Ctx) SubjectIDs() []string

SubjectIDs are the ids of ctx's subjects, for a handler that hands them on (Request.Subjects of the request it runs next: a confirm, a chooser's choice).

type DefaultBinding added in v0.34.0

type DefaultBinding struct {
	Binding
	Source string `json:"source"`
}

DefaultBinding is one row of the default layer and where it is declared: a file's source, SourceGo or SourceGenerated.

type Detail added in v0.2.0

type Detail struct {
	// contains filtered or unexported fields
}

Detail is the component a detail instance fronts: a page's read-only detail region (the text beside or below a list about its selected row) that scrolls while the focus stays in the list. The application names the item the detail is about with the func it gives AddDetail (the selected row's id) and draws the text through Window; the framework keeps the offset. A new item starts at the top, checked on every Window and every scroll, so a scroll typed before the next frame lands on the new item; the same item keeps its offset while its text refreshes (clamped to the new length). J and K on the app noun (detail.scroll, counts work: 10J) scroll the current page's detail while it overflows and the focus is on the page outside it; a focused noun that binds J or K (the outline) keeps them, as inner rows answer first. A detail is not a focus stop.

func (*Detail) ID added in v0.2.0

func (d *Detail) ID() string

ID is the detail's instance id.

func (*Detail) Item added in v0.2.0

func (d *Detail) Item() string

Item is the item the offset belongs to.

func (*Detail) Marker added in v0.2.0

func (d *Detail) Marker() string

Marker is the position marker of the last Window: first and last visible lines and the total, "12–40 / 300", or "" when the text fits.

func (*Detail) Offset added in v0.2.0

func (d *Detail) Offset() int

Offset is the first visible line, from 0.

func (*Detail) Overflows added in v0.2.0

func (d *Detail) Overflows() bool

Overflows reports whether the last Window had more lines than it showed.

func (*Detail) Scroll added in v0.2.0

func (d *Detail) Scroll(lines int) int

Scroll moves the window by lines (down when positive), clamped to the text the last Window measured, and returns the new offset. A new item starts at the top first.

func (*Detail) Window added in v0.2.0

func (d *Detail) Window(text string, width, height int) (body, marker string)

Window returns the lines of text the detail shows in a width by height window (text wrapped to width first when width > 0, so a line is a row on screen) and the position marker, "12–40 / 300", when the text overflows the window ("" when it fits).

type EditNode

type EditNode struct {
	Text     string `json:"text"`
	Cursor   int    `json:"cursor"`
	Parent   int    `json:"parent"`
	Children []int  `json:"children"`
}

EditNode is one state in an input's undo tree: its text, cursor, parent and children.

type FocusTargeter

type FocusTargeter interface {
	FocusTarget() string
}

FocusTargeter is a component that knows where focus lands inside it: a list on its selected row, a grid on its cursor cell.

type Footer struct {
	Text  string
	Error bool
	Jobs  []string
}

Footer is the bottom row of the page chrome: a raw label, styled as an error when Error is set. Jobs are the jobs ticker's entries (Model.JobsTicker), drawn in the rule line above the footer (TickerRule); the body keeps its size, and no jobs means the plain rule.

type Guard

type Guard func(*Runtime, KeyEvent) bool

Guard is a named condition: an action's availability (Available, the verb guard) or, the older use kept while areas move, a condition a row requires (Binding.When; bindings check lists such rows as debts). The built-in guards are "input" and "no-input" (the focus is or is not an input), "multiline", "count" and "no-count" (the pending count is present or absent), "self" (the focus is the instance whose level the row sits on, not a descendant), "shadow-idle" (the mode a shadow mode such as macro_recording re-dispatches in is normal with an empty payload) and "op:<key>" (the pending operator). Applications add their own with DefineGuard.

type Handler

type Handler func(*Runtime, Context, Arguments) (any, error)

Handler runs an interaction operation on the UI loop and returns its result.

type Header struct {
	Title    string
	Badge    string
	Focus    []string
	Subtitle string
	Tabs     []string
	Active   int
	Segments []SegmentCell
}

Header is the top of the page chrome: the title row with an optional right-aligned badge (the shell shows the mode there) and the subtitle row, which is a label or, when Tabs is set, the page tabs with Active marked. Focus is the focus beside the badge (the focus-display setting): its levels, outermost first, drawn dim after "·" and cut from the left (FitFocusPath) so the innermost level stays. Segments are the status segments (Runtime.SegmentCells), drawn right-aligned in the rule under the header (HeaderRule); they take no row. Every field is a raw label.

type HelpCell added in v0.15.0

type HelpCell struct {
	Mode string    `json:"mode"`
	Rows []HelpRow `json:"rows"`
}

HelpCell is one mode's rows for a key in the help's table view: the first answers, the ones after answer when its guard is false.

type HelpKey added in v0.15.0

type HelpKey struct {
	Key   string     `json:"key"`
	Modes []HelpCell `json:"modes"`
}

HelpKey is one row of the help's table view: a key (or key class) and its rows per mode, in column order; a mode where nothing answers has no cell.

type HelpRow

type HelpRow struct {
	Key         string    `json:"key"`
	Prefix      string    `json:"prefix,omitempty"`
	Final       string    `json:"final,omitempty"`
	Operation   string    `json:"operation"`
	Request     string    `json:"request,omitempty"`
	Hint        string    `json:"hint,omitempty"`
	Args        Arguments `json:"args,omitempty"`
	Description string    `json:"description,omitempty"`
	Noun        string    `json:"noun"`
	Section     string    `json:"section,omitempty"`
	// Guard is the guard a hypothetical row (the help's pick and table
	// views) answers under; the live help judges guards and leaves it out.
	Guard string `json:"when,omitempty"`
	// Aliases are the other names of the row's operation (ActionAlias),
	// listed once on the row and after its description ("also: …"); the
	// row itself always names the canonical operation.
	Aliases []string `json:"aliases,omitempty"`
	// Enters is the pending mode the row's key enters when the help lists
	// that mode's rows after it (the terminal leader: leader_pending).
	Enters string `json:"enters,omitempty"`
	// Group marks a prefix row: its key starts longer sequences (g of
	// g c, the leader ctrl+a), which are listed Under it. A prefix bound
	// to nothing alone is a row helpGroups makes, with no Operation.
	Group bool `json:"group,omitempty"`
	// Under is the prefix row this row is listed under (its longest
	// prefix that is a Group row: "ctrl+a" for "ctrl+a v", "g c" for
	// "g c x"); "" at the top of its section. Key is the whole sequence.
	Under string `json:"under,omitempty"`
	// Steps are a sequence's keys one by one, each with what it does at
	// that point: entering a mode, waiting for the next key, then the
	// final operation. A single key has none.
	Steps []HelpStep `json:"steps,omitempty"`
	// contains filtered or unexported fields
}

HelpRow is one binding as the help shows it: the key, the operation it runs (noun.verb, or the row's Hint when it has one), arguments resolved when help opens, its description, the answering noun, and the help's section: that noun, or "movement" for a motion (Operation.Motion). Prefix and Final are Key split by SplitKeys (the help's prefix and key columns); Request is the operation the row runs (noun.verb, the noun by its shortest name, also when Operation shows the Hint; "" for a mode transition) and Hint the row's own Hint.

func BindingHelp

func BindingHelp(r *Runtime, focus string) []HelpRow

BindingHelp lists the rows a focus answers to in its current mode with their descriptions, grouped by the noun of the level that answers, from the innermost instance outward (the focused instance's rows, its noun's, its parents' up to the page, then app; a capture row sits with its own level, so app's ctrl+c comes after the focused row's verbs), and the motions of every level last, under "movement". A row whose key enters a pending mode (the terminal leader) is followed by that mode's rows as whole sequences (helpEnters, R6.10).

func NounHelp added in v0.15.0

func NounHelp(r *Runtime, noun string, mode ModeState) []HelpRow

NounHelp is BindingHelp for a hypothetical focus on noun in mode, with no instance (nounRows: the noun's tables, app, the editor fallback for an input noun): a guarded row is listed with its Guard, its operation marked "?", and the rows answering when that guard is false follow it; arguments a row takes from the key, the payload or the row read as "$source".

func PlaceHelp added in v0.15.0

func PlaceHelp(r *Runtime, place NounPlace, mode ModeState) []HelpRow

PlaceHelp is NounHelp with a parent chain: the hypothetical focus on place's noun has the levels the place recorded between it and app (its list, its page; the modal noun inside a modal), each answering with its noun's tables, so a list row's keys from its list and page are there.

type HelpStep added in v0.29.0

type HelpStep struct {
	Key         string `json:"key"`
	Operation   string `json:"operation,omitempty"`
	Mode        string `json:"mode,omitempty"`
	Description string `json:"description"`
}

HelpStep is one key of a sequence as the help's steps show it: the key, and what it does at that point in the sequence: the pending mode it enters (Mode), waiting for the next key, or the final operation.

type HistoryContext

type HistoryContext struct {
	Focus  string `json:"focus"`
	Page   string `json:"page,omitempty"`
	Mode   string `json:"mode"`
	Source string `json:"source"`
}

HistoryContext is the interaction state a request ran in. Source is "key:<keys>", "macro:<register>", "palette", "cli" or "api".

type HistoryEntry

type HistoryEntry struct {
	Seq     uint64         `json:"seq"`
	At      time.Time      `json:"at"`
	Kind    string         `json:"kind,omitempty"`
	Request Request        `json:"request"`
	Context HistoryContext `json:"context"`
	Result  HistoryResult  `json:"result"`
	Moved   *HistoryMove   `json:"moved,omitempty"`
	Job     uint64         `json:"job,omitempty"`
	// Depth is how many requests enclose this one: a request an operation
	// runs while it runs (a choices source, a macro's keys) is one deeper.
	Depth int `json:"depth,omitempty"`
	// Restored marks an entry read from the history file at start
	// (EnsureHistory): an earlier session ran it. Dot-repeat skips it.
	Restored bool `json:"restored,omitempty"`
}

HistoryEntry is one user action: what ran, where, from which source, how it ended and, when it moved the interaction, from where to where (Moved). It holds ids and the request, never the data the operation returned, so an entry costs the same over a list of ten rows or ten thousand. Kind is the operation's HistoryKind. Job is the job id an application verb launched.

type HistoryMove added in v0.7.0

type HistoryMove struct {
	From HistoryPlace `json:"from"`
	To   HistoryPlace `json:"to"`
}

HistoryMove is how a request moved the interaction: j on a list moves the focus from row A to row B, page.next from one page to the next, a modal's open into the modal and its close back out.

type HistoryPlace added in v0.7.0

type HistoryPlace struct {
	Focus string `json:"focus"`
	Page  string `json:"page,omitempty"`
	Mode  string `json:"mode"`
}

HistoryPlace is where the interaction stands: the focused instance (a list's focus is its selected row), the page and the mode.

type HistoryResult

type HistoryResult struct {
	OK    bool   `json:"ok"`
	Error string `json:"error,omitempty"`
}

HistoryResult is the outcome: ok, or the error. The value the operation returned is the caller's (the CLI prints it), not the history's; a reference to it (a hash into a content-addressed store) would go here, beside ok.

type InfoSink deprecated added in v0.31.0

type InfoSink interface {
	Info(text string)
}

InfoSink was the optional interface for the status line's plain tone. Every UISink now has Info.

Deprecated: Info is part of UISink.

type Input

type Input struct {
	ID         string
	Text       []rune
	Cursor     int
	Mode       string // the editing mode: normal, insert or a visual mode; pending modes live on the instance
	Multiline  bool
	Kind       string
	PathFilter string
	// Completions are the input's completion triggers (Completions, the
	// AddInput option); none falls back to its noun's.
	Completions []CompletionTrigger
	Anchor      int
	History     []EditNode
	Head        int
	// contains filtered or unexported fields
}

Input is a single- or multi-line modal text field with an undo tree.

func NewInput

func NewInput(id, text string, multiline bool) *Input

NewInput returns an input in normal mode holding text, with text as its undo root.

func (*Input) Count

func (in *Input) Count() int

Count is the pending Vim count, read from the global mode payload while the input is focused.

func (*Input) Pending

func (in *Input) Pending() string

Pending describes the pending editor mode the way the old grammar did: "d" for an operator, "g" for a g prefix, "dg" for both, "register" while a register name is awaited, "" otherwise.

func (*Input) Redo

func (in *Input) Redo(steps, branch int) error

func (*Input) Replace

func (in *Input) Replace(start, end int, text string) error

func (*Input) SetMode

func (in *Input) SetMode(mode string)

SetMode is the mode-transition hook for both editor operations and set.mode. Direct assignment to Mode cannot delimit insert sessions.

func (*Input) Snapshot

func (in *Input) Snapshot() map[string]any

func (*Input) Undo

func (in *Input) Undo(steps int) error

type Instance

type Instance struct {
	ID          string    `json:"id"`
	Noun        string    `json:"noun"`
	Parent      string    `json:"parent,omitempty"`
	Payload     Payload   `json:"payload"`
	Bindings    []Binding `json:"bindings,omitempty"`
	Unfocusable bool      `json:"unfocusable,omitempty"`
	Label       string    `json:"label,omitempty"` // a display name (a list row's label, a header name); {{label}} in a confirm prompt
	Data        any       `json:"data,omitempty"`  // an opaque application value (a list row's ListItem.Data)
	Component   any       `json:"-"`
	// contains filtered or unexported fields
}

Instance is one addressable thing on screen: a page, a list, a list row, a modal body, or an input. Parent links form the focus stack; walking them from the focused instance outward is the path Context.Path spells out. An instance holds no mode: the mode is global (Runtime.Mode). Payload is the data a modal body carries (a confirm's prompt and request). Bindings are this instance's own rows, resolved above its noun's table at the same stack level. Component holds the typed component the instance fronts. Unfocusable opts the instance out of being a focus stop although its noun is Focusable (a disabled panel). Label is an optional display name a confirm prompt reads as {{label}}. Data is an opaque application value (a list row's ListItem.Data); the framework never reads it.

type InstanceOption

type InstanceOption func(*instanceSpec)

InstanceOption configures the instance constructors: Instance, AddPage, AddList, AddInput, AddTerminal and RegisterPage.

func CellSelect added in v0.13.0

func CellSelect() InstanceOption

CellSelect makes a table (AddList with Columns) cell-based: a current column beside the selected row, moved with h/l, 0/$, N| and g c, and the cell keys y and z >/z </z =. For a table one reads cell by cell (a JSON or CSV view). AddTree ignores it: a tree's rows answer h and l.

func ClearFilterOnEnter added in v0.10.0

func ClearFilterOnEnter() InstanceOption

ClearFilterOnEnter makes enter in a list's (or a tree's list's) filter line choose the selected match: list.filter-choose clears the filter with the selection staying on that row, every row shows again, the mode is normal and the page keeps the row on screen. For a list one picks a row from, such as a pane list. With no match enter keeps the filter, as on every other list (list.filter-keep; its verb guard, list-chooses, is false there and off this option).

func CloseOnDetach

func CloseOnDetach() InstanceOption

CloseOnDetach makes terminal.detach remove the terminal instance after returning focus, so the stream is released (see TerminalReleaser) instead of kept for a later attach. Only AddTerminal reads it.

func Columns

func Columns(cols ...Column) InstanceOption

Columns makes AddList's list a table with these columns (List.SetColumns changes them later). A list without columns draws its labels.

func Completions added in v0.5.0

func Completions(triggers ...CompletionTrigger) InstanceOption

Completions gives an input its completion triggers (AddInput reads it). They replace the triggers its noun declares (NounBuilder.Completions).

func Focus

func Focus(id string) InstanceOption

Focus names the instance a page starts focused on (default: the page itself). Other constructors ignore it.

func FocusOnClose

func FocusOnClose(id string) InstanceOption

FocusOnClose names the instance the focus moves to when this instance is removed while the focus is on it or inside it: a CloseOnDetach detach, Pop of a modal body, a list row dropped by SetItems, RemoveInstance. A target that is gone (or removed with it) falls back to the default (the parent, or the return Pop restores). Every constructor reads it.

func FollowTail added in v0.17.0

func FollowTail() InstanceOption

FollowTail makes a timeline follow its end: the window sticks to the bottom while the last card is selected and the bottom is in view; a card appended then is selected and a growing last card keeps its bottom in view. Moving or scrolling up stops it; G or reaching the bottom again resumes it.

func HeaderLines added in v0.17.0

func HeaderLines(n int) InstanceOption

HeaderLines caps a closed card's header at n lines, the rest read "… N more lines"; an open card shows its whole header. 0 shows every line.

func Kind

func Kind(noun string) InstanceOption

Kind names the noun the instance answers to (default: the instance id for plain instances, "editor" for inputs, "list" for lists, "page" for pages, "terminal" for terminals).

func NoMarks added in v0.2.0

func NoMarks() InstanceOption

NoMarks makes a list (or a tree's list) a read-only view: its rows take no marks, so space does not answer with list.mark there (the verb's guard, list-marks, is false; the footer and ? help leave it out) and list.mark refuses. list.apply still runs on the selected row.

func NoSpill

func NoSpill() InstanceOption

NoSpill makes a list trap its motions: list.move past its last (first) row stays on that row instead of moving the focus to the next (previous) stop on the page. For a list that must keep the focus, such as a picker.

func Parent

func Parent(id string) InstanceOption

Parent places the instance under another instance (default app).

func ParentStop added in v0.30.0

func ParentStop(id string) InstanceOption

ParentStop names where h on a closed card of the timeline goes (an instance; a stop lands on its remembered or selected child). Without it h on a closed card stays.

func RowSelect deprecated added in v0.12.0

func RowSelect() InstanceOption

RowSelect is a no-op kept for code written against v0.12.0, where every table was cell-based unless added with it.

Deprecated: tables select rows unless added with CellSelect; drop it.

type Invocation added in v0.28.0

type Invocation struct {
	Name  string `json:"name"`
	Noun  string `json:"noun"`
	Verb  string `json:"verb"`
	Alias bool   `json:"alias,omitempty"`
}

Invocation is the name a request ran an action by: the action's own key or one of its aliases. Noun is that name's noun (the subject side: for project.associate, an alias of pane.associate, it is project), Verb its verb. Handlers read it from Ctx.Invoked (application verbs) or Runtime.Invoked (interaction handlers).

type JobInfo

type JobInfo struct {
	Job     uint64    `json:"job"`
	Owner   string    `json:"owner"`
	Verb    string    `json:"verb"`
	Label   string    `json:"label,omitempty"`
	Slot    string    `json:"slot,omitempty"`
	Scope   string    `json:"scope,omitempty"`
	Started time.Time `json:"started"`
	State   string    `json:"state"`
	Error   string    `json:"error,omitempty"`
}

JobInfo is one row of the jobs list: what jobs.list returns and what the snapshot's /jobs holds for running jobs.

type JobLingerMsg

type JobLingerMsg struct{ At time.Time }

JobLingerMsg is the ticker's one-shot timer: it fires when the oldest remembered finished job passes JobLinger, so the frame it causes drops the row with no key pressed. At is the deadline it was scheduled for; only the message for the timer in flight clears it.

type KeyEvent

type KeyEvent struct {
	Key   MacroKey
	Mode  ModeState
	Focus string
	Level string // the instance whose level is answering, "" for a noun-only level
	// Args are the arguments of the request whose availability is judged
	// (Available): a row's own, or a call's; nil for a row's own guard.
	Args Arguments
}

KeyEvent is what a row receives: the key, the mode it was resolved in and the focused instance.

type KeyOutcome added in v0.36.0

type KeyOutcome int

KeyOutcome is what resolving one key came to (DispatchOutcome).

const (
	// KeyUnhandled: no level had a row on the key whose action is
	// available, so it went past app.
	KeyUnhandled KeyOutcome = iota
	// KeyHandled: a row ran, or the key started or continued a sequence.
	KeyHandled
	// KeyConsumed: the row that answered runs the noop (NoopAction), the
	// always-available action a clamping container binds a key to so it
	// goes no further out: handled, and nothing happened.
	KeyConsumed
)

type Layout

type Layout struct {
	Width, Height int
	Styles        Styles
	// Shift moves the divider of every split this layout draws, in percent
	// points of the split's length: positive grows the first (left or top)
	// panel, negative the second, 0 is the split's default. It is the
	// page's remembered divider (Runtime.PaneLayout sets it; pane.grow,
	// pane.shrink and pane.reset change it). Divide applies it.
	Shift int
}

Layout renders page chrome, bounded panels and panel pairs for a viewport. It is a plain value: every method only renders and keeps no state.

Example
layout := Layout{
	Width: 80, Height: 24,
	Styles: NewStyles(Appearance{Theme: "adaptive"}),
}
body := layout.Split("Files", "main.go\nlayout.go", "Preview", "Selected file contents")
page := layout.Page("Workspace", "Browse files", body, "Enter open · Esc back")
fmt.Print(page)

func (Layout) BodySize

func (l Layout) BodySize() (width, height int)

BodySize returns the exact cells available to Page's body. Viewports that cannot hold any body report 0, 0. Small viewports drop the header and keep only a footer row.

func (Layout) BodySizeWith

func (l Layout) BodySizeWith(f Footer) (width, height int)

BodySizeWith is the body's size with the footer f: BodySize, since the jobs ticker rides the rule line above the footer and takes no body row, so a job starting or lingering out never moves or hides the body (UX review 2 F13).

func (Layout) Divide added in v0.4.0

func (l Layout) Divide(first, total, least int) int

Divide is where a split of total cells puts its divider: first is the split's default size of its first (left or top) panel, moved by Shift percent points of total and clamped so each panel keeps least cells. A split with no shift, or too small for two panels of least cells, keeps first. A layout of its own (a page body split by hand) calls it with its default; Split and its kin call it for theirs.

func (Layout) HeaderRule added in v0.36.0

func (l Layout) HeaderRule(cells []SegmentCell, width int) string

HeaderRule is the rule line under the header, width cells: the plain rule with no segments; with segments, rule characters, then the segments right-aligned, joined by " · ", then " ──". Segments are dropped from the last while they do not fit, and when the first alone does not, it is cut with "…"; at least "── " of rule stays on the left. Each segment draws in its tone (muted without one), the rule and separators muted, so the chrome still reads as a rule.

func (Layout) Page

func (l Layout) Page(title, subtitle, body, footer string) string

Page renders title, subtitle, body and footer into exactly Width x Height cells. Body is trusted ANSI; title, subtitle and footer are raw labels. Overlong bodies are clipped; use Panel for a visible overflow marker.

func (Layout) PageBlock

func (l Layout) PageBlock(title, subtitle string, body Block, footer string) Block

PageBlock is Page for a body that may carry a cursor; the cursor is moved by the page chrome around the body.

func (Layout) Panel

func (l Layout) Panel(title, body string, width, height int, focused bool) string

Panel renders a rounded, titled box of exactly width x height outer cells. Focused panels use the heavy accent border. Body is trusted ANSI; title is a raw label. Content that does not fit is clipped and marked with "…" (columns) or a "… N more" last row (lines). Below 3x3 cells the border is dropped.

func (Layout) PanelBlock

func (l Layout) PanelBlock(title string, body Block, width, height int, focused bool) Block

PanelBlock is Panel for a body that may carry a cursor; the cursor is moved inside the border and padding.

func (Layout) Shell

func (l Layout) Shell(h Header, body Block, f Footer) Block

Shell is the one page chrome: title row, subtitle row, a rule, the body, a rule, the footer and a bottom margin, inside a gutter, into exactly Width x Height cells. Viewports too small for the chrome keep the body and, from three rows up, the footer. The shell and PageBlock both render through it, so a page and the application around it reserve the same rows. The body's cursor is moved by the chrome around it.

func (Layout) Sidebar

func (l Layout) Sidebar(title string, items []string, selected int, mainTitle, mainBody string) string

Sidebar renders a 28-cell navigation rail beside the main panel. Below 72 columns it stacks above the main panel. Items are raw labels; mainBody is trusted ANSI. Selection is zero-based and clamped for rendering. The caller owns selection and key handling; this method keeps the selected item visible.

Example
layout := Layout{Width: 80, Height: 24, Styles: NewStyles(Appearance{Theme: "adaptive"})}
body := layout.Sidebar("Workspace", []string{"Files", "Search", "Settings"}, 1, "Search", "Search results")
fmt.Print(layout.Page("Project", "Workspace navigation", body, "↑↓ select · Enter open"))

func (Layout) SidebarBlock

func (l Layout) SidebarBlock(title string, items []string, selected int, mainTitle string, mainBody Block) Block

SidebarBlock is Sidebar for a main body that may carry a cursor.

func (Layout) Split

func (l Layout) Split(leftTitle, leftBody, rightTitle, rightBody string) string

Split renders two panels filling BodySize: side by side (left ~40%) when the viewport is at least 72 cells wide, otherwise stacked (top ~40%), the divider moved by Shift (Divide). The left or top panel is the focused one; SplitFocusBlock takes which.

func (Layout) SplitBlock

func (l Layout) SplitBlock(leftTitle string, leftBody Block, rightTitle string, rightBody Block) Block

SplitBlock is Split for bodies that may carry a cursor. One body should carry it; when both do, the left or top panel wins.

func (Layout) SplitFocusBlock added in v0.2.0

func (l Layout) SplitFocusBlock(focused SplitSide, leftTitle string, leftBody Block, rightTitle string, rightBody Block) Block

SplitFocusBlock is SplitBlock with the focused panel named: the one holding the focus draws in the focused style (accent title and border), the other plain, so a view whose focus moves between its panes (o-json's tree and detail) shows where it is. When both bodies carry a cursor, the focused panel's wins.

func (Layout) TickerRule added in v0.2.0

func (l Layout) TickerRule(jobs []string, width int) string

TickerRule is the rule line above the footer, width cells: the plain rule with no jobs; with jobs, "── " then the ticker entries (Footer.Jobs, at most JobTickerRows) joined by " · ", cut with "…" so at least " ─" of rule is left, and the rest of the row filled with rule characters, all muted, so the chrome still reads as a rule.

type Lines

type Lines []any

Lines is a result printed as JSON Lines (one compact value per line, no outer array) by a CLI, so a reader can decode it as it arrives; over JSON it is an array. HandleCall keeps the type through its copy.

type List

type List struct {
	// contains filtered or unexported fields
}

List owns flat selection, marks and scrolling on the runtime's event loop. Focus sits on the selected row when there is one, else on the list.

func NewList

func NewList(r *Runtime, id string, items []ListItem, opts ...InstanceOption) (*List, error)

NewList is AddList for callers that hold the runtime as a value.

func (*List) Column added in v0.12.0

func (l *List) Column() int

Column is a cell-mode table's current column (an index into Columns); 0 for any other list.

func (*List) ColumnOffset added in v0.9.0

func (l *List) ColumnOffset() int

ColumnOffset is the table's column offset: 0 unscrolled, else k for the view that starts the scrolling columns at the k-th.

func (*List) ColumnWidths added in v0.12.0

func (l *List) ColumnWidths() map[int]int

ColumnWidths is the widths given to columns with z > and z < (column index to cells); a column left out is laid out automatically.

func (*List) Columns

func (l *List) Columns() []Column

Columns returns a copy of the list's columns (nil for a plain list).

func (*List) EndVisual added in v0.12.0

func (l *List) EndVisual()

EndVisual ends a visual selection: the marks go back to the ones it started over.

func (*List) Filter added in v0.8.0

func (l *List) Filter() string

Filter is the list's filter query ("" when every row is shown).

func (*List) Filtering added in v0.8.0

func (l *List) Filtering() bool

Filtering reports whether the filter line shows: a filter is set, or its query is being typed on this list.

func (*List) Focus

func (l *List) Focus() error

Focus points the active frame at the list's selected row, or the list.

func (*List) FocusTarget

func (l *List) FocusTarget() string

FocusTarget is where a focus jump to the list lands: its selected row.

func (*List) Focused

func (l *List) Focused() bool

Focused derives focus from the active frame instead of retaining a stale flag.

func (*List) Height added in v0.8.0

func (l *List) Height() int

Height is the lines the list needs to draw every row without windowing (a table's header aside): Len, plus the filter line while it shows. A host that sizes a list to its rows sizes it with Height, so opening the filter line neither windows the rows nor moves what is drawn below the list as the filter narrows it.

func (*List) Items

func (l *List) Items() []ListItem

Items returns a copy of every row in display order, the ones a filter hides included.

func (*List) Len

func (l *List) Len() int

Len is the number of rows, the ones a filter hides included (Shown counts the rest).

func (*List) Marked

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

Marked lists the marked rows in list order, the ones a filter hides included (a mark survives the filter).

func (*List) Move

func (l *List) Move(delta int)

Move clamps at either end and does not take focus from another component.

func (*List) Patch added in v0.8.0

func (l *List) Patch(p ListPatch) error

Patch applies a record-level change (ListPatch) without the caller rebuilding the rows: it is SetItems over the patched rows, so a row that stays keeps its instance (and the bindings and pending jobs on it), the selection and the marks follow their ids, and only a removed row's instance is closed. An empty patch does nothing; an invalid one (an index out of order or out of range, a repeated id) leaves the list unchanged. The patch applies to every row, the ones a filter hides included, and SetItems filters the result again.

func (*List) PinColumn added in v0.25.0

func (l *List) PinColumn(i int)

PinColumn pins column i; UnpinColumn unpins it.

func (*List) PinnedColumns added in v0.25.0

func (l *List) PinnedColumns() []int

PinnedColumns is the table's pinned columns (indexes into Columns) in declared order.

func (*List) ResetColumnWidths added in v0.12.0

func (l *List) ResetColumnWidths()

ResetColumnWidths makes every column automatic again and remembers it.

func (*List) ResetPins added in v0.25.0

func (l *List) ResetPins()

ResetPins puts the default pins back (Column.Pinned, else the first).

func (*List) Select

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

Select changes the selected row by stable ID without taking focus. A row the filter hides is shown by clearing the filter.

func (*List) Selected

func (l *List) Selected() (ListItem, bool)

Selected returns a value copy; empty lists have no selected item.

func (*List) SetColumn added in v0.12.0

func (l *List) SetColumn(i int)

SetColumn makes column i (clamped) the current column of a cell-mode table and remembers it; on any other list it does nothing.

func (*List) SetColumnWidth added in v0.12.0

func (l *List) SetColumnWidth(i, width int)

SetColumnWidth fixes column i at width cells (0 or less: automatic again) and remembers it.

func (*List) SetColumns

func (l *List) SetColumns(cols ...Column)

SetColumns replaces the list's columns; none makes it a plain list again.

func (*List) SetFilter added in v0.8.0

func (l *List) SetFilter(query string)

SetFilter sets the filter query and shows the rows that match it; "" shows every row. The selection stays on its row when that row still shows, else it moves to the first match (none when nothing matches); the focus follows it.

func (*List) SetItems

func (l *List) SetItems(items []ListItem) error

SetItems copies rows, preserves selection by ID, and falls back to the nearest remaining index if that ID disappears. Invalid updates leave the list unchanged. A filter (SetFilter) stays and applies to the new rows. It creates one instance per new row under the list and removes the instances of rows that disappear; a retained row keeps its instance and any bindings the application put on it. Rows whose cells are unchanged keep a table's measured widths. A record-level change is Patch.

func (*List) SetPinnedColumns added in v0.25.0

func (l *List) SetPinnedColumns(pins []int)

SetPinnedColumns makes these columns (indexes into Columns) the table's pins and remembers them; the default pins are remembered as no choice.

func (*List) Shown added in v0.8.0

func (l *List) Shown() int

Shown is the number of rows the filter lets through (Len without one).

func (*List) Snapshot

func (l *List) Snapshot() map[string]any

Snapshot is the list's state with a copy of its rows (the snapshot's /lists entry); changing it never reaches the list.

func (*List) StartVisual added in v0.12.0

func (l *List) StartVisual() error

StartVisual starts a visual row selection at the selected row; a list without rows or one added with NoMarks refuses.

func (*List) ToggleMark

func (l *List) ToggleMark(id string) error

ToggleMark marks or unmarks a row for fan-out verbs; a list added with NoMarks refuses.

func (*List) UnpinColumn added in v0.25.0

func (l *List) UnpinColumn(i int)

UnpinColumn unpins column i.

func (*List) UnpinColumns added in v0.25.0

func (l *List) UnpinColumns()

UnpinColumns leaves no column pinned.

func (*List) View

func (l *List) View(width, height int) string

View fits the viewport and updates its scroll offset to keep selection visible; a list with columns draws as a table (tableView). Calling it places the list in its page's drawing order (Placed), even at zero height, and reports its selected row's line (PlacedAt) so the page scrolls to keep that row on screen.

func (*List) Visual added in v0.12.0

func (l *List) Visual() bool

Visual reports whether a V selection is active on the list.

type ListItem

type ListItem struct {
	ID      string     `json:"id"`
	Label   string     `json:"label"`
	Noun    string     `json:"noun"`
	Data    any        `json:"data,omitempty"`
	Cells   []string   `json:"cells,omitempty"`
	Section string     `json:"section,omitempty"`
	Tones   []ToneSpan `json:"tones,omitempty"`
}

ListItem is application-owned identity, display text, and the registered noun the row answers to. Each row is an Instance whose ID is the item ID and whose Parent is the list; its bindings and actions come from its noun. Data is an opaque value the application keeps on the row (a host's record of the row, an extension's own id); SetItems copies it to the row instance's Data, where a verb reads it as Ctx.Instance.Data. The framework never reads it. Cells are the row's text per column when the list has columns (a table, Columns); Label stays the row's name for the palette, search and {{label}}, and is the first cell of a row without cells. Section groups a table's rows: List.View draws the section as a label line above the first row of each run of rows with the same Section (a list without columns ignores it). Tones give cells, or runs inside them, a named tone the theme colours (ToneSpan, tone.go; in a list without columns cell 0 is the label); they never change the text.

func PatchItems added in v0.8.0

func PatchItems(rows []ListItem, p ListPatch) ([]ListItem, error)

PatchItems is rows with p applied (ListPatch); rows is not modified.

type ListPatch added in v0.8.0

type ListPatch struct {
	Upsert []ListItem
	At     []int
	Remove []string
}

ListPatch is a record-level change to a list's rows: the rows that changed or were added, whole (Upsert), and the ids of the rows that went away (Remove). Every other row stays as it is, in its order.

At, when given, is each upserted row's index in the patched list, one per Upsert and ascending: the rows neither upserted nor removed keep their relative order and fill the indexes the upserts leave free. Without At a row whose id the list has replaces it in place and a new row is appended, in Upsert order.

func (ListPatch) Empty added in v0.8.0

func (p ListPatch) Empty() bool

Empty reports whether the patch changes nothing.

type MacroKey

type MacroKey struct {
	Key   string `json:"key"`
	Paste bool   `json:"paste,omitempty"`
}

MacroKey is one recorded key of a macro; Paste marks pasted text.

type MacroState

type MacroState struct {
	Recording string     `json:"recording,omitempty"`
	Last      string     `json:"last,omitempty"`
	Keys      []MacroKey `json:"keys,omitempty"`
	Depth     int        `json:"depth"`
	Steps     int        `json:"steps"`
}

MacroState is the application-wide recording and replay state. Awaiting a register name is the macro_pending or replay_pending mode of the focused instance, not a field here.

type ModalBody added in v0.2.0

type ModalBody interface {
	ModalTitle() string
	ModalBlock(s Styles, width, height int) Block
}

ModalBody is a modal body component that draws itself: a modal whose instance's Component implements it needs no ModalView (a record editor opened as a modal, its chooser, a Chooser). A ModalView for the id still wins.

type ModalOption

type ModalOption func(*modalSpec)

ModalOption configures OpenModal.

func WithInput

func WithInput(in *Input, opts ...InstanceOption) ModalOption

WithInput places an input inside the modal body and focuses it, so a one-field prompt is one call. The input is registered under the body (a Kind option names its noun; Parent is ignored) and is removed with the body on Pop; the *Input keeps its text and undo history and may be passed again to the next OpenModal.

type ModalView

type ModalView struct {
	Title  string
	Render func(*Runtime, *Instance, int, int) Block
}

ModalView renders one modal without replacing or inspecting its parent page. Render receives the modal instance (its Payload carries what OpenModal gave it) and returns the body as a Block; its cursor, when the modal holds the focused input, is relative to the body's top-left cell.

type ModeBuilder

type ModeBuilder struct {
	*NounBuilder
	// contains filtered or unexported fields
}

ModeBuilder binds rows into one mode table of a noun.

func (*ModeBuilder) Bind

func (m *ModeBuilder) Bind(keys string, row Binding) *ModeBuilder

Bind adds an exact-key (or key-sequence) row, replacing the row with the same keys and guard (Binding.Guard, or When after Bind).

func (*ModeBuilder) Extends

func (m *ModeBuilder) Extends(parent string) *ModeBuilder

Extends makes the selected mode's table read parent's rows after its own, as the editor spec's "extends" does: three motion tables become one table and two that extend it. A mode extends one parent; Extends("") removes the link.

func (*ModeBuilder) Pattern

func (m *ModeBuilder) Pattern(pattern string, row Binding) *ModeBuilder

Pattern adds a key-class row.

func (*ModeBuilder) Unbind

func (m *ModeBuilder) Unbind(keysOrPattern string) *ModeBuilder

Unbind removes the unguarded row with these keys or this pattern; a guarded row on them stays (UnbindGuarded removes that one).

func (*ModeBuilder) UnbindGuarded added in v0.6.0

func (m *ModeBuilder) UnbindGuarded(keysOrPattern, guard string) *ModeBuilder

UnbindGuarded removes the row with these keys or this pattern and this guard ("" is the unguarded row).

func (*ModeBuilder) When

func (m *ModeBuilder) When(guard string) *ModeBuilder

When guards the last bound row with a registered guard name. The guard is part of the row's identity, so the row is placed again under it: a row it replaced as an unguarded row comes back, and it replaces the row with the same keys and this guard instead.

type ModeObserver

type ModeObserver interface {
	ModeChanged(from, to string)
}

ModeObserver is implemented by a component that follows the base mode while the focus sits on it or beneath it: the grid starts and ends its selection when a visual shape is entered or left.

type ModeSpec added in v0.33.0

type ModeSpec struct {
	Pending     bool `json:"pending,omitempty"`
	Passthrough bool `json:"passthrough,omitempty"`
}

ModeSpec is one mode's entry in a bindings file's "modes".

type ModeState

type ModeState struct {
	Name    string  `json:"name"`
	Payload Payload `json:"payload"`
}

ModeState is a mode name with the payload the mode carries.

type Model

type Model struct {
	Runtime    *Runtime
	Palette    *Palette
	Appearance *Appearance
	ModalViews map[string]ModalView
	Title      string
	Width      int
	Height     int
	// Status is the status line in its error tone: the last key's error,
	// or what a handler set with ctx.Error (ctx.Status); the footer reads
	// "Error: TEXT" in the error style in place of the hints. Info is the
	// line in its neutral tone (ctx.Info), drawn plain before the hints.
	// Setting one clears the other, and the next key clears both.
	Status string
	Info   string
	// Notices are persistent lines (a host's mount warnings), kept across
	// keys and errors until notice.clear (or the application) clears them.
	// The footer shows the newest before the hints, cut to half the width,
	// with "(+N more)" for the rest (notice.list has them all), so notices
	// never hide the hints. A notice does not go with the next key. The
	// Model is the runtime's UI sink (Runtime.UI), so ctx.Notify,
	// ctx.Error and ctx.Info land here.
	Notices []string

	// Sidebar replaces page tabs with a navigation rail. Existing page operations
	// remain the only navigation state; input focus and modal routing are unchanged.
	Sidebar bool

	// Now is the Model's clock: the ticker's memory of finished jobs and
	// the linger timer read it (time.Now by default, time.Now when nil). A
	// headless host fakes it, advances it to JobLingerTick's deadline and
	// delivers that message instead of waiting JobLinger.
	Now func() time.Time
	// contains filtered or unexported fields
}

Model is the Bubble Tea adapter. Pages and modal instances retain their state in Runtime; components (picker, registers, application components) are instances the runtime drives through the delivery contract, so the adapter holds no component list of its own.

func NewModel

func NewModel(r *Runtime) *Model

NewModel returns the Bubble Tea model that renders and drives r.

func (*Model) Init

func (m *Model) Init() tea.Cmd

Init installs the user actions file (Runtime.EnsureUserActions) and applies the user bindings file (Runtime.EnsureUserBindings), after the application defined its own nouns and rows, declares the framework's settings (EnsureConfigSettings), reads settings.json (its problems are notices), draws themes.json's schemes and the theme setting's (applyThemeSetting), restores and persists the action history (EnsureHistory), starts the settings.json poll (SettingsPoll) when the application declared settings, and asks for the background colour.

func (*Model) JobLingerTick

func (m *Model) JobLingerTick() (JobLingerMsg, bool)

JobLingerTick is the linger timer in flight, the message its tea.Tick will deliver at its deadline (At), and false when none is scheduled. A host with a fake Now advances the clock to At and delivers the message itself rather than waiting JobLinger.

func (*Model) JobsTicker

func (m *Model) JobsTicker(width int) []string

JobsTicker is the ticker the shell draws in the rule line above the footer for the current frame, as plain entries at most width cells each: none when no job shows.

func (*Model) Notify

func (m *Model) Notify(text string) NoticeID

Notify adds a persistent notice to the footer (Notices) through the runtime's UI, as ctx.Notify does, and returns its id; notice.clear removes them.

func (*Model) SegmentTick added in v0.36.0

func (m *Model) SegmentTick() (SegmentTickMsg, bool)

SegmentTick is the segment timer in flight, the message its tea.Tick will deliver at its deadline (At), and false when none is scheduled. A host with a fake Now advances the clock to At and delivers the message itself rather than waiting.

func (*Model) SegmentsInFlight added in v0.36.0

func (m *Model) SegmentsInFlight() int

SegmentsInFlight counts the refreshes started and not yet answered.

func (*Model) StatusSegments added in v0.36.0

func (m *Model) StatusSegments() []string

StatusSegments are the segment texts the header rule draws this frame, first to last, before fitting (Layout.HeaderRule cuts what does not fit).

func (*Model) Styles added in v0.10.0

func (m *Model) Styles() Styles

Styles is the Model's appearance with the dim settings applied (modal-dim, dim-unfocused); the shell, modals and page lists draw in it.

func (*Model) Update

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

func (*Model) View

func (m *Model) View() tea.View

View renders the shell as a full-screen tea.View; View().Content is the string. The terminal cursor follows the rendered block through the page or modal that holds it: a bar in insert mode, a block in normal mode and an underline in visual modes. It is hidden when the block reports no cursor.

type NoticeID added in v0.4.0

type NoticeID uint64

NoticeID names one notice so the code that posted it can clear it. The UI assigns it, whatever sink shows the notice (or none), so a handler can always clear what it posted.

type Noun

type Noun struct {
	Name      string               `json:"name"`
	Verbs     []string             `json:"verbs,omitempty"`
	Pin       string               `json:"pin,omitempty"`
	Focusable bool                 `json:"focusable,omitempty"`
	Region    bool                 `json:"region,omitempty"`
	Bindings  map[string][]Binding `json:"bindings"`
	Extends   map[string]string    `json:"extends,omitempty"`
	Guards    map[string]string    `json:"guards,omitempty"`
	Actions   map[string]Handler   `json:"-"`
	// Columns are the table columns a list of the noun's rows draws
	// (NounBuilder.Columns, the catalog's columns): declared data a host
	// passes to Columns when it builds the list, never applied on its own.
	Columns []Column `json:"columns,omitempty"`
	// Completions are the completion triggers of the noun's inputs that
	// declare none of their own (NounBuilder.Completions).
	Completions []CompletionTrigger `json:"-"`
}

Noun is the behaviour shared by every instance of one kind: the verbs it answers to, its key bindings per mode, the action handlers that override an operation's default while the noun is on the focus stack, and an optional pinned mode (the prompt noun pins insert). Focusable marks every instance of the noun as a focus stop (focus.next and the mode rule read it); Region marks a grouping that focus.next {scope: "region"} jumps between.

Bindings is keyed by mode name. "" holds rows that apply in every base mode; a pending mode (operator_pending, a key-sequence prefix, ...) is exclusive and reads only its own table.

type NounAlias added in v0.16.0

type NounAlias struct {
	Alias    string `json:"alias"`
	Noun     string `json:"noun"`
	Shadowed bool   `json:"shadowed,omitempty"`
}

NounAlias is one alias: the short name, the noun it names, and whether an application noun of that name shadows it (the short name then reaches the application's noun and the framework's stays reachable by Noun).

type NounBuilder

type NounBuilder struct {
	// contains filtered or unexported fields
}

NounBuilder is the declaration surface of one noun.

func (*NounBuilder) Action

func (b *NounBuilder) Action(key string, h Handler) *NounBuilder

Action overrides any operation ("noun.verb") while this noun is on the stack.

func (*NounBuilder) Alias added in v0.28.0

func (b *NounBuilder) Alias(verbs ...string) *NounBuilder

Alias makes each name another verb of this noun for the last declared verb (show for get, forget for kill): a permanent alias. A name may hold a dot (base.add). It panics on a clash (AddActionAlias), as Verb does on a duplicate.

func (*NounBuilder) AliasOn added in v0.28.0

func (b *NounBuilder) AliasOn(noun, verb string) *NounBuilder

AliasOn makes noun.verb another name of the last declared verb: one operation reachable under both nouns' names (project.associate for pane.associate). The handler reads which side named it from Ctx.Invoked.

func (*NounBuilder) Columns

func (b *NounBuilder) Columns(cols ...Column) *NounBuilder

Columns declares the table columns a list of the noun's rows draws, in order, each row's ListItem.Cells the same order (Noun.Columns). The catalog carries them, so a host mounting the noun's rows draws a table; an application passes r.Noun(noun).Columns to the Columns list option.

func (*NounBuilder) Completions added in v0.5.0

func (b *NounBuilder) Completions(triggers ...CompletionTrigger) *NounBuilder

Completions declares the completion triggers of every input of the noun that has none of its own (Input.Completions).

func (*NounBuilder) Confirm

func (b *NounBuilder) Confirm(prompt string) *NounBuilder

Confirm asks before the last declared verb runs: the prompt template ({{id}}, {{noun}}, {{<arg>}}) opens a confirm modal whose y runs the stored request and n or esc pops it. An empty prompt removes the confirm.

func (*NounBuilder) Fetch added in v0.7.0

func (b *NounBuilder) Fetch() *NounBuilder

Fetch marks the last declared verb as a data request (Operation.HistoryKind HistoryFetch): a state, list or show verb. Its calls are history entries of kind fetch, which history.list leaves out by default and the ring bounds apart from the user's actions and changes.

func (*NounBuilder) Focusable

func (b *NounBuilder) Focusable() *NounBuilder

Focusable makes every instance of the noun a focus stop.

func (*NounBuilder) Many added in v0.36.0

func (b *NounBuilder) Many() *NounBuilder

Many marks the last declared verb as taking many subjects at once (Operation.Many): run over a visual selection, its handler is called once with every selected row in Ctx.Subjects instead of once per row. Use it for a verb that asks something (a chooser, a confirm naming what it affects) or that should make one change and one notice for all.

func (*NounBuilder) Mode

func (b *NounBuilder) Mode(mode string) *ModeBuilder

Mode selects the table rows are bound into; "" is the any-mode table.

func (*NounBuilder) Motion

func (b *NounBuilder) Motion() *NounBuilder

Motion marks the last declared verb as a motion (Operation.Motion): the footer leaves its rows to ? help, as it does for list.move.

func (*NounBuilder) Noun

func (b *NounBuilder) Noun() *Noun

func (*NounBuilder) PersistArgs added in v0.2.0

func (b *NounBuilder) PersistArgs() *NounBuilder

PersistArgs marks the last declared verb's text-like arguments as safe to keep in the history file (Operation.PersistArgs).

func (*NounBuilder) Pin

func (b *NounBuilder) Pin(mode string) *NounBuilder

Pin keeps every instance of the noun in one base mode.

func (*NounBuilder) Positional

func (b *NounBuilder) Positional(names ...string) *NounBuilder

Positional names the parameters of the last declared verb that bare words fill first, in order (Operation.Positional): Positional("name") makes BIN project link foo set name even though the boolean all sorts first. The CLI gives a boolean a bare word only when it is named here.

func (*NounBuilder) Quiet

func (b *NounBuilder) Quiet() *NounBuilder

Quiet marks the last declared verb as bookkeeping (Operation.Quiet): its jobs stay off the jobs ticker. The framework names no quiet verb.

func (*NounBuilder) Region

func (b *NounBuilder) Region() *NounBuilder

Region makes every instance of the noun a focus region: a grouping (a panel) that focus.next {scope: "region"} jumps between, never a stop.

func (*NounBuilder) Renamed added in v0.28.0

func (b *NounBuilder) Renamed(old, since, removeAfter string) *NounBuilder

Renamed keeps old, the verb's name before a rename, as a deprecated alias of the last declared verb: since is the release that renamed it and removeAfter the last release that keeps the old name (one release, per Keep actions; record it in the CHANGELOG). Its first use posts a one-time notice naming the new name.

func (*NounBuilder) Rows

func (b *NounBuilder) Rows(noun string) *NounBuilder

Rows declares that the last declared verb's result lists rows of noun (Operation.Rows): a host reading the catalog knows which noun a list verb's rows answer to. An empty noun removes the declaration.

func (*NounBuilder) State

func (b *NounBuilder) State() *NounBuilder

State marks the last declared verb as the runtime's state source (Operation.State): one call answers every list's rows as JSON Lines, each line tagged with the list noun (see cli.CatalogDoc). A host reading the catalog's state runs it once instead of one row source per list. The framework names no state verb.

func (*NounBuilder) Verb

func (b *NounBuilder) Verb(verb string, h any, description ...string) *NounBuilder

Verb registers noun.verb as an operation when it is new, else installs the handler as this noun's override of the existing operation. The handler is a Handler (an interaction verb, run on the loop) or an AppHandler (func(Ctx) Result: an application verb whose pending work runs off the loop). An override of an existing operation must be a Handler.

func (*NounBuilder) Verbs

func (b *NounBuilder) Verbs(verbs ...string) *NounBuilder

Verbs records the verbs the noun answers to, for help and the palette.

func (*NounBuilder) When

func (b *NounBuilder) When(guard string) *NounBuilder

When guards the last declared verb with a registered guard name: while the guard is false at the focus and mode the verb would run in, the palette does not offer it. The guard reads a KeyEvent whose Focus and Level are that focus and whose Mode is that mode. A palette noun's default verb is never guarded (RegisterNoun refuses one). An empty name removes the guard.

type NounPlace added in v0.15.0

type NounPlace struct {
	Noun      string    `json:"noun"`
	Ancestors []string  `json:"ancestors"`
	Page      string    `json:"page,omitempty"`
	PageTitle string    `json:"pageTitle,omitempty"`
	Modal     string    `json:"modal,omitempty"`
	Count     int       `json:"count"`
	First     time.Time `json:"first"`
	Last      time.Time `json:"last"`
}

NounPlace is one place a noun was seen: an instance of Noun registered under instances of the Ancestors nouns (outermost first, app left out), on Page, or inside the open modal Modal. Inside a modal the ancestors start at the modal instance, where the stack walk stops; Page is then the page the modal opened over. Instance ids are never part of a placement (rows and generated instances differ per run), only nouns and the page id (page ids are stable). A list row counts once per SetItems for each noun its rows answer to, not once per row.

func (NounPlace) Chain added in v0.15.0

func (p NounPlace) Chain() []string

Chain is the stack walk of the placement from outside in: the ancestors, then the noun itself.

type Operation

type Operation struct {
	Verb        string               `json:"verb"`
	Noun        string               `json:"noun"`
	Description string               `json:"description"`
	Parameters  map[string]Parameter `json:"parameters"`
	Result      string               `json:"result"`
	Kind        string               `json:"kind"`
	Confirm     string               `json:"confirm,omitempty"`
	Repeatable  bool                 `json:"repeatable,omitempty"`
	// Rows names the noun whose rows the verb's result lists (a list
	// verb's JSON array of rows of that noun); "" when the result is not
	// rows. It is data for hosts (the catalog's rows); the runtime does
	// not read it.
	Rows string `json:"rows,omitempty"`
	// State marks the verb whose answer is the rows of every one of a
	// host's lists in one call (NounBuilder.State), as JSON Lines of
	// cli.Row tagged by list noun ({"noun":"agent","id":..,"label":..}).
	// Data for hosts (the catalog's state); the runtime does not read it.
	State bool `json:"state,omitempty"`
	// Quiet marks bookkeeping (a list reload) the jobs ticker does not
	// show; set by NounBuilder.Quiet. The catalog's quiet.
	Quiet bool `json:"quiet,omitempty"`
	// Builtin marks an operation the framework registered itself: every
	// one NewRuntime and NewModel register, and those a framework
	// constructor registers later (NewPicker, NewPalette, grid.Register,
	// markdown.Register, cli.AddCommands), each inside Runtime.Builtin. An
	// application's verb is never builtin, even on a framework noun
	// (history.capture-hook). Data for hosts (the catalog's builtin): a
	// host mounting another tool's verbs skips these, which every
	// ochazuke tool has; the runtime does not read it.
	Builtin bool `json:"builtin,omitempty"`
	// PersistArgs lets the history file keep this operation's arguments
	// named like text (text, value, keys, args, password, secret, token), which
	// it otherwise leaves out; set by NounBuilder.PersistArgs. Only for an
	// operation whose arguments are never typed secrets (settings.set).
	PersistArgs bool `json:"persistArgs,omitempty"`
	// Motion marks movement (list.move, focus.next, surface.scroll,
	// page.next): the footer's hint line leaves its rows out, since motions
	// are standard and the footer is for the verbs the focus offers; ?
	// help and bindings list keep them. Set by NounBuilder.Motion; the
	// framework's own motions are marked as they register (motionOps).
	Motion bool `json:"motion,omitempty"`
	// HistoryKind is the kind of entry the operation records
	// (HistoryAction, HistoryChange, HistoryFetch); empty derives it
	// (HistoryKindOf). Set it to HistoryFetch on a verb that only asks for
	// data (NounBuilder.Fetch, the catalog's history), so history.list's
	// default view and the bounded ring leave its calls to the fetches.
	HistoryKind string `json:"historyKind,omitempty"`
	// Positional names the parameters bare words fill first, in this
	// order (NounBuilder.Positional), ahead of ParameterOrder's
	// required-then-by-name rest. The CLI gives a boolean a bare word only
	// when it is named here. The catalog's positional.
	Positional []string `json:"positional,omitempty"`
	// Many marks a verb whose handler takes every subject of a request at
	// once (Ctx.Subjects: a visual selection's rows), so it asks one
	// question, makes one change and posts one notice for all of them; set
	// by NounBuilder.Many. A verb without it that is asked to act on
	// several subjects runs once per subject. The catalog's many.
	Many bool `json:"many,omitempty"`
	// Aliases are the action's other names (ActionAlias), display
	// spelling, sorted. Catalog and operations.list fill it; an entry of
	// Runtime.Operations does not hold it (the alias table does).
	Aliases []string   `json:"aliases,omitempty"`
	Run     Handler    `json:"-"`
	App     AppHandler `json:"-"`
}

Operation is one registered noun.verb. Kind is "interaction" (the default: Run executes synchronously on the loop) or "application" (App is an AppHandler whose pending work runs off the loop and reports through the delivery contract). Confirm, when set, is a prompt template ({{id}}, {{noun}}, {{label}}, {{<arg>}}) shown in a confirm modal before the operation runs. Repeatable marks a change (not a motion or navigation) that dot-repeat (history.repeat, bound to . on app in normal mode) may run again; set it on the Operation passed to Register.

func (Operation) HistoryKindOf added in v0.7.0

func (o Operation) HistoryKindOf() string

HistoryKindOf is the kind of history entry the operation records: its HistoryKind when set; else a fetch for a verb that answers rows (Rows, State), a change for a Repeatable one, an action for a Motion, a change for any other application verb (work that may change something outside the runtime: the safe reading of an extension verb that declares nothing), and an action for any other interaction verb.

func (Operation) Key

func (o Operation) Key() string

func (Operation) ParameterOrder

func (o Operation) ParameterOrder() []string

ParameterOrder lists the operation's parameters in positional order: the Positional names first, in their order, then the rest required first, then by name. Parameters is a map, so this is the declared order the palette's words and the CLI's bare arguments fill.

type OverlayMark added in v0.14.0

type OverlayMark struct {
	Row   int            `json:"row"`
	Col   int            `json:"col"`
	Text  string         `json:"text"`
	Style lipgloss.Style `json:"-"`
}

OverlayMark is plain text drawn from one cell of the frame (Row and Col, 0-based, in the pane's cells) to the right, cut at the frame's edge; a wide rune it half covers becomes a space. Style draws the text; the empty style draws it reverse and bold, so a mark stands out in mono too. Marks are drawn in order, a later one over an earlier one.

type Page

type Page struct {
	ID         string                         `json:"id"`
	Title      string                         `json:"title"`
	Focus      string                         `json:"focus"`
	Remembered string                         `json:"remembered"`
	Render     func(*Runtime, int, int) Block `json:"-"`
	// OnOpen, when set, runs on the loop each time OpenPage makes this page
	// current from another page (or from none: the first page opened), after
	// the focus and mode are restored; from is the page left, "" for none.
	// Reopening the current page does not run it. An application refreshes
	// what the page shows here (SetItems, or Launch a reload job owned by
	// one of its instances) instead of polling. Every way a page opens goes
	// through OpenPage: page.open, page.next/previous, ctrl+j/k, the page
	// picker.
	OnOpen func(r *Runtime, from string) `json:"-"`
	// Scrolloff is how many lines the page keeps visible above and below
	// the focused element when it scrolls to follow the focus: 0 means the
	// default, 2; a negative value keeps no margin.
	Scrolloff int `json:"scrolloff,omitempty"`
	// contains filtered or unexported fields
}

Page is the component a page instance fronts: a title, the instance it starts focused on (Focus), the focus it remembers while another page is open (Remembered, restored with its mode by OpenPage), and a renderer. Render returns the page body for the given cell bounds as a Block; its cursor, when the page holds the focused input, is relative to the body's top-left cell. Pages without inputs return Text(body).

type Palette

type Palette struct {
	// contains filtered or unexported fields
}

Palette is the ex-mode command line: one prompt input whose text is the source of truth, parsed by the grammar in palette_parse.go, with a completion list for the next word beneath it. It never edits the originating input. Create one palette per runtime: operation keys are globally unique.

func NewPalette

func NewPalette(r *Runtime) *Palette

NewPalette returns the command palette for r.

func (*Palette) Close

func (p *Palette) Close()

func (*Palette) HandleKey

func (p *Palette) HandleKey(key string) (bool, error)

HandleKey dispatches one key while the palette is the active modal.

func (*Palette) IsActive

func (p *Palette) IsActive() bool

IsActive reports whether the palette is the top modal.

func (*Palette) IsOpen

func (p *Palette) IsOpen() bool

func (*Palette) Open

func (p *Palette) Open() error

func (*Palette) RegisterNoun

func (p *Palette) RegisterNoun(noun string, config PaletteNoun) error

RegisterNoun adds or replaces a live instance source without changing operation forms.

func (*Palette) Text

func (p *Palette) Text() string

Text is the command line.

func (*Palette) View

func (p *Palette) View() string

func (*Palette) ViewSized

func (p *Palette) ViewSized(width, height int) string

type PaletteEntry

type PaletteEntry struct {
	Value string `json:"value"`
	Kind  string `json:"kind"`
	Label string `json:"label"`
	ID    string `json:"id,omitempty"`
	Verb  string `json:"verb,omitempty"`
	// Aliases are the other verb words that name this entry's action
	// (ActionAlias): typing one narrows to this entry, which is listed
	// once under its canonical name.
	Aliases []string `json:"aliases,omitempty"`
}

PaletteEntry is one completion: Value is the word it appends, Kind is verb, noun or choice, Label what the list shows.

type PaletteInstance

type PaletteInstance struct {
	ID    string `json:"id"`
	Label string `json:"label"`
}

PaletteInstance is a labeled object whose ID becomes the default action argument.

func PagesPaletteSource

func PagesPaletteSource(r *Runtime, _ Context) []PaletteInstance

PagesPaletteSource reads the runtime registry on every listing, not at startup.

type PaletteNoun

type PaletteNoun struct {
	Source      PaletteSource
	DefaultVerb string
	Argument    string
}

PaletteNoun explicitly chooses the action and string argument used for an instance.

type PaletteSource

type PaletteSource func(*Runtime, Context) []PaletteInstance

PaletteSource reads current instances in the context captured when the palette opened.

type PaletteWord

type PaletteWord struct {
	Text       string   `json:"text"`
	Kind       string   `json:"kind,omitempty"`
	Value      string   `json:"value,omitempty"`
	Param      string   `json:"param,omitempty"`
	Candidates []string `json:"candidates,omitempty"`
}

PaletteWord is one word of the line and what it resolved to: Kind is verb, noun, instance or argument; Value is the full word ("" while it is unresolved); Candidates are the words an ambiguous prefix could be.

type Parameter

type Parameter struct {
	Type        string `json:"type"`
	Required    bool   `json:"required,omitempty"`
	Description string `json:"description,omitempty"`
	// Enum lists the static choices of a string parameter; Source names an
	// operation (noun.verb) whose result lists more. Free also accepts a
	// value outside them.
	Enum   []string `json:"enum,omitempty"`
	Source string   `json:"source,omitempty"`
	Free   bool     `json:"free,omitempty"`
	// Noun says the value names one of that noun's things: a row of its
	// list verb (NOUN.list, when it is a read with no required parameter,
	// stands in for an empty Source), else the id of a live instance of
	// the noun. A Source still wins; Enum values come first.
	Noun string `json:"noun,omitempty"`
}

Parameter describes one argument of an operation: its type, whether it is required, and its choices.

type Payload

type Payload struct {
	Return  string     `json:"return,omitempty"`
	Count   int        `json:"count,omitempty"`
	OpCount int        `json:"opCount,omitempty"`
	Op      string     `json:"op,omitempty"`
	Prefix  string     `json:"prefix,omitempty"`
	Prompt  string     `json:"prompt,omitempty"`
	Request *Request   `json:"request,omitempty"`
	Shadow  *ModeState `json:"shadow,omitempty"`
}

Payload is the small typed state a mode carries (Runtime.Mode.Payload). For a pending mode Return is the mode it returns to on leave. Count and OpCount are the Vim counts before and after an operator; Op is the pending operator; Prefix is the key sequence typed so far; Request is a stored request (the completed match a sequence prefix falls back to, or a confirm's deferred action); Prompt is the text a confirm modal shows; Shadow is the mode the macro_recording shadow mode re-dispatches in.

type Picker

type Picker struct {
	Runtime   *Runtime
	Origin    Context
	Directory string
	Filter    string
	Entries   []PickerEntry
	Selected  int
	Error     string
	Loading   bool
	// contains filtered or unexported fields
}

Picker owns one modal and one ordinary editor. Background commands only read immutable snapshots; HandleMessage is the sole result-application boundary. Its work runs as four replacing jobs on the picker instance (the "index", "search", "preview" and "stat" slots), so the runtime's jobs list decides which result is current: a newer job in a slot drops the older, and Close cancels them all.

func NewPicker

func NewPicker(r *Runtime) *Picker

NewPicker returns the picker for r.

func (*Picker) Close

func (p *Picker) Close()

Close cancels indexing, searching and previews; the picker instance stays registered for the next open.

func (*Picker) HandleMessage

func (p *Picker) HandleMessage(msg tea.Msg) bool

HandleMessage applies only the current job of each slot (index, search, preview) on the UI loop; the runtime's jobs list says which that is.

func (*Picker) IsOpen

func (p *Picker) IsOpen() bool

func (*Picker) PendingCommand

func (p *Picker) PendingCommand() tea.Cmd

PendingCommand is drained by Runtime.PendingCommands after every event, including CallMsg. It schedules work, never waits on filesystem or search.

func (*Picker) View

func (p *Picker) View(width, height int) string

func (*Picker) ViewBlock

func (p *Picker) ViewBlock(width, height int) Block

ViewBlock renders the picker and reports the cursor of its path editor on the first row, after the label.

type PickerEntry

type PickerEntry struct {
	Path string `json:"path"`
	Dir  bool   `json:"dir"`
}

PickerEntry is relative to the indexed root. Symlinks are selectable files, never traversal edges.

type Placement

type Placement struct {
	ID string `json:"id"`
	// Line is the body line the view's first line landed on, -1 when the
	// view reported no text or the page did not draw it; Height is its line
	// count.
	Line   int `json:"line"`
	Height int `json:"height"`
	// Focus and FocusHeight are the focused lines, relative to Line.
	Focus       int `json:"focus"`
	FocusHeight int `json:"focusHeight"`
}

Placement is one Placed call of a frame: the instance drawn and, when its view reported them (PlacedAt), the lines it covers in the page body and the lines inside it that hold the focus (a list's selected row, a grid's cursor row, an input's whole box, a terminal's top line).

type Pos

type Pos struct {
	X, Y int
}

Pos is a cell position inside a rendered block: zero-based, counted from the block's top-left corner.

type Predicate

type Predicate struct {
	Name string      `json:"name"`
	Arg  string      `json:"arg,omitempty"`
	Of   []Predicate `json:"of,omitempty"`
}

Predicate is one named test over an instance: a value, never a string selector. A query is a list of predicates, ANDed. Name is a registered predicate (noun, id, kind, within, child-of, focusable, region, state or one an application adds) and Arg its argument; the combinators any, all and not take nested predicate lists in Of instead. The wire form is a JSON array: ["noun","row"], ["any",[["noun","a"],["noun","b"]]].

func AllOf

func AllOf(of ...Predicate) Predicate

func AnyOf

func AnyOf(of ...Predicate) Predicate

func ByID

func ByID(id string) Predicate

func ByKind

func ByKind(kind string) Predicate

func ByNoun

func ByNoun(noun string) Predicate

Go spellings of the framework predicates and combinators.

func ChildOf

func ChildOf(id string) Predicate

func InState

func InState(mode string) Predicate

func IsFocusable

func IsFocusable() Predicate

func IsRegion

func IsRegion() Predicate

func Not

func Not(of ...Predicate) Predicate

func ParsePredicates

func ParsePredicates(v any) ([]Predicate, error)

ParsePredicates reads the wire form: a list of [name, arg] pairs, or [combinator, [nested pairs]] for any, all and not. A []Predicate from Go passes through.

func Where

func Where(name, arg string) Predicate

Where builds a predicate by name.

func Within

func Within(id string) Predicate

type PredicateFunc

type PredicateFunc func(inst *Instance, r *Runtime, arg string) bool

PredicateFunc tests one instance; arg is the predicate's argument.

type QueryMatch

type QueryMatch struct {
	ID     string `json:"id"`
	Noun   string `json:"noun"`
	Parent string `json:"parent,omitempty"`
}

QueryMatch is one instances.query result row.

type Record added in v0.2.0

type Record map[string]string

Record is one record's values by field name, as an editor loads and writes them.

type RecordDebounceMsg added in v0.2.0

type RecordDebounceMsg struct {
	Editor string
	Seq    uint64
}

RecordDebounceMsg is the timer a pause in typing sets off: when Seq is still the editor's latest change, the record is written.

type RecordEditor added in v0.2.0

type RecordEditor struct {

	// Styles colour the labels (the zero value draws them plain); a modal
	// editor uses the Model's.
	Styles Styles
	// contains filtered or unexported fields
}

RecordEditor is a form over one record: a field per schema entry, each a focus stop, and every change written as it is made. Typing in a text field is written after a pause (Debounce) and at once when the field is left or insert mode ends; a choice, an edit in normal mode, an undo or a redo is written at once. A value that is not valid (Required, the type, the choices, Validate) is not written, and its label says why. u and ctrl+r undo and redo the record's written changes, each written like any other; q and esc leave, and nothing is ever discarded. There is no save key.

The editor is an instance of the form noun (id) with a child per field (id/name): text fields are inputs of form-field (form-text when multiline), choice fields instances of form-choice, whose enter opens the chooser (a modal list of form-option rows).

func (*RecordEditor) Block added in v0.2.0

func (e *RecordEditor) Block(width, height int) Block

Block draws the form: per field its label (with why a value is not written) and its value, a text field as its input, a choice field as its value with the one highlight while focused; a failed write is the last line. Each field is placed in drawing order, so motions follow it.

func (*RecordEditor) Close added in v0.2.0

func (e *RecordEditor) Close()

Close writes what is pending (the editor is being removed).

func (*RecordEditor) Closed added in v0.2.0

func (e *RecordEditor) Closed() bool

Closed reports whether the editor was left (form.leave) or removed.

func (*RecordEditor) DebounceCurrent added in v0.2.0

func (e *RecordEditor) DebounceCurrent(tick RecordDebounceMsg) bool

DebounceCurrent reports whether a pause timer is for the latest change.

func (*RecordEditor) DebounceTick added in v0.16.0

func (e *RecordEditor) DebounceTick() (RecordDebounceMsg, bool)

DebounceTick is the pause timer in flight, for a host that delivers it at once (explore): false when no change waits on one.

func (*RecordEditor) Failure added in v0.2.0

func (e *RecordEditor) Failure() error

Failure is the last background failure, cleared when read.

func (*RecordEditor) Flush added in v0.2.0

func (e *RecordEditor) Flush() error

Flush writes what is pending now (on quit).

func (*RecordEditor) Focus added in v0.2.0

func (e *RecordEditor) Focus() error

Focus focuses the first field (the spec's Start; in insert mode when the spec says so).

func (*RecordEditor) FocusTarget added in v0.2.0

func (e *RecordEditor) FocusTarget() string

FocusTarget is where a jump to the editor lands: its first field (the spec's Start).

func (*RecordEditor) HandleMessage added in v0.2.0

func (e *RecordEditor) HandleMessage(msg tea.Msg) bool

HandleMessage writes the record when its pause timer is the latest.

func (*RecordEditor) ID added in v0.2.0

func (e *RecordEditor) ID() string

ID is the editor's instance id.

func (*RecordEditor) ModalBlock added in v0.2.0

func (e *RecordEditor) ModalBlock(s Styles, width, height int) Block

func (*RecordEditor) ModalTitle added in v0.2.0

func (e *RecordEditor) ModalTitle() string

ModalTitle and ModalBlock draw a modal editor (ModalBody).

func (*RecordEditor) PendingCommand added in v0.2.0

func (e *RecordEditor) PendingCommand() tea.Cmd

PendingCommand notices changes: typing in a text field sets the pause timer; leaving a field, ending insert mode or any other change writes at once and closes the field's undo step.

func (*RecordEditor) Problem added in v0.2.0

func (e *RecordEditor) Problem(name string) string

Problem says why a field's value is not written, "" when it is.

func (*RecordEditor) Ran added in v0.36.0

func (e *RecordEditor) Ran() bool

Ran reports whether form.run handed the values to Submit.

func (*RecordEditor) Runnable added in v0.36.0

func (e *RecordEditor) Runnable() bool

Runnable reports whether form.run would run the form now: it has a Submit, is open, and every field is valid.

func (*RecordEditor) Values added in v0.2.0

func (e *RecordEditor) Values() Record

Values is every field's value as shown, valid or not.

func (*RecordEditor) WriteError added in v0.2.0

func (e *RecordEditor) WriteError() string

WriteError is the last write's failure, "" once a write succeeds.

func (*RecordEditor) Written added in v0.2.0

func (e *RecordEditor) Written() Record

Written is the record as written (or queued to be): the last valid value of every field.

type RecordField added in v0.2.0

type RecordField struct {
	Name  string `json:"name"`
	Label string `json:"label,omitempty"` // shown above the value (default Name)
	Parameter
	Multiline bool `json:"multiline,omitempty"`
	// Validate checks a value the type, Required and the choices allow;
	// its error is why the value is not written.
	Validate func(value string) error `json:"-"`
}

RecordField is one editable field of a record schema. Its Parameter is the same description a verb's parameter carries (Type, Required, Enum, Source, Noun, Free, Description), so a form can come from a verb's catalog parameters (RecordFields). A field with choices (Enum, Source or Noun: Runtime.Choices) and not Free, or of Type boolean, is edited through a chooser, never as free text. A Free field with choices is a text field with suggestions (form-suggest): typing stays free text, enter in normal mode or ctrl+space opens the framework chooser over the suggestions (the Enum, then the Source's result, loaded off the loop when the Source verb is Pending), a pick fills the field and is written at once, and a value outside the suggestions is written as typed; it is one line high (Multiline is ignored). The rest are text inputs, Multiline ones several lines high.

func ArgsFields added in v0.36.0

func ArgsFields(op Operation) ([]RecordField, error)

ArgsFields is the args form's schema for a verb: its form-held parameters (RecordFields), the required ones first, an optional one's label marked "(optional)", a description after the name. It fails when a required parameter cannot be held by a field.

func RecordFields added in v0.2.0

func RecordFields(op Operation) []RecordField

RecordFields is a form's schema from a verb's parameters, in the order bare CLI words fill them (Operation.ParameterOrder): a mounted tool's edit verb gets a form from its catalog alone.

type RecordSpec added in v0.2.0

type RecordSpec struct {
	Title  string // the modal's title; a placed editor's heading is the application's
	Fields []RecordField
	// Load reads the record when the editor opens (nil: every field empty).
	Load func() (Record, error)
	// Save writes one change: Done, Failed, or Pending work that runs off
	// the loop. At most one write is in flight; changes made meanwhile go
	// in the next write. A failed write keeps the values on screen, says
	// so, and is retried with the next change.
	Save func(RecordWrite) Result
	// Debounce is the pause in typing before a text field is written
	// (DefaultRecordDebounce when zero).
	Debounce time.Duration
	// Insert opens the editor with the first field in insert mode when it
	// is a text field.
	Insert bool
	// OnLeave runs after the editor closed (form.leave, or form.run's
	// Submit): every change is written or queued by then; a placed editor
	// stops being drawn.
	OnLeave func()
	// Submit, when set, makes the editor a form that runs once instead of
	// writing each change: Save is never called, changes stay on screen,
	// and form.run (available once every field is valid) closes a modal
	// editor and hands Submit every value as shown. A failed Submit
	// reopens the modal with the values and the error (`! not run:`);
	// leaving (form.leave, esc) runs nothing.
	Submit func(Record) Result
	// Start names the field focused first ("" the first field).
	Start string
}

RecordSpec is what an application gives a record editor: the schema, how to load the record and how to write it.

type RecordWrite added in v0.2.0

type RecordWrite struct {
	Values  Record
	Changed []string
}

RecordWrite is one write an editor hands Save: the record as it is to be stored (every field's last valid value) and the fields this write changes, in schema order.

func (RecordWrite) String added in v0.2.0

func (w RecordWrite) String() string

String reads a write in one line: "write title=milk status=doing", the changed fields in order (a value with spaces or empty is quoted).

type RecordingSink added in v0.4.0

type RecordingSink struct {
	// contains filtered or unexported fields
}

RecordingSink is a UISink that keeps every message, for tests. Attach it with r.UI().Attach(&ochazuke.RecordingSink{}).

func (*RecordingSink) ClearNotice added in v0.4.0

func (s *RecordingSink) ClearNotice(id NoticeID)

ClearNotice records a clear.

func (*RecordingSink) Info added in v0.31.0

func (s *RecordingSink) Info(text string)

Info records a plain status.

func (*RecordingSink) LastError added in v0.36.0

func (s *RecordingSink) LastError() string

LastError is the last error status recorded (Error), "" for none.

func (*RecordingSink) LastInfo added in v0.31.0

func (s *RecordingSink) LastInfo() string

LastInfo is the last plain status recorded (Info), "" for none.

func (*RecordingSink) LastStatus deprecated added in v0.4.0

func (s *RecordingSink) LastStatus() string

LastStatus is the last error status recorded.

Deprecated: use LastError.

func (*RecordingSink) Messages added in v0.4.0

func (s *RecordingSink) Messages() []UIMessage

Messages is a copy of what the sink recorded, in order.

func (*RecordingSink) Notices added in v0.4.0

func (s *RecordingSink) Notices() []string

Notices is the text of the notices still standing (posted and not cleared), oldest first.

func (*RecordingSink) Notify added in v0.4.0

func (s *RecordingSink) Notify(id NoticeID, text string)

Notify records a notice.

func (*RecordingSink) Status added in v0.4.0

func (s *RecordingSink) Status(text string)

Status records an error status (UIError).

type Register

type Register struct {
	Yank Yank       `json:"yank"`
	Keys []MacroKey `json:"keys,omitempty"`
}

Register holds a yank and, when a macro was recorded into it, its keys.

type Registers

type Registers struct {
	Named   map[string]Register
	History []Yank
	Active  string
	Macro   MacroState
	// contains filtered or unexported fields
}

Registers is the component that owns the shared yanks, named registers, yank history, the register selected for the next operation and the macro state. It is one instance under app, reached through the register, macro and yank-history operations and Runtime.Registers(), never through Runtime fields. Its key rows (q, @, " and the pending modes they enter) sit on the app noun so a page or list focus can record and replay too.

func (*Registers) Close

func (g *Registers) Close()

Close abandons a recording in progress; the registers themselves stay.

func (*Registers) HandleMessage

func (g *Registers) HandleMessage(tea.Msg) bool

HandleMessage: the registers do no off-loop work.

func (*Registers) PendingCommand

func (g *Registers) PendingCommand() tea.Cmd

PendingCommand: nothing is ever scheduled.

type Request

type Request struct {
	Verb      string    `json:"verb"`
	Noun      string    `json:"noun"`
	Target    string    `json:"target,omitempty"`
	Args      Arguments `json:"args,omitempty"`
	Confirmed bool      `json:"confirmed,omitempty"`
	// Via is the alias the request was spelled by (an old verb name, a
	// user's actions.json alias): the request is rewritten to the action it
	// names (Noun, Verb) and Via keeps the spelling, so the handler is told
	// which name ran it (Ctx.Invoked) and history shows it. "" for a
	// request that named its action directly.
	Via string `json:"via,omitempty"`
	// Subjects are the instances a request acts on when it acts on
	// several (the rows of a visual selection), in display order; Target
	// is one of them, the one a confirm or a chooser names. Empty means
	// Target alone. A verb that takes many subjects (Operation.Many) runs
	// once with all of them in Ctx.Subjects; any other runs once per
	// subject (Runtime.eachSubject).
	Subjects []string `json:"subjects,omitempty"`
}

Request names an operation and, optionally, the instance it addresses. Confirmed skips the operation's Confirm prompt: the confirm modal's y sets it on the stored request, and a script may set it to run unattended.

func (Request) Key

func (q Request) Key() string

Key returns the request's operation as noun.verb.

type Response

type Response struct {
	// Result is the operation's result. From HandleCall it is detached
	// from application state: Lines stays Lines (each line encoded), any
	// other value is its JSON encoding as a json.RawMessage, so object
	// keys keep the order the handler's value marshals in (a struct's
	// field order); a nil or null result is nil.
	Result any    `json:"result,omitempty"`
	Error  string `json:"error,omitempty"`
	// Argument is true when Error is an ArgumentError: the request was
	// refused for its arguments, before any instance was chosen.
	Argument bool `json:"argument,omitempty"`
	// Messages are what the call told the user while it ran (ctx.Notify,
	// ctx.ClearNotice, ctx.Status), in order; the runtime's sink got them
	// too. Messages posted later, by a job's Then, reach only the sink.
	Messages []UIMessage `json:"messages,omitempty"`
}

Response is the reply to a socket call: a result or an error message.

func CallResponse added in v0.4.0

func CallResponse(ctx context.Context, path string, q Request) (Response, error)

CallResponse is Call returning the whole Response: the result and the Messages the call told the user. A failed call is an error.

type Result

type Result struct {
	// contains filtered or unexported fields
}

Result is what an application handler returns.

func Done

func Done(value any) Result

Done reports a synchronous result.

func Failed

func Failed(err error) Result

Failed reports a synchronous failure.

func Pending

func Pending(work func(context.Context) (any, error)) Result

Pending schedules work off the loop as a job that accumulates: it runs beside the owner's other jobs and its completion is delivered whenever it finishes. The context is cancelled when the owning instance closes.

func (Result) Label added in v0.2.0

func (res Result) Label(label string) Result

Label names this job for people reading the jobs list: the ticker shows it after the verb ("agent.launch claude running") and jobs.list carries it. It says which of a verb's jobs this is (the program a launch starts, the file a build writes); "" leaves the verb alone.

func (Result) Replacing

func (res Result) Replacing(slot string) Result

Replacing puts the pending work in a slot on its owner: starting it cancels the owner's running job in the same slot, and a newer job in the slot cancels this one. Use it for replaceable work such as a preview or a search, never for independent work.

func (Result) ReplacingAt

func (res Result) ReplacingAt(scope, slot string) Result

ReplacingAt is Replacing with the slot scoped to scope, the owner or one of its ancestors: the newest job in (scope, slot) cancels the older one whichever descendant of scope asked, so previews launched from the rows of one list replace each other while another list keeps its own. The job still belongs to the row that asked (its completion and Then land there and closing it cancels the job); Replacing(slot) is ReplacingAt(owner, slot). A scope off the owner's ancestry fails the launch.

func (Result) Then

func (res Result) Then(fn func(Ctx, any, error)) Result

Then runs on the loop when the pending work's completion is delivered and still current; it never runs for a stale or cancelled ticket.

type Runtime

type Runtime struct {
	Nouns          map[string]*Noun
	Instances      map[string]*Instance
	Focus          string    // the focused instance; Context() is derived from it
	Mode           ModeState // the one interaction mode; mode.set writes it, a focus change to another stop resets it
	Page           string    // the current page instance; always an ancestor of BaseFocus()
	Modes          map[string]bool
	Operations     map[string]Operation
	Quit           bool
	Pending        string
	BindingTimeout time.Duration
	// ResolveTarget, when set, maps a request target that is not an
	// instance (a foreign id: an extension's row id, a database key) to
	// the instance that stands for it; ExecuteAt consults it (TargetInstance)
	// before refusing an unknown target, so keys, the palette, the CLI and
	// the API all accept the foreign id. An error refuses the request with
	// it; "" and no error leaves the target unknown.
	ResolveTarget func(target string) (string, error)
	// contains filtered or unexported fields
}

Runtime is owned by one event loop. Transport callers submit messages to that loop. It holds the operation registry (Operations, Modes and their guards), the instance registry (Nouns, Instances and their parent links, including list rows), the three plain interaction fields Focus, Mode and Page with the chain of open modals, the pending key-sequence state (Pending, prefix, BindingTimeout) and the hook that hands off-loop work to the adapter (commands). Components such as inputs, lists, the picker and the registers are instances, never fields; TestRuntimeHoldsNoComponentFields pins this list.

func NewRuntime

func NewRuntime() *Runtime

NewRuntime returns a runtime with the framework nouns registered and focus on the app instance.

func (*Runtime) Accepts

func (r *Runtime) Accepts(t Ticket) bool

Accepts reports whether a ticket names a running job.

func (*Runtime) ActionAlias added in v0.28.0

func (r *Runtime) ActionAlias(name string) (ActionAlias, bool)

ActionAlias is the alias a name is, its noun resolved (false when the name is no alias).

func (*Runtime) ActionAliasProblems added in v0.28.0

func (r *Runtime) ActionAliasProblems() []string

ActionAliasProblems are the aliases dropped because an action registered under their name after them.

func (*Runtime) ActionAliases added in v0.28.0

func (r *Runtime) ActionAliases() []ActionAlias

ActionAliases lists every alias, sorted by name.

func (*Runtime) ActionFor

func (r *Runtime) ActionFor(focus, key string) Handler

ActionFor returns the innermost noun handler for an operation key on the focus stack, or nil when the operation's own Run applies.

func (*Runtime) AddActionAlias added in v0.28.0

func (r *Runtime) AddActionAlias(a ActionAlias) error

AddActionAlias registers an alias. The alias names a registered noun and a verb (dirs.base.add: the longest registered noun prefix is the noun, the rest the verb); action names a registered action, by its key or by another alias (which resolves to its action). An alias may not be the name of a registered action, nor of another alias for a different action; registering the same alias for the same action again is a no-op. Source defaults to app, or framework inside Builtin.

func (*Runtime) AddDetail added in v0.2.0

func (r *Runtime) AddDetail(id string, item func() string, opts ...InstanceOption) (*Detail, error)

AddDetail registers a detail instance under its parent (Parent(page) or an instance beneath the page) and returns its component. item names what the detail shows now (the selected row's id); a different answer than the last one starts the detail at the top. A nil item never resets.

func (*Runtime) AddInput

func (r *Runtime) AddInput(in *Input, opts ...InstanceOption) error

AddInput registers an editor instance. Options: Parent(id) (default app), Kind(noun) (default "editor"; "prompt" pins insert mode) and Completions(triggers...).

func (*Runtime) AddList

func (r *Runtime) AddList(id string, items []ListItem, opts ...InstanceOption) (*List, error)

AddList registers a list instance of the list noun with its rows. Options: Parent(id) (default app), Kind(noun) for a list that answers to its own noun as well, NoSpill() for a list whose motions stay inside it, NoMarks() for a read-only view, ClearFilterOnEnter() for a list whose filter enter chooses a row and clears, Columns(...) for a table, and CellSelect() for a table with a current column (table_cells.go).

func (*Runtime) AddPage

func (r *Runtime) AddPage(id, title string, render func(*Runtime, int, int) Block, opts ...InstanceOption) (*Page, error)

AddPage registers a page instance of kind page under app with a title and a renderer. Options: Focus(id) names where the page starts (the page itself or an instance beneath it, registered first) and Kind(noun) a noun of the application's own. Registration does not activate the page; see OpenPage. It is RegisterPage for callers that do not hold a *Page.

func (*Runtime) AddRecordEditor added in v0.2.0

func (r *Runtime) AddRecordEditor(id string, spec RecordSpec, opts ...InstanceOption) (*RecordEditor, error)

AddRecordEditor places a record editor under Parent (default app); the application draws it with Block in its page. FocusOnClose names where the focus goes when it leaves. Call Focus to start editing.

func (*Runtime) AddSegment added in v0.36.0

func (r *Runtime) AddSegment(rows ...Segment) error

AddSegment adds status segments. A row whose id is already registered changes that segment: its non-zero fields replace the old ones, and a row naming a source replaces the old source, so an application turns a library row on with AddSegment(Segment{ID: "cpu", Enabled: On(true)}). Each segment's setting (SegmentSetting) is declared with the row's Enabled as its default.

func (*Runtime) AddSettingsPage added in v0.7.0

func (r *Runtime) AddSettingsPage() (*Page, error)

AddSettingsPage registers the Settings page as a permanent page, a tab in registration order like the application's own; settings.edit then switches to it and settings.close goes back without removing it. Without it, settings.edit registers the page when it opens and settings.close removes it.

func (*Runtime) AddTerminal

func (r *Runtime) AddTerminal(id string, stream TerminalStream, opts ...InstanceOption) (*Terminal, error)

AddTerminal registers a terminal instance of the terminal noun fed by stream, in terminal mode: every key forwards to the pane except the leader. Options: Parent(id) (default app), Kind(noun) for a noun that supplies its own tables and CloseOnDetach to remove the instance (and release the stream) on detach. The first frame read is scheduled at once.

func (*Runtime) AddTimeline added in v0.17.0

func (r *Runtime) AddTimeline(id string, cards []Card, opts ...InstanceOption) (*Timeline, error)

AddTimeline registers a timeline under Parent (default app) with its cards. Options: FollowTail, HeaderLines(n), NoSpill (motions stay in it), ParentStop(id) (where h on a closed card goes).

func (*Runtime) AddTree

func (r *Runtime) AddTree(id string, nodes []TreeNode, opts ...InstanceOption) (*Tree, error)

AddTree registers a tree under Parent (default app) with its nodes. Columns(...) makes its rows a table; NoSpill keeps motions inside it; NoMarks makes it a read-only view whose rows take no marks; ClearFilterOnEnter makes enter in its filter line choose the match and clear the filter.

func (*Runtime) Alias added in v0.16.0

func (r *Runtime) Alias(alias, noun string) error

Alias makes alias another name of noun: input naming alias (keys, binding files, actions.json, the palette, the CLI, Do) reaches noun unless the application registers a noun named alias, which then owns the name. The framework registers one per framework noun (settings for ochazuke.settings). An alias holding a dot, or naming a registered noun of its own, is refused.

func (*Runtime) AliasAction added in v0.28.0

func (r *Runtime) AliasAction(alias, action string) error

AliasAction makes alias another name of the action registered as action (an application's permanent alias: show for get, forget for kill, or the same operation under another noun, project.associate for pane.associate). See AddActionAlias for the rules.

func (*Runtime) AliasOf added in v0.16.0

func (r *Runtime) AliasOf(noun string) string

AliasOf is the alias that reaches noun right now ("" when none does: no alias, or an application noun shadows it).

func (*Runtime) Aliases added in v0.16.0

func (r *Runtime) Aliases() []NounAlias

Aliases lists the alias names, each with the noun it names, sorted by alias.

func (*Runtime) AliasesOf added in v0.28.0

func (r *Runtime) AliasesOf(name string) []ActionAlias

AliasesOf lists the aliases of an action (by its key or any of its names), sorted by name; nil when it has none.

func (*Runtime) Ancestry

func (r *Runtime) Ancestry(id string) []string

Ancestry walks the parent links from id outward: id first, app last. An unknown id yields nil.

func (*Runtime) Available added in v0.36.0

func (r *Runtime) Available(q Request, focus string, mode ModeState) bool

Available reports whether an action can run now: its availability (the verb guard, NounBuilder.When or SetAvailability) holds at the focus in the mode for the request's arguments, or it has none. The guard reads a KeyEvent whose Focus and Level are focus, Mode is mode and Args are the request's (a binding row's own arguments, or a CLI call's), so an action that acts on an argument (timeline.open {card}) judges that thing. The resolver runs, at each level, the first row on a key whose action is available (an unavailable one stands aside and the key goes on, out to the next level when none at this one is); the palette, the hints, the help and Execute ask the same question. A guard name that is not registered hides nothing.

func (*Runtime) BaseFocus

func (r *Runtime) BaseFocus() string

BaseFocus is the focus beneath every open modal: the return of the outermost modal, else Focus. Runtime.Page is always an ancestor of it.

func (*Runtime) BaseMode

func (r *Runtime) BaseMode() string

BaseMode is the mode Runtime.Mode stands in: its name, or for a pending mode or a key-sequence prefix the base mode it returns to.

func (*Runtime) Bind

func (r *Runtime) Bind(rows ...Binding) error

Bind installs rows from config or code onto their noun or instance tables by identity (mode, keys or pattern, and When): the row with the same identity is replaced in place, a new row is added (a guarded one before the unguarded row on its keys, so it answers first while its guard holds), and an unbind row removes only the row with its identity. A key sequence in progress (d of d d) is kept unless a row added or removed is on it: in its mode, with the keys typed so far, a sequence extending them or one they extend. Such a row cancels the sequence, so the next key starts fresh against the new tables.

The rows of one call are one batch (rowBatch): a key repeated with the same identity is not a replacement but an alternative after the first, so Bind(Do("timeline.open"), Do("timeline.enter")) on l keeps both, and the first whose action is available answers.

func (*Runtime) BindingDebts added in v0.36.0

func (r *Runtime) BindingDebts() []string

BindingDebts lists the stored rows that carry a guard of their own (Binding.When; the editor's implicit input guard aside): the older way to say when a key answers, kept working while areas move the condition onto the action's availability (Available), so the row needs none.

func (*Runtime) BindingTimeoutCommand

func (r *Runtime) BindingTimeoutCommand() tea.Cmd

BindingTimeoutCommand schedules the latest deadline once. Custom Tea models call it after dispatch and send BindingTimeoutMsg to HandleBindingTimeout. A prefix without a deadline (the terminal leader with terminal-leader-timeout 0) schedules nothing.

func (*Runtime) BindingTimeoutTick added in v0.16.0

func (r *Runtime) BindingTimeoutTick() (BindingTimeoutMsg, bool)

BindingTimeoutTick is the sequence timer in flight as the message its command delivers at the deadline, already due: a host with a fake clock (explore) delivers it at once instead of waiting BindingTimeout, as Model.JobLingerTick does for the linger timer. False when no sequence waits on a deadline (none pending, or the leader with no timeout).

func (*Runtime) BindingsDoc added in v0.33.0

func (r *Runtime) BindingsDoc() BindingsDoc

BindingsDoc is every stored table as a bindings file: the pending and passthrough modes, every noun's mode tables (with their extends) and every instance's, in GroupBindings order, rows in table order.

func (*Runtime) Builtin added in v0.8.0

func (r *Runtime) Builtin(register func())

Builtin runs register and marks every operation it registered as the framework's own (Operation.Builtin). NewRuntime and NewModel wrap their registrations in it, and so does every framework constructor that registers operations after them (NewPicker, NewPalette, NewAppearance, grid.Register, markdown.Register, cli.AddCommands), so an operation registered lazily is marked too. Operations registered before it, or by the application at any other time, keep Builtin false. Calls nest. It is for framework packages; an application's own verbs are not builtin.

func (*Runtime) CancelPendingBinding

func (r *Runtime) CancelPendingBinding()

CancelPendingBinding leaves a key-sequence prefix mode without running its completed match and invalidates its timer.

func (*Runtime) CanonicalKey added in v0.16.0

func (r *Runtime) CanonicalKey(key string) string

CanonicalKey is an operation key with its noun resolved (CanonicalNoun): settings.set is ochazuke.settings.set unless the application has a settings noun of its own. A name is looked up whole first: a registered action or action alias keeps its name, and a dotted verb (dirs.base.add) resolves where it was declared; otherwise each dot, last first, is tried as the noun/verb split, and an unknown name reads its last dot (SplitKey).

func (*Runtime) CanonicalNoun added in v0.16.0

func (r *Runtime) CanonicalNoun(name string) string

CanonicalNoun is the registered noun a noun name stands for. A noun the application registered under that exact name owns it (an app's own settings noun shadows the framework's alias); otherwise an alias (settings for ochazuke.settings) names its noun; otherwise the name is returned unchanged. While framework code registers (Runtime.Builtin) an alias wins over an application noun of the same name, so the framework's rows and tables always reach its own nouns. The v0.16.0 spelling app.<noun> of a framework noun is read as ochazuke.<noun> (deprecated, for one release) unless a noun of that exact name exists or the app noun has a verb of that name (app.state, an application verb, shadows the old spelling of ochazuke.state).

func (*Runtime) CanonicalRequest added in v0.28.0

func (r *Runtime) CanonicalRequest(q Request) Request

CanonicalRequest is q as the runtime runs it: its noun resolved and an alias rewritten to its action, Via keeping the alias (a host building a request from words, the CLI shell, calls it before reading the operation by q.Key()).

func (*Runtime) Catalog

func (r *Runtime) Catalog() []Operation

func (*Runtime) CheckDefaultBindings added in v0.34.0

func (r *Runtime) CheckDefaultBindings(source string, data []byte) []string

CheckDefaultBindings reports what is wrong with a default bindings file on this runtime, every problem at once: a file that does not parse, a table whose noun, instance or mode is unknown, an extends to an unknown mode, a row that does not validate (an unknown operation, argument, source or guard), a mode.enter into a mode that is not pending, and two rows that clash (the same key and guard in one table: the second would replace the first). It is the rule check for embedded defaults: an application runs it in a test on its built runtime, after its files loaded, for each file it embeds.

func (*Runtime) CheckUserActions added in v0.28.0

func (r *Runtime) CheckUserActions() []string

CheckUserActions reports what a load of actions.json would skip, without installing anything: entries that do not parse or validate, and alias rows that clash with an action or another alias. An entry the last load installed unchanged is not a problem.

func (*Runtime) CheckUserBindings added in v0.2.0

func (r *Runtime) CheckUserBindings() []string

CheckUserBindings reports what a load of the bindings file would skip, without installing anything, and any row of the merged tables that no longer validates; then warnings ("warning: " lines): each guard row of the file (a debt, BindingDebts) and each merged row that can never answer (UnreachableBindings). A row spelled by an action alias is accepted and gets a line saying what it resolves to (not "skipped"). The problems of actions.json (CheckUserActions) follow.

func (*Runtime) Children

func (r *Runtime) Children(id string) []*Instance

Children lists the instances parented at id in registration order.

func (*Runtime) Choices

func (r *Runtime) Choices(c Context, p Parameter) ([]Choice, error)

Choices lists a parameter's accepted values in context c: its Enum, then the result of its choices source (ChoicesSource), or, for a Noun with no list verb, the noun's live instances. A source that asks for confirmation is refused; it must be a read.

func (*Runtime) ChoicesSource added in v0.14.0

func (r *Runtime) ChoicesSource(p Parameter) string

ChoicesSource is the operation (noun.verb) a parameter's choices come from: its Source, else its Noun's list verb when that is registered, asks for no confirmation and needs no argument; empty when there is none (a Noun parameter then lists the noun's live instances).

func (*Runtime) Chooser added in v0.2.0

func (r *Runtime) Chooser(kind string) *Chooser

Chooser is the open chooser of kind, nil when none is open.

func (*Runtime) Completer added in v0.5.0

func (r *Runtime) Completer() *Completer

Completer is the inline completion component.

func (*Runtime) CompletionTriggers added in v0.5.0

func (r *Runtime) CompletionTriggers(id string) []CompletionTrigger

CompletionTriggers are the triggers an input completes with: its own, else its noun's.

func (*Runtime) Config added in v0.2.0

func (r *Runtime) Config() *Config

Config returns the config component.

func (*Runtime) Context

func (r *Runtime) Context() Context

Context derives the path view of the focus.

func (*Runtime) DeclareSetting added in v0.2.0

func (r *Runtime) DeclareSetting(settings ...Setting) error

DeclareSetting declares settings; each must have a unique name, a known type and a default of that type (in the enum when there is one).

func (*Runtime) DefaultBindings added in v0.34.0

func (r *Runtime) DefaultBindings() []DefaultBinding

DefaultBindings are the rows of the default layer, every table as it stands without the user's bindings.json (the rows it replaced back in place, the rows it added left out), each with its source: nouns by name, each noun's modes by name, then instances by id, rows in table order.

func (*Runtime) Define

func (r *Runtime) Define(name string) *NounBuilder

Define declares a noun, or returns the builder of an existing one. A name the framework's nouns answer to (settings for ochazuke.settings: CanonicalNoun) returns that noun's builder, so an application's verbs and rows extend it; DefineOwn claims the short name for a noun of the application's own instead. Inside Builtin a plain name is the framework noun ochazuke.<name>.

func (*Runtime) DefineGuard

func (r *Runtime) DefineGuard(name string, fn Guard)

DefineGuard registers a named row guard.

func (*Runtime) DefineMode

func (r *Runtime) DefineMode(name string, pending bool) error

DefineMode registers a mode name. A pending mode is exclusive: only rows in its own table answer, and a row that leaves the mode unchanged returns the instance to the mode it shadowed.

func (*Runtime) DefineOwn added in v0.16.0

func (r *Runtime) DefineOwn(name string) *NounBuilder

DefineOwn declares a noun of the application's own under exactly name, or returns its builder when it exists. When name is the alias of a framework noun (settings for ochazuke.settings) the application's noun owns the short name from then on: keys, binding files, actions.json, the palette and the CLI reach it by that name, and the framework's noun stays reachable by its canonical name (ochazuke.settings), its verbs and rows intact. An application with a settings, config or page noun of its own declares it with DefineOwn instead of removing the framework's.

func (*Runtime) DefinePassthrough

func (r *Runtime) DefinePassthrough(name string) error

DefinePassthrough marks a defined mode as a passthrough mode: while it is the base mode (a pending mode over it included) capture rows stand aside, so the focused level's rows answer every key. The terminal mode is one: ctrl+c reaches the pane instead of the app's captured quit.

func (*Runtime) Deliver

func (r *Runtime) Deliver(msg tea.Msg) (bool, error)

Deliver routes a message on the loop: a Completion goes to its owner when its job is running and is dropped otherwise; any other message is offered to every component until one claims it. A Then callback that removes its own owner is safe: the job is already settled and the completion is not offered to the gone component. It reports whether a component consumed the message and the delivered completion's error, if any.

func (*Runtime) Detail added in v0.2.0

func (r *Runtime) Detail(id string) *Detail

Detail returns the detail an instance fronts, or nil.

func (*Runtime) Dispatch

func (r *Runtime) Dispatch(key MacroKey) (bool, error)

Dispatch resolves one key through the stack walk and runs the first row that answers. It reports whether any row claimed the key. Live keys and macro playback both arrive here.

func (*Runtime) DispatchOutcome added in v0.36.0

func (r *Runtime) DispatchOutcome(key MacroKey) (KeyOutcome, error)

DispatchOutcome is Dispatch reporting which of the three outcomes the key had: handled, unhandled (no level had an available row on it), or consumed by a noop row.

func (*Runtime) DisplayKey added in v0.16.0

func (r *Runtime) DisplayKey(key string) string

DisplayKey is an operation key with its noun as DisplayNoun.

func (*Runtime) DisplayNoun added in v0.16.0

func (r *Runtime) DisplayNoun(noun string) string

DisplayNoun is the shortest name that reaches noun: its alias while no application noun shadows it (list for ochazuke.list), else noun itself. The footer's hints, the ? help and the jobs ticker draw it; history, bindings.list, operations.list and the catalog print canonical names.

func (*Runtime) EnsureConfigSettings added in v0.2.0

func (r *Runtime) EnsureConfigSettings()

EnsureConfigSettings declares the settings the framework reads from the user's settings.json, unless the application declared one of the same name first: theme (the colour scheme; its choices are the built-in themes and themes.json's, read here and again when the config directory changed), focus-display (the focus beside the mode) and history-limit while the history is persisted. It applies terminal-leader when an application declared it (EnsureTerminalLeader). Model.Init and the CLI shell call it, so settings set works in both; an application may call it earlier (after setting Config.App) to have them before the Model starts.

func (*Runtime) EnsureHistory added in v0.2.0

func (r *Runtime) EnsureHistory()

EnsureHistory persists the action history, once: it reads the history file (the last history-limit entries, marked Restored, before what this session already recorded), compacts it when it holds more or has lines that do not parse (each such file is one notice), and starts the writer every later entry is appended through. Nothing happens without a state directory (Config.Disable, a test binary) or with Config.NoHistoryFile. Model.Init calls it; the CLI shell does not (a one-shot call is not history worth keeping).

func (*Runtime) EnsureNounPlaces added in v0.15.0

func (r *Runtime) EnsureNounPlaces()

EnsureNounPlaces persists the placements record, once: it reads the placements file into the record (a file that does not parse is one notice and is replaced at the next write) and starts the writer the record is saved through, off the loop, at most every NounPlaceWriteEvery and on Config.Flush. Nothing is read or written without a state directory (Config.Disable, a test binary). Model.Init calls it.

func (*Runtime) EnsureSegments added in v0.36.0

func (r *Runtime) EnsureSegments() []string

EnsureSegments declares the segments area's settings and reads the user's segments.json, once. EnsureConfigSettings calls it (Model.Init and the CLI shell), so settings set reaches every segment.

func (*Runtime) EnsureTerminalLeader added in v0.2.0

func (r *Runtime) EnsureTerminalLeader()

EnsureTerminalLeader declares the terminal-leader and terminal-leader-timeout settings, each unless the application declared it, and applies the leader: an application with terminal panes calls it at setup (cli.AddCommands does), so settings.json, the Settings page and the CLI's settings set know the setting before the first pane opens. An application without terminals does not, and its settings list stays free of it.

func (*Runtime) EnsureUserActions added in v0.2.0

func (r *Runtime) EnsureUserActions() []string

EnsureUserActions installs the user's actions.json once; later calls do nothing. Model.Init, cli.Mount and the CLI shell call it before the user bindings, so a bindings.json row can name an action's verb.

func (*Runtime) EnsureUserBindings added in v0.2.0

func (r *Runtime) EnsureUserBindings() []string

EnsureUserBindings applies the user bindings file once; later calls do nothing. Model.Init and the CLI shell call it, after the application defined its own rows, so a file row replaces an application default.

func (*Runtime) Execute

func (r *Runtime) Execute(q Request) (any, error)

Execute runs an operation at the current focus context; see ExecuteAt.

func (*Runtime) ExecuteAt

func (r *Runtime) ExecuteAt(c Context, q Request) (any, error)

ExecuteAt runs an operation for a context. The innermost noun on the focus stack that overrides the operation handles it; otherwise its Run does. Every request is appended to the action history with its kind, the context it ran in, its outcome and where it moved the interaction.

func (*Runtime) ExportConfig added in v0.6.0

func (r *Runtime) ExportConfig(dir string, force bool) (ConfigExport, error)

ExportConfig writes a complete reference of the user configuration into dir ("" is <config dir>/reference): settings.json with every declared setting at its current value, settings.reference.json with each setting's type, default, choices, description and value, and bindings.reference.json with every stored table in the grouped bindings format (FormatBindings of BindingsDoc), settings.reference.json and actions.example.json one row per line (FormatRows). Each file is written atomically, a temp file renamed over it. A file that would land on one of the live config files (dir is the config directory) is refused unless force is set, and then nothing is written.

func (*Runtime) FocusDisplay added in v0.20.0

func (r *Runtime) FocusDisplay() string

FocusDisplay is the focus-display value, off when it is undeclared or holds anything else.

func (*Runtime) FocusInTopLevel added in v0.16.0

func (r *Runtime) FocusInTopLevel() bool

FocusInTopLevel reports the focus invariant: the focus is inside the top level, the top modal's subtree while a modal is open (else the current page, else app).

func (*Runtime) FocusNoun added in v0.20.0

func (r *Runtime) FocusNoun() string

FocusNoun is the focused instance's noun as DisplayNoun, "" without a focus.

func (*Runtime) FocusPath added in v0.20.0

func (r *Runtime) FocusPath() []string

FocusPath is the focus as the stack shows it, outermost level first and the focused instance last, app left out: each level's label when it has one no wider than FocusLabelMax (a page's is its title), else its noun as DisplayNoun. Open modals are levels in the chain: a modal's levels follow the focus it opened over.

func (*Runtime) GuardHolds added in v0.16.0

func (r *Runtime) GuardHolds(name, focus string) (holds, known bool)

GuardHolds evaluates the named guard at focus in the current mode, as the footer's hints and ? help judge a row's guard (KeyEvent{Mode, Focus}); known is false for a name no guard answers to.

func (*Runtime) GuardNames added in v0.16.0

func (r *Runtime) GuardNames() []string

GuardNames lists every registered row guard (the built-ins and those of DefineGuard), sorted. The "op:<key>" family is not listed: it is a pattern, not a name.

func (*Runtime) HandleBinding

func (r *Runtime) HandleBinding(key string) (bool, error)

HandleBinding dispatches a named key; kept for callers that only know keys.

func (*Runtime) HandleBindingTimeout

func (r *Runtime) HandleBindingTimeout(msg BindingTimeoutMsg) (bool, error)

HandleBindingTimeout executes the longest completed match, or clears an incomplete prefix. Its result is false for a stale or cancelled timer.

func (*Runtime) HandleCall

func (r *Runtime) HandleCall(msg CallMsg)

func (*Runtime) HelpModes added in v0.15.0

func (r *Runtime) HelpModes(place NounPlace, pending bool) []ModeState

HelpModes are the modes the help's pick view offers for place's noun: the registered modes, and with pending the pending modes and every key-sequence prefix the place's walk binds ("normal g" after g). NounPlace{Noun: noun} is the noun with no place.

func (*Runtime) HelpTable added in v0.15.0

func (r *Runtime) HelpTable(place NounPlace, pending bool) ([]string, []HelpKey)

HelpTable is the help's table view for place's noun (NounPlace{Noun: noun} for no place): the modes whose own tables hold a row on its walk (the registered modes, and with pending the pending ones; a key sequence is a row of its base mode, so prefixes add no column), and a row per key or key class with its rows per mode (nounRows; a generated prefix cancel left out). The keys the noun or a level of its place answers come first, then app's, each sorted with key classes last.

func (*Runtime) History

func (r *Runtime) History(n int) []HistoryEntry

History returns the last n entries of every kind, oldest first (all of them for n <= 0): the user's and the fetches, merged by Seq.

func (*Runtime) HistoryOf added in v0.7.0

func (r *Runtime) HistoryOf(n int, kinds ...string) []HistoryEntry

HistoryOf returns the last n entries of the given kinds (every kind when none is given), oldest first; n <= 0 returns all of them.

func (*Runtime) Input

func (r *Runtime) Input(id string) *Input

Input returns the input an instance fronts, or nil.

func (*Runtime) Inputs

func (r *Runtime) Inputs() []*Input

Inputs lists the registered inputs in registration order.

func (*Runtime) Instance

func (r *Runtime) Instance(id string, opts ...InstanceOption) *Instance

Instance registers a plain instance: a page body, a panel, a modal body. Its noun is defined when it does not exist yet; a framework alias names the framework's noun (Kind("prompt") is ochazuke.prompt). Registering an existing id returns it unchanged.

func (*Runtime) InstancePath

func (r *Runtime) InstancePath(id string) []string

InstancePath is Ancestry reversed: app first, id last.

func (*Runtime) Invoked added in v0.28.0

func (r *Runtime) Invoked() Invocation

Invoked is the name the running request named its action by (the action's own key unless an alias ran it); the zero Invocation outside a request.

func (*Runtime) IsPassthrough added in v0.16.0

func (r *Runtime) IsPassthrough(name string) bool

IsPassthrough reports whether name is a passthrough base mode (DefinePassthrough).

func (*Runtime) IsPendingMode added in v0.16.0

func (r *Runtime) IsPendingMode(name string) bool

IsPendingMode reports whether name is a registered pending mode.

func (*Runtime) Issue

func (r *Runtime) Issue(owner, verb, slot string, c Ctx) (Ticket, context.Context, error)

Issue admits a job for owner and returns its ticket and the context its work runs under; the caller runs the work and reports back with a message carrying the ticket, checked with Accepts and closed with Settle. A slot name cancels the owner's running job in that slot; "" accumulates. Launch and Start build on it for work the runtime runs as a tea.Cmd.

func (*Runtime) IssueAt

func (r *Runtime) IssueAt(owner, scope, verb, slot string, c Ctx) (Ticket, context.Context, error)

IssueAt is Issue with the slot scoped to scope, owner or one of its ancestors: the running job in (scope, slot) is cancelled whichever instance owns it. See Result.ReplacingAt.

func (*Runtime) Jobs

func (r *Runtime) Jobs() []JobInfo

Jobs lists every job the runtime knows in start order: the running ones and the finished ones retained since the last jobs.list read.

func (*Runtime) JumpFocus

func (r *Runtime) JumpFocus(steps int, scope string) error

JumpFocus moves the focus steps stops (scope "stop") or regions (scope "region") forward, or backward for negative steps, through the focus root in drawing order (units), wrapping. The current unit is the nearest stop (or region) on the focus path; the next one is the first after that unit's whole subtree. A stop is never descended into (a grid is one stop, its cells are not); the focus lands on the unit's remembered child.

func (*Runtime) Launch

func (r *Runtime) Launch(owner, verb string, res Result, c Ctx) (Ticket, error)

Launch starts a pending result as a job of owner under verb and queues its work for the adapter. The completion reaches the owner's Component.HandleMessage and the result's Then callback when the job is still current.

func (*Runtime) List

func (r *Runtime) List(id string) *List

List returns the list an instance fronts, or nil.

func (*Runtime) ListBindings

func (r *Runtime) ListBindings(focus string) []Binding

ListBindings prints the rows the focus answers to in its current mode, one per key or pattern, sorted by key with pattern rows last.

func (*Runtime) Lists

func (r *Runtime) Lists() []*List

Lists lists the registered lists in registration order.

func (*Runtime) LoadBindings

func (r *Runtime) LoadBindings(data []byte) ([]Binding, error)

LoadBindings installs rows from a JSON array in the binding row shape and returns the installed rows.

func (*Runtime) LoadDefaultBindings added in v0.34.0

func (r *Runtime) LoadDefaultBindings(source string, data []byte) error

LoadDefaultBindings installs a default bindings file (the grouped format, ParseBindings in bindings_file.go) into the default layer: the noun and instance tables, below the user's bindings.json, exactly where the same rows bound in Go would sit. Modes come first, then each group's rows in file order (a row replaces the row with its identity in place, a guarded row goes before the unguarded row on its keys, a sequence gets its prefix cancel), then the group's extends.

Unlike a user file, an embedded default is checked strictly: an unknown operation, argument, source, guard, mode or instance is an error naming source, the table and the key, and the rows after it are not installed. An application loads its files at setup, after it registered the verbs and guards they name and before Model.Init applies the user's file; MustLoadDefaultBindings panics instead, so a test that builds the app catches a bad row. source tags the rows in bindings defaults ("mtm/ bindings/app.json").

func (*Runtime) LoadDefaultBindingsFS added in v0.34.0

func (r *Runtime) LoadDefaultBindingsFS(fsys fs.FS, patterns ...string) error

LoadDefaultBindingsFS installs every file of fsys matching the patterns (fs.Glob), each pattern's files in name order, each tagged with its path. A pattern that matches nothing is an error.

func (*Runtime) ModalFocus

func (r *Runtime) ModalFocus(id string) string

ModalFocus is the instance an open modal points at: Focus for the top modal, else the return focus stored in the modal opened over it.

func (*Runtime) ModalReturn added in v0.16.0

func (r *Runtime) ModalReturn(id string) (focus string, mode ModeState)

ModalReturn is what closing an open modal restores: the focus and mode it opened over. Unknown or closed modals report "" and the zero mode.

func (*Runtime) Modals

func (r *Runtime) Modals() []*Instance

Modals lists the open modal instances, outermost first.

func (*Runtime) ModeKnown added in v0.16.0

func (r *Runtime) ModeKnown() bool

ModeKnown reports whether the current mode is one the runtime defined: a base mode, a pending mode, or a key-sequence prefix over a base mode.

func (*Runtime) MustLoadDefaultBindings added in v0.34.0

func (r *Runtime) MustLoadDefaultBindings(source string, data []byte)

MustLoadDefaultBindings is LoadDefaultBindings that panics on an error: embedded defaults are code, and a bad row fails at startup.

func (*Runtime) Noun

func (r *Runtime) Noun(name string) *Noun

Noun returns the registered noun a name stands for (an alias resolved: CanonicalNoun), or nil.

func (*Runtime) NounPlaces added in v0.15.0

func (r *Runtime) NounPlaces(noun string) []NounPlace

NounPlaces is where noun was seen (every noun when noun is ""): one entry per distinct place, most seen first, then most recently seen. It includes the placements file's when the Model read it (EnsureNounPlaces). A consumer resolving a key for a noun that is not on the focus path walks a placement's Chain (outermost first) from its Page, or from its Modal.

func (*Runtime) NounWords added in v0.16.0

func (r *Runtime) NounWords(words []string) (noun string, rest []string, ok bool)

NounWords reads the noun a command line's leading words name and returns it (canonical) with the words after it. The noun is the longest run of leading words that, joined with dots, names a noun or an alias of one: "ochazuke settings list" and "ochazuke.settings list" are ochazuke.settings, "settings list" is ochazuke.settings unless the application has a settings noun of its own, "app quit" is app. At least one word is left for the verb. ok is false when no leading run names a noun.

func (*Runtime) OnConfigReload added in v0.15.0

func (r *Runtime) OnConfigReload(name string, fn func() []string)

OnConfigReload adds an application's own configuration to config.reload: fn reads its file again and returns the problems it found (an entry skipped, a file that does not parse), never fatal. Hooks run in registration order after the framework's files. Each problem not already starting with "name: " gets that prefix, is reported through Config.Report like a framework file's and withdrawn at the next config.reload. fn runs on the update loop.

func (*Runtime) OpenArgsForm added in v0.36.0

func (r *Runtime) OpenArgsForm(q Request, run func(Request) error) (*RecordEditor, error)

OpenArgsForm opens the args form over the focus for a request whose verb lacks arguments: one field per parameter (ArgsFields), prefilled from q's arguments, the first missing required one focused (typing, when it is a text field). Every field is filled by the user: nothing is read from the focus or a guess (R12.5). enter runs the form once every required field is valid (form.run) and otherwise goes to the field that still needs a value; esc in normal mode cancels and runs nothing. run executes the completed request after the form closed, at the focus it opened over; its error reopens the form with the values.

func (*Runtime) OpenChooser added in v0.2.0

func (r *Runtime) OpenChooser(spec ChooserSpec) (*Chooser, error)

OpenChooser opens a chooser modal with its search input focused in insert mode. Like OpenModal, it opens only from the top level.

func (*Runtime) OpenModal

func (r *Runtime) OpenModal(kind string, payload Payload, opts ...ModalOption) (*Instance, error)

OpenModal opens a modal whose body is a new instance of noun kind (its id is the kind), parented at the current focus and carrying payload, and focuses it (or the input WithInput places in it). The instance lives until Pop closes the modal. The kind's rows answer first, then the focus it opened over, then the modal noun.

func (*Runtime) OpenPage

func (r *Runtime) OpenPage(id string) error

OpenPage makes a page current and restores the focus and mode it was left in; an open modal blocks navigation. Runtime.Page is always an ancestor of the focus.

func (*Runtime) OpenPagePicker added in v0.2.0

func (r *Runtime) OpenPagePicker() (*Chooser, error)

OpenPagePicker opens the page chooser (page.pick): the pages visited before, most recent first, then the pages never visited in registration order, then the current page, marked "current", so the highlighted first row is the page last left and enter goes back to it. Typing filters by title; enter opens the highlighted page, esc closes.

func (*Runtime) OpenRecordEditor added in v0.2.0

func (r *Runtime) OpenRecordEditor(id string, spec RecordSpec) (*RecordEditor, error)

OpenRecordEditor opens a record editor as a modal over the focus, its first field focused; leaving closes the modal and restores the focus and mode it opened over.

func (*Runtime) Operation added in v0.16.0

func (r *Runtime) Operation(key string) (Operation, bool)

Operation is the registered operation a key names, aliases resolved: noun aliases (settings.set) and action aliases (an old verb name) alike.

func (*Runtime) PageFocus

func (r *Runtime) PageFocus(id string) string

PageFocus is the focus a page shows: the live base focus while it is the current page, else the focus it remembers.

func (*Runtime) PageList

func (r *Runtime) PageList() []*Page

PageList lists pages in registration order: app's children of kind page. It reads the app instance's page order (appPages), so a frame that asks several times costs the number of pages, not of instances; without one (an application gave app a component of its own) it scans the instances.

func (*Runtime) PageOf

func (r *Runtime) PageOf(id string) *Page

PageOf returns the page an instance fronts, or nil.

func (*Runtime) PaneLayout added in v0.4.0

func (r *Runtime) PaneLayout(id string, l Layout) Layout

PaneLayout is l with the divider of the page id is on (or is): a page renderer that draws a list/detail split passes its Layout through it, so Split, SplitBlock, SplitFocusBlock and Divide follow the page's pane keys. Called while the page renders, it also records that the page draws a split, which is when pane.grow, pane.shrink and pane.reset answer their keys and show in the hints.

func (*Runtime) PaneShift added in v0.4.0

func (r *Runtime) PaneShift(page string) int

PaneShift is the remembered divider of page (Layout.Shift), 0 for the split's default.

func (*Runtime) PendingCommands

func (r *Runtime) PendingCommands() tea.Cmd

PendingCommands drains the work scheduled since the last call: the runtime's own queue and every component's PendingCommand.

func (*Runtime) PendingModes added in v0.16.0

func (r *Runtime) PendingModes() []string

PendingModes lists the registered pending modes (DefineMode with pending), sorted.

func (*Runtime) Placed

func (r *Runtime) Placed(id string)

Placed records that the instance id is being drawn now, during its page's render: the order of these calls in a frame is the page's drawing order, which focus jumps (focus.next, focus.previous) and list spills walk, so motions follow the screen as drawn and cannot drift from it. List, Input (InputBlock), Terminal and grid views call it; a component of the application's own calls it from its view. Outside a page render it does nothing. There is no other way to order stops: before the first frame the order is the tree's, and once a frame drew any stop, a stop the last frame did not draw is skipped (it is not on screen). A stop drawn by an application view that never calls Placed is therefore skipped too.

func (*Runtime) PlacedAt

func (r *Runtime) PlacedAt(id, text string, focus, height int) string

PlacedAt records where the view text of instance id holds the focus (lines focus to focus+height of text; a list's selected row, an input's whole box) and returns text marked so its page finds the body line it lands on, which is how the page scrolls to keep the focus visible. Call it with the view's final text after Placed; it amends that placement (or places id, if Placed was not called). Outside a page render it returns text unchanged. List, Input, Terminal and grid views call it.

func (*Runtime) Pop

func (r *Runtime) Pop() error

Pop closes the active modal and restores the focus and mode it opened over. A body the modal created is closed through the delivery contract and removed with what it holds.

func (*Runtime) Push

func (r *Runtime) Push(id, focus string) error

Push opens a modal with id focused on focus (app when empty). An id that is not already an instance becomes one of the modal noun for as long as it is open. The modal remembers the focus and mode it opened over.

func (*Runtime) Query

func (r *Runtime) Query(preds ...Predicate) []*Instance

Query returns the instances matching every predicate, in tree order: app first, then depth-first with siblings in the order they were added. An unknown predicate matches nothing; instances.query reports it instead.

func (*Runtime) ReadYank

func (r *Runtime) ReadYank(register string) (Yank, error)

ReadYank reads a register ("" is the active register, else the unnamed one).

func (*Runtime) RecordEditor added in v0.2.0

func (r *Runtime) RecordEditor(id string) *RecordEditor

RecordEditor is the record editor with this id, or nil.

func (*Runtime) Register

func (r *Runtime) Register(o Operation)

Register adds an operation. An operation with App set is of kind application and its Run is the framework's dispatcher for it. Its noun may be dotted (ai.skills) and so may its verb (base.add): the key is a name in a flat table, looked up whole. A noun named by a framework alias (settings) is the noun it names (ochazuke.settings), as Define reads it; inside Builtin a plain noun is ochazuke.<noun>. A verb y on noun x beside a noun x.y is refused (checkOverlap).

func (*Runtime) RegisterPage

func (r *Runtime) RegisterPage(page *Page, opts ...InstanceOption) error

RegisterPage registers a page as an instance of kind page under app (the instance may already exist, registered through Instance with the inputs and lists it holds). Focus names where the page starts: the page itself or an instance beneath it. Registration does not activate the page.

func (*Runtime) RegisterPredicate

func (r *Runtime) RegisterPredicate(name string, fn PredicateFunc)

RegisterPredicate adds a named predicate an application's queries (and the CLI's instances.query) can use, such as marked.

func (*Runtime) Registers

func (r *Runtime) Registers() *Registers

Registers returns the registers component.

func (*Runtime) ReloadConfig added in v0.15.0

func (r *Runtime) ReloadConfig() ([]string, error)

ReloadConfig reads every configuration file the application uses again, in dependency order: themes.json (when it was read: the theme setting's choices are then current), settings.json (when settings are declared; observers see every value that changed, the Model's theme among them), actions.json (when it was installed), bindings.json (when it was applied, once: a row can name an action's verb), segments.json (when it was read), then each OnConfigReload hook. Each file's last problems are withdrawn and its new ones reported, so no stale notice survives; a bad file is skipped, never fatal. It returns the problems in that order, each naming its file, and an error only when the application has no config directory.

func (*Runtime) ReloadThemes added in v0.2.0

func (r *Runtime) ReloadThemes() []string

ReloadThemes reads themes.json again: the schemes that validate replace the user's, every problem is reported (and the last read's withdrawn), and the theme setting's choices follow when the framework declared it. A missing file is no schemes and no problem.

func (*Runtime) ReloadUserActions added in v0.2.0

func (r *Runtime) ReloadUserActions() []string

ReloadUserActions removes the actions a previous load installed, then reads actions.json again and installs each valid entry as an application verb. An entry that does not validate (an unknown noun, a verb that exists, a bad template) is skipped and reported, never fatal; a file that does not parse installs nothing. A missing file is no actions and no problem. It returns the problems, each also sent to Config.Report.

func (*Runtime) ReloadUserBindings added in v0.2.0

func (r *Runtime) ReloadUserBindings() []string

ReloadUserBindings takes out the rows a previous load installed (the rows they replaced come back), then reads the bindings file again and installs its rows one by one: a row that does not validate is skipped and reported, never fatal, and a file that does not parse installs nothing. A missing file is no rows and no problem. It returns the problems, each also sent to Config.Report.

func (*Runtime) ReloadUserSegments added in v0.36.0

func (r *Runtime) ReloadUserSegments() []string

ReloadUserSegments reads segments.json again: the rows it held before are taken back (a user-only segment goes, a patched one keeps the application's row), then each valid row patches or adds its segment. A row that does not validate is skipped and reported; a missing file is no rows and no problem.

func (*Runtime) RemoveInstance

func (r *Runtime) RemoveInstance(id string) error

RemoveInstance closes an instance and its descendants (components closed, jobs cancelled) and drops them. Every focus reference into the removed subtree (Focus, a page's remembered focus, an open modal's return) moves to the nearest surviving ancestor (the Focus itself to a removed instance's FocusOnClose target when that survives); an open modal inside the subtree is kept and reparented there. app cannot be removed.

func (*Runtime) ResolveAction added in v0.28.0

func (r *Runtime) ResolveAction(name string) (Operation, Invocation, bool)

ResolveAction is the action a name runs and how it was named: the action's own key (noun aliases resolved: settings.set is ochazuke.settings.set) or an alias of it.

func (*Runtime) RetargetFocus

func (r *Runtime) RetargetFocus(match func(id string) bool, to string)

RetargetFocus points every focus reference that match accepts (the focus, a page's remembered focus, an open modal's return) at to. A component that recreates its focused child (the grid's cell, the list's row) calls it after the move; staying inside one focus stop, it keeps the mode.

func (*Runtime) SegmentCells added in v0.36.0

func (r *Runtime) SegmentCells() []SegmentCell

SegmentCells are the segments the header rule draws now: the active rows that have answered, in order. A failed refresh keeps the last text and draws it in state.errored; one that never answered draws its label and "!" in state.errored.

func (*Runtime) SegmentProvider added in v0.36.0

func (r *Runtime) SegmentProvider(name string, fn SegmentProvider)

SegmentProvider registers a Go source under name; a segment names it in its Provider field. A later registration of the name replaces it.

func (*Runtime) Segments added in v0.36.0

func (r *Runtime) Segments() []SegmentState

Segments lists every segment in drawing order, with whether it shows and its last answer.

func (*Runtime) SegmentsShown added in v0.36.0

func (r *Runtime) SegmentsShown() bool

SegmentsShown reports whether the segments area is on (its setting undeclared reads on).

func (*Runtime) SetAvailability added in v0.36.0

func (r *Runtime) SetAvailability(key, guard string)

SetAvailability makes a registered action available only while the named guard holds (Available): what NounBuilder.When does for the verb it follows, for an operation registered with Register (the framework's components). An empty guard makes the action always available.

func (*Runtime) SetConfirm

func (r *Runtime) SetConfirm(key, prompt string) error

SetConfirm sets or clears (empty prompt) the confirm prompt of noun.verb.

func (*Runtime) SetFocus

func (r *Runtime) SetFocus(id string) error

SetFocus points the focus at one instance: an input, a list, a row, a page or a modal body. Without a modal, the focus must sit inside the current page; a hidden page's instances are addressed with an explicit target.

func (*Runtime) SetMode

func (r *Runtime) SetMode(mode string) error

SetMode sets the global mode by name and clears its payload, as mode.set does: a focused input runs its own transition (insert sessions, visual anchor), and a pinned input stays in its pinned mode.

func (*Runtime) SetPaneShift added in v0.4.0

func (r *Runtime) SetPaneShift(page string, shift int) (int, error)

SetPaneShift remembers page's divider, clamped to PaneShiftLimit either way, and writes the panes file (the state directory's PanesFile) when there is one; a failed write is a notice and the divider still holds for the session. It returns the shift kept.

func (*Runtime) SetTableState added in v0.12.0

func (r *Runtime) SetTableState(id string, s TableState)

SetTableState remembers the view of the table with this id and writes the tables file (the state directory's TablesFile) when it changed; a failed write is a notice and the view still holds for the session.

func (*Runtime) SetTerminalLeader added in v0.2.0

func (r *Runtime) SetTerminalLeader(key string) error

SetTerminalLeader moves every leader to key: on every noun, each row that enters leader_pending on one key (the framework's terminal row and an application's own, such as a richer hint) is rebound to key with key as its prefix and its timeout request, and the leader_pending row that sends the old leader to the pane now sends key on key. Hints that name the old leader name the new one. A row already bound to key in those tables gives way. The rows stay data: this rewrites them as a binding file would. The terminal-leader setting calls it; a key that is not one ctrl+ or alt+ combination is refused.

func (*Runtime) Settings added in v0.2.0

func (r *Runtime) Settings() *Settings

Settings returns the settings of the config component.

func (*Runtime) Settle

func (r *Runtime) Settle(t Ticket, err error) bool

Settle records the outcome of a running job and reports whether the ticket was current; a stale ticket leaves the list unchanged. Deliver settles the completions it routes; a component that runs its own commands settles the tickets it issued.

func (*Runtime) Snapshot

func (r *Runtime) Snapshot() map[string]any

func (*Runtime) Sourced

func (r *Runtime) Sourced(source string) (restore func())

Sourced sets the source the requests executed until restore is called are recorded with, unless an outer caller already set one (a row run by a key inside a macro replay keeps "macro:q"). Callers: the resolver ("key:<keys>"), the transport ("cli"), the palette ("palette").

func (*Runtime) Start

func (r *Runtime) Start(owner string, work func(context.Context) (any, error), then func(Ctx, any, error), c Ctx) (Ticket, error)

Start schedules accumulating work owned by an instance and returns its ticket; it is Launch with Pending(work).Then(then) under the verb "work".

func (*Runtime) StopBindings added in v0.2.0

func (r *Runtime) StopBindings() []StopBindings

StopBindings is the app-wide keyboard: the effective rows of every focus stop, as ListBindings gives them for the focus a jump to the stop lands on. The order is fixed: the pages in tab order (PageList), each page's stops in drawing order (the order focus.next walks, undrawn stops after in tree order), then the open modals bottom to top with theirs, then app in normal mode. The mode is the one the focus would have there: the current mode at the current focus, else the stop's pinned mode (a prompt is insert, a terminal terminal), else normal. Nothing moves: the focus and mode are back as they were on return.

It differs from bindings.list --all, which exports every stored row (each noun's and instance's tables, every mode, as configuration JSON), active or not: this answers "what does each key do where", after shadowing, guards and the modal boundary.

func (*Runtime) StoreYank

func (r *Runtime) StoreYank(y Yank, register string, deletion bool) error

StoreYank puts a yank into the shared registers with the bookkeeping the editor's yank operations use: the named or active register, the unnamed register, "0" for a yank or "1".."9" and "-" for a deletion, and the yank history. A component outside the package (a grid) yanks shapes the editor does not know ("column"), which register.set refuses, so it stores through this call and the editor still sees the text.

func (*Runtime) TableState added in v0.12.0

func (r *Runtime) TableState(id string) TableState

TableState is the remembered view of the table with this id (a list's or a grid's instance id): the zero TableState when none was kept.

func (*Runtime) TargetInstance added in v0.2.0

func (r *Runtime) TargetInstance(target string) (string, error)

TargetInstance is the instance a request target names: the target when it is an instance, else what ResolveTarget maps it to. An unknown target is an error naming it.

func (*Runtime) Terminal

func (r *Runtime) Terminal(id string) *Terminal

Terminal returns the terminal an instance fronts, or nil.

func (*Runtime) TerminalLeader added in v0.2.0

func (r *Runtime) TerminalLeader() string

TerminalLeader is the key the terminal noun's leader row binds (the terminal-leader setting once applied), DefaultTerminalLeader when the noun has no leader row.

func (*Runtime) Timeline added in v0.17.0

func (r *Runtime) Timeline(id string) *Timeline

Timeline is the timeline with this id, or nil.

func (*Runtime) TopModal

func (r *Runtime) TopModal() string

TopModal is the id of the innermost open modal, "" when none is open.

func (*Runtime) Tree

func (r *Runtime) Tree(id string) *Tree

Tree is the tree with this id, or nil.

func (*Runtime) UI added in v0.4.0

func (r *Runtime) UI() *UI

UI returns the UI component: Notify, ClearNotice, Error and Info for code without a Ctx (setup, daemon events), Attach for a host.

func (*Runtime) UnreachableBindings added in v0.36.0

func (r *Runtime) UnreachableBindings() []string

UnreachableBindings reports every stored row that can never answer (unreachableRows), table by table.

func (*Runtime) UserActions added in v0.2.0

func (r *Runtime) UserActions() []Action

UserActions are the actions installed from actions.json, in file order.

func (*Runtime) UserAliases added in v0.28.0

func (r *Runtime) UserAliases() []ActionAlias

UserAliases are the alias rows installed from actions.json, in file order.

func (*Runtime) ValidateBindings

func (r *Runtime) ValidateBindings() ([]string, error)

ValidateBindings checks every row of every noun and instance table the way Bind checks a row it installs: keys or a pattern, a registered mode and guard, and a Request to a registered operation with known arguments and sources, or an unbind. Every row is therefore atomic. It is the check behind TestBindingRowsAreAtomic and is for an application to run after it loads its own rows.

func (*Runtime) VerbGuard

func (r *Runtime) VerbGuard(key string) string

VerbGuard names the guard on noun.verb, "" when the verb is unguarded.

func (*Runtime) VerbShown

func (r *Runtime) VerbShown(key, focus string, mode ModeState) bool

VerbShown reports whether noun.verb is offered at a focus in a mode: true for an unguarded verb or one whose guard holds there. A guard name that is not registered hides nothing. It is Available for a request with no arguments.

type Scheme added in v0.2.0

type Scheme struct {
	Background string               `json:"background,omitempty"`
	Text       SchemeColor          `json:"text"`
	Muted      SchemeColor          `json:"muted"`
	Accent     SchemeColor          `json:"accent"`
	Border     SchemeColor          `json:"border"`
	Surface    SchemeColor          `json:"surface"`
	Error      SchemeColor          `json:"error"`
	Tones      map[string]ToneStyle `json:"tones,omitempty"`
	// contains filtered or unexported fields
}

Scheme is a named colour scheme: the palette roles NewStyles draws pages, panels, inputs and modals with. Background is which side of each role it draws: "adaptive" (the terminal's, the default), "light" or "dark". Text is body text and the title; Muted hints and inactive tabs; Accent the active tab, badge, subtitle and focused borders; Border unfocused panel borders; Surface the active tab's and badge's background; Error errors, kept apart from Accent and Muted. Selection stays reverse video in every scheme, so the selected row reads without colour.

type SchemeColor added in v0.2.0

type SchemeColor struct {
	Light string `json:"light"`
	Dark  string `json:"dark"`
}

SchemeColor is one palette role: Light is drawn on a light background, Dark on a dark one. Each is "#RRGGBB", "#RGB" or an ANSI index "0" to "255". In themes.json a role is one colour for both, or {"light": "...", "dark": "..."}.

func (*SchemeColor) UnmarshalJSON added in v0.2.0

func (c *SchemeColor) UnmarshalJSON(data []byte) error

UnmarshalJSON reads "#hex" (both sides) or {"light", "dark"} (a missing side takes the other).

type SchemeSet added in v0.2.0

type SchemeSet struct {
	// contains filtered or unexported fields
}

SchemeSet is a read-only set of named schemes; a nil set is empty.

func NewSchemeSet added in v0.2.0

func NewSchemeSet(schemes map[string]Scheme) *SchemeSet

NewSchemeSet holds a copy of schemes.

func (*SchemeSet) Get added in v0.2.0

func (s *SchemeSet) Get(name string) (Scheme, bool)

Get is the scheme of that name.

func (*SchemeSet) Names added in v0.2.0

func (s *SchemeSet) Names() []string

Names are the scheme names, sorted; an entry that only adds tones to a built-in theme is not a scheme of its own and is left out.

type Segment added in v0.36.0

type Segment struct {
	ID       string        `json:"id"`
	Label    string        `json:"label,omitempty"`
	Format   string        `json:"format,omitempty"`
	Provider string        `json:"provider,omitempty"`
	Command  Argv          `json:"command,omitempty"`
	Verb     string        `json:"verb,omitempty"`
	Args     Arguments     `json:"args,omitempty"`
	Every    time.Duration `json:"-"`
	Timeout  time.Duration `json:"-"`
	Tone     string        `json:"tone,omitempty"`
	Order    int           `json:"order,omitempty"`
	Enabled  *bool         `json:"enabled,omitempty"`
	Path     string        `json:"path,omitempty"`
	// Description is the line segment list and the segment's setting show.
	Description string `json:"description,omitempty"`
}

Segment is one status segment row. Format is a template over {{label}} and {{value}} (default "{{label}} {{value}}", or "{{value}}" without a label). Exactly one of Provider, Command and Verb is the source; Args are the verb's arguments. Every is the refresh interval (default DefaultSegmentEvery, at least MinSegmentEvery) and Timeout the most one refresh may take (default DefaultSegmentTimeout, at most Every). Tone is a named tone (tone.go) the text draws in, unless the answer brings its own (SegmentValue.Tone); none draws muted like the rule. Order sorts the rows (lower first, then the order they were added) before status.segment-order. Enabled is the default of the segment's setting (nil: on). Path is the provider's path (disk free reads it).

In segments.json the durations are strings ("2s"), as settings.json writes them.

func (Segment) MarshalJSON added in v0.36.0

func (s Segment) MarshalJSON() ([]byte, error)

MarshalJSON writes the durations as strings ("2s").

func (*Segment) UnmarshalJSON added in v0.36.0

func (s *Segment) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the durations as strings ("2s", "500ms"); an unknown field is an error.

type SegmentCell added in v0.36.0

type SegmentCell struct {
	ID   string
	Text string
	Tone string
}

SegmentCell is one drawn segment: its text and tone ("" muted).

type SegmentMsg added in v0.36.0

type SegmentMsg struct {
	ID    string
	Value SegmentValue
	Err   error
}

SegmentMsg is a refresh's answer, delivered on the loop.

type SegmentProvider added in v0.36.0

type SegmentProvider func(ctx context.Context, s Segment) (SegmentValue, error)

SegmentProvider is a Go source: it returns the segment's value, off the loop, within ctx's deadline (the segment's Timeout).

type SegmentState added in v0.36.0

type SegmentState struct {
	Segment
	Source   string    `json:"source"` // provider, command or verb
	From     string    `json:"from"`   // app, or segments.json
	Shown    bool      `json:"shown"`  // its setting is on (and the area is)
	Position int       `json:"position"`
	Text     string    `json:"text,omitempty"`
	Error    string    `json:"error,omitempty"`
	At       time.Time `json:"at,omitzero"`
}

SegmentState is a segment as segment.list and segment.show answer: the row as merged, where it came from, whether it shows, its position and its last answer.

type SegmentTickMsg added in v0.36.0

type SegmentTickMsg struct{ At time.Time }

SegmentTickMsg is the segments' one-shot timer: it fires at the earliest next refresh of a shown segment (Model.segmentCommand), so a reading changes on an idle screen. At is the deadline it was scheduled for; only the message for the timer in flight clears it. Nothing shown, nothing is scheduled.

type SegmentValue added in v0.36.0

type SegmentValue struct {
	Text string `json:"text"`
	Tone string `json:"tone,omitempty"`
}

SegmentValue is one answer of a source: the text and, optionally, the tone it draws in (a disk nearly full answers state.errored).

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server serves runtime requests over a Unix socket; see Listen.

func Listen

func Listen(path string, send func(CallMsg)) (*Server, error)

Listen requires a private, current-user-owned socket directory and never removes an existing socket.

func (*Server) Close

func (s *Server) Close() error

type Setting added in v0.2.0

type Setting struct {
	Name        string   `json:"name"`
	Type        string   `json:"type"`
	Default     any      `json:"default"`
	Description string   `json:"description,omitempty"`
	Enum        []string `json:"enum,omitempty"`
	// Section files the setting under a section of the Settings page; ""
	// takes the name before its last dot ("ext.ai.poll-every" is in
	// "ext.ai"), and a name without a dot is in SettingsGeneral
	// (SectionName).
	Section string `json:"section,omitempty"`
	// Validate, when set, checks a value of the right type further: Set
	// refuses a value it rejects and a file value it rejects reads the
	// default, as a value of the wrong type does.
	Validate func(value any) error `json:"-"`
}

Setting is one declared user preference: its name, type, default, the line the settings list shows and, for a string, the allowed values. Settings are not for secrets: the file is plain JSON in the config directory.

func (Setting) SectionName added in v0.7.0

func (s Setting) SectionName() string

SectionName is the section the Settings page files the setting under: Section when declared, else the name before its last dot ("ext.ai.poll-every" is in "ext.ai"), else "" (SettingsGeneral).

type SettingValue added in v0.2.0

type SettingValue struct {
	Setting
	Value any  `json:"value"`
	Set   bool `json:"set"`
}

SettingValue is a setting with its current value, as settings.list and settings.get answer: Value and Default in their file form (a duration as "400ms"), Set when settings.json holds a value for it.

type Settings added in v0.2.0

type Settings struct {
	// contains filtered or unexported fields
}

Settings holds the declared settings and their values from settings.json in the config directory. It is part of the config component, reached through Runtime.Settings(). The file is read on first use (and again when the config directory changes), written atomically (a temp file renamed over it) on every Set and Reset, and keeps every key it holds: an unknown or invalid key is reported and written back unchanged, never dropped. Without a config directory the values last for the session. Model polls the file (SettingsPoll) so a value another process set (the CLI) reaches a running TUI.

func (*Settings) Bool added in v0.2.0

func (s *Settings) Bool(name string) bool

Bool is a boolean setting's value.

func (*Settings) Declared added in v0.2.0

func (s *Settings) Declared() []Setting

Declared lists the declared settings in declaration order.

func (*Settings) Duration added in v0.2.0

func (s *Settings) Duration(name string) time.Duration

Duration is a duration setting's value.

func (*Settings) Float added in v0.2.0

func (s *Settings) Float(name string) float64

Float is a number setting's value.

func (*Settings) Get added in v0.2.0

func (s *Settings) Get(name string) (SettingValue, error)

Get is one setting with its value.

func (*Settings) Int added in v0.2.0

func (s *Settings) Int(name string) int

Int is an integer setting's value.

func (*Settings) List added in v0.2.0

func (s *Settings) List() []SettingValue

List is every declared setting with its value, in declaration order.

func (*Settings) Observe added in v0.2.0

func (s *Settings) Observe(name string, fn func(name string, value any))

Observe calls fn with a setting's new value whenever it changes (Set, Reset, or a reload that read another value); name "" observes every setting. Observers run on the caller's goroutine, the update loop.

func (*Settings) Problems added in v0.2.0

func (s *Settings) Problems() []string

Problems are the problems the last read of settings.json found.

func (*Settings) Reload added in v0.2.0

func (s *Settings) Reload() []string

Reload reads settings.json again and tells the observers of every value that changed. It returns the problems the file has.

func (*Settings) ReloadIfChanged added in v0.2.0

func (s *Settings) ReloadIfChanged() bool

ReloadIfChanged reloads when the file changed on disk since it was last read or written (another process set a value); it reports whether it did.

func (*Settings) Reset added in v0.2.0

func (s *Settings) Reset(name string) error

Reset removes a setting's value from settings.json, so it reads its default again.

func (*Settings) Set added in v0.2.0

func (s *Settings) Set(name string, value any) error

Set validates value against the setting's type and enum (a string is parsed: "3", "true", "400ms"), writes settings.json and tells the observers. A value that does not validate changes nothing.

func (*Settings) String added in v0.2.0

func (s *Settings) String(name string) string

String is a string setting's value ("" when undeclared or another type).

func (*Settings) Strings added in v0.8.0

func (s *Settings) Strings(name string) []string

Strings is a list setting's value, a copy (nil when undeclared or another type).

func (*Settings) Value added in v0.2.0

func (s *Settings) Value(name string) (any, bool)

Value is a setting's current value (the file's when it holds a valid one, else the default), typed as its getter returns it.

type SettingsPollMsg added in v0.2.0

type SettingsPollMsg struct{}

SettingsPollMsg is the Model's settings.json check.

type SplitSide added in v0.2.0

type SplitSide int

SplitSide names the panel of a split that draws focused.

const (
	SplitLeft  SplitSide = iota // the left (or top) panel, Split's default
	SplitRight                  // the right (or bottom) panel
	SplitNone                   // neither: the focus is elsewhere
)

type StopBindings added in v0.2.0

type StopBindings struct {
	Page     string    `json:"page,omitempty"`
	Stop     string    `json:"stop"`
	Focus    string    `json:"focus"`
	Mode     string    `json:"mode"`
	Bindings []Binding `json:"bindings"`
}

StopBindings is one entry of bindings.stops: the rows a focus stop answers to (ListBindings at Focus in Mode), with the page it sits on. Stop is the stop instance, Focus where a jump to it lands (its remembered child, a list's selected row), Mode the mode the focus has there. The last entry is app itself (Stop and Focus "app").

type Styles

type Styles struct {
	Title        lipgloss.Style
	Subtitle     lipgloss.Style
	Muted        lipgloss.Style
	Accent       lipgloss.Style
	Error        lipgloss.Style
	Panel        lipgloss.Style
	FocusedPanel lipgloss.Style
	Tab          lipgloss.Style
	ActiveTab    lipgloss.Style
	Badge        lipgloss.Style
	Padding      int
	// Backdrop draws what a modal covers (modal-dim); Unfocused draws a page
	// list without the focus (dim-unfocused). Either is the empty style when
	// its dim is 0. WithDims sets both from percents.
	Backdrop  lipgloss.Style
	Unfocused lipgloss.Style
	// contains filtered or unexported fields
}

Styles is a render-time value; it does not retain mutable appearance state.

func NewStyles

func NewStyles(a Appearance) Styles

NewStyles keeps layout and focus cues usable without terminal color support. Theme names the scheme (a built-in theme or one of Schemes; an unknown name draws adaptive); mono and NO_COLOR draw no colour at all.

func (Styles) CardBorders added in v0.23.0

func (s Styles) CardBorders() (focused, selected, other lipgloss.Style)

CardBorders are the border styles of a card timeline's three states (the selection is the border, never reverse video): selected while the timeline has the focus, the accent in bold; selected without the focus, halfway from the accent to the border role; every other card, the border role faded toward the surface by dim-unfocused. With no colour (mono, NO_COLOR) they are bold, plain and faint; the timeline also draws the two selected states in heavy lines, so they read without any attribute.

func (Styles) Fade added in v0.10.0

func (s Styles) Fade(percent int) lipgloss.Style

Fade is a style that carries text percent of the way from the scheme's text colour toward its surface, blended in CIELAB (fadeRamp, lipgloss.Blend1D); with no colour (mono, NO_COLOR) any percent above 0 is the faint attribute. 0 is the empty style.

func (Styles) Tone added in v0.26.0

func (s Styles) Tone(name string) lipgloss.Style

Tone is the style a tone draws in outside a list (an application's own line, a footer): its colour and attributes, the empty style for an unknown tone; mono and NO_COLOR keep the attributes alone.

func (Styles) WithDims added in v0.10.0

func (s Styles) WithDims(modal, unfocused int) Styles

WithDims sets Backdrop and Unfocused from percents: the backdrop is the fade plus the faint attribute (the look it always had at the default), an unfocused list the fade alone.

type Suggestion added in v0.5.0

type Suggestion struct {
	Text   string `json:"text"`
	Label  string `json:"label,omitempty"`
	Detail string `json:"detail,omitempty"`
}

Suggestion is one completion a search offers. Text replaces the token after its trigger; Label is what the list shows ("" shows Text); Detail is drawn muted after the label.

type TableState added in v0.12.0

type TableState struct {
	Column int         `json:"column,omitempty"`
	Widths map[int]int `json:"widths,omitempty"`
	Pins   *[]int      `json:"pins,omitempty"`
}

TableState is what a table remembers across a restart: its current column (a cell-mode table's), the widths its columns were given with z > and z < (column index to cells; a column left out is automatic) and the pinned columns the user chose (any table's; nil keeps the default pins, an empty list pins none).

type Terminal

type Terminal struct {
	// contains filtered or unexported fields
}

Terminal is the component of a terminal instance: it holds the last frame the stream delivered, renders it into the area a page gives it and reports the pane's cursor. Frames arrive through the delivery contract as Completions owned by the instance in the "read" slot, one read at a time, so a frame that arrives after Close, or after a newer read started, is stale and dropped before it reaches the component.

func (*Terminal) Close

func (t *Terminal) Close()

Close stops reading and releases a TerminalReleaser stream, once; the runtime has already cancelled the read in flight.

func (*Terminal) Err

func (t *Terminal) Err() string

Err is the stream's last error, "" when it is healthy.

func (*Terminal) Focused

func (t *Terminal) Focused() bool

Focused reports whether keys go to this pane.

func (*Terminal) Frame

func (t *Terminal) Frame() TerminalFrame

Frame is the last frame the stream delivered.

func (*Terminal) HandleMessage

func (t *Terminal) HandleMessage(msg tea.Msg) bool

HandleMessage stores a frame delivered to this terminal and schedules the next read. A stream error ends reading; a cancelled read is silent.

func (*Terminal) ID

func (t *Terminal) ID() string

ID is the terminal's instance id.

func (*Terminal) Overlay added in v0.14.0

func (t *Terminal) Overlay() *TerminalOverlay

Overlay is the overlay SetOverlay set, nil for none.

func (*Terminal) PendingCommand

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

PendingCommand is nil: reads are scheduled through Runtime.Start.

func (*Terminal) SetOverlay added in v0.14.0

func (t *Terminal) SetOverlay(o *TerminalOverlay)

SetOverlay draws o over the pane from the next frame on; nil clears it. The overlay stays until the application clears or replaces it: a new frame from the stream does not clear it, and neither do a focus change, a mode change or a detach, so an application that pairs it with a mode clears it where it leaves the mode (and in its own focus or close handling). It changes only what ViewBlock draws: Frame, the stream and every other reader of the pane see the pane's own cells.

func (*Terminal) Size

func (t *Terminal) Size() (cols, rows int)

Size is the area the pane was last told to fill.

func (*Terminal) View

func (t *Terminal) View(width, height int) string

View renders the pane without a cursor; see ViewBlock.

func (*Terminal) ViewBlock

func (t *Terminal) ViewBlock(width, height int) Block

ViewBlock renders the pane's cells into exactly width x height cells and reports the pane's cursor only while focused. The stream is told the area the first time it is seen and whenever it changes, so the pane is sized to the detail area it fills.

type TerminalFrame

type TerminalFrame struct {
	Lines  []string `json:"lines"`
	Cursor Pos      `json:"cursor"`
	Shape  string   `json:"shape,omitempty"`
	Hidden bool     `json:"hidden,omitempty"`
}

TerminalFrame is one rendering of a pane: its rows of trusted ANSI, the cursor cell, the cursor shape the pane reports ("block", "bar", "underline"; "" for the default block) and whether the cursor is hidden.

type TerminalOverlay added in v0.14.0

type TerminalOverlay struct {
	Dim   bool          `json:"dim,omitempty"`
	Marks []OverlayMark `json:"marks,omitempty"`
}

TerminalOverlay is what an application draws over a terminal's frame without touching the frame: Dim fades the pane's cells (the modal-dim backdrop style; faint in mono and NO_COLOR) and each mark replaces the cells it covers. Hints over a live pane (labels on its URLs and paths) are an overlay plus an application mode; see SetOverlay.

type TerminalReleaser

type TerminalReleaser interface {
	Release() error
}

TerminalReleaser is a TerminalStream that holds something to give back when its terminal closes: a daemon size lease, a connection. Terminal.Close calls Release once, so Pop, SetItems, RemoveInstance and a CloseOnDetach detach all release the stream; its error is kept as the terminal's Err.

type TerminalStream

type TerminalStream interface {
	Next(ctx context.Context) (TerminalFrame, error)
	Send(keys ...string) error
	Paste(text string) error
	Resize(cols, rows int) error
}

TerminalStream is the application's connection to one pane: a daemon client, never a PTY the framework owns. The framework reads frames from it, forwards keys and pasted text to it and tells it the pane's size. Next blocks until the pane changes and returns its next frame; it returns ctx's error when the terminal instance closes or newer work supersedes the read. Keys are named as bindings spell them ("ctrl+a", "d", "esc").

type Ticket

type Ticket struct {
	Owner string `json:"owner"`
	Job   uint64 `json:"job"`
	Slot  string `json:"slot,omitempty"`
}

Ticket names one job: the instance that owns it, its runtime-wide id and the slot it was started in. Slot is "" for a job that accumulates (every completion is delivered, in finish order) and a name for a job that replaces: a newer job in the same slot on the same owner (or, with ReplacingAt, on the same scope) drops the older. A completion is stale, and dropped, only when its job was cancelled by a newer job in its slot or by its owner closing.

type Timeline added in v0.17.0

type Timeline struct {
	// contains filtered or unexported fields
}

Timeline is a scrolling column of cards of varying height, each a box (Card). The selection, motions, counts, spills, the / filter and the card instances are those of a list child (id+"/list") whose rows are the cards, so j/k, g g, ctrl+d/ctrl+u and 3j behave as on any page list; the timeline draws the cards itself, in a window of its own measured in lines (top, the first card drawn and how many of its lines are above the window), and reports the selected card's lines with PlacedAt so the page keeps it on screen.

Selection rule (the cards' exception to a page list's reverse video): the selection is the border, and no text on a card is ever reversed. The selected card of a focused timeline has a heavy border with heavy corners in the accent, bold; the selected card of a timeline without the focus a heavy border with rounded corners halfway from the accent to the border role; every other card a light rounded border in the border role faded by dim-unfocused (Styles.CardBorders). Header, meta and body text keep their own styles whether selected or not. A timeline without the focus is also faded as a whole (dim-unfocused), as a page list is.

func (*Timeline) BodyID added in v0.30.0

func (t *Timeline) BodyID() string

BodyID is the id of the instance that holds the focus inside a card.

func (*Timeline) Cards added in v0.17.0

func (t *Timeline) Cards() []Card

Cards is a copy of the cards in order.

func (*Timeline) Focus added in v0.17.0

func (t *Timeline) Focus() error

Focus focuses the selected card.

func (*Timeline) FocusTarget added in v0.17.0

func (t *Timeline) FocusTarget() string

FocusTarget is where a jump to the timeline lands: its selected card.

func (*Timeline) Folded added in v0.17.0

func (t *Timeline) Folded(id, section string) bool

Folded reports whether section (its Name, Title or number from 1) of the card with id is folded.

func (*Timeline) Following added in v0.17.0

func (t *Timeline) Following() bool

Following reports whether the timeline follows its end (FollowTail).

func (*Timeline) ID added in v0.17.0

func (t *Timeline) ID() string

ID is the timeline's instance id.

func (*Timeline) InBody added in v0.30.0

func (t *Timeline) InBody() bool

InBody reports whether the focus is inside the selected card.

func (*Timeline) IsOpen added in v0.17.0

func (t *Timeline) IsOpen(id string) bool

IsOpen reports whether the card with id is open.

func (*Timeline) List added in v0.17.0

func (t *Timeline) List() *List

List is the list whose rows are the cards (selection, motions, filter).

func (*Timeline) Patch added in v0.17.0

func (t *Timeline) Patch(cards ...Card) error

Patch updates the cards with these ids in place and appends the others at the end, in order: the cheap path for a streaming last card (bump its Revision) or a new one. The list's rows are rebuilt only when a card is added or its noun, label or data changed.

func (*Timeline) Select added in v0.17.0

func (t *Timeline) Select(id string) error

Select selects the card with id without taking the focus.

func (*Timeline) Selected added in v0.17.0

func (t *Timeline) Selected() (Card, bool)

Selected is the selected card; an empty timeline has none.

func (*Timeline) SelectedID added in v0.17.0

func (t *Timeline) SelectedID() string

SelectedID is the selected card's id, or "".

func (*Timeline) SetAllOpen added in v0.17.0

func (t *Timeline) SetAllOpen(open bool)

SetAllOpen opens or closes every card.

func (*Timeline) SetCards added in v0.17.0

func (t *Timeline) SetCards(cards []Card) error

SetCards replaces the cards, keyed by id: the selection, open cards and section folds follow their ids (as List.SetItems keeps the selection), a new card starts as its Open says. While the timeline follows its end (FollowTail) and the last card is selected, the new last card is selected. Invalid cards leave the timeline unchanged.

func (*Timeline) SetFolded added in v0.17.0

func (t *Timeline) SetFolded(id, section string, folded bool) error

SetFolded folds or unfolds a section of the card with id (section is its Name, Title or number from 1).

func (*Timeline) SetOpen added in v0.17.0

func (t *Timeline) SetOpen(id string, open bool) error

SetOpen opens or closes the card with id.

func (*Timeline) SetParentStop added in v0.30.0

func (t *Timeline) SetParentStop(id string)

SetParentStop changes where h on a closed card goes ("" stays).

func (*Timeline) Step added in v0.30.0

func (t *Timeline) Step() (card string, step int, ok bool)

Step is the card the focus is inside and its current step (from 0); false outside a card.

func (*Timeline) StepText added in v0.30.0

func (t *Timeline) StepText() (string, bool)

StepText is the current step's text (what y yanks); false outside a card.

func (*Timeline) View added in v0.17.0

func (t *Timeline) View(width, height int) string

View draws the cards in a width by height window and records the timeline in its page's drawing order (Placed) with the selected card's lines (PlacedAt). When the cards overflow, the last line reads which cards are on screen, "3–5 / 40". A filter line (/) takes the first line.

type ToneSpan added in v0.26.0

type ToneSpan struct {
	Cell int    `json:"cell"`
	Tone string `json:"tone"`
	From int    `json:"from,omitempty"`
	To   int    `json:"to,omitempty"`
}

ToneSpan gives a run of one cell a tone. Cell is the column index (in a list without columns, and a tree without them, 0 is the label); From and To are rune offsets into the cell's text as drawn (escape sequences, which a cell never draws, do not count), To 0 meaning the cell's end, so {Cell, Tone} alone tones the whole cell. A later span wins where two overlap; a span outside the text draws nothing.

func CellTone added in v0.26.0

func CellTone(cell int, tone string) ToneSpan

CellTone tones a whole cell.

func SpanTone added in v0.26.0

func SpanTone(cell, from, to int, tone string) ToneSpan

SpanTone tones the runes [from, to) of a cell (to 0: to its end).

type ToneStyle added in v0.26.0

type ToneStyle struct {
	Fg   SchemeColor `json:"fg"`
	Bold bool        `json:"bold,omitempty"`
	Dim  bool        `json:"dim,omitempty"`
}

ToneStyle is how a scheme draws one tone: Fg is a colour as a scheme role takes it ("#RRGGBB", "#RGB", an ANSI index, {"light", "dark"}) or the name of a palette role (text, muted, accent, border, surface, error) so the tone follows the scheme; empty keeps the text's colour. Bold and Dim (faint) are the attributes, the only part mono draws. In themes.json a tone is {"fg": ..., "bold": true, "dim": true}, or a colour or role alone ("#5FAFD7", "error").

func (*ToneStyle) UnmarshalJSON added in v0.26.0

func (t *ToneStyle) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a colour or role alone, or {"fg", "bold", "dim"}.

type Tree

type Tree struct {

	// OnChange makes the tree editable and receives each change as it is
	// made; an error keeps the change on screen, marks the row and retries
	// it before the next change.
	OnChange func(TreeChange) error
	// NewID names a new node (default "<tree>-n<seq>").
	NewID func() string
	// RowNoun is the noun of nodes the tree adds (default TreeRowNoun).
	RowNoun string
	// Debounce is the pause in typing before a line is written.
	Debounce time.Duration
	// contains filtered or unexported fields
}

Tree is a list whose rows are nested nodes: expand and collapse, the same motions as a list (it is one: rows spill to the neighbouring stops in drawing order), the one highlighted row, columns like a table. Given OnChange it is an outline editor: every structural edit is written at once, typing within a line is written after a pause (Debounce) and when the line is left, undo and redo are changes too, and esc never discards.

The tree is an instance of the outline noun with a list child (id+"/list") holding the visible rows; editing adds an input (id+"/line", noun outline-line) under the tree while a line is edited. (The nouns are "outline", not "tree", so palette prefixes such as "tr" stay unique in apps.)

func (*Tree) Block

func (t *Tree) Block(width, height int) Block

Block renders the rows as the list does; while a line is open its row is the input, with the terminal cursor in it.

func (*Tree) Close

func (t *Tree) Close()

Close writes an open line.

func (*Tree) CollapseAll

func (t *Tree) CollapseAll() error

CollapseAll collapses every node that has children.

func (*Tree) Collapsed

func (t *Tree) Collapsed(id string) bool

Collapsed reports whether a node is collapsed.

func (*Tree) DebounceCurrent

func (t *Tree) DebounceCurrent(tick TreeDebounceMsg) bool

DebounceCurrent reports whether a pause timer is for the line's latest change (an older timer writes nothing).

func (*Tree) DebounceTick added in v0.16.0

func (t *Tree) DebounceTick() (TreeDebounceMsg, bool)

DebounceTick is the pause timer in flight, for a host that delivers it at once (explore): false when no typing waits on one.

func (*Tree) Editable

func (t *Tree) Editable() bool

Editable reports whether the tree takes edits (OnChange is set).

func (*Tree) Editing

func (t *Tree) Editing() string

Editing is the node whose line is open, or "".

func (*Tree) Failure

func (t *Tree) Failure() error

Failure is the last background failure, cleared when read: Deliver returns it for the message the tree handled, so it reaches the status.

func (*Tree) Flush

func (t *Tree) Flush() error

Flush writes the open line now (on quit, and when the line is left).

func (*Tree) Focus

func (t *Tree) Focus() error

Focus focuses the tree's selected row.

func (*Tree) FocusTarget

func (t *Tree) FocusTarget() string

FocusTarget is where a jump to the tree lands: its selected row.

func (*Tree) HandleMessage

func (t *Tree) HandleMessage(msg tea.Msg) bool

HandleMessage writes the open line when its pause timer is the latest.

func (*Tree) ID

func (t *Tree) ID() string

ID is the tree's instance id.

func (*Tree) List

func (t *Tree) List() *List

List is the list that holds the visible rows.

func (*Tree) Nodes

func (t *Tree) Nodes() []TreeNode

Nodes is a copy of every node in display order, collapsed ones included.

func (*Tree) PendingCommand

func (t *Tree) PendingCommand() tea.Cmd

PendingCommand notices typing in the open line and sets the pause timer; a line whose input lost the focus is closed (and written).

func (*Tree) Record added in v0.1.1

func (t *Tree) Record(a TreeAction) error

Record puts an application change on the undo stack: an open line is sealed first (so its typing undoes after the action, not before), and the redo stack is cleared, as for any new change.

func (*Tree) Select

func (t *Tree) Select(id string) error

Select selects a node, expanding its ancestors, without taking focus.

func (*Tree) Selected

func (t *Tree) Selected() (TreeNode, bool)

Selected is the node of the selected row.

func (*Tree) SetCollapsed

func (t *Tree) SetCollapsed(id string, collapsed bool) error

SetCollapsed collapses or expands one node.

func (*Tree) SetNodes

func (t *Tree) SetNodes(nodes []TreeNode) error

SetNodes replaces the nodes, keeping collapse state and the selection by id. Nodes must list each parent before its children.

func (*Tree) View

func (t *Tree) View(width, height int) string

View renders the visible rows (see Block).

type TreeAction added in v0.1.1

type TreeAction struct {
	ID    string
	Label string // what the change is ("done n1"), for logs
	Undo  func() error
	Redo  func() error
}

TreeAction is an application change that goes on an outline's undo stack in order with the tree's own edits: a change the application keeps outside the tree's nodes, such as a task's status. u runs Undo and ctrl+r runs Redo; each is written by the application like any change. ID is the node selected after either runs ("" keeps the selection).

type TreeChange

type TreeChange struct {
	Op     string `json:"op"`
	ID     string `json:"id"`
	Parent string `json:"parent,omitempty"`
	Index  int    `json:"index"`
	Text   string `json:"text,omitempty"`
}

TreeChange is one committed edit of an outline: add, rename, move or delete. Index is the node's position among its parent's children after the change. The application writes each one to its store as it comes (Tree.OnChange); undo is a further change, never a discard.

func (TreeChange) String

func (c TreeChange) String() string

String reads a change in one line: "add n1 under root at 1: text", "rename n1: text", "move n1 under a at 0", "delete n1".

type TreeDebounceMsg

type TreeDebounceMsg struct {
	Tree string
	Seq  uint64
}

TreeDebounceMsg is the timer a pause in typing sets off: when Seq is still the line's latest change, the line is written.

type TreeNode

type TreeNode struct {
	ID     string     `json:"id"`
	Parent string     `json:"parent,omitempty"`
	Label  string     `json:"label"`
	Noun   string     `json:"noun,omitempty"`
	Data   any        `json:"data,omitempty"`
	Cells  []string   `json:"cells,omitempty"`
	Tones  []ToneSpan `json:"tones,omitempty"`
	// Section is the label line drawn above a run of rows in one section
	// (ListItem.Section); "" none.
	Section string `json:"section,omitempty"`
}

TreeNode is one node of a tree. Nodes are flat and in display order: a node's children follow it, and Parent "" is a root. Noun is the row's noun (default TreeRowNoun); Data, Cells and Tones are as on ListItem (cell 0 is the label: its tones stay on it, after the indent and fold marker).

type UI added in v0.4.0

type UI struct {
	// contains filtered or unexported fields
}

UI is the component that routes handler messages to the host's sink. It is one instance under app (Runtime.UI), never a Runtime field. With no sink it discards, except that until the first sink attaches it keeps the last 32 messages (the startup ring) and replays them, in order, into that sink; after the first attach nothing is kept, so memory stays bounded whether or not a sink ever attaches.

Like the rest of Runtime it is called from the update loop; work off the loop posts from its Then (which gets a Ctx).

func (*UI) Attach added in v0.4.0

func (u *UI) Attach(s UISink) UISink

Attach makes s the sink every later message goes to and returns the one it replaces (nil for none). The first attach of the runtime's life replays the startup ring into s and drops it. Attach(nil) detaches: messages are discarded from then on.

func (*UI) ClearNotice added in v0.4.0

func (u *UI) ClearNotice(id NoticeID)

ClearNotice takes back the notice Notify returned id for.

func (*UI) Close added in v0.4.0

func (u *UI) Close()

Close keeps everything: the UI lives as long as the runtime.

func (*UI) Error added in v0.31.0

func (u *UI) Error(text string)

Error sets the status line to an error, drawn "Error: TEXT" in the error style until the next key; "" clears it. Use it for what failed and was not returned as a Failed result (a background refresh, a daemon event).

func (*UI) HandleMessage added in v0.4.0

func (u *UI) HandleMessage(tea.Msg) bool

HandleMessage: the UI does no off-loop work.

func (*UI) Info added in v0.31.0

func (u *UI) Info(text string)

Info sets the status line to a plain message ("copied 3 rows", "queued"), drawn without the error style before the hints until the next key; "" clears it.

func (*UI) Notify added in v0.4.0

func (u *UI) Notify(text string) NoticeID

Notify posts a persistent notice and returns its id for ClearNotice.

func (*UI) PendingCommand added in v0.4.0

func (u *UI) PendingCommand() tea.Cmd

PendingCommand: nothing is ever scheduled.

func (*UI) Sink added in v0.4.0

func (u *UI) Sink() UISink

Sink is the attached sink, nil when messages are discarded.

func (*UI) Status deprecated added in v0.4.0

func (u *UI) Status(text string)

Status is Error: the status line in its error tone. The name read as "any status", so plain messages posted through it were drawn as errors.

Deprecated: use Error for what failed, Info for a plain message.

type UIMessage added in v0.4.0

type UIMessage struct {
	Kind string   `json:"kind"`
	ID   NoticeID `json:"id,omitempty"`
	Text string   `json:"text,omitempty"`
}

UIMessage is one thing a handler told the user: a notice (Kind notice, ID and Text), the clearing of one (Kind clear, ID), or the status line in its error tone (Kind status, Text) or its neutral tone (Kind info, Text); "" clears the line. The startup ring, a call's Response.Messages and RecordingSink hold these.

func (UIMessage) Apply added in v0.4.0

func (msg UIMessage) Apply(s UISink)

Apply hands the message to s.

func (UIMessage) Line added in v0.31.0

func (msg UIMessage) Line() string

Line is the message as a host prints it on a line of its own (the CLI's stderr): "notice: TEXT", "error: TEXT" for the error line, "status: TEXT" for the neutral one; "" for a clear or an empty status.

type UISink added in v0.4.0

type UISink interface {
	// Notify shows a persistent notice until ClearNotice(id) or the host
	// clears it.
	Notify(id NoticeID, text string)
	// ClearNotice takes back a notice; an unknown id is ignored.
	ClearNotice(id NoticeID)
	// Status replaces the one-line status with an error ("" clears it):
	// what Ctx.Error posts (UIError). Only the error tone reaches it.
	Status(text string)
	// Info replaces the one-line status with plain text ("" clears it):
	// what Ctx.Info posts (UIInfo).
	Info(text string)
}

UISink is how a host shows what handlers tell the user: the Model puts notices in the footer and the status in the status line, the CLI shell prints them on stderr, a test records them. Handlers never reach the sink: they post through Ctx (ctx.Notify, ctx.ClearNotice, ctx.Error, ctx.Info) or Runtime.UI, and the host decides how it looks. Every sink shows both tones of the status line, so a plain message is never drawn as an error for want of a method. A sink that wraps another forwards every message (UIMessage.Apply does it).

type Verbatim added in v0.34.0

type Verbatim string

Verbatim is a result a CLI prints as its text, unquoted (a file the verb writes: bindings defaults); over JSON it is a string.

type Yank

type Yank struct {
	Text  string `json:"text"`
	Shape string `json:"shape"`
	Width int    `json:"width,omitempty"`
}

Yank retains the geometry needed to paste independently of the source field.

Directories

Path Synopsis
cli
Package cli is the generic command line of an ochazuke application: the NOUN VERB shell over a runtime (Shell), the oden mount that makes every noun a command of the bin (Mount) and the catalog answer that describes the runtime as JSON (Catalog).
Package cli is the generic command line of an ochazuke application: the NOUN VERB shell over a runtime (Shell), the oden mount that makes every noun a command of the bin (Mount) and the catalog answer that describes the runtime as JSON (Catalog).
inspect
Package inspect merges ochazuke tool catalogs (the JSON BIN catalog prints) into one model and writes it as a self-contained HTML page for browsing nouns, verbs, key rows, user actions, relations and, when annotations are given, the verbs someone suggested adding (the Gaps view).
Package inspect merges ochazuke tool catalogs (the JSON BIN catalog prints) into one model and writes it as a self-contained HTML page for browsing nouns, verbs, key rows, user actions, relations and, when annotations are given, the verbs someone suggested adding (the Gaps view).
cmd
charmrefs command
Command charmrefs writes docs/CHARM-INVENTORY.md: for every project listed in docs/charm-refs.txt, the Charm (charm.land, charmbracelet) modules it depends on, how many files import each package, its entry points, and its agent docs.
Command charmrefs writes docs/CHARM-INVENTORY.md: for every project listed in docs/charm-refs.txt, the Charm (charm.land, charmbracelet) modules it depends on, how many files import each package, its entry points, and its agent docs.
codemap command
Command codemap writes docs/CODEMAP.md: one entry per Go file in the root package and in each importable subpackage (cli/, grid/), listing what each file declares and which sibling files it leans on.
Command codemap writes docs/CODEMAP.md: one entry per Go file in the root package and in each importable subpackage (cli/, grid/), listing what each file declares and which sibling files it leans on.
docgen command
Command docgen generates the offline reference linked from the architecture guide.
Command docgen generates the offline reference linked from the architecture guide.
o-story command
Command o-story is a storybook-style sandbox for ochazuke: a story is a JSON file describing a synthetic page, a script of events and what must hold afterwards.
Command o-story is a storybook-style sandbox for ochazuke: a story is a JSON file describing a synthetic page, a script of events and what must hold afterwards.
ochazuke command
renamekeys command
Command renamekeys rewrites operation keys from one spelling to the other across the repository's .go, .json, .md and .html files.
Command renamekeys rewrites operation keys from one spelling to the other across the repository's .go, .json, .md and .html files.
examples
core command
csv command
Package main is the CSV reader and editor from docs/CSV-APP-SKETCH.md: the framework's lightweight second consumer.
Package main is the CSV reader and editor from docs/CSV-APP-SKETCH.md: the framework's lightweight second consumer.
demo
Package demo is the runnable example behind `ochazuke run`: seven pages, nested draft modals, the picker and the keyboard guide, built only from the public API.
Package demo is the runnable example behind `ochazuke run`: seven pages, nested draft modals, the picker and the keyboard guide, built only from the public API.
full-project command
hello command
Command hello is an oden tool whose TUI is ochazuke's Commands page: with no word it opens the page (cli.TUI as oden's Default), listing its inline commands and every hello-<name> on PATH.
Command hello is an oden tool whose TUI is ochazuke's Commands page: with no word it opens the page (cli.TUI as oden's Default), listing its inline commands and every hello-<name> on PATH.
Package explore explores an ochazuke application's interaction states by pressing keys, checks properties in every state it reaches, and reports bad and impossible states and actions with the shortest key path to each.
Package explore explores an ochazuke application's interaction states by pressing keys, checks properties in every state it reaches, and reports bad and impossible states and actions with the shortest key path to each.
Package grid is a two-dimensional cursor component for ochazuke: a header and rows the application owns, a cursor and viewport in two axes, a visual selection in three shapes plus marks, and one instance per focused cell, parented to a row instance, parented to a column instance, parented to the grid, so the cell, row and column nouns all sit on the focus stack.
Package grid is a two-dimensional cursor component for ochazuke: a header and rows the application owns, a cursor and viewport in two axes, a visual selection in three shapes plus marks, and one instance per focused cell, parented to a row instance, parented to a column instance, parented to the grid, so the cell, row and column nouns all sit on the focus stack.
internal
keytest
Package keytest drives Bubble Tea models from tests using the key names that bindings use ("ctrl+c", "esc", "enter", "shift+tab", "alt+b", "space", "x").
Package keytest drives Bubble Tea models from tests using the key names that bindings use ("ctrl+c", "esc", "enter", "shift+tab", "alt+b", "space", "x").
Package markdown renders Markdown with Charm's Glamour and edits its task lists in place.
Package markdown renders Markdown with Charm's Glamour and edits its task lists in place.
cliview
Package cliview is the "markdown" detail kind of a mounted data page (cli.CatalogDetail.Kind): an editable markdown.Doc over the file the selected row's PathField names.
Package cliview is the "markdown" detail kind of a mounted data page (cli.CatalogDetail.Kind): an editable markdown.Doc over the file the selected row's PathField names.
Package story is o-story's engine: a story is data (a synthetic page, a script of events, an expectation), Build turns it into a live runtime and model, a Driver plays events against it headless, and a Recorder turns the messages of a live session into the same events.
Package story is o-story's engine: a story is data (a synthetic page, a script of events, an expectation), Build turns it into a live runtime and model, a Driver plays events against it headless, and a Recorder turns the messages of a live session into the same events.
Package strictcheck fails an application's tests when its code uses a name ochazuke has deprecated.
Package strictcheck fails an application's tests when its code uses a name ochazuke has deprecated.
Package sysstat samples the machine for status segments: CPU busy percent, memory used and total, and free disk space at a path, on macOS and Linux, without cgo.
Package sysstat samples the machine for status segments: CPU busy percent, memory used and total, and free disk space at a path, on macOS and Linux, without cgo.

Jump to

Keyboard shortcuts

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