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

ochazuke

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 31 Imported by: 0

README

Ochazuke

A Go application framework on Charmbracelet Bubble Tea v2 (charm.land/bubbletea/v2, Lip Gloss v2, Bubbles v2), with 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.

Status: working standalone framework and demo. Neither gmtm nor Matomeru is changed. Existing editors are retained; Bubbles provides the new page/modal scrolling viewports.

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

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.

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; type page in the palette to complete a page ID.

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. 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, capped at 78 columns and about 75% of the terminal height. 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.
  • 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. They are in-memory settings, not persisted preferences. NewStyles(*model.Appearance) supplies the same style vocabulary to application renderers.

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
Normal Appearance t, d Cycle theme / density
Normal demo Ctrl-J/K Next/previous 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; an incomplete line stays and the error names its first missing word (missing name (string, pick)). 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.
  • 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         -> "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.
  • 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.

// 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} runs <row noun>.verb with the row as target for every marked row (the selected row when none is marked), skipping and reporting rows whose noun lacks the verb.
  • 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.

The operation equivalents are list.focus {id}, list.move {id?, delta}, list.select {id?, item}, list.mark {id?, item?} and list.apply {id?, verb}. 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

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. Rebinding the leader is binding another key to the same Enter row with that key as its prefix and request, in Go or in a binding file. 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.

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
editor_bindings.json, registers_bindings.json, editor_keys.go The editor noun's mode tables and the app noun's count, register and macro rows as data, loaded at startup
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
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, and focus isolation
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
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 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.

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" } } }
]

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 with the same noun, mode and keys or pattern replaces the default, a new one adds, request: null unbinds. 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 generated from editor_bindings.json, a data file in this row shape plus three generators (expand a row over a key map, extends one mode from another, pending the exclusive modes). 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, or unbind it (an unbind removes both rows).

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.

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.

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 but history.list and state.get is appended to a 500-entry ring:

{ seq: 42, at: "2026-10-24T10:00:00Z",
  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, value: { ... } },                                         // or { ok: false, error: "..." }
  job: 7 }                                                                      // when an application verb launched work

Read it with history.list {last} (default 50) or state get /history (the last 50). 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 request 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.

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": "..." }. The CLI prints the result as JSON and returns a nonzero exit code for errors.

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 (
	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 (
	SourceKey     = "key"
	SourceDigit   = "digit"
	SourceText    = "text"
	SourceCount   = "count"
	SourceLine    = "line"
	SourceSteps   = "steps"
	SourceOp      = "op"
	SourceOpCount = "opcount"
	SourcePrefix  = "prefix"
)

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). 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"
	Paste     = "paste"
	Any       = "any"
)

Pattern names.

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

Tree change operations.

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 DefaultBindingTimeout = 1000 * time.Millisecond

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

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 HistoryLimit = 500

HistoryLimit bounds the action history ring.

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 rows the jobs ticker takes above the footer.

View Source
const MovementSection = "movement"

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

View Source
const TreeRowNoun = "outline-row"

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

Variables

This section is empty.

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

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 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 ticker rows: one message per row, "noun.verb state", with ": error" on a failure, truncated to width.

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

func RegisterInputOperations(r *Runtime)

func StringArg

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

Types

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"`
}

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.

func NewAppearance

func NewAppearance(r *Runtime) *Appearance

NewAppearance registers appearance operations once per runtime.

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 Binding

type Binding struct {
	Noun     string            `json:"noun,omitempty"`
	Instance string            `json:"instance,omitempty"`
	Mode     string            `json:"mode,omitempty"`
	Keys     string            `json:"keys,omitempty"`
	Pattern  string            `json:"pattern,omitempty"`
	Request  Request           `json:"request"`
	From     map[string]string `json:"from,omitempty"`
	When     string            `json:"when,omitempty"`
	Unbind   bool              `json:"unbind,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"`
}

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"). From fills request arguments from the key or the mode payload (see Sources). When names a registered guard. Unbind removes the default row with the same mode and keys or pattern. 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 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) 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 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 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 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, []map with "id" and optional "label", or []any of strings or such maps (the JSON-decoded form).

type Column

type Column struct {
	Title    string `json:"title"`
	Width    int    `json:"width,omitempty"`
	Min      int    `json:"min,omitempty"`
	Align    Align  `json:"align,omitempty"`
	Priority int    `json:"priority,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).

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

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

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 rows (Model.JobsTicker), drawn above the rule over the footer; the body gives up one row per job row, so no jobs means no rows.

type Guard

type Guard func(*Runtime, KeyEvent) bool

Guard is a named condition a row requires. 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
	Subtitle string
	Tabs     []string
	Active   int
}

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. Every field is a raw label.

type HelpRow

type HelpRow struct {
	Key         string `json:"key"`
	Operation   string `json:"operation"`
	Description string `json:"description,omitempty"`
	Noun        string `json:"noun"`
	Section     string `json:"section,omitempty"`
}

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), the operation's description, the noun of the level that answers with it, and the help's section: that noun, or "movement" for a motion (Operation.Motion).

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".

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"`
	Request Request        `json:"request"`
	Context HistoryContext `json:"context"`
	Result  HistoryResult  `json:"result"`
	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"`
}

HistoryEntry is one executed request: what ran, where, from which source and how it ended. Value is the result as JSON at the time it ran; Job is the job id an application verb launched.

type HistoryResult

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

HistoryResult is the outcome: ok with the value, or the error.

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

type JobInfo

type JobInfo struct {
	Job     uint64    `json:"job"`
	Owner   string    `json:"owner"`
	Verb    string    `json:"verb"`
	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
}

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

type Layout

type Layout struct {
	Width, Height int
	Styles        Styles
}

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 BodySize less the rows the footer's jobs ticker takes (only with the complete chrome; a small viewport draws no ticker).

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 left or top panel is the focused one.

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.

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

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

Columns returns a copy of the list's columns (nil for a plain 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) Items

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

Items returns a copy of the rows in display order.

func (*List) Len

func (l *List) Len() int

Len is the number of rows.

func (*List) Marked

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

Marked lists the marked rows in list order.

func (*List) Move

func (l *List) Move(delta int)

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

func (*List) Select

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

Select changes the selected row by stable ID without taking focus.

func (*List) Selected

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

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

func (*List) SetColumns

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

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

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

func (*List) Snapshot

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

func (*List) ToggleMark

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

ToggleMark marks or unmarks a row for fan-out verbs.

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.

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"`
}

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.

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 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 an unguarded row with the same keys.

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 rows with these keys or this pattern.

func (*ModeBuilder) When

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

When guards the last bound row with a registered guard name.

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 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     string
	// Notices are persistent lines (a host's mount warnings): drawn in the
	// footer before the hints, kept across keys and errors until notice.clear
	// (or the application) clears them. Status is the last key's error and
	// goes with the next key; a notice does not.
	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

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 above the footer for the current frame, as plain lines at most width cells: zero rows when no job shows.

func (*Model) Notify

func (m *Model) Notify(text string)

Notify adds a persistent notice to the footer (Notices); notice.clear removes them.

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 focused input 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 no input is focused.

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"`
}

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

func (b *NounBuilder) Focusable() *NounBuilder

Focusable makes every instance of the noun a focus stop.

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) 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) 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 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"`
	// 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"`
	// 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"`
	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) 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 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:"-"`
	// 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"`
}

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"`
}

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 three replacing jobs on the picker instance (the "index", "search" and "preview" 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 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"`
}

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 any    `json:"result,omitempty"`
	Error  string `json:"error,omitempty"`
}

Response is the reply to a socket call: a result or an error message.

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

func (r *Runtime) AddInput(in *Input, opts ...InstanceOption) error

AddInput registers an editor instance. Options: Parent(id) (default app) and Kind(noun) (default "editor"; "prompt" pins insert mode).

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, and Columns(...) for a table.

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

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) 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: the same mode and keys or pattern replaces, a new one adds, Unbind removes.

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.

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

func (r *Runtime) Catalog() []Operation

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 Source operation. A source that asks for confirmation is refused; it must be a read.

func (*Runtime) Context

func (r *Runtime) Context() Context

Context derives the path view of the focus.

func (*Runtime) Define

func (r *Runtime) Define(name string) *NounBuilder

Define declares a noun, or returns the builder of an existing one.

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) 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) 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) 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 but history.list and state.get is appended to the action history with the context it ran in and its result.

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

func (r *Runtime) History(n int) []HistoryEntry

History returns the last n entries, oldest first (all of them for n <= 0).

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

func (r *Runtime) LoadBindingSpec(noun string, data []byte) error

LoadBindingSpec installs a spec onto one noun: modes, then rows.

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

func (r *Runtime) Modals() []*Instance

Modals lists the open modal instances, outermost first.

func (*Runtime) Noun

func (r *Runtime) Noun(name string) *Noun

Noun returns the registered noun of that name, or nil.

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

func (*Runtime) PageOf

func (r *Runtime) PageOf(id string) *Page

PageOf returns the page an instance fronts, or nil.

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) 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: an instance no frame drew follows the drawn ones in tree order.

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

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

func (r *Runtime) Terminal(id string) *Terminal

Terminal returns the terminal an instance fronts, 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) 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.

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

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.

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

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

PendingCommand is nil: reads are scheduled through Runtime.Start.

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; compose the cursor only while the terminal is 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 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 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) 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) 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 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"`
}

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 and Cells are as on ListItem.

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
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).
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`: four 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`: four pages, nested draft modals, the picker and the keyboard guide, built only from the public API.
full-project command
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 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.

Jump to

Keyboard shortcuts

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