headless

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package headless is behaviour without appearance.

Everything here holds the state an interactive thing needs and answers the input that changes it, and none of it decides what any of that looks like. A list knows which item is selected and what the arrow keys do; it does not know what a selected row looks like, and it draws one by calling back to whoever does. A scroll position knows it is following the end of a log; it has no scrollbar.

That division is the whole point. Appearance is where every interface differs and behaviour is where they are all the same, so the part worth sharing is the part with no appearance in it. An appearance layer supplies those decisions through callbacks and styles without this package knowing which one was chosen.

How a widget works

A widget is a mutable object owned by one event goroutine. It is asked to draw itself into the space it was given, and asked whether it wants an event. It does not return a new copy of itself, and it does not know where on the screen it is: the view it is handed is already positioned and already clipped, so a widget's coordinates are its own and it cannot draw outside its box.

Measurement is separate from drawing because a container has to know how much its children want before it can decide where they go. A widget whose size along one axis follows from the other says so by implementing Sized.

How a key reaches a widget

A widget names what it can do and answers to the name — see Doer — and an keymap.Map says which keystrokes produce which name. Nothing here owns a keystroke. That is what makes every key reboundable without replacing anything, what makes a binding several chords long expressible at all, and what lets the same action be reached from a menu or from a command typed by name.

Each widget kind has a map of its own, because the same key means different things in different places: the down arrow moves a cursor in a field, a selection in a list and a window in a reader, and one table cannot say all three. Within a kind, one map can serve a whole interface — a widget answers the actions it knows and lets the rest past — which is how a program binds its own keys alongside a field's:

keys := headless.DefaultEditorKeys()
keys.Bind("send", input.Chord{Code: input.Enter})

Index

Examples

Constants

View Source
const (
	MoveLeft      keymap.Action = "move-left"
	MoveRight     keymap.Action = "move-right"
	MoveUp        keymap.Action = "move-up"
	MoveDown      keymap.Action = "move-down"
	MoveWordLeft  keymap.Action = "move-word-left"
	MoveWordRight keymap.Action = "move-word-right"
	MoveLineStart keymap.Action = "move-line-start"
	MoveLineEnd   keymap.Action = "move-line-end"

	DeleteBack     keymap.Action = "delete-back"
	DeleteForward  keymap.Action = "delete-forward"
	DeleteWordBack keymap.Action = "delete-word-back"
	KillToEnd      keymap.Action = "kill-to-end"
	KillToStart    keymap.Action = "kill-to-start"
	Yank           keymap.Action = "yank"
	YankPop        keymap.Action = "yank-pop"
	InsertNewline  keymap.Action = "newline"
	Undo           keymap.Action = "undo"
	Redo           keymap.Action = "redo"

	SelectAll keymap.Action = "select-all"
	Copy      keymap.Action = "copy"
	Cut       keymap.Action = "cut"
	Paste     keymap.Action = "paste"
)

The actions an Editor answers to.

Nothing about a keystroke is here. What produces one of these is a keymap.Map's business, which is what lets the same field be driven by a key, by a menu, or by a command typed by name — see Editor.Do.

There is deliberately no action for selecting in a direction. Shift with any way of moving is what selects, so every movement selects and none of them was taught to.

View Source
const (
	SelectPrev     keymap.Action = "select-prev"
	SelectNext     keymap.Action = "select-next"
	SelectPageUp   keymap.Action = "select-page-up"
	SelectPageDown keymap.Action = "select-page-down"
	SelectFirst    keymap.Action = "select-first"
	SelectLast     keymap.Action = "select-last"
)

The actions a List answers to. They are the list's own and not the editor's or a scroll's, because moving a selection, moving a cursor and moving a window are three things a reader can tell apart and would be dismayed to find bound together.

View Source
const (
	ScrollUp       keymap.Action = "scroll-up"
	ScrollDown     keymap.Action = "scroll-down"
	ScrollPageUp   keymap.Action = "scroll-page-up"
	ScrollPageDown keymap.Action = "scroll-page-down"
	ScrollTop      keymap.Action = "scroll-top"
	ScrollBottom   keymap.Action = "scroll-bottom"
)

The actions a Scroll answers to.

View Source
const (
	Accept  keymap.Action = "accept"
	Dismiss keymap.Action = "dismiss"
)

The actions a Completion answers to, on top of the list movement inside it.

View Source
const (
	Expand   keymap.Action = "expand"
	Collapse keymap.Action = "collapse"
)

The actions a Tree answers to, on top of the list movement through its rows.

They are a pair rather than one "toggle", because what a reader means by the right arrow on something already open is "go into it", and by the left arrow on something already closed is "go up" — which is a tree behaving like a tree and not two names for one thing.

View Source
const (
	NextTab keymap.Action = "next-tab"
	PrevTab keymap.Action = "prev-tab"
)

The actions a Tabs answers to.

View Source
const (
	Decrease  keymap.Action = "decrease"
	Increase  keymap.Action = "increase"
	ToMinimum keymap.Action = "to-minimum"
	ToMaximum keymap.Action = "to-maximum"
)

The actions a Slider answers to.

View Source
const (
	// Toggle takes the choice under the cursor, or gives it back.
	Toggle keymap.Action = "toggle"
	// Submit finishes a [Form], and Cancel abandons it.
	Submit keymap.Action = "submit"
	Cancel keymap.Action = "cancel"
)

The actions a form and its fields answer to, on top of the list movement a choice uses and the editing a line of text uses.

View Source
const (
	FocusNext keymap.Action = "focus-next"
	FocusPrev keymap.Action = "focus-prev"
)

The actions a Container answers to: which of its children has the keyboard.

View Source
const Activate keymap.Action = "activate"

Activate invokes a button-like control such as a DialogTrigger.

View Source
const Close keymap.Action = "close"

Close is what dismisses the top layer of a Stack.

View Source
const DefaultCompletionRows = 8

DefaultCompletionRows is how many candidates are shown at once when nothing says otherwise: enough to choose from, few enough to leave the text visible behind it.

View Source
const DefaultHistoryLimit = 1000

DefaultHistoryLimit is how many entries a history keeps when it is not told.

View Source
const DefaultMultiClick = 400 * time.Millisecond

DefaultMultiClick is how close together two presses have to be to count as one gesture. It is what desktop systems have settled on.

Variables

This section is empty.

Functions

func DefaultActivationKeys added in v0.1.0

func DefaultActivationKeys() *keymap.Map

DefaultActivationKeys are the two conventional ways to invoke a focused control.

func DefaultCompletionKeys

func DefaultCompletionKeys() *keymap.Map

DefaultCompletionKeys are the keystrokes a terminal completion is expected to answer: the list's, because the candidates are a list, and the two of its own.

func DefaultConfirmKeys added in v0.0.2

func DefaultConfirmKeys() *keymap.Map

DefaultConfirmKeys are the keystrokes a yes or no answers.

Left and right rather than up and down, because the two answers are drawn side by side and a key that moved the other way would be pointing at nothing.

func DefaultContainerKeys added in v0.0.2

func DefaultContainerKeys() *keymap.Map

DefaultContainerKeys are the keystrokes that walk the keyboard around an interface.

Shift+Tab and not backtab: they are one keystroke, and input reports them as one — tab with shift held, whichever way the terminal spelled it.

func DefaultEditorKeys

func DefaultEditorKeys() *keymap.Map

DefaultEditorKeys are the keystrokes a terminal text field is expected to answer.

The control chords are the ones a terminal has always had, because they are the ones a reader's fingers already know and the ones that still work when the terminal cannot report anything richer.

Enter is deliberately unbound. Whether it sends or breaks the line is the interface's decision, and a field that took it would take that decision away from every interface that has one.

func DefaultFormKeys added in v0.0.2

func DefaultFormKeys() *keymap.Map

DefaultFormKeys are the keystrokes a form answers: the two that walk between its fields, and the two that finish with it.

Enter submits rather than moving on, because a form is finished by saying so and a field that took enter to mean "next" would leave nothing to mean "done". A field with its own use for enter keeps it, since a field is asked first.

func DefaultListKeys

func DefaultListKeys() *keymap.Map

DefaultListKeys are the keystrokes a terminal list is expected to answer.

func DefaultMultiSelectKeys added in v0.0.2

func DefaultMultiSelectKeys() *keymap.Map

DefaultMultiSelectKeys are the keystrokes a list of choices answers: the movement any list has, and the one that takes what is under the cursor.

func DefaultScrollKeys

func DefaultScrollKeys() *keymap.Map

DefaultScrollKeys are the keystrokes a terminal reader is expected to answer.

func DefaultSettingsKeys added in v0.3.0

func DefaultSettingsKeys() *keymap.Map

DefaultSettingsKeys are the value actions in a settings row. Up and down remain the embedded list's navigation; left and right adjust the current value, and the conventional activation keys invoke values that open or toggle.

func DefaultSliderKeys added in v0.3.0

func DefaultSliderKeys() *keymap.Map

DefaultSliderKeys are the conventional keys for a bounded value. Both axes work so the same headless controller can sit behind a horizontal track or a compact setting row without changing its action vocabulary.

func DefaultStackKeys added in v0.0.2

func DefaultStackKeys() *keymap.Map

DefaultStackKeys are the keystrokes a layered interface is expected to answer.

func DefaultTabsKeys added in v0.0.2

func DefaultTabsKeys() *keymap.Map

DefaultTabsKeys are the keystrokes that move between panes.

Alt with the arrows, and not control with tab. Control and tab is what a desktop application binds and what a terminal cannot report: the two are the same byte there unless the terminal speaks the Kitty protocol, so a binding on it works on some terminals and silently does nothing on the rest.

func DefaultTreeKeys added in v0.0.2

func DefaultTreeKeys() *keymap.Map

DefaultTreeKeys are the keystrokes a tree is expected to answer: the movement any list has, and the two that open and close a branch.

Types

type Accessor added in v0.0.2

type Accessor[T any] interface {
	Value() T
	Set(v T)
}

Accessor is caller-owned readable and writable state.

Fields use one because a form is collecting into an application value. Controlled controllers use the same small contract so their operations update the caller's single source of truth instead of maintaining a private shadow copy.

Reading an accessor is pure but cannot announce that its owner wrote a new value. Fields reconcile at their next semantic operation and project the current value in Draw. Controllers whose reconciliation also moves focus or modal stack membership expose Sync, keeping those transitions explicit and outside presentation.

Value must be a pure read because presentation may call it. Set is synchronous: when it returns, Value reports the state the owner accepted. Set may also persist, validate or publish that transition, which is why components avoid redundant calls.

Bind is the usual case: a variable of the caller's own. A setting or record field can implement the same contract, which is why this is an interface and not a pointer.

func Bind added in v0.0.2

func Bind[T any](p *T) Accessor[T]

Bind is an accessor for a variable of the caller's own. A nil pointer is a programmer error and panics at construction rather than later in an unrelated control operation.

var name string
field := &headless.Text{Label: "Name", Value: headless.Bind(&name)}

type Backdrop

type Backdrop interface {
	Modal
	// Backdrop is given the whole space the stack is drawn into, before this
	// layer's own drawing and after everything below it.
	Backdrop(v grid.View)
}

Backdrop is a modal that wants to touch the space it is covering before it is drawn into its own corner of it — dimming what is behind, usually.

It exists because a layer is handed a view of the area it asked for and nothing else, which is what stops it drawing outside its own box. That is the right default and it makes dimming impossible, so a layer that means to reach further has to say so, and gets the whole space exactly once.

Nothing in this package decides what a backdrop looks like. It calls back.

type Block added in v0.1.0

type Block interface {
	grid.Drawable
}

Block is finished or deliberately retained drawable content.

Unlike a live Widget, a Block has no routing geometry and draws directly into a grid view. This is the shape accepted by transcripts and inline publication: once a block is committed, it can leave the active component tree without carrying a frame transaction or interaction lifecycle with it.

type BlockID added in v0.1.0

type BlockID uint64

BlockID is the stable identity of one block in a Transcript.

IDs increase as blocks are appended and are never reused. Committing a leading block invalidates its ID without changing the IDs of blocks that remain live. That is what lets a sticky header or another retained part keep referring to a live block while committed storage is physically removed from the transcript. The maximum value is reserved as the exhausted next identity; reaching it panics before an old identity could be reused.

type Candidate

type Candidate struct {
	// Text is what accepting it puts in place of the token.
	Text string
	// Label is the row as shown. Empty shows Text, which is the common case: what is
	// offered and what it inserts are usually the same thing.
	Label string
	// Detail is shown after the label, receding — a description, a kind, a size.
	Detail string
	// Matched are byte offsets in the label that the query matched, picked out as the
	// row is drawn. It is what [fuzzy.Match.At] returns, and leaving it nil simply
	// highlights nothing.
	Matched []int
}

Candidate is one thing a completion offers.

type Caret added in v0.0.2

type Caret struct{ Line, Col int }

Caret is a position in an editor's text: a logical line, and a byte offset into it.

It is not a Point. A transcript numbers visual rows, because everything it answers is about what is on the screen; an editor's text has lines of its own that wrapping turns into rows, and a position in the text has to survive the window changing width. The two coordinate spaces are different questions, and one type for both would let an answer to one be passed as an answer to the other.

func (Caret) Before added in v0.0.2

func (c Caret) Before(d Caret) bool

Before reports whether c comes earlier in the text than d.

type Clicks added in v0.0.2

type Clicks struct {
	// Within is how close together presses must be. Zero uses [DefaultMultiClick].
	Within time.Duration
	// contains filtered or unexported fields
}

Clicks counts a run of presses in the same place as one gesture: one for a single click, two for a double, three for a triple.

Why this is not somewhere else

A terminal does not report a double-click. It reports two presses, and whether they are one gesture is a question about when they arrived — so it can only be answered by whatever has a clock.

The time is an argument rather than read here, which is the same bargain the rest of this library makes: a type that called time.Now could not be told to be at a particular moment, and every test of it would be a test of how fast the machine ran.

func (*Clicks) Press added in v0.0.2

func (c *Clicks) Press(ev input.Mouse) int

Press records a press and reports which of the run it is.

A press far from the last one, or long after it, starts a new run — and so does the first press of all, because the zero value has never seen one, and so does one that arrived with no time on it, because there is nothing to compare.

The time comes from the event rather than from a clock here, because arrival is a fact about the input and the thing that read it is the only thing that knows.

func (*Clicks) Reset added in v0.0.2

func (c *Clicks) Reset()

Reset forgets the run, which a caller does when something else has happened that makes the next press a first one.

type Clipboard added in v0.0.2

type Clipboard interface {
	// Copy puts text where a paste would find it, reporting false for text it will
	// not carry.
	Copy(text string) bool
	// Paste asks for what is there and reports whether the request was accepted. The
	// answer arrives later, as an [input.Paste] among the editor's ordinary events.
	Paste() bool
}

Clipboard is where an editor's copy and cut go.

A runtime adapter commonly provides it, but the interface is declared here where it is consumed. A caller with somewhere else to put text is equally valid.

type Closer

type Closer interface {
	Modal
	Closed()
}

Closer is a modal that wants to know when it has been popped, whether that was its own doing or the stack's.

type Command added in v0.0.2

type Command struct {
	// Name is the canonical name a user searches or types. It is the identity: two
	// commands with the same name are one command, and the second registration wins.
	Name string
	// Title is the one-line description a list shows beside the name.
	Title string
	// Aliases are other names that find this command. They are matched but never
	// listed, so a command can be renamed without the old name disappearing from
	// under everyone who learned it.
	Aliases []string
}

Command is the searchable description of one thing a user can ask for by name. What the command means belongs to the caller and is stored as the value of Commands, not prescribed here.

type Commands added in v0.0.2

type Commands[T any] struct {
	// contains filtered or unexported fields
}

Commands is a set of named commands that can be found by typing part of a name.

Why the matching is not simply a prefix

A user who knows a command types enough of it and expects to be right. A user who does not know it types what they remember, which is rarely the beginning: "sess" for "new-session", "clr" for "clear". Matching on subsequences with a bias towards word starts finds both, which is why this ranks with fuzzy rather than filtering on a prefix.

Why order is remembered

The command somebody ran a moment ago is overwhelmingly the one they want next, and no amount of scoring the names will discover that. So ties are broken by how recently a command was used, and an empty query lists the recent ones first. It is the only part of the ranking that knows anything about this particular user.

T is the caller-owned meaning associated with a command. The registry keeps and returns it by assignment, as a map does; it never interprets, invokes, or copies through references inside it. This keeps command execution, arguments, and product syntax outside the component while avoiding a second application-side lookup table.

The zero value is an empty registry. A Commands value must not be copied after first use; registration and recency are one mutable index.

func (*Commands[T]) Add added in v0.0.2

func (c *Commands[T]) Add(cmd Command, value T)

Add associates value with a command, replacing both for an existing name. The registry copies the command's text and aliases; value follows ordinary assignment semantics for T.

func (*Commands[T]) Find added in v0.0.2

func (c *Commands[T]) Find(query string) []Found

Find is the commands a query matches, best first.

An empty query is every command, most recently used first, which is what a palette shows when it opens.

func (*Commands[T]) Len added in v0.0.2

func (c *Commands[T]) Len() int

Len is how many commands are registered.

func (*Commands[T]) Lookup added in v0.0.2

func (c *Commands[T]) Lookup(name string) (Command, T, bool)

Lookup returns a command snapshot and its caller-owned value for exactly this name or alias. An exact name wins over every alias: an older spelling must not shadow the canonical name of another command.

func (*Commands[T]) Remove added in v0.0.2

func (c *Commands[T]) Remove(name string) bool

Remove forgets the command with canonical name, reporting whether it was there. Aliases are lookup spellings rather than identities.

func (*Commands[T]) Used added in v0.0.2

func (c *Commands[T]) Used(name string)

Used records that a command was run, which is what moves it up the list.

type Completion

type Completion struct {

	// Look is how the rows are drawn: the text, the row under the cursor, the
	// characters the query matched, and the detail beside a candidate. It is the one
	// way anything here that draws itself is dressed — see [Look].
	Look Look
	// MaxRows caps how tall the list gets, so a thousand files do not become a
	// thousand rows. Zero uses [DefaultCompletionRows].
	MaxRows int
	// Keys say which keystrokes produce which of the actions a completion answers to —
	// its own two, and the list movement inside it. Nil reads through
	// [DefaultCompletionKeys].
	Keys *keymap.Map
	// Accept is called when the user takes a candidate, with the token it replaces.
	// The completion has closed itself by then, so this is free to change the text the
	// token came from.
	//
	// Without it there is nothing accepting could do, and the keystroke is left for
	// whatever else might want it rather than swallowed.
	Accept func(c Candidate, t Token)
	// contains filtered or unexported fields
}

Completion offers candidates for a token someone is typing.

It is the list and the keys, not the source: what the candidates are, where they came from and what accepting one means are the caller's. That is the whole of why this is reusable — a file list, a command palette and an emoji picker are this widget with different candidates, and none of them is something a terminal library should know about.

It draws itself into the space it is given, and knows nothing about floating. A caller that wants it over the text composes it with an appearance-layer placement wrapper; that wrapper, not completion behavior, owns the geometry.

The zero value is closed and ready. A Completion must not be copied after first use: its candidates, matcher and acceptance lifecycle are one mutable owner.

func (*Completion) Current

func (c *Completion) Current() (Candidate, bool)

Current is a snapshot of the candidate under the cursor, and whether there is one.

func (*Completion) Dismiss

func (c *Completion) Dismiss()

Dismiss closes the completion.

func (*Completion) Do added in v0.0.2

func (c *Completion) Do(action keymap.Action) bool

Do runs one of the completion's actions by name, or the list's, reporting whether it was one either of them knows. See Doer.

The list is driven by name rather than handed the event, because both read through the same map: an event offered twice would be resolved twice, and the second lookup would know nothing about the first.

func (*Completion) Draw

func (c *Completion) Draw(v Frame)

Draw renders the visible candidates.

func (*Completion) Handle

func (c *Completion) Handle(ev input.Event) bool

Handle answers movement, acceptance and dismissal while the completion is open, and nothing at all while it is closed.

A closed completion consuming keys would be a completion that had opinions about text it is not offering anything for.

func (*Completion) Measure

func (c *Completion) Measure(int) int

Measure is how tall the list wants to be: a row per candidate, capped.

func (*Completion) Offer

func (c *Completion) Offer(t Token, candidates []Candidate)

Offer opens the completion on a token, or closes it when there is nothing to offer.

A popup with nothing in it is a popup in the way, so an empty offer is a dismissal rather than an empty box. The selection returns to the first candidate: the query changed, so which candidate was under the cursor is about a question nobody asked. The completion copies the token and candidates, including match offsets; the caller may reuse or change its inputs after this returns.

func (*Completion) Open

func (c *Completion) Open() bool

Open reports whether anything is being offered, which is what tells the interface around it to make room and to offer it keys first.

func (*Completion) Token

func (c *Completion) Token() (Token, bool)

Token is what is being completed, and whether anything is.

func (*Completion) Width

func (c *Completion) Width() int

Width is how wide the widest row wants to be, so a caller sizing a layer around it does not have to measure the candidates itself.

type Confirm added in v0.0.2

type Confirm struct {

	// Label is what the field is asking.
	Label string
	// Value is the caller-owned answer. Reads observe it directly and answers write it
	// immediately. Nil keeps the answer local.
	Value Accessor[bool]
	// Yes and No are the two answers as they are shown. Empty uses "yes" and "no".
	Yes, No string
	// Check says what is wrong with the answer, or nil.
	Check func(v bool) error
	// Keys say which keystrokes answer. Nil reads through [DefaultConfirmKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Confirm is a field holding a yes or a no.

The zero value answers no and is ready. A Confirm must not be copied after first use: its answer, matcher and committed pointer split are one mutable field.

func (*Confirm) Answer added in v0.0.2

func (c *Confirm) Answer() bool

Answer is what has been answered.

func (*Confirm) Ask added in v0.0.2

func (c *Confirm) Ask() string

Ask is the question and the two words that answer it.

func (*Confirm) Do added in v0.0.2

func (c *Confirm) Do(action keymap.Action) bool

Do runs one of the field's actions by name. See Doer.

func (*Confirm) Draw added in v0.0.2

func (c *Confirm) Draw(v Frame)

Draw paints the label and the two answers.

func (*Confirm) Error added in v0.0.2

func (f *Confirm) Error() error

Error is what checking the answer last found.

func (*Confirm) Focus added in v0.0.2

func (c *Confirm) Focus(has bool)

Focus takes the keyboard or gives it up, and checks the answer on the way out.

func (*Confirm) Handle added in v0.0.2

func (c *Confirm) Handle(ev input.Event) bool

Handle answers the field, by key or by pressing one of the two answers.

func (*Confirm) Measure added in v0.0.2

func (c *Confirm) Measure(int) int

Measure is the label, the two answers on one row, and the problem if there is one.

func (*Confirm) Prompt added in v0.0.2

func (c *Confirm) Prompt() string

Prompt is what the field is asking.

func (*Confirm) Reply added in v0.0.2

func (c *Confirm) Reply(said string) error

Reply takes either answer, or as much of one as is unambiguous — nobody types "yes" in full twice.

func (*Confirm) Say added in v0.0.2

func (c *Confirm) Say(yes bool)

Say answers the field.

func (*Confirm) Validate added in v0.0.2

func (c *Confirm) Validate() error

Validate checks the answer.

type Container added in v0.0.2

type Container struct {

	// Axis is which way the children are arranged. The zero value stacks them down
	// the region.
	Axis layout.Axis

	// Gap is how many blank rows or columns go between one child and the next. Zero
	// puts them against each other.
	//
	// It is the layout's — see [layout.Flow] — rather than a blank child inserted
	// between every pair, which is what spacing used to be and which put things in
	// the ring that are not children: a hole nothing can focus, nothing can be
	// clicked in, and every index has to be corrected for.
	Gap int
	// Keys say which keystrokes move the keyboard along the ring. Nil reads through
	// [DefaultContainerKeys], which is tab and shift+tab.
	//
	// They are tried only after the focused child has declined the event, so a widget
	// that means something by tab — a completion, a field with columns in it — keeps
	// it.
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Container arranges widgets in a region and decides which of them an event is for.

It is the piece that was missing while every interface here was a single widget with everything hand-wired underneath it. A caller that had two things on screen laid them out itself, forwarded events itself, and worked out for itself which of them a click had landed on — and the answer to the last one is only knowable while a frame is being drawn, so it had to be remembered by hand as well.

The two routings

A key goes to the widget that has the keyboard. A mouse event goes to the widget it is over. They are different questions with different answers, and treating them as one is what makes an interface where clicking a pane does not let you type in it, or where the wheel scrolls whatever was last typed into.

A press is captured: everything until the release goes to whichever child took it, wherever the pointer wanders. Without that a selection stops extending the moment the drag leaves the pane it started in, which is not what any interface does.

What it does not do

It does not draw. There is no border, no gap, no highlight for the focused child: those are appearance, they belong a layer up, and a container that had an opinion about them would be one nobody could dress differently.

The zero Container is an empty column, ready to have items appended. A Container must not be copied after first use: children, focus, pointer capture and committed routing geometry are one mutable owner.

func NewContainer added in v0.11.0

func NewContainer(axis layout.Axis, items ...Item) *Container

NewContainer constructs a container that arranges its children along axis. layout.Down stacks rows and layout.Across places columns side by side.

Repeating a non-empty Item.Key is a programmer error and panics, here and in Container.Set and Container.Add. A key exists so focus and an in-progress pointer gesture follow a child that moves; two children answering to one key would send them to whichever was found first, which changes as the children are reordered.

func (*Container) Add added in v0.0.2

func (c *Container) Add(items ...Item) *Container

Add appends a child and returns the container, so a tree can be built in one expression. A key already used by a child that is staying is a programmer error and panics, as described on NewContainer.

func (*Container) Do added in v0.0.2

func (c *Container) Do(action keymap.Action) bool

Do runs one of the container's actions by name, reporting whether it was one a container knows and whether it changed anything. See Doer.

func (*Container) Draw added in v0.0.2

func (c *Container) Draw(v Frame)

Draw arranges the children and draws each into the room it got.

func (*Container) Focus added in v0.0.2

func (c *Container) Focus(has bool)

Focus takes the keyboard for this container, or gives it up, and passes the news to the child that holds it. A container is a widget like any other, so a container inside a container is how an interface gets more than one row of panes.

func (*Container) FocusNext added in v0.0.2

func (c *Container) FocusNext() bool

FocusNext moves the keyboard to the next child that will take it, wrapping round. It reports whether anything moved, which is false when no child takes the keyboard or only one does.

func (*Container) FocusPrev added in v0.0.2

func (c *Container) FocusPrev() bool

FocusPrev moves the keyboard to the previous child that will take it.

func (*Container) Focused added in v0.0.2

func (c *Container) Focused() Widget

Focused is the child with the keyboard, or nil when no child will take it.

func (*Container) Give added in v0.0.2

func (c *Container) Give(index int) bool

Give hands the keyboard to the child at index, reporting whether it took it. An index that does not exist, or a child that does not want the keyboard, is declined. Addressing the item rather than comparing Widget interface values keeps this API valid for every implementation the interface permits.

func (*Container) Handle added in v0.0.2

func (c *Container) Handle(ev input.Event) bool

Handle gives the event to whichever child it is for.

A key goes to the child with the keyboard, and the ring is walked only if that child did not want it. Anything nobody wanted is declined, so a container inside a container passes what it cannot use back up rather than swallowing it.

func (*Container) Items added in v0.0.2

func (c *Container) Items() []Item

Items returns a copy of the children in arrangement and keyboard order.

func (*Container) Len added in v0.2.0

func (c *Container) Len() int

Len reports how many children the container owns.

func (*Container) Measure added in v0.0.2

func (c *Container) Measure(across int) int

Measure is how much of the divided axis the children want altogether, which is what a container inside a measured slot answers with.

func (*Container) Set added in v0.1.0

func (c *Container) Set(items ...Item)

Set replaces the children. Focus follows a non-empty Item.Key, or the old position for an unnamed item. A repeated key is a programmer error and panics, as described on NewContainer.

type Dialog added in v0.1.0

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

Dialog is the behavior and semantic owner of one modal interaction.

Its DialogContent is a compound part that adapts an appearance-supplied Modal to the owning Stack. Show, Dismiss and Sync are the only open-state transitions; they keep caller-owned state, stack membership and focus restoration in one state machine. Dialog chooses no border, colour, placement or product wording beyond the semantic title supplied by its caller. A Dialog must not be copied after first use: its content carries a back-reference to this controller and its stack membership.

func NewDialog added in v0.1.0

func NewDialog(config DialogConfig) *Dialog

NewDialog constructs one dialog from config.

With Open set, controller operations write the accessor directly. When its owner writes it independently, it calls Dialog.Sync to perform the corresponding stack and focus transition.

DialogConfig.Stack and DialogConfig.Content are required; omitting either is a programmer error and panics here. They are what a dialog is — somewhere to open and something to open there — and a dialog missing one would construct, accept an open and then present nothing, which reads to the user as the keystroke having been lost.

func (*Dialog) Content added in v0.1.0

func (d *Dialog) Content() *DialogContent

Content returns the modal compound part owned by the dialog.

func (*Dialog) Description added in v0.1.0

func (d *Dialog) Description() string

Description returns the dialog's semantic description.

func (*Dialog) Dismiss added in v0.1.0

func (d *Dialog) Dismiss()

Dismiss closes the dialog and restores focus to the layer or base beneath it.

func (*Dialog) Open added in v0.1.0

func (d *Dialog) Open() bool

Open reports whether the dialog is semantically open.

func (*Dialog) Semantics added in v0.1.0

func (d *Dialog) Semantics() SemanticNode

Semantics returns the dialog independently of its visual boxes.

func (*Dialog) SetDescription added in v0.1.0

func (d *Dialog) SetDescription(description string)

SetDescription changes the dialog's semantic description.

func (*Dialog) SetTitle added in v0.1.0

func (d *Dialog) SetTitle(title string)

SetTitle changes the dialog's semantic title.

func (*Dialog) Show added in v0.1.0

func (d *Dialog) Show()

Show opens the dialog and gives its content the stack's keyboard ownership.

func (*Dialog) Sync added in v0.1.0

func (d *Dialog) Sync() bool

Sync applies caller-written controlled state to stack membership and focus, and reports whether the layer was added or removed.

Accessors are deliberately not observable. Making this transition explicit keeps focus changes out of Draw and preserves the rule that drawing cannot advance semantic state.

func (*Dialog) Title added in v0.1.0

func (d *Dialog) Title() string

Title returns the dialog's semantic title.

func (*Dialog) Trigger added in v0.1.0

func (d *Dialog) Trigger(label string, of Widget) *DialogTrigger

Trigger constructs an activation part for this dialog around appearance of.

type DialogConfig added in v0.11.0

type DialogConfig struct {
	// Stack owns modal ordering and is required.
	Stack *Stack
	// Open is optional caller-owned state. Nil starts locally closed.
	Open Accessor[bool]
	// Title and Description are copied semantic text.
	Title       string
	Description string
	// Content is the required modal part placed on Stack while open.
	Content Modal
}

DialogConfig is the complete construction state of Dialog.

Stack and Content are required. A nil Open gives the dialog local ownership and an initially closed state. An accessor gives ownership to the caller; its current value is applied to stack membership during construction.

type DialogContent added in v0.1.0

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

DialogContent is the modal compound part of a Dialog.

It delegates drawing, placement and input to an appearance-supplied Modal while keeping closure and focus in the controller. Callers receive it from Dialog.Content; constructing one directly would leave it without an owner. A DialogContent must not be copied: its identity is how the Dialog recognizes the one stack member whose closure settles the controller.

func (*DialogContent) Backdrop added in v0.1.0

func (c *DialogContent) Backdrop(view grid.View)

Backdrop delegates the optional backdrop part without making it behavior.

func (*DialogContent) Closed added in v0.1.0

func (c *DialogContent) Closed()

Closed settles controller state when any stack path removes the content.

func (*DialogContent) Draw added in v0.1.0

func (c *DialogContent) Draw(frame Frame)

Draw delegates to the appearance-supplied content.

func (*DialogContent) Focus added in v0.1.0

func (c *DialogContent) Focus(has bool)

Focus records semantic focus and delegates it to the appearance content.

func (*DialogContent) Handle added in v0.1.0

func (c *DialogContent) Handle(event input.Event) bool

Handle delegates input to the appearance-supplied content.

func (*DialogContent) Insists added in v0.1.0

func (c *DialogContent) Insists() bool

Insists delegates whether the content currently requires an answer.

func (*DialogContent) Place added in v0.1.0

func (c *DialogContent) Place(space image.Point) layout.Placement

Place delegates placement to the appearance-supplied content.

func (*DialogContent) Semantics added in v0.1.0

func (c *DialogContent) Semantics() SemanticNode

Semantics delegates to the owning controller.

type DialogTrigger added in v0.1.0

type DialogTrigger struct {

	// Keys maps activation. Nil reads through [DefaultActivationKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

DialogTrigger is the activation compound part of a Dialog.

Of supplies appearance only. The trigger owns activation, pointer capture, focus and semantic state, so a different appearance cannot accidentally change behavior. A DialogTrigger must not be copied after first use: those transitions and its committed hit region have one owner.

func (*DialogTrigger) Appearance added in v0.3.0

func (t *DialogTrigger) Appearance() Widget

Appearance returns the widget that paints the trigger.

func (*DialogTrigger) Do added in v0.1.0

func (t *DialogTrigger) Do(action keymap.Action) bool

Do runs the activation action by name.

func (*DialogTrigger) Draw added in v0.1.0

func (t *DialogTrigger) Draw(frame Frame)

Draw paints the appearance and claims a pending press over its committed box.

func (*DialogTrigger) Focus added in v0.1.0

func (t *DialogTrigger) Focus(has bool)

Focus records semantic focus and passes it to the appearance.

func (*DialogTrigger) Handle added in v0.1.0

func (t *DialogTrigger) Handle(event input.Event) bool

Handle opens the dialog on an activation key or completed primary click.

func (*DialogTrigger) Measure added in v0.1.0

func (t *DialogTrigger) Measure(across int) int

Measure delegates to a measured appearance.

func (*DialogTrigger) Semantics added in v0.1.0

func (t *DialogTrigger) Semantics() SemanticNode

Semantics returns the trigger as a button associated with the dialog's open state.

func (*DialogTrigger) SetAppearance added in v0.3.0

func (t *DialogTrigger) SetAppearance(appearance Widget)

SetAppearance replaces the trigger's visual part and transfers keyboard ownership. Activation and semantics remain owned by the trigger.

type Doer added in v0.0.2

type Doer interface {
	Do(action keymap.Action) bool
}

Doer is a widget that can be asked for one of its actions by name.

It is the other half of keymap.Map. A widget names what it can do and answers to the name; the map says which keystrokes produce which name. Neither knows the other, which is what lets a program rebind every key without touching a widget, and what lets the same action be reached from somewhere that is not the keyboard at all — a menu, a command typed by name, a test that presses nothing.

Do reports whether the action was one this widget knows. An action it does not know is not an error: one keymap often drives a whole interface, and every widget reading through it answers what it recognises and lets the rest past.

type Editor

type Editor struct {

	// Placeholder is shown while the field is empty, and is not part of the text.
	Placeholder string
	// Look is how the text, the placeholder and the selection are drawn — see [Look],
	// which is the one way anything here that draws itself is dressed. The zero value
	// draws in the terminal's own colours and lays nothing over a selection, which is
	// what a field that never selects wants.
	Look Look
	// Keys say which keystrokes produce which of the actions this field answers to —
	// see [Editor.Do]. Nil reads through [DefaultEditorKeys].
	//
	// It is a map and not a struct of one field per action, so a program can hand the
	// same map to a field, to the container around it and to its own keys, and rebind
	// any of them without replacing anything.
	Keys *keymap.Map
	// Clipboard is where copy and cut send text and where paste asks for it. Nil
	// leaves those keys doing nothing, which is the right answer for a field in a
	// program that has no terminal to ask.
	//
	// A runtime adapter can satisfy this directly; an editor depends only on these
	// two operations.
	Clipboard Clipboard
	// MaxRows caps how tall the field grows. Beyond it the field scrolls and keeps
	// the cursor in view. Zero means it grows without limit, which only suits a
	// field that owns its whole pane.
	MaxRows int
	// Gutter draws beside the field's visual rows. Nil gives every column to the
	// text. The gutter is not part of selection or clipboard content, and pointer
	// input in it is left for a containing component to interpret.
	Gutter RowGutter
	// CursorStyle chooses the terminal cursor's shape and blink while this editor has
	// the keyboard. The zero value leaves both to the terminal's configured default.
	CursorStyle grid.CursorStyle
	// contains filtered or unexported fields
}

Editor is a multi-line text field.

The cursor is a byte offset into a line, and it only ever sits on a grapheme cluster boundary. It cannot sit between a letter and the accent that modifies it, because that is not a place a terminal could draw it, and it cannot sit inside a multi-column display atom for the same reason.

Vertical movement is by visual row, not by logical line. In a field that wraps, pressing down inside a long paragraph has to move down the screen; a cursor that jumped to the next paragraph instead would be moving somewhere the user cannot see the reason for.

The zero value is ready to use. An Editor must not be copied after first use.

func (*Editor) Anchor added in v0.0.2

func (e *Editor) Anchor()

Anchor begins or continues a selection at the cursor.

A selection is not a separate mode with commands of its own. It is what movement means while the shift key is held, so every way of moving a cursor selects with shift and none of them had to be taught to — see Editor.Handle.

func (*Editor) At added in v0.0.2

func (e *Editor) At(x, y, width int) (Caret, bool)

At is the position in the text under a point in the field's box, and whether the point is in the text at all.

The point is in the field's own coordinates, which is what a widget is handed. The answer accounts for the field having scrolled, because the field is what knows it.

It reads the same rows the cursor is placed from and the selection is painted from, which is the only way a click can land where the reader thinks they clicked: three walks over three wraps agree until the text is interesting, and then they do not.

func (*Editor) Clear

func (e *Editor) Clear()

Clear empties the field.

func (*Editor) Copy added in v0.0.2

func (e *Editor) Copy() bool

Copy puts the selection where a paste would find it, and reports whether anything was sent. Nothing selected sends nothing, which is not a failure.

func (*Editor) Cursor

func (e *Editor) Cursor() (line, col int)

Cursor is the cursor's logical line and byte offset, for anything that needs to know where the user is.

func (*Editor) Cut added in v0.0.2

func (e *Editor) Cut() bool

Cut copies the selection and removes it.

The text is removed only if the clipboard took it. A cut that emptied the field into a clipboard that refused it would lose the text with nothing to paste back, and a terminal is free to refuse.

func (*Editor) DeleteBack

func (e *Editor) DeleteBack()

DeleteBack removes the cluster before the cursor, or joins this line to the one above when the cursor is at the start of a line.

func (*Editor) DeleteForward

func (e *Editor) DeleteForward()

DeleteForward removes the cluster after the cursor, or joins the line below.

func (*Editor) DeleteSelection added in v0.0.2

func (e *Editor) DeleteSelection() bool

DeleteSelection removes the selected text and reports whether there was any.

func (*Editor) DeleteWordBack

func (e *Editor) DeleteWordBack()

DeleteWordBack removes from the cursor back to the start of the word behind it.

func (*Editor) Do added in v0.0.2

func (e *Editor) Do(action keymap.Action) bool

Do runs one of the field's actions by name, reporting whether it was one this field knows. See Doer for why a widget answers to a name at all.

func (*Editor) Draw

func (e *Editor) Draw(frame Frame)

Draw paints the field and places the cursor.

func (*Editor) DrawWith added in v0.6.0

func (e *Editor) DrawWith(frame Frame, look Look)

DrawWith paints one projection with look without changing the editor's configured appearance.

Appearance components use this when an editor participates in a larger theme. The editor remains the single owner of its text, cursor and input configuration; drawing it through another look does not make that look its configuration.

func (*Editor) ElementAt added in v0.0.2

func (e *Editor) ElementAt(line, col int) (Element, bool)

ElementAt is the element covering a position, and whether there is one. The end is exclusive, so the position just after an element is outside it.

func (*Editor) Elements added in v0.0.2

func (e *Editor) Elements() []Element

Elements is every element in the text, in the order they appear. The slice is a copy: a caller cannot move an element by writing to it.

func (*Editor) Empty

func (e *Editor) Empty() bool

Empty reports whether there is nothing in the field.

func (*Editor) Focus added in v0.0.2

func (e *Editor) Focus(has bool)

Focus takes the keyboard, or gives it up. A field without it draws no cursor.

A frame has one cursor and the terminal draws it, so two fields both asking for it is not two cursors: it is one, wherever the last of them happened to draw. This is how the question is settled — see Focusable, and note that a field nobody has told anything believes it has the keyboard, which is what makes a lone field work.

func (*Editor) Handle

func (e *Editor) Handle(ev input.Event) bool

Handle answers keys, reporting whether it consumed the event.

Enter is deliberately not bound. Whether it sends or breaks the line is the container's decision, and an editor that swallowed it would take that decision away from every container that embeds one.

func (*Editor) Insert

func (e *Editor) Insert(s string)

Insert puts text in at the cursor. Newlines in it split lines, so a paste arrives as the text that was pasted rather than as a run of keystrokes.

func (*Editor) InsertElement added in v0.0.2

func (e *Editor) InsertElement(kind ElementKind, body string) Element

InsertElement puts text at the cursor as one atomic unit, and returns it.

Line breaks in body become spaces even in a multi-line editor. An element is one contiguous run of cells; allowing its source to span logical lines would make its returned line-local range describe only a fragment of what was inserted.

A separator space follows it, which is what makes a chip in a prompt something a user can type after. The space is ordinary text and not part of the element: it is there to be deleted.

An empty body inserts nothing and returns the zero Element. Identities are never reused, and InsertElement panics once every one has been issued: an Element the caller kept in order to replace or remove what it stands for would otherwise begin naming a different insertion.

func (*Editor) InsertRune

func (e *Editor) InsertRune(r rune)

InsertRune puts one character in.

func (*Editor) KillToEnd

func (e *Editor) KillToEnd()

KillToEnd cuts from the cursor to the end of the line, keeping what it cut.

On an already-empty tail it takes the line break instead, which is what makes repeated presses swallow a paragraph rather than stop at the first line.

func (*Editor) KillToStart

func (e *Editor) KillToStart()

KillToStart cuts from the start of the line to the cursor.

func (*Editor) Mask added in v0.0.2

func (e *Editor) Mask() string

Mask reports what each text cluster is drawn as. Empty means text is shown.

func (*Editor) Measure

func (e *Editor) Measure(width int) int

Measure is how many rows the field needs at a width, within its cap.

func (*Editor) MoveDown

func (e *Editor) MoveDown()

MoveDown moves the cursor down one visual row.

func (*Editor) MoveLeft

func (e *Editor) MoveLeft()

MoveLeft moves one cluster left, over a line break when there is nowhere else.

func (*Editor) MoveLineEnd

func (e *Editor) MoveLineEnd()

MoveLineEnd moves to the end of the logical line.

func (*Editor) MoveLineStart

func (e *Editor) MoveLineStart()

MoveLineStart moves to the start of the logical line.

func (*Editor) MoveRight

func (e *Editor) MoveRight()

MoveRight moves one cluster right, over a line break when there is nowhere else.

func (*Editor) MoveUp

func (e *Editor) MoveUp()

MoveUp moves the cursor up one visual row, keeping the column it started from.

func (*Editor) MoveWordLeft

func (e *Editor) MoveWordLeft()

MoveWordLeft moves to the start of the word behind the cursor.

func (*Editor) MoveWordRight

func (e *Editor) MoveWordRight()

MoveWordRight moves past the end of the word in front of the cursor.

func (*Editor) Newline

func (e *Editor) Newline()

Newline splits the line at the cursor, and does nothing at all in a field that holds one line.

func (*Editor) Paste added in v0.0.2

func (e *Editor) Paste() bool

Paste asks the clipboard for its contents and reports whether the request was accepted. What comes back arrives later as an ordinary paste event, which this editor already inserts.

func (*Editor) Redo

func (e *Editor) Redo()

Redo steps forward again.

func (*Editor) RemoveElement added in v0.0.2

func (e *Editor) RemoveElement(id uint64) bool

RemoveElement deletes an element's text and forgets it, reporting whether it was there to remove.

func (*Editor) Replace

func (e *Editor) Replace(start, end int, s string)

Replace swaps the byte range [start, end) of the line the cursor is on for s, and leaves the cursor after what was put in. The range is clamped to the line and expanded to whole grapheme clusters; a terminal cursor cannot address half of one.

It is one edit rather than a delete and an insert so that it is one step to undo: accepting a completion is one thing the user did, and taking it back should not take two. A token never spans lines, which is why the range does not either.

func (*Editor) Revision added in v0.12.0

func (e *Editor) Revision() uint64

Revision reports the generation of the editor's semantic content.

It advances once for every change to text or atomic elements, whether the change came from input, a programmatic editing method, undo or redo. Cursor movement, selection, scrolling, focus, copying and an edit that has no effect leave it alone. This is what lets a caller decide whether to validate, persist or mark a draft dirty without guessing from a key or an action name.

A revision is an opaque, process-local observation token. Compare it with an earlier value from this editor; do not persist it or give the number itself meaning.

func (*Editor) Scroll

func (e *Editor) Scroll() *Scroll

Scroll exposes the field's position, for a scrollbar beside a tall field.

func (*Editor) SelectAll added in v0.0.2

func (e *Editor) SelectAll()

SelectAll selects the whole text.

func (*Editor) SelectNone added in v0.0.2

func (e *Editor) SelectNone()

SelectNone drops the selection, leaving the cursor where it is.

func (*Editor) Selected added in v0.0.2

func (e *Editor) Selected() string

Selected is the selected text, or empty when nothing is selected.

func (*Editor) Selection added in v0.0.2

func (e *Editor) Selection() (start, end Caret, ok bool)

Selection is the selected range in reading order, and whether there is one.

It reports false for a selection of nothing, which is what a shift-arrow pressed and then taken back leaves: an anchor at the cursor is not a selection, and treating it as one would make a copy put an empty string on the clipboard.

func (*Editor) SelectionSpans added in v0.0.2

func (e *Editor) SelectionSpans(width int) []RowSpan

SelectionSpans is where the selection is, or nothing when there is none.

func (*Editor) SetCursor added in v0.0.2

func (e *Editor) SetCursor(line, col int)

SetCursor moves the cursor to a logical line and a byte offset within it.

Both are clamped to the text, and the offset is pulled back to the start of the cluster it lands inside: a cursor between a letter and the accent that modifies it is not a place a terminal could draw one.

It is what a caller needs to restore a draft where they left it, and what placing the cursor from a click will be built on — the editor could report where its caret was and not be told where to put it, which made the round trip only half a round.

func (*Editor) SetMask added in v0.12.0

func (e *Editor) SetMask(mask string)

SetMask changes what each text cluster is drawn as.

A mask must be valid, visible terminal text without tabs or control characters; invalid configuration panics. A non-empty mask makes the field one-line, applying the same semantic transition as Editor.SetSingleLine.

func (*Editor) SetSingleLine added in v0.12.0

func (e *Editor) SetSingleLine(enabled bool)

SetSingleLine changes whether the field holds one line.

Enabling it turns existing line breaks into spaces as one semantic change, keeps element identities, settles the cursor from the same whole-document offset, and clears undo history that could otherwise restore an invalid multi-line state. Disabling it leaves the current one-line content in place; later insertions may add lines.

func (*Editor) SetText

func (e *Editor) SetText(s string)

SetText replaces the content and puts the cursor at the end, which is where someone who just had text put in front of them wants to carry on from.

func (*Editor) SingleLine added in v0.0.2

func (e *Editor) SingleLine() bool

SingleLine reports whether this field was explicitly configured to hold one line. A non-empty Editor.Mask also makes the effective field one-line.

func (*Editor) Spans added in v0.0.2

func (e *Editor) Spans(from, to Caret, width int) []RowSpan

Spans is the runs of columns that the text between two carets covers, one per visual row it crosses.

A range in a wrapped field is not a rectangle and is rarely one run. It starts part way along a row, covers whole rows, and ends part way along another — and where the rows begin and end is decided by the wrap, which is decided by the width. So this is a question only the field can answer, and only at a width. Width is the whole field, including any Editor.Gutter, and returned columns use that same coordinate space.

It reads the same rows the cursor is placed with. That is the whole point of it being here rather than being worked out by whatever draws: a selection painted from one wrap and a cursor placed from another disagree about where the text is, and the disagreement shows up exactly when the text is interesting — a long word, a wide character, a line that just fits.

func (*Editor) Text

func (e *Editor) Text() string

Text is the whole content, lines joined by newlines.

func (*Editor) Undo

func (e *Editor) Undo()

Undo steps back to before the last change.

func (*Editor) Yank

func (e *Editor) Yank()

Yank puts back the most recently killed text.

func (*Editor) YankPop added in v0.3.0

func (e *Editor) YankPop()

YankPop replaces the immediately preceding yank with the next older kill, cycling through the bounded ring. Any intervening edit, movement, or selection ends the sequence and makes this a no-op.

type Element added in v0.0.2

type Element struct {
	// ID is unique within one editor and stable for as long as the element exists.
	// It is what a program keys its own record of the element by.
	ID uint64
	// Kind is the program's own label.
	Kind ElementKind
	// Line is the logical line the element sits on, and Start and End its byte range
	// within that line, the end exclusive.
	//
	// An element never spans a line break. There is nothing to show for one that did:
	// what makes it one thing on screen is that it is one run of cells.
	Line       int
	Start, End int
}

Element is a run of an editor's text that behaves as one character.

A prompt is often not only text. A dropped image, a picked file, a mentioned name: each is shown as a word or two and stands for something the word is not. Letting the cursor walk into the middle of one, or a backspace take a letter off the end of one, leaves a fragment that still looks like the thing and no longer is.

So an element is atomic: the cursor steps over it, a delete takes all of it, and nothing lands inside. Its identity survives editing around it, which is what lets a program keep whatever the element stands for beside it.

It is a text.Mark in the coordinates this editor speaks. The rule that keeps it over the same words while the text around it changes is that type's, and it is the same rule a highlight or a search result would need — see text.Edit.Shift.

func (Element) Text added in v0.0.2

func (el Element) Text(e *Editor) string

Text is the element's own text, given the editor it belongs to.

type ElementKind added in v0.0.2

type ElementKind uint8

ElementKind lets a host tell one family of atomic elements from another.

The package assigns no meanings. What an element stands for — a file the user picked, an image they dropped, somebody they mentioned — is the program's business; what is this package's business is that the text behaves as one thing.

type Field added in v0.0.2

type Field interface {
	Focusable
	layout.Measurer

	// Prompt is what the field is asking for.
	Prompt() string
	// Validate checks what has been entered, remembers what was wrong with it, and
	// reports that. It is called when the field loses the keyboard and again when the
	// form is submitted.
	Validate() error
	// Error is what Validate last found, or nil when it found nothing.
	Error() error
}

Field is one thing a Form collects.

It draws itself, including what it is asking for and what was wrong with the answer. That is not the division this package usually makes, and the reason is that a field is generic over what it holds: a renderer that drew every kind of field would have to name every kind, and there is no way to write down "a select of something". So the look travels the other way, as a Look the form hands down.

type Filter added in v0.0.2

type Filter[T any] struct {

	// Row draws one of the items that matched. at is where it sits among the rows on
	// screen, match says which characters of its text answered the pattern, and
	// selected says whether it is the one under the cursor. It runs during drawing
	// and must be an observationally pure projection.
	Row func(v grid.View, at int, item T, match fuzzy.Match, selected bool)
	// Keys say which keystrokes move the cursor. Nil reads through [DefaultListKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Filter is a list narrowed by a pattern: the items that match it, best first, with the characters that matched marked.

It puts together the three pieces that were already here — a list, a fuzzy match, and somewhere to scroll — and it deliberately does not include the fourth. Where the pattern is typed is the caller's: an editor at the bottom of a dialog, a composer already on screen, a command's argument, or nothing at all in a test. Owning a text field as well would make this two widgets in a trench coat, and would decide where the field goes for everyone.

filter.SetPattern(composer.Editor().Text())

The zero Filter shows everything, in the order it was given. A Filter must not be copied after first use: its source, ranked list and cursor are one mutable owner.

func (*Filter[T]) Current added in v0.0.2

func (f *Filter[T]) Current() (T, bool)

Current is the item under the cursor and whether there is one.

func (*Filter[T]) Do added in v0.0.2

func (f *Filter[T]) Do(action keymap.Action) bool

Do runs one of the list's actions by name. See Doer.

func (*Filter[T]) Draw added in v0.0.2

func (f *Filter[T]) Draw(v Frame)

Draw paints the matches that fit.

func (*Filter[T]) Focus added in v0.0.3

func (f *Filter[T]) Focus(has bool)

Focus takes the keyboard, or gives it up — see List.Focus.

func (*Filter[T]) Focused added in v0.0.3

func (f *Filter[T]) Focused() bool

Focused reports whether this list has the keyboard.

func (*Filter[T]) Handle added in v0.0.2

func (f *Filter[T]) Handle(ev input.Event) bool

Handle answers the keys, the wheel and a press that move the cursor. What narrows the list is not an event here: it is Filter.SetPattern, because the pattern is typed somewhere this does not own.

func (*Filter[T]) Items added in v0.0.2

func (f *Filter[T]) Items() []T

Items returns a copy of everything there is to choose from, matched or not.

func (*Filter[T]) Len added in v0.2.0

func (f *Filter[T]) Len() int

Len reports how many unfiltered items the filter owns.

func (*Filter[T]) Matched added in v0.0.2

func (f *Filter[T]) Matched() int

Matched is how many items answered the pattern.

func (*Filter[T]) Measure added in v0.0.2

func (f *Filter[T]) Measure(int) int

Measure is one row per match. Match state is rebuilt by semantic operations, so measuring only observes the last complete projection.

func (*Filter[T]) Pattern added in v0.0.2

func (f *Filter[T]) Pattern() string

Pattern is what the items are being narrowed by.

func (*Filter[T]) Scroll added in v0.0.2

func (f *Filter[T]) Scroll() *Scroll

Scroll exposes the position, for a scrollbar drawn beside the list.

func (*Filter[T]) Select added in v0.0.2

func (f *Filter[T]) Select(at int)

Select moves the cursor to one of the matches, clamped to them.

func (*Filter[T]) Selected added in v0.0.2

func (f *Filter[T]) Selected() int

Selected is the row the cursor is on among the matches, or -1 when nothing matched.

func (*Filter[T]) SetItems added in v0.0.2

func (f *Filter[T]) SetItems(items []T, text func(item T) string)

SetItems replaces the source: the values there are to choose from and the one projection that says how each value reads for matching. They travel together because ranked matches are a fact about both; separate setters would expose a transient source whose values and projection disagree. Nil text makes no item match. The filter copies items, so the caller may reuse or change its input.

func (*Filter[T]) SetPattern added in v0.0.2

func (f *Filter[T]) SetPattern(pattern string)

SetPattern narrows the list, keeping the cursor at the top of what matched.

The cursor goes to the top rather than staying where it was, because after a keystroke the rows under it are different rows: staying on the third one would leave the cursor on whatever happened to land there, which is how a reader picks something they did not mean.

type Focusable added in v0.0.2

type Focusable interface {
	Interactive
	// Focus is told true when this widget takes the keyboard and false when it loses
	// it. It is called when the answer changes rather than every frame.
	//
	// It may be told the same thing twice — every child of a container is told where
	// it stands as soon as there is a container to say so, whether or not it had
	// supposed otherwise. A widget that does something on losing the keyboard, such
	// as validating what was typed, has to check that it had it.
	Focus(has bool)
}

Focusable is a widget that can hold the keyboard.

Why a widget has to be told

A keystroke has one destination and a frame has one cursor, so with more than one field on screen something has to decide which of them the typing is for. Deciding it by letting an event fall through until somebody claims it works while the widgets are in a line and nothing else: two editors both claim every key, and both place the terminal's cursor, and the one that draws last wins.

So the answer is pushed rather than pulled. A Container tells the widget that has the keyboard, and tells the ones that do not, and a widget draws itself accordingly — a cursor, a lit border, a highlighted row.

Why the zero value has the keyboard

A widget that has never been told anything assumes it has the keyboard. That is what makes a single field work when it is the whole interface, with no container above it to say so — which is how most interfaces start, and how every one of this library's examples began. A container tells every child where it stands as soon as it has one, so nothing is ever left guessing once there is a choice to make.

Answering input is not the same as wanting the keyboard, which is why this is its own interface. A transcript answers the wheel and a drag; it is not somewhere the user types, and it has no business in the ring that tab walks.

type Form added in v0.0.2

type Form struct {

	// Keys say which keystrokes submit and abandon the form, and which walk between
	// its fields. Nil reads through [DefaultFormKeys].
	Keys *keymap.Map
	// Look is how the fields draw themselves. It is handed to each of them as the form
	// draws, so changing it changes them.
	Look Look
	// Gap is how many blank rows go between one field and the next. Zero puts them
	// against each other, which is right for a short form and cramped for a long one.
	Gap int

	// Check is what is wrong with the answers taken together, and is asked only once
	// each of them is acceptable on its own. Nil means nothing is.
	Check func() error
	// Done is called when the form is submitted and everything checks out, and Given
	// up when it is abandoned. Either may be nil.
	Done, GaveUp func()
	// contains filtered or unexported fields
}

Form is a set of fields, one of which has the keyboard.

It is a Container of them with two things added: an answer is checked when the keyboard leaves the field it was given to, and the whole set is checked when the form is submitted. Everything else — which field has the keyboard, what tab does, which field a click landed in — is the container's, because it is the same question there as anywhere else.

The zero Form has no fields and answers nothing. A Form must not be copied after first use: its fields, focus, matcher and validation state are one mutable owner.

Example
// A field does not own what it collects: these three variables do, and the form
// writes into them as the answers are given.
var (
	name  string
	model string
	sure  bool
)
modelField := &headless.Select[string]{Label: "Model", Value: headless.Bind(&model)}
modelField.SetOptions(headless.Options("fast", "good"))
form := headless.NewForm(
	&headless.Text{
		Label: "Name",
		Value: headless.Bind(&name),
		Check: func(s string) error {
			if s == "" {
				return errors.New("a name is needed")
			}
			return nil
		},
	},
	modelField,
	&headless.Confirm{Label: "Sure?", Value: headless.Bind(&sure)},
)
form.Look = headless.Look{Taken: "x", Free: "-"}
form.Done = func() { fmt.Println("collected:", name, model, sure) }

for _, r := range "ada" {
	form.Handle(input.Key{Code: input.Character, Rune: r})
}
form.Handle(input.Key{Code: input.Tab})   // on to the model
form.Handle(input.Key{Code: input.Down})  // which is the second one
form.Handle(input.Key{Code: input.Tab})   // on to the question
form.Handle(input.Key{Code: input.Left})  // which is yes
form.Handle(input.Key{Code: input.Enter}) // done

showWidget(16, form.Measure(16), form)
Output:
collected: ada good true
|Name            |
|ada             |
|Model           |
|- fast          |
|x good          |
|Sure?           |
|x yes  - no     |

func NewForm added in v0.3.0

func NewForm(fields ...Field) *Form

NewForm constructs a form from fields in keyboard order. A nil field is a programmer error and panics here, on the same terms as Form.Set.

func (*Form) Add added in v0.3.0

func (f *Form) Add(fields ...Field) *Form

Add appends fields and returns the form, so a form can be built in one expression.

func (*Form) Cancel added in v0.0.2

func (f *Form) Cancel()

Cancel abandons the form.

func (*Form) Do added in v0.0.2

func (f *Form) Do(action keymap.Action) bool

Do runs one of the form's actions by name. See Doer.

func (*Form) Draw added in v0.0.2

func (f *Form) Draw(v Frame)

Draw dresses the fields with Look and lays them out down the region.

func (*Form) DrawWith added in v0.3.0

func (f *Form) DrawWith(v Frame, look Look)

DrawWith draws the form with look for this projection only.

An appearance wrapper uses this instead of changing Form.Look before drawing. Neither the form nor its fields store look, so two appearances can project the same controller without the last one silently becoming its configuration.

func (*Form) Error added in v0.0.2

func (f *Form) Error() error

Error is what checking the answers together last found.

func (*Form) Fields added in v0.0.2

func (f *Form) Fields() []Field

Fields returns a copy of the fields in keyboard order.

func (*Form) Focus added in v0.0.2

func (f *Form) Focus(has bool)

Focus takes the keyboard, or gives it up, and passes the news to the field that has it.

func (*Form) Focused added in v0.0.2

func (f *Form) Focused() Field

Focused is the field with the keyboard, or nil.

func (*Form) Handle added in v0.0.2

func (f *Form) Handle(ev input.Event) bool

Handle gives the event to the field with the keyboard, then to the form itself.

func (*Form) Measure added in v0.0.2

func (f *Form) Measure(across int) int

Measure is how tall the fields are altogether, which is what a form in a measured slot asks for.

func (*Form) Set added in v0.3.0

func (f *Form) Set(fields ...Field)

Set replaces the fields in keyboard order. The form copies the collection, releases a focused field that was removed, and settles focus on its replacement. A field is still caller-owned; only the ordered collection belongs to the form. A nil field is a programmer error and panics here rather than in a later draw or submission.

func (*Form) Submit added in v0.0.2

func (f *Form) Submit() bool

Submit checks every answer and reports whether the form is complete.

Every field is checked and not just the first that fails, because a form that reported its problems one at a time would make somebody submit four times to find out there were four.

type Found added in v0.0.2

type Found struct {
	Command Command
	// At is the byte offsets in the name that the query matched, for underlining
	// them. It is empty for a match on an alias or for an empty query, because
	// neither has anything to point at in the name being shown.
	At []int
}

Found is a command a query matched, and where in its name it matched.

type Frame added in v0.1.0

type Frame struct {
	grid.View
	// contains filtered or unexported fields
}

Frame is the drawing space of one headless component frame.

The embedded grid view is already positioned and clipped. Sub and Subs preserve the presentation transaction while deriving child views; code drawing a passive Block can pass View to it directly.

A Frame is created by Root. Components must not construct one: the transaction is what prevents routing geometry from becoming visible before the complete root frame has been built.

Staging presentation state through a Frame no active Root.Draw owns — a zero value, or one kept past the frame it came from — is a programmer error and panics, as does staging one state object twice in a frame or from two roots. Each is two producers for a value only one can win, and the losing write would otherwise surface much later as a widget scrolled or focused somewhere nobody asked it to be.

func (Frame) Sub added in v0.1.0

func (f Frame) Sub(r image.Rectangle) Frame

Sub returns a child frame over r, whose coordinates begin at zero.

func (Frame) Subs added in v0.1.0

func (f Frame) Subs(rects []image.Rectangle) []Frame

Subs returns child frames over rects, preserving their order.

type History added in v0.0.2

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

History is what the user typed before, and a place in it.

The draft

Walking back through history has to be undoable, and the thing that gets lost is what the user had already typed: they are half way through a line, press up to check something, and press down again expecting their line back. So the first step backwards keeps the draft, and coming forward past the newest entry gives it back. Nothing else in a prompt is as annoying to lose, because it is the only text the user cannot get again by scrolling.

What is not kept

Consecutive duplicates, and empty lines. Somebody who runs the same thing twice has not made two entries worth stepping through, and a blank line was not an entry at all. Duplicates that are not consecutive are kept, because the order tells the truth about what happened.

The zero value is an empty history. A History must not be copied after first use; its retained entries, draft and current walk are one mutable sequence.

func (*History) Add added in v0.0.2

func (h *History) Add(line string)

Add records a line, unless it is empty or the same as the newest entry.

It also ends any walk in progress, which is what submitting a line means: whatever the user was stepping through, they have now said something, and the next press of up starts again from the end.

func (*History) At added in v0.0.2

func (h *History) At(n int) (string, bool)

At is the entry n steps back from the end, one being the newest.

func (*History) Back added in v0.0.2

func (h *History) Back(current string) (string, bool)

Back steps one entry further into the past, and reports what should now be in the field.

current is what is in the field now, which is kept as the draft on the first step so that History.Forward can give it back. It reports false at the oldest entry, so a caller can leave the field alone rather than clearing it.

func (*History) Cancel added in v0.0.2

func (h *History) Cancel() (string, bool)

Cancel abandons a walk and reports the draft it began with, if there was one.

func (*History) Forward added in v0.0.2

func (h *History) Forward() (string, bool)

Forward steps one entry towards the present.

Stepping forward past the newest entry gives back the draft the walk began with, which is the whole reason the draft is kept.

func (*History) Len added in v0.0.2

func (h *History) Len() int

Len is how many entries are kept.

func (*History) Limit added in v0.0.2

func (h *History) Limit() int

Limit reports the effective entry limit.

func (*History) Recall added in v0.0.2

func (h *History) Recall(query string) []Recalled

Recall is the entries a query matches, newest first.

The order is the point. Scoring alone would put the best-matching line first and bury the one from a minute ago behind six from last week, and "the one I ran recently" is what somebody searching their own history is nearly always after — so matches are found by score and then shown newest first.

func (*History) SetLimit added in v0.3.0

func (h *History) SetLimit(n int)

SetLimit changes how many entries to keep, dropping the oldest if there are already more. Zero restores DefaultHistoryLimit. A negative limit is a programmer error and panics, because zero is already the way to ask for the default and there is no number of entries below none.

func (*History) Walking added in v0.0.2

func (h *History) Walking() bool

Walking reports whether a walk through the history is in progress.

type Insistent added in v0.0.2

type Insistent interface {
	Modal
	// Insists reports whether the modal currently refuses to be dismissed. It is a
	// method rather than a marker so a layer can stop insisting once it has what
	// it needs.
	Insists() bool
}

Insistent is a modal that the escape key does not close.

It is for the layer that has to be answered rather than dismissed — a confirmation, a required choice. Without it the way to make one would be to consume the escape key and do nothing, which reads at the call site as a bug.

type Interactive

type Interactive interface {
	Widget
	Handle(event input.Event) bool
}

Interactive is a widget that answers input.

Any consumer of the same drawing and event method set can use an Interactive without an adapter, while neither side knows the other exists.

type Item added in v0.0.2

type Item struct {
	// Key is the child's stable identity across [Container.Set]. Empty uses its
	// position. Name a child when it may move: focus and an in-progress pointer
	// gesture then follow the part rather than whichever part took its old slot.
	// Non-empty keys must be unique within one container.
	Key string
	// Size is how much of the divided axis this child takes. It means exactly what
	// it means in [layout.Slot], including the zero value, which asks for nothing —
	// deliberately, because [layout.Fixed] of zero is that same zero value, and a
	// container that read it as "however much you want" would put back a row a
	// caller had just asked to have none of.
	//
	// A child as big as its content wants to be is [layout.Measured].
	Size layout.Sizing
	// Of is the child. A child that can answer how big it wants to be — anything
	// implementing [Sized] — is asked when Size says the slot is measured.
	Of Widget
}

Item is one child of a Container: what goes there, and how much room it gets.

type LayerID added in v0.3.0

type LayerID uint64

LayerID is one insertion into a Stack.

A handle, rather than Modal interface equality, makes duplicate insertions unambiguous and lets every valid Modal implementation be removed even when its concrete value is not comparable. The zero ID names no layer.

type List

type List[T any] struct {

	// Row draws one item. at is where it sits among the items and selected says
	// whether it is the one under the cursor, which the caller renders however it
	// likes — a list does not know what selected looks like in its surroundings.
	//
	// The index is there because a row is often about more than the item: a number
	// down the left, a mark for what has been chosen, a colour that alternates. Only
	// the list knows it, and a caller finding it again by comparing items would be
	// guessing whenever two of them were alike.
	//
	// Row runs during measurement or drawing. It must be an observationally pure
	// projection: mutate list or application state from Handle or another owner-side
	// operation, never from this callback.
	Row func(v grid.View, at int, item T, selected bool)
	// Keys say which keystrokes produce which of the actions the list answers to —
	// see [List.Do]. Nil reads through [DefaultListKeys].
	Keys *keymap.Map
	// Wrap moves the selection from the last item to the first and back. Off by
	// default: in a long list, wrapping loses the user's place.
	Wrap bool
	// contains filtered or unexported fields
}

List is a vertical list of one-row items with a selection.

It is generic over the item so a list of sessions and a list of files are the same widget with different rows, and so nothing here has to know what an item is. The row is drawn by a function the caller supplies: a list that formatted its own items would be a list that had opinions about them.

Selection and scrolling are separate concerns that have to agree: moving the selection past the edge of the window scrolls to keep it visible, because a selection the user cannot see is a selection they will act on by mistake.

The zero value is ready. A List must not be copied after first use: its items, matcher, scroll and committed routing state are one mutable owner.

func (*List[T]) At added in v0.2.0

func (l *List[T]) At(index int) (T, bool)

At returns the item at index and whether it exists.

func (*List[T]) Current

func (l *List[T]) Current() (T, bool)

Current is the item under the cursor, and whether there was one.

func (*List[T]) Do added in v0.0.2

func (l *List[T]) Do(action keymap.Action) bool

Do runs one of the list's actions by name, reporting whether it was one this list knows. See Doer.

func (*List[T]) Draw

func (l *List[T]) Draw(v Frame)

Draw paints the visible items.

func (*List[T]) DrawRows added in v0.3.0

func (l *List[T]) DrawRows(v Frame, draw func(grid.View, int, T, bool))

DrawRows paints the visible items with row.

Supplying the renderer for this frame lets an appearance component reuse geometry it computed once at the frame's width. The list still owns selection, scrolling and committed pointer routing; List.Draw is this method with Row.

func (*List[T]) Focus added in v0.0.3

func (l *List[T]) Focus(has bool)

Focus takes the keyboard, or gives it up.

A list draws no differently for it: what a selection looks like in a list nobody is typing at is a matter of taste, and taste is what this layer refuses. It is here because a container hands the keyboard to its children by asking for this, so a list without it could not be one of them — and because List.Focused is how a row asks, which is where the answer belongs.

func (*List[T]) Focused added in v0.0.3

func (l *List[T]) Focused() bool

Focused reports whether this list has the keyboard.

func (*List[T]) Handle

func (l *List[T]) Handle(ev input.Event) bool

Handle answers keys, the wheel and a press, reporting whether it consumed the event.

func (*List[T]) Items

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

Items returns a copy of the items in list order.

func (*List[T]) Len added in v0.2.0

func (l *List[T]) Len() int

Len reports how many items the list owns.

func (*List[T]) Measure

func (l *List[T]) Measure(int) int

Measure is one row per item, which is what a container needs to decide whether the list can have all the room it wants.

func (*List[T]) Move

func (l *List[T]) Move(n int)

Move shifts the selection by n items, wrapping only if asked to.

func (*List[T]) Scroll

func (l *List[T]) Scroll() *Scroll

Scroll exposes the position, for a scrollbar drawn beside the list.

func (*List[T]) Select

func (l *List[T]) Select(i int)

Select moves the cursor to an index, clamped to the list.

func (*List[T]) Selected

func (l *List[T]) Selected() int

Selected is the index under the cursor, or -1 for an empty list.

func (*List[T]) SetItems

func (l *List[T]) SetItems(items []T)

SetItems replaces the contents, keeping the selection on the same index where that still exists. The list copies the slice; the caller may reuse or change its input after this returns.

Keeping the index rather than the item: a list that is refreshed while the user is reading it should not jump, and following an item by identity would need this widget to know how to compare items, which is knowledge it has no business holding.

type Look added in v0.0.2

type Look struct {
	// Text is the answer, Label what the field is asking for, and Subtle a placeholder
	// or a hint.
	Text, Label, Subtle grid.Style
	// Selection is the row under the keyboard, and Accent the answer that has been
	// given.
	Selection, Accent grid.Style
	// Danger is what is wrong with the answer.
	Danger grid.Style
	// Taken and Free are the marks beside a choice that has been made and one that is
	// still on offer. They are drawn in the same column, so they are the same width or
	// nothing beside them lines up.
	Taken, Free string
}

Look is how a widget here draws itself, for the few that draw themselves at all.

Most of this package draws nothing: a list calls back to whoever knows what a row looks like, and that is what makes the ring above it optional. The exceptions are the widgets whose drawing cannot be handed out — a field is generic over what it holds, so nothing above could name every kind of one; an editor lays a selection over text it alone knows the shape of; a completion picks out the characters a query matched. They take this, and there is one of it rather than a style field per part, keeping one coherent appearance value for the whole field.

A field is given one by the form it is in, and a form is given one by whatever appearance layer dressed it. A single field is a Form with one field in it: that is a widget like any other, it goes wherever a widget goes, and it is the whole of the wiring.

The zero value draws in the terminal's own colours with no marks beside a choice, which is legible and plain, and is what a widget nobody dressed gets.

type Match added in v0.0.2

type Match struct {
	Row   int
	Spans []Span
}

Match is one occurrence of a query.

Spans has one entry per row the match covers, starting at Row, because a match can cross a break the width made: the query was written as one line and the window wrapped it, and a search that only looked at rows would not find it at all.

type Modal interface {
	Widget

	// Handle answers an event, reporting whether it was consumed. Mouse positions
	// are in the modal's own coordinates: the stack has already translated them,
	// and events outside the modal never arrive here.
	//
	// Consumed, not closed. A modal that wants to close itself is built with a
	// callback by whoever pushed it, the same way a completion is — the stack does
	// not have to guess what an unconsumed key meant.
	Handle(ev input.Event) bool

	// Place says where the modal goes in the space it floats over.
	Place(space image.Point) layout.Placement
}

Modal is a layer that floats over an interface and takes its input while it is on top.

It is a Widget that also says where it wants to go and answers events. Where it goes is a layout.Placement rather than a rectangle, so the same modal is placed correctly whatever it is floating over and does not have to be told the size of the screen.

type MultiSelect added in v0.0.2

type MultiSelect[T any] struct {

	// Label is what the field is asking for.
	Label string

	// Value is the caller-owned set, in option order. A caller change is observed at the
	// next semantic operation and by drawing. Unavailable choices are discarded,
	// duplicates are folded and option order is restored through one write; the field
	// then adopts the set the owner accepts. A value already in canonical form is not
	// rewritten. Nil keeps the set local.
	Value Accessor[[]T]
	// Same says whether two values are the same one — see [Select.Same]. It is a pure
	// projection callback and may run during drawing.
	Same func(a, b T) bool
	// Check says what is wrong with the choices, or nil.
	Check func(v []T) error
	// Rows caps how many options are shown at once. Zero shows them all.
	Rows int
	// Keys say which keystrokes move and take. Nil reads through
	// [DefaultMultiSelectKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

MultiSelect is a field holding any number of choices out of several.

The cursor and the choice are two things here, unlike in a Select: moving is not choosing, and something has to be pressed. That is the whole difference between picking one and picking some.

The zero value is empty and ready. A MultiSelect must not be copied after first use: its options, chosen set, cursor and matcher are one mutable field.

func (*MultiSelect[T]) Ask added in v0.0.2

func (m *MultiSelect[T]) Ask() string

Ask is the label and the choices, numbered, with a word about giving several.

func (*MultiSelect[T]) Do added in v0.0.2

func (m *MultiSelect[T]) Do(action keymap.Action) bool

Do runs one of the field's actions by name. See Doer.

func (*MultiSelect[T]) Draw added in v0.0.2

func (m *MultiSelect[T]) Draw(v Frame)

Draw paints the label, the options and whatever was wrong with the choices.

func (*MultiSelect) Error added in v0.0.2

func (f *MultiSelect) Error() error

Error is what checking the answer last found.

func (*MultiSelect[T]) Focus added in v0.0.2

func (m *MultiSelect[T]) Focus(has bool)

Focus takes the keyboard or gives it up, and checks the choices on the way out.

func (*MultiSelect[T]) Handle added in v0.0.2

func (m *MultiSelect[T]) Handle(ev input.Event) bool

Handle moves the cursor and takes choices.

func (*MultiSelect[T]) Limit added in v0.0.2

func (m *MultiSelect[T]) Limit() int

Limit reports how many choices may be taken at once. Zero allows every option.

func (*MultiSelect[T]) Measure added in v0.0.2

func (m *MultiSelect[T]) Measure(int) int

Measure is the label, the options within their cap, and the problem if there is one.

func (*MultiSelect[T]) Options added in v0.0.2

func (m *MultiSelect[T]) Options() []Option[T]

Options returns a copy of what is on offer.

func (*MultiSelect[T]) Prompt added in v0.0.2

func (m *MultiSelect[T]) Prompt() string

Prompt is what the field is asking for.

func (*MultiSelect[T]) Reply added in v0.0.2

func (m *MultiSelect[T]) Reply(said string) error

Reply takes numbers or labels, separated by commas. Nothing at all takes nothing, which is how a reader says they want none of them.

func (*MultiSelect[T]) SetLimit added in v0.8.0

func (m *MultiSelect[T]) SetLimit(limit int)

SetLimit changes how many choices may be taken at once. Zero allows every option; a negative limit is a programmer error and panics, because zero already means "no limit" and there is no smaller quantity of choices for a negative one to name. Lowering the limit keeps the earliest choices in option order and writes the settled set back to a bound value. A nil receiver ignores the change.

func (*MultiSelect[T]) SetOptions added in v0.2.0

func (m *MultiSelect[T]) SetOptions(options []Option[T])

SetOptions replaces what is on offer. MultiSelect owns the slice and preserves each taken choice that remains available under Same, or under its label when Same is nil, wherever it moved.

func (*MultiSelect[T]) Taken added in v0.0.2

func (m *MultiSelect[T]) Taken() []T

Taken is what has been chosen, in the order the options are listed.

func (*MultiSelect[T]) Toggle added in v0.0.2

func (m *MultiSelect[T]) Toggle() bool

Toggle takes the option under the cursor, or gives it back, and reports whether anything changed. Nothing changes when the limit is reached.

func (*MultiSelect[T]) Validate added in v0.0.2

func (m *MultiSelect[T]) Validate() error

Validate checks the choices.

type Node added in v0.0.2

type Node[T any] struct {
	Item     T
	Children []Node[T]
}

Node is one item of a Tree and whatever is under it.

type Option added in v0.0.2

type Option[T any] struct {
	// Label is the row as shown.
	Label string
	// Value is what choosing it means.
	Value T
}

Option is one thing a choice offers.

func Options added in v0.0.2

func Options[T ~string](values ...T) []Option[T]

Options is the usual case, where what is shown is what it means.

type Pinned added in v0.0.2

type Pinned struct {
	// Block is the identity of the block being pinned.
	Block BlockID
	// Height is how many of its rows to draw, which is fewer than it has when it is
	// collapsed or being pushed off.
	Height int
	// ClipTop is how many of its rows to leave off the top, which is what being
	// pushed off looks like.
	ClipTop int
	// Rows is the whole footprint at the top of the view: the visible header and the
	// gap under it. It is what a caller subtracts before drawing the content.
	Rows int
	// Fade is one for a header sitting still and falls towards zero as the next one
	// pushes it off, for blending it towards the background.
	Fade float64
}

Pinned is the header for one frame.

func (Pinned) Visible added in v0.0.2

func (p Pinned) Visible() int

Visible is how many of the header's rows are drawn.

type Point added in v0.0.2

type Point struct{ Row, Col int }

Point addresses a cell of a transcript: a row in its coordinate space, and a column across.

The row is absolute rather than a position on screen, which is the whole reason the transcript numbers its rows. A selection made and then scrolled past is still over the same words; one held in screen coordinates would slide up the text as the view moved, which is not what anybody dragged.

func (Point) Before added in v0.0.2

func (p Point) Before(q Point) bool

Before reports whether p comes earlier in the text than q.

type Pointer

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

Pointer tracks the mouse across the frames of one interface.

A mouse event says where the pointer is; it does not say what is there. Working that out is layout's business, and layout only exists while a frame is being drawn — so a widget learns it was clicked by claiming the region it drew itself into, and asking.

Why a press is remembered

A button that fired on the way down fires when the user was aiming at it and changed their mind. Every interface people already use commits on release, over the same target that took the press, which means something has to remember which target that was between two events. That is what this type is for, and it is why hover and press cannot be answered by a widget looking at one event on its own.

It belongs to the goroutine that draws and holds no lock.

func (*Pointer) Claim

func (p *Pointer) Claim(region image.Rectangle)

Claim records that a region owns the press being held, if one is unclaimed and landed inside it. It is called while drawing, by the widget that drew the region.

func (*Pointer) Clicked

func (p *Pointer) Clicked(region image.Rectangle, button input.Button) bool

Clicked reports that a press taken by a region has been released over it, and takes the click so nothing else can answer the same one.

Taken, rather than reported repeatedly: a click is an event, and a widget asking twice in one frame — or two widgets asking in turn — must not both act on it.

func (*Pointer) Handle

func (p *Pointer) Handle(ev input.Event) bool

Handle takes a mouse event, reporting whether it was one.

Everything else is left alone: a pointer that consumed keys would be a pointer that swallowed typing.

func (*Pointer) Left

func (p *Pointer) Left()

Left reports that the pointer is no longer over the interface, so nothing is hovered. A terminal does not report the mouse leaving, but a window losing focus is as close as it gets and is worth honouring: a hover left highlighted under an unfocused window looks like the interface is still live.

func (*Pointer) Over

func (p *Pointer) Over(region image.Rectangle) bool

Over reports whether the pointer is inside a region, in the coordinates of whatever drew it. A widget passes the box it is drawing into.

func (*Pointer) Position

func (p *Pointer) Position() (image.Point, bool)

Position is where the pointer is, and whether it is anywhere.

func (*Pointer) Pressing

func (p *Pointer) Pressing(region image.Rectangle) bool

Pressing reports whether a press is being held over a region, which is what draws a control as pushed in.

It follows the press rather than the pointer: dragging off a button and back again keeps it pushed, because the press was never released.

type PointerRegion added in v0.2.0

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

PointerRegion is where one interactive child was drawn, and who owns a gesture that began there.

Why this is a type rather than a rule everyone keeps

Anything that draws a child inside itself creates the same boundary: it has to move pointer coordinates into the child's box, and it has to decide what happens when the gesture leaves. Both answers are easy to get subtly wrong, and getting them wrong looks like a bug in the child. A wrapper that re-tests its own bounds on every event drops the drag at its frame and never delivers the release, so the child goes on believing it is being dragged. A wrapper that remembers the geometry instead of the child keeps translating by a rectangle the child has since moved out of.

So the rule lives here once, in the ring that owns behaviour, and an appearance layer composes it rather than reimplementing it. Container and Stack keep the same rule for the several children and the several layers they own; this is the same rule for exactly one.

The rule

A press over the region gives its child the gesture. The drag and release that follow belong to that child wherever the pointer then goes — and are translated by where **this** frame drew it, not where it was when the press landed, because the user is aiming at what is on the screen now. A child that is no longer presented has nowhere to send its gesture, so the remainder is dropped rather than handed to whatever took its place.

The zero value has no child and declines everything. It must be staged during Root.Draw and read only from Handle. A PointerRegion must not be copied after first use: its committed child and captured gesture are one routing owner.

func (*PointerRegion) Handle added in v0.2.0

func (r *PointerRegion) Handle(event input.Mouse) (handled, delivered bool)

Handle offers a pointer event to the child, in the child's own coordinates.

Handled reports whether the child consumed it. Delivered reports whether it reached the child at all, which is a different question and the one a wrapper with a second pointer target asks: a strip of tabs above a pane needs to know that an event was outside the pane, not merely that the pane declined it.

func (*PointerRegion) Stage added in v0.2.0

func (r *PointerRegion) Stage(frame Frame, area image.Rectangle, child Widget)

Stage publishes where child was drawn, to take effect with the complete root frame.

A child that does not answer input is staged as an absence: the region is still not somewhere a press can land, and saying so here keeps the caller from having to.

type Recalled added in v0.0.2

type Recalled struct {
	// Entry is the line.
	Entry string
	// Step is how far back it is, one being the newest, so a caller can jump to it
	// with [History.At].
	Step int
	// At is the byte offsets of the query's characters within the entry, for
	// underlining them.
	At []int
}

Recalled is one entry a search of the history turned up.

type Result added in v0.0.2

type Result struct {
	// Query is what was searched for, so a caller can tell an answer to the question
	// it is still asking from an answer to one it has moved on from.
	Query string
	// Matches is the complete geometry in document order. It is deliberately not a
	// viewport-sized window: a result must support stepping to any occurrence and
	// highlighting a different viewport after scrolling without retaining or
	// rescanning the transcript snapshot that produced it. Its retained cost is
	// therefore proportional to occurrences, while the source rows are released when
	// the scan finishes.
	Matches []Match
	// Err is set when the query was a pattern that would not compile. It is a
	// result rather than a refusal at submission time, because a user typing a
	// pattern spends most of the typing with an unfinished one and should not be
	// interrupted about it.
	Err error
}

Result is a finished scan.

type Role added in v0.1.0

type Role uint8

Role identifies what a semantic node means rather than how many cells draw it.

Roles are typed constants instead of strings so inspection, automation and future host integrations can distinguish controls without agreeing on an attribute vocabulary at runtime.

const (
	// RoleNone is a meaningful node whose specialized role is not known.
	RoleNone Role = iota
	// RoleButton is an activatable control.
	RoleButton
	// RoleDialog is modal content with an open lifecycle.
	RoleDialog
	// RoleTabList is a controller grouping tabs and their selected panel.
	RoleTabList
	// RoleTab is one selectable label in a tab list.
	RoleTab
	// RoleTabPanel is the content selected by a tab.
	RoleTabPanel
	// RoleSlider is a bounded numeric control.
	RoleSlider
)

type Root added in v0.1.0

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

Root is the composition boundary between a headless widget tree and a program.

Draw stages every nested Snapshot and publishes them together only after the whole tree returns. Handle therefore routes against the last complete logical frame, never a mixture of children from a frame still being built. Root depends only on grid and input; program sees it structurally as its consumer-defined Component.

The zero Root draws nothing and declines input. A Root must not be copied after first use: its transaction, committed tree and held gesture are one frame owner.

func NewRoot added in v0.1.0

func NewRoot(of Widget) *Root

NewRoot wraps a live widget tree in its presentation transaction.

func (*Root) Content added in v0.13.0

func (r *Root) Content() Widget

Content returns the widget tree used to build the next frame. Input continues to target the last completely drawn tree until another Draw commits the replacement.

func (*Root) Draw added in v0.1.0

func (r *Root) Draw(view grid.View)

Draw builds and atomically commits one logical component frame.

It is not reentrant, and a widget that reaches it again while it is running — by drawing a nested Root, or by drawing from a callback this frame invoked — is a programmer error and panics. The commit is what makes routing geometry appear all at once; an inner Draw would publish a tree the outer one had not finished building, so Root.Handle would route the next event against a mixture of two frames.

func (*Root) Handle added in v0.1.0

func (r *Root) Handle(event input.Event) bool

Handle offers input to the last completely drawn tree. A root replaced before its next frame remains the input target the user can see. A root that accepted a pointer press also receives that gesture's drag and release even if it is replaced meanwhile.

func (*Root) SetContent added in v0.13.0

func (r *Root) SetContent(of Widget)

SetContent replaces the widget tree used to build the next frame. An accepted pointer gesture remains owned by its original target through release; replacing a root cannot hand half a gesture to a different tree.

type RowGutter added in v0.3.0

type RowGutter interface {
	Width(lines int) int
	Draw(view grid.View, rows []text.Row)
}

RowGutter draws decoration beside visual text rows.

Width is asked with the number of logical lines before the content wraps them. Draw receives only the rows visible in the current frame; text.Row.Line says which logical line each came from and Joined distinguishes its continuations. The row text is provided so a gutter can derive diagnostics from it, but the content owner still owns the text and input geometry.

This is an appearance seam rather than an appearance decision. A line-number, breakpoint or diagnostic gutter can live in a higher package while the text component stays independent of all of them.

type RowSpan added in v0.0.2

type RowSpan struct {
	Row        int
	Col, Width int
}

RowSpan is a run of columns on one visual row of a field.

The row is counted from the top of the whole wrapped text and not from the top of the box, because the field scrolls: a caller that wanted rows on screen would have to be told the scroll position to make sense of them, and the field already knows it.

type Scroll

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

Scroll shows a window onto something taller than the space available.

It holds two things: how many rows are hidden above the window, and whether the window is following the end of the content.

Both are needed, and neither on its own will do. Holding only the offset means a live log stops showing what arrives. Holding only a distance from the end means a reader who scrolled up gets dragged forward every time something is appended: twenty rows from the end becomes thirty rows from the end, and the text under their eyes moves even though they did not ask it to.

The zero value shows the start and does not follow, which is what a list of items wants. A transcript asks to follow, once, with Scroll.ToBottom.

A Scroll must not be copied after first use: its committed and staged positions, wheel remainder and key sequence are one mutable owner.

func (*Scroll) AtBottom

func (s *Scroll) AtBottom() bool

AtBottom reports whether the window is following the end of the content.

func (*Scroll) By

func (s *Scroll) By(rows int)

By scrolls a number of rows: negative towards the start, positive towards the end.

Reaching the end starts following again, which is what every log viewer does and what a reader means by scrolling to the bottom.

func (*Scroll) Discard added in v0.1.0

func (s *Scroll) Discard(rows int)

Discard removes rows from the start of the content while preserving the row under the reader when it still exists.

A streaming transcript uses this after publishing a prefix. A following window stays at the new end; a reader above the new start lands on the first retained row.

func (*Scroll) Do added in v0.0.2

func (s *Scroll) Do(action keymap.Action) bool

Do runs one of the scroll's actions by name, reporting whether it was one a scroll knows. See Doer.

func (*Scroll) Handle

func (s *Scroll) Handle(ev input.Event, keys *keymap.Map) bool

Handle scrolls in response to keys and the mouse wheel, reporting whether it consumed the event.

The map is a parameter rather than a field because a scroll is a part rather than a widget: it is what a transcript, a list and a viewport each keep inside themselves, and each of them already has a map of its own to read through. Nil reads through DefaultScrollKeys.

func (*Scroll) Offset

func (s *Scroll) Offset() int

Offset is how many rows are hidden above the window, which is what a scrollbar and a hit test both want.

func (*Scroll) Pages

func (s *Scroll) Pages(n int)

Pages scrolls whole windows, keeping one row of overlap so the reader has something to recognise on the other side of the jump.

func (*Scroll) Reveal added in v0.0.2

func (s *Scroll) Reveal(first, last int)

Reveal scrolls as little as it can to bring as much of [first, last] into the window as will fit.

As little as it can, rather than centring it: a reader stepping through search results wants the surrounding text to stay put where it already fits, and a view that jumped every time would lose the context that made the result worth finding. A range already visible moves nothing at all. Passing the same row twice reveals one row; there is deliberately no second spelling for that degenerate range.

It stops following the end, because a range was asked for and following would immediately scroll away from it. When the range is taller than the window its start wins, because that is where reading begins. Anything else would show the end of a match and leave the reader to scroll backwards to find out what it was part of.

func (*Scroll) Stage added in v0.1.0

func (s *Scroll) Stage(frame Frame, total, window int) ScrollLayout

Stage derives the scroll layout for a component frame.

The returned layout is the one drawing should use. Its bounds and offset become current only when the complete Root frame commits, so input during a nested draw continues to see the previous frame. ScrollLayout.Reveal updates this staged layout rather than the committed scroll. One Scroll may be staged once per frame; use the returned ScrollLayout to refine that one pending value.

func (*Scroll) ToBottom

func (s *Scroll) ToBottom()

ToBottom follows the end of the content.

func (*Scroll) ToTop

func (s *Scroll) ToTop()

ToTop shows the start of the content and stops following.

func (*Scroll) Wheel added in v0.0.2

func (s *Scroll) Wheel(w input.Wheel)

Wheel says what the terminal's wheel reports are worth, which is not a constant: terminals disagree about how many of them one notch is. Pass what input.WheelFor answered, once.

Left alone, the common arrangement is assumed — which is right on most terminals and wrong by at most a factor of three on the rest. That is still better than the fixed number of rows per report this used to scroll, which was wrong by that factor on half of them.

type ScrollLayout added in v0.1.0

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

ScrollLayout is the derived position of one Scroll in a component frame.

It is a short-lived value returned by Scroll.Stage. Offset is used to paint the frame; adjustments are staged with the same root transaction.

A ScrollLayout must not be copied after construction. Two copies would carry two refinements of one pending Scroll, and whichever wrote last would silently erase the other's geometry.

func (*ScrollLayout) Offset added in v0.1.0

func (l *ScrollLayout) Offset() int

Offset is the first row shown by this layout.

func (*ScrollLayout) Resize added in v0.11.0

func (l *ScrollLayout) Resize(window int)

Resize changes how many rows this frame-local layout can show while retaining its total and any Reveal adjustment already made. A transcript uses it after discovering that a sticky header consumes part of the provisional window; creating a second layout for the same Scroll would make sibling and call order decide which one wins.

func (*ScrollLayout) Reveal added in v0.1.0

func (l *ScrollLayout) Reveal(first, last int)

Reveal brings as much of [first, last] into this staged window as fits. Passing the same row twice reveals one row, matching Scroll.Reveal.

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

Search scans a transcript's text off the interface's goroutine.

A search box searches on every keystroke, and the answer to a query three letters old is worth nothing. So the newest query wins: a waiting scan and an unread result are replaced, and a running scan is discarded at its next cancellation boundary. The standard regexp matcher itself is not interruptible, so one in-progress match pass completes before the newer scan starts. Search keeps that one matcher rather than maintaining a subtly different regular-expression implementation of its own.

Strings, and nothing else. The transcript belongs to the goroutine that draws, and this never touches it: Search.Submit takes the live rows on the caller's goroutine, where reading them is safe, and hands over what it took. The rows are already strings, so taking them copies headers rather than text. A superseded job releases that snapshot instead of turning search into a second owner of committed history.

Results arrive on Search.Results. A caller reads that from a goroutine of its own and posts what it gets back to the event owner. This package does not prescribe the dispatcher used to cross that boundary. Each submission owns its row snapshot until it finishes or is superseded; Search does not retain an older transcript generation after the work that needs it is gone. A Search must not be copied after construction; its worker, mailboxes and cancellation state are one owner.

func NewSearch added in v0.0.2

func NewSearch() *Search

NewSearch starts a scanner. Close it when the interface it serves is done.

func (*Search) Close added in v0.0.2

func (s *Search) Close()

Close stops the scanner and waits for its worker to release the current corpus. It is safe to call more than once. When it returns, Results is closed and Search retains no submitted rows or unread result.

func (*Search) Results added in v0.0.2

func (s *Search) Results() <-chan Result

Results is where finished scans arrive. It closes after Search.Close stops the scanner, so a consumer may range over it without another lifetime signal. The nil and zero Search return an already-closed stream.

func (*Search) Submit added in v0.0.2

func (s *Search) Submit(t *Transcript, query string, regex bool)

Submit schedules a scan of t for query, replacing any scan not yet finished.

An empty query cancels an older scan and delivers nothing: it is what a search box looks like after its text was cleared, and answering it with every row in the session — or with the previous query — is not what was asked.

type Select added in v0.0.2

type Select[T any] struct {

	// Label is what the field is asking for.
	Label string

	// Value is the caller-owned choice. A caller change moves the cursor at the next
	// semantic operation and is projected by drawing; cursor movement writes it
	// immediately and adopts the value the owner accepts. A value that names no option
	// falls back to the current choice and is written when the field is validated. Nil
	// keeps the choice local.
	Value Accessor[T]
	// Same says whether two values are the same one, which is what puts the cursor on
	// the choice already made. Nil matches a bound string or Stringer value against
	// the label, and matches old and new options by label when they are replaced. A
	// different value type whose initial bound value must identify an option supplies
	// Same; Go cannot safely compare an arbitrary T on the field's behalf. Same may run
	// during drawing and must not mutate either value or unrelated state.
	Same func(a, b T) bool
	// Check says what is wrong with the choice, or nil.
	Check func(v T) error
	// Rows caps how many options are shown at once. Zero shows them all.
	Rows int
	// Keys say which keystrokes move the cursor. Nil reads through [DefaultListKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Select is a field holding one choice out of several.

The choice follows the cursor: what is under it is what is chosen, and there is nothing to press to confirm. A list that made somebody move to a row and then take it is a list that can be left on a row nobody took.

The zero value is empty and ready. A Select must not be copied after first use: its options, cursor, scroll and controlled-state settlement are one mutable field.

func (*Select[T]) Ask added in v0.0.2

func (s *Select[T]) Ask() string

Ask is the label and the choices, numbered.

func (*Select[T]) Chosen added in v0.0.2

func (s *Select[T]) Chosen() (Option[T], bool)

Chosen is the option under the cursor, and whether there is one.

func (*Select[T]) Do added in v0.0.2

func (s *Select[T]) Do(action keymap.Action) bool

Do runs one of the field's actions by name. See Doer.

func (*Select[T]) Draw added in v0.0.2

func (s *Select[T]) Draw(v Frame)

Draw paints the label, the options and whatever was wrong with the choice.

func (*Select) Error added in v0.0.2

func (f *Select) Error() error

Error is what checking the answer last found.

func (*Select[T]) Focus added in v0.0.2

func (s *Select[T]) Focus(has bool)

Focus takes the keyboard or gives it up, and checks the choice on the way out.

func (*Select[T]) Handle added in v0.0.2

func (s *Select[T]) Handle(ev input.Event) bool

Handle moves the cursor, and takes the choice with it.

func (*Select[T]) Measure added in v0.0.2

func (s *Select[T]) Measure(int) int

Measure is the label, the options within their cap, and the problem if there is one.

func (*Select[T]) Options added in v0.0.2

func (s *Select[T]) Options() []Option[T]

Options returns a copy of what is on offer.

func (*Select[T]) Prompt added in v0.0.2

func (s *Select[T]) Prompt() string

Prompt is what the field is asking for.

func (*Select[T]) Reply added in v0.0.2

func (s *Select[T]) Reply(said string) error

Reply takes a number or one of the labels.

func (*Select[T]) SetOptions added in v0.2.0

func (s *Select[T]) SetOptions(options []Option[T])

SetOptions replaces what is on offer. Select owns the slice. If the selected choice still exists under Same, or has the same label when Same is nil, the cursor follows it to its new position; otherwise the cursor is clamped and the bound value follows the resulting choice.

func (*Select[T]) Validate added in v0.0.2

func (s *Select[T]) Validate() error

Validate checks the choice.

type Selection added in v0.0.2

type Selection struct {
	// Clicks counts the run a press belongs to, which is what tells a double-click
	// from two clicks. It lives here because a selection is the only thing that asks:
	// a second press takes the word and a third takes the line, and both are
	// questions about this selection rather than about the pointer in general.
	//
	// It is a field rather than something the caller keeps and passes in, because a
	// widget that had to be handed the state of its own gesture is a widget its
	// caller has to understand to use.
	Clicks Clicks
	// contains filtered or unexported fields
}

Selection is a range of a transcript the user has dragged over.

The two ends are an anchor, where the drag began, and an extent, where it is now. They are kept in that order rather than sorted, because a drag upwards is a real thing and the anchor has to stay where the user put it: sorting on the way in would make the selection turn inside out as the pointer crossed its own start.

The zero value selects nothing.

func (*Selection) Active added in v0.0.2

func (s *Selection) Active() bool

Active reports whether anything is selected.

func (*Selection) Begin added in v0.0.2

func (s *Selection) Begin(p Point)

Begin starts a selection at p and marks a drag in progress.

func (*Selection) Clear added in v0.0.2

func (s *Selection) Clear()

Clear removes the selection.

func (*Selection) Covers added in v0.0.2

func (s *Selection) Covers(row, col int) bool

Covers reports whether a cell is inside the selection, which is what painting the highlight asks once per cell.

func (*Selection) DiscardBefore added in v0.1.0

func (s *Selection) DiscardBefore(row int)

DiscardBefore removes the part of a selection whose rows no longer belong to the transcript. A selection wholly before row is cleared; one crossing row begins at the first cell that remains.

func (*Selection) Done added in v0.0.2

func (s *Selection) Done()

Done ends the drag, leaving the selection where it is.

func (*Selection) Dragging added in v0.0.2

func (s *Selection) Dragging() bool

Dragging reports whether the pointer is still down and the far end still moving.

func (*Selection) Extend added in v0.0.2

func (s *Selection) Extend(p Point)

Extend moves the far end to p. It does nothing unless a drag is in progress, so a pointer moving over the text without a button held changes nothing.

func (*Selection) Range added in v0.0.2

func (s *Selection) Range() (start, end Point)

Range is the selection in reading order: the earlier end first.

Both ends are inclusive, which is what a drag means — a pointer dragged over a character has selected that character, and an exclusive end would leave the one under the pointer out.

func (*Selection) SelectLine added in v0.0.2

func (s *Selection) SelectLine(t *Transcript, p Point) bool

SelectLine selects a whole row, which is what a triple-click means.

The row, not the logical line the width broke it out of. What a triple-click selects is what the reader sees as a line, and asking for the line behind it would take text they can neither see nor point at.

func (*Selection) SelectWord added in v0.0.2

func (s *Selection) SelectWord(t *Transcript, p Point) bool

SelectWord selects the word at a point, which is what a double-click means.

Where a word begins and ends is text.WordAt's to say — including the part that matters for text written without spaces, where the run of one script is the word. It reports false when there is no word there, so a double-click in the margin selects nothing rather than selecting the gap.

func (*Selection) Text added in v0.0.2

func (s *Selection) Text(t *Transcript) string

Text is what the selection would put on the clipboard.

Rows are joined the way the text was written rather than the way it was laid out. A row the width made is rejoined to the one above it with whatever the wrap consumed at that break, and a row the text made begins a new line — so a paragraph copied out of a narrow window pastes as a paragraph, not as a column of fragments hard-wrapped at whatever the window happened to be. See text.Row.

Columns are sliced on cluster boundaries. A wide character is taken only when it lies wholly inside the selection: half of one cannot be put on a clipboard, and including a character the user only touched the edge of is the error that is noticed, because it is the one that appears at the ends of every copy.

type Semantic added in v0.1.0

type Semantic interface {
	Semantics() SemanticNode
}

Semantic is implemented by controls that expose a structural semantic projection.

type SemanticNode added in v0.1.0

type SemanticNode struct {
	Role        Role
	Label       string
	Description string
	// Value is the control's current human-readable value, when it has one.
	Value    string
	State    SemanticState
	Children []SemanticNode
}

SemanticNode is one meaningful control or part of a control.

The tree is structural, not visual: decorative borders do not appear, one control may be painted by several boxes, and a child need not occupy a child rectangle. Callers that retain the result own it; components may rebuild slices on each call.

type SemanticState added in v0.1.0

type SemanticState uint8

SemanticState is a set of independent facts about a semantic node.

const (
	// StateFocused says the node or its active part owns the keyboard.
	StateFocused SemanticState = 1 << iota
	// StateSelected says the node is the selected choice among peers.
	StateSelected
	// StateOpen says the node has exposed content which can be dismissed.
	StateOpen
)

func (SemanticState) Has added in v0.1.0

func (state SemanticState) Has(flags SemanticState) bool

Has reports whether state contains every bit in flags.

type Settings added in v0.3.0

type Settings[T any] struct {
	List[T]
	// Change applies an action to the selected item. It reports whether the item
	// accepted it. Nil makes the list read-only.
	Change func(index int, item T, action keymap.Action) bool
	// EditKeys binds value actions. Nil reads through [DefaultSettingsKeys]. List.Keys
	// independently controls navigation, so rebinding a value never replaces how the
	// list is browsed.
	EditKeys *keymap.Map
	// contains filtered or unexported fields
}

Settings is a browsable list whose selected item answers value actions.

The embedded List owns selection, scrolling, navigation and row drawing. Change owns the one behaviour a plain list does not have: left, right or activation changes the selected value. What an item is and how a change is stored remain the caller's domain; this controller only routes an action to it.

The zero value is an empty read-only list. A Settings value must not be copied after first use: its list navigation and value-action matcher are one mutable owner.

func (*Settings[T]) Do added in v0.3.0

func (s *Settings[T]) Do(action keymap.Action) bool

Do navigates the list or applies a value action to its selected item.

func (*Settings[T]) Focus added in v0.5.0

func (s *Settings[T]) Focus(has bool)

Focus takes or releases the keyboard with the embedded list. Releasing it also cancels a partial value binding owned by the settings controller.

func (*Settings[T]) Handle added in v0.3.0

func (s *Settings[T]) Handle(event input.Event) bool

Handle first lets list navigation and pointer selection act, then offers a key to the selected value.

type Shown added in v0.0.2

type Shown[T any] struct {
	Item T
	// Depth is how many items are above this one, from zero at the top.
	Depth int
	// Branch says there is something under this item, and Open says it is showing.
	// A branch that is closed is what the reader is being invited to open, and a
	// branch with nothing under it is not a branch.
	Branch, Open bool
	// contains filtered or unexported fields
}

Shown is one row a Tree is showing: an item, how deep it sits, and what can be done with it.

A tree is a list of these — which is not a simplification but the whole design. Everything a tree does that is not opening and closing is what a list does, and building it on one is what keeps the selection, the scrolling, the wheel and the click from being written a second time and getting the edges wrong.

type Sized

type Sized interface {
	Widget
	layout.Measurer
}

Sized is a widget whose size along the axis being divided follows from the room it has across the other: wrapped text, a list of variable-height rows, anything that reflows.

It is layout.Measurer and nothing more, so a sized widget goes straight into a layout.Slot:

rects := (layout.Flow{Axis: layout.Down}).Rects(v.Bounds().Size(), []layout.Slot{
	{Size: layout.Measured(0, 3), Of: header},
	{Size: layout.Flex(1)},
})

Measure is asked before Draw and must agree with it. A widget that reports one size and draws another gets clipped or leaves a gap, and both look like a defect elsewhere in the layout tree.

type Slider added in v0.3.0

type Slider struct {

	// Keys maps slider actions. Nil reads through [DefaultSliderKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Slider owns one integer constrained to a range and the interactions that change it.

It is behavior rather than appearance. An appearance calls Slider.Stage with the track it drew, reads Slider.Position, and chooses its own glyphs and styles. Keys, pointer dragging, controlled state, bounds, and semantics remain one state machine regardless of how the track looks.

Construct a slider with NewSlider. Its zero value is the inert range [0, 0] with a step of one. A Slider must not be copied after first use: its controlled value, gesture, matcher and committed track are one mutable owner.

func NewSlider added in v0.3.0

func NewSlider(config SliderConfig) *Slider

NewSlider constructs one slider from config.

With Value set, later owner-written values are applied with Slider.Sync, matching the explicit controlled-state rule used by dialogs and tabs.

func (*Slider) Bounds added in v0.3.0

func (s *Slider) Bounds() (minimum, maximum int)

Bounds returns the inclusive minimum and maximum.

func (*Slider) Do added in v0.3.0

func (s *Slider) Do(action keymap.Action) bool

Do applies a slider action by name.

func (*Slider) Focus added in v0.3.0

func (s *Slider) Focus(has bool)

Focus takes or gives up keyboard ownership.

func (*Slider) Focused added in v0.3.0

func (s *Slider) Focused() bool

Focused reports whether the slider owns the keyboard.

func (*Slider) Handle added in v0.3.0

func (s *Slider) Handle(event input.Event) bool

Handle applies bound keys and a left-button drag to the value.

func (*Slider) Label added in v0.3.0

func (s *Slider) Label() string

Label returns the control's semantic label.

func (*Slider) Move added in v0.3.0

func (s *Slider) Move(steps int) bool

Move changes the value by a number of steps, saturating at either bound.

func (*Slider) Position added in v0.3.0

func (s *Slider) Position(cells int) int

Position maps the current value onto cells positions from zero through cells-1.

func (*Slider) Semantics added in v0.3.0

func (s *Slider) Semantics() SemanticNode

Semantics describes the slider independently of its track appearance.

func (*Slider) Set added in v0.3.0

func (s *Slider) Set(value int) bool

Set changes the value, clamped to the slider's bounds, and reports whether the stored value changed.

func (*Slider) SetBounds added in v0.3.0

func (s *Slider) SetBounds(minimum, maximum int)

SetBounds changes the inclusive range and clamps the current value.

A reversed range or one whose span cannot be represented by int is a programmer error and panics. The latter cannot be mapped onto a finite terminal extent without giving the same position two incompatible integer meanings.

func (*Slider) SetLabel added in v0.3.0

func (s *Slider) SetLabel(label string)

SetLabel changes the control's semantic label.

func (*Slider) SetStep added in v0.3.0

func (s *Slider) SetStep(step int)

SetStep changes the keyboard increment. A non-positive step is a programmer error and panics: zero would make an arrow key do nothing and a negative one would make it move the value the way the other arrow does, neither of which a slider can report to the user as anything but being broken.

func (*Slider) Stage added in v0.3.0

func (s *Slider) Stage(frame Frame, track image.Rectangle)

Stage publishes the track rectangle an appearance drew, in the slider widget's local coordinates. Pointer events are routed against this rectangle only after the complete root frame commits.

func (*Slider) Step added in v0.3.0

func (s *Slider) Step() int

Step returns how much one increase or decrease changes the value. The zero Slider uses one.

func (*Slider) Sync added in v0.3.0

func (s *Slider) Sync() bool

Sync clamps a caller-written controlled value and reports whether it wrote the normalized value. It is harmless for an uncontrolled slider.

func (*Slider) Value added in v0.3.0

func (s *Slider) Value() int

Value returns the current value, clamped to the slider's bounds.

type SliderConfig added in v0.11.0

type SliderConfig struct {
	// Value is optional caller-owned state. Nil starts local state at Minimum.
	Value Accessor[int]
	// Minimum and Maximum are inclusive and may be equal.
	Minimum, Maximum int
	// Step is the keyboard increment. Zero means one.
	Step int
	// Label names the value in semantics and any appearance.
	Label string
	// Keys maps slider actions. Nil uses [DefaultSliderKeys].
	Keys *keymap.Map
}

SliderConfig is the complete construction state of Slider.

A nil Value gives the controller local ownership starting at Minimum. An accessor gives ownership to the caller and is clamped during construction. Step zero means one, matching the useful zero Slider.

type Snapshot added in v0.1.0

type Snapshot[T any] struct {
	// contains filtered or unexported fields
}

Snapshot holds derived presentation state committed with a complete Root frame.

Value is the geometry or other recomputable presentation fact used by Handle. Stage makes a replacement visible only when the complete root Draw returns. A panic or another aborted draw releases the pending value and leaves Value unchanged. Snapshot is not application state: using it for semantic values would make Draw advance meaning and violate the ownership model.

Snapshot commits T by ordinary Go assignment; it does not clone or synchronize data reachable through pointers, slices, maps or interfaces inside T. Presentation data behind such references must therefore be independently owned or treated as immutable by its producers and consumers. A reference deliberately used as a live identity or behavior, such as a Widget, keeps that reference's normal semantics.

One Snapshot may be staged once in a root frame. Sharing it between siblings is an ownership error and panics instead of making the sibling drawn last win. Refine a staged rich-model value through the value returned by its Stage operation rather than staging the same owner again.

The zero value contains the zero T and is ready to stage. A Snapshot must not be copied after first use: its pending value is enlisted with exactly one transaction.

func (*Snapshot[T]) Stage added in v0.1.0

func (s *Snapshot[T]) Stage(frame Frame, value T)

Stage prepares value for publication with frame's complete root draw. It must be called at most once for this Snapshot in one frame. See Snapshot for the ownership rule when value contains references.

func (*Snapshot[T]) Value added in v0.1.0

func (s *Snapshot[T]) Value() T

Value returns the last completely drawn value by ordinary Go assignment. See Snapshot for the ownership rule when T contains references.

type Span added in v0.0.2

type Span struct{ Col, Width int }

Span is a run of columns on one row.

type Stack

type Stack struct {

	// Keys say which keystrokes produce which of the actions a stack answers to, which
	// is the one that closes the top layer. Nil reads through [DefaultStackKeys].
	//
	// A map rather than one field for the one key, because the same layer appears in
	// interfaces where escape means "back" and interfaces where it means "close", and
	// which it is here is the program's to say. A layer that must be answered rather
	// than dismissed implements [Insistent].
	Keys *keymap.Map
	// KeepOnClickOutside stops a press outside the top layer from popping it.
	// Off by default, because a click on what a modal is covering means the user
	// is finished with the modal, and every interface they already use agrees.
	KeepOnClickOutside bool
	// contains filtered or unexported fields
}

Stack is an interface with layers floating over it, and the answer to which of them the keyboard belongs to.

The top layer owns keyboard input and pointer input inside its area; with nothing on it, the interface underneath has its input back. Wheel and move reports outside a layer follow the visible stack downward, while a press outside is consumed as the dismissal gesture. A layer that accepts a press captures its drag and release. That is the whole focus and pointer model between layers. Within a layer, or within the interface underneath, a Container is what decides.

The zero Stack is empty and ready. A Stack must not be copied after first use: its layer identities, focus, pointer capture and committed geometry are one mutable owner.

func NewStack added in v0.3.0

func NewStack(base Widget) *Stack

NewStack constructs a stack over base. The zero Stack has no base and is ready.

func (*Stack) Area

func (s *Stack) Area() (image.Rectangle, bool)

Area is where the top layer was last drawn, and whether there is one.

func (*Stack) Base added in v0.0.2

func (s *Stack) Base() Widget

Base returns the interface the layers float over.

func (*Stack) Clear

func (s *Stack) Clear()

Clear pops every layer, from the top down, so each is told in the order it would have been dismissed.

func (*Stack) Contains added in v0.1.0

func (s *Stack) Contains(id LayerID) bool

Contains reports whether id is currently in the stack.

func (*Stack) Depth

func (s *Stack) Depth() int

Depth is how many layers there are.

func (*Stack) Do added in v0.0.2

func (s *Stack) Do(action keymap.Action) bool

Do runs one of the stack's actions by name, reporting whether it was one a stack knows. See Doer.

func (*Stack) Draw

func (s *Stack) Draw(v Frame)

Draw paints the interface and then the layers from the bottom up, each into the space it asked for, and records where they went.

func (*Stack) Focus added in v0.0.2

func (s *Stack) Focus(has bool)

Focus takes the keyboard for the whole stack, or gives it up, and passes the news to whichever of the layers or the interface underneath currently holds it. A stack is a widget, so one can sit inside a Container like anything else.

func (*Stack) Handle

func (s *Stack) Handle(ev input.Event) bool

Handle gives the event to the top layer, and reports whether the stack dealt with it.

An empty stack consumes nothing, so an interface can offer it every event and carry on when it is not interested. A stack with anything in it consumes every key, because a key reaching what a modal is covering is a keystroke going somewhere the user cannot see.

func (*Stack) Pop

func (s *Stack) Pop() bool

Pop removes the top layer and reports whether there was one. The keyboard goes back to whatever was underneath.

func (*Stack) Push

func (s *Stack) Push(m Modal) LayerID

Push puts a layer on top, gives it the keyboard, and returns its stable handle. A nil modal is not a layer: it is ignored and reported as the zero LayerID.

Handles are drawn from a counter that never reuses one, and Push panics once every handle has been issued. Wrapping would let a handle held by a caller name a different layer than the one it was given for, so a dismissal aimed at a closed dialog would close whatever had since taken its number.

func (*Stack) Remove added in v0.1.0

func (s *Stack) Remove(id LayerID) bool

Remove takes the insertion named by id out of the stack and reports whether it was present. A controller uses this when its layer closes while another is above it; popping would dismiss a different control.

func (*Stack) SetBase added in v0.3.0

func (s *Stack) SetBase(base Widget)

SetBase replaces the interface beneath the layers and settles keyboard ownership.

func (*Stack) Top

func (s *Stack) Top() Modal

Top is the layer with the input, or nil when there is none.

type Static added in v0.1.0

type Static struct{ Of Block }

Static adapts a passive Block into a measured live Widget.

It is the explicit bridge for a document or other finished value shown in a Viewport. The block remains passive; the active tree owns only its placement.

func (Static) Draw added in v0.1.0

func (s Static) Draw(frame Frame)

Draw draws the passive block into frame.

func (Static) Measure added in v0.1.0

func (s Static) Measure(across int) int

Measure forwards the block's measurement.

type Sticky

type Sticky struct {

	// MinHeight is how far a header may be collapsed before it stops shrinking and
	// starts scrolling off instead. Zero means it does not collapse.
	//
	// A tall prompt pinned in full eats the view it was meant to give context to.
	MinHeight int
	// Gap is the rows kept clear between a pinned header and the content below it,
	// so that the two do not read as one block.
	Gap int
	// contains filtered or unexported fields
}

Sticky pins a block to the top of the view once it has been scrolled past.

It is the section header of a scrolling list, and a transcript wants it for the same reason a list does: the thing that gives the rows below their meaning — which question this answer belongs to — is exactly the thing that scrolls away first. A reader halfway down a long answer has nothing on screen telling them what it answers.

How it behaves

The pinned block sits at the top of the view while its own rows are above it. When the next pinned block comes up from below, it pushes this one off rather than appearing over it: the header slides up, is clipped from the top, and fades as it goes. That is what makes the change feel like one thing replacing another instead of two things flickering.

Why it is arithmetic and not drawing

All of it is a question about rows: which block is pinned, how many of its rows are left, and how far along the push is. None of that needs a cell, so none of it is here. What draws a header takes these numbers and draws.

The zero value has no pinnable blocks. A Sticky must not be copied after first use; its ordered identity set has one mutable owner.

func (*Sticky) Add added in v0.2.0

func (s *Sticky) Add(blocks ...BlockID)

Add appends pinnable block identities in transcript order.

func (*Sticky) At added in v0.0.2

func (s *Sticky) At(t TranscriptLayout, from, rows int) (Pinned, bool)

At works out the header for a view of rows starting at from, and reports whether there is one.

There is none while the block that would be pinned is still fully on screen: a header repeating something already visible two rows below is noise, and the moment it stops being visible is exactly the moment it starts being worth showing.

func (*Sticky) Blocks added in v0.0.2

func (s *Sticky) Blocks() []BlockID

Blocks returns a copy of the pinnable identities in transcript order.

func (*Sticky) DiscardBefore added in v0.1.0

func (s *Sticky) DiscardBefore(first BlockID)

DiscardBefore forgets pinnable blocks that can no longer be addressed.

A transcript calls for this after committing a prefix. Keeping one scalar identity per terminal-owned block would otherwise make the sticky state grow with session age even though the transcript itself had released the payload.

func (*Sticky) Len added in v0.2.0

func (s *Sticky) Len() int

Len reports how many pinnable identities Sticky owns.

func (*Sticky) SetBlocks added in v0.2.0

func (s *Sticky) SetBlocks(blocks []BlockID)

SetBlocks replaces the identities that can be pinned. Sticky owns the slice; the caller may reuse or change its input afterwards.

type Tab added in v0.0.2

type Tab struct {
	// Title is what the tab is called. It is here rather than beside whatever draws
	// the strip because the name belongs to the pane: an appearance built from a
	// separate list of titles is a list that can drift out of step with the panes it
	// names. Semantics reads the same value for the same reason.
	Title string
	// Of is the pane. It may be nil, which is a tab with nothing in it yet.
	Of Widget
}

Tab is one compound part of Tabs: a named pane.

type Table added in v0.0.2

type Table[T any] struct {
	List[T]
	// contains filtered or unexported fields
}

Table is a list of rows with more than one column: a cursor, a window onto more rows than fit, and an order.

It is a List and says so — everything about moving a selection, keeping it in view, taking the wheel and answering a click is the same question in one column as in six, and a table that answered it again would be a second place for it to be wrong. What a table has that a list does not is which column it is sorted by, so that is all this adds.

Where the columns are is not here either. A row is drawn by List.Row into a view of the whole row, and how that row is divided belongs to its appearance layer.

The zero Table is an empty list in no particular order. A Table must not be copied after first use: its rows, cursor, ordering and scroll are one mutable owner.

func (*Table[T]) ClearSort added in v0.13.0

func (t *Table[T]) ClearSort()

ClearSort forgets the order, leaving the rows where they are. It is what a caller calls when it has replaced the rows with something whose order means something.

func (*Table[T]) SetItems added in v0.0.2

func (t *Table[T]) SetItems(items []T)

SetItems replaces the rows, keeping the order the table is sorted by.

It is List.SetItems with the sort applied, under the same name on purpose: there is one way to give a table its rows, and it cannot be the one that quietly throws the order away. A table that lost its order every time its rows were refreshed would be a table nobody could read while it was updating.

func (*Table[T]) SetLess added in v0.2.0

func (t *Table[T]) SetLess(less func(a, b T, column int) bool)

SetLess changes how columns order rows. A table already in a sorted state is immediately reordered by the new comparison; nil leaves the current row order in place and marks it unsorted. Keeping this transition inside Table prevents its reported order from getting out of step with its rows.

func (*Table[T]) SortBy added in v0.0.2

func (t *Table[T]) SortBy(column int) bool

SortBy orders the rows by a column, and reports whether anything changed.

Asking for the column it is already sorted by turns the order round, which is what a reader means by pressing the same header twice.

func (*Table[T]) Sorted added in v0.0.2

func (t *Table[T]) Sorted() (column int, descending, ok bool)

Sorted is the column the rows are in the order of, whether that order is reversed, and whether they are sorted at all.

It is what a header asks to draw the mark beside the column being sorted by, which is the only way a reader can tell an order from a coincidence.

type Tabs added in v0.0.2

type Tabs struct {

	// Keys say which keystrokes move between panes. Nil reads through
	// [DefaultTabsKeys].
	Keys *keymap.Map
	// NoWrap stops the walk at either end. Wrapping is on by default because tabs are
	// few and walked as a ring, unlike a long list where wrapping loses the reader's
	// place.
	NoWrap bool
	// contains filtered or unexported fields
}

Tabs is the behavior and semantic owner of a set of named panes.

It draws only the selected pane. The strip of names is appearance — where it sits, what marks the one selected, whether there is a rule under it — and a behavior that drew one would have decided all of that for everybody. Whatever draws the strip asks which tab is selected and calls Tabs.Select when one is clicked, the same division a list keeps between selection and rows. SemanticNode describes the tab list and panel without coupling either to those cells.

Construct it with NewTabs so selection ownership is explicit in one configuration. The zero value is an empty locally owned controller and is safe, but Set is the only way to give it parts. A Tabs value must not be copied after first use: its parts, selection, focus and matcher are one mutable owner.

func NewTabs added in v0.1.0

func NewTabs(config TabsConfig) *Tabs

NewTabs constructs one tabs controller from config.

With Selection set, selection operations write the accessor directly. When its owner writes it independently, it calls Tabs.Sync so focus moves as the same semantic transition.

func (*Tabs) At added in v0.1.0

func (t *Tabs) At(index int) (Tab, bool)

At returns one tab part and whether index exists.

func (*Tabs) Current added in v0.0.2

func (t *Tabs) Current() (Tab, bool)

Current is the pane that is showing, and whether there is one.

func (*Tabs) Do added in v0.0.2

func (t *Tabs) Do(action keymap.Action) bool

Do offers an action to the selected pane before answering tab movement by name. A pane driven from a menu keeps the same priority it has for key events.

func (*Tabs) Draw added in v0.0.2

func (t *Tabs) Draw(frame Frame)

Draw paints the selected pane into the whole frame.

func (*Tabs) Focus added in v0.0.2

func (t *Tabs) Focus(has bool)

Focus takes or releases keyboard ownership and settles it on the selected pane.

func (*Tabs) Handle added in v0.0.2

func (t *Tabs) Handle(event input.Event) bool

Handle offers an event to the selected pane before answering tab movement.

The pane is first because a strip that took keys before its contents would steal an arrow from an editor or list inside it. This is the same rule a viewport keeps with the content it is showing.

func (*Tabs) Items added in v0.0.2

func (t *Tabs) Items() []Tab

Items returns a copy of the current tab parts. Replacing the returned slice cannot bypass selection clamping or focus settlement; use Tabs.Set to change the parts.

func (*Tabs) Len added in v0.1.0

func (t *Tabs) Len() int

Len returns the number of tab parts.

func (*Tabs) Measure added in v0.0.2

func (t *Tabs) Measure(across int) int

Measure is what the selected pane asks for.

func (*Tabs) Move added in v0.0.2

func (t *Tabs) Move(n int) bool

Move steps by n panes, wrapping unless told not to.

func (*Tabs) Select added in v0.0.2

func (t *Tabs) Select(at int)

Select shows a pane, clamped to the parts present, and transfers focus with it.

func (*Tabs) Selected added in v0.0.2

func (t *Tabs) Selected() int

Selected is which pane is showing, or -1 when there are none.

func (*Tabs) Semantics added in v0.1.0

func (t *Tabs) Semantics() SemanticNode

Semantics returns a tab list, its tab parts and the selected panel.

func (*Tabs) Set added in v0.1.0

func (t *Tabs) Set(items ...Tab)

Set replaces the tab parts and preserves a valid selected index.

func (*Tabs) Sync added in v0.1.0

func (t *Tabs) Sync() bool

Sync applies a caller-written controlled selection to pane focus and reports whether focus or the stored selection changed. An index outside the current parts is clamped and written back, so the caller and controller keep one valid selection rather than observing different forms of the same state.

Accessors are not observable. Keeping this explicit prevents Draw from performing a hidden semantic transition merely because external storage changed.

type TabsConfig added in v0.11.0

type TabsConfig struct {
	// Items are copied in display order.
	Items []Tab
	// Selection is optional caller-owned state. Nil keeps state local.
	Selection Accessor[int]
	// Keys maps tab actions. Nil uses [DefaultTabsKeys].
	Keys *keymap.Map
	// NoWrap stops movement at the first and last tab.
	NoWrap bool
}

TabsConfig is the complete construction state of Tabs.

A nil Selection gives the controller local ownership. An accessor gives ownership to the caller without selecting a different constructor or maintaining a shadow value. The zero value constructs an empty, locally owned controller.

type Text added in v0.0.2

type Text struct {

	// Label is what the field is asking for.
	Label string
	// Value is the caller-owned text. A caller change is observed at the next semantic
	// operation and by drawing; that operation writes back the one-line canonical form.
	// Edits write immediately and adopt the value the owner accepts. Nil keeps the text
	// local.
	Value Accessor[string]
	// Check says what is wrong with what has been entered, or nil. It is asked when the
	// keyboard leaves the field and when the form is submitted.
	Check func(s string) error
	// Placeholder is shown while the field is empty.
	Placeholder string
	// Keys say which keystrokes edit. Nil reads through [DefaultEditorKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Text is a field holding a line of text.

It is a one-line Editor with a label and a check around it, so everything a field does — the cursor, selecting, undo, the clipboard, a click landing where the reader meant it — is that field's and was not written again.

One of these on its own is a Form with one field in it, which is where its look comes from and is what an interface that wants a single input asks for.

The zero value is ready. A Text value must not be copied after first use: its editor, validation and caller-owned reconciliation are one mutable field.

func (*Text) Ask added in v0.0.2

func (t *Text) Ask() string

Ask is the label, with the placeholder as the hint it already is.

func (*Text) Do added in v0.0.2

func (t *Text) Do(action keymap.Action) bool

Do runs one of the field's actions by name. See Doer.

func (*Text) Draw added in v0.0.2

func (t *Text) Draw(v Frame)

Draw paints the label, the field and whatever was wrong with the answer.

func (*Text) Editor added in v0.0.2

func (t *Text) Editor() *Editor

Editor is the field itself, for a caller that needs the cursor, clipboard, or one-line appearance such as Editor.SetMask.

func (*Text) Error added in v0.0.2

func (f *Text) Error() error

Error is what checking the answer last found.

func (*Text) Focus added in v0.0.2

func (t *Text) Focus(has bool)

Focus takes the keyboard or gives it up, and checks the answer on the way out.

On the way out and not on the way in: a form that greeted somebody with a column of complaints about answers they have not given yet would be a form nobody finishes. A field that has never had the keyboard has nothing to check.

func (*Text) Handle added in v0.0.2

func (t *Text) Handle(ev input.Event) bool

Handle passes input to the field and keeps the value in step with it.

func (*Text) Measure added in v0.0.2

func (t *Text) Measure(int) int

Measure is the label, a row of text, and the problem with it if there is one.

func (*Text) Prompt added in v0.0.2

func (t *Text) Prompt() string

Prompt is what the field is asking for.

func (*Text) Reply added in v0.0.2

func (t *Text) Reply(said string) error

Reply takes what was said as the whole of the answer.

func (*Text) Validate added in v0.0.2

func (t *Text) Validate() error

Validate checks what has been entered.

type TextProjector added in v0.13.0

type TextProjector interface {
	// Rows is what the block's rows say at a width, and there are as many of them as
	// Measure reports at that width.
	Rows(width int) []text.Row
}

TextProjector is a block that can project its meaningful text at a width.

It is separate from drawing because text projection is not painting: selection, search and copying need the words without the box around them, the accent in a gutter, or the padding that made them look right. Naming the lower capability after its projection rather than one consumer also avoids suggesting that its Go value is safe to copy. A block without the capability contributes empty rows to a selection rather than nothing at all — it still occupies the rows, and a selection dragged across it has to produce as many lines as the user dragged over.

type Token

type Token struct {
	// Start and End are the byte range in the line that accepting a candidate
	// replaces: everything after the prefix, through the end of the token. The prefix
	// itself stays where it is — it is what the user typed to ask for the completion,
	// not part of the answer.
	Start, End int
	// Query is that range up to the cursor, which is what candidates are matched
	// against. What is after the cursor is replaced but not matched: a cursor put back
	// into the middle of a word is a request to reconsider its beginning.
	Query string
	// Trigger is what opened the token, so that a caller offering several can tell
	// which kind of thing is being asked for.
	Trigger Trigger
}

Token is the run of text a completion is being offered for.

func TokenAt

func TokenAt(line string, cursor int, triggers ...Trigger) (Token, bool)

TokenAt finds the token the cursor is inside, if any. The rightmost trigger before the cursor wins, so a file mentioned inside a command completes as a file.

A token ends at the first space after it, which is the one rule general enough to be worth having: anything more — a path that may contain spaces, a quote that protects them — is the caller's grammar and not a terminal library's.

type Transcript added in v0.0.2

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

Transcript is the live, retained part of output, in one coordinate space.

It is what everything that has to talk about a position in a session's output talks about. A scroll offset, the ends of a selection, a search match, a prompt pinned to the top of the view — all of them are rows, and they only mean the same thing to each other if there is one numbering they all use. That is what this holds: an ordered list of blocks, each of a height that depends on the width, and the row each of them starts at.

Why the output has to be held at all

An inline interface can print output and let the terminal keep it, which is the right answer for output nobody will touch again. Text the terminal owns cannot be selected by the program, searched, re-wrapped when the window changes, or scrolled back over under the program's own control — the terminal does all of that, in its own way, and tells the program nothing. A transcript is for output the program means to keep answering questions about.

What it costs

Appending is constant time. So is a block growing at the end, which is what a streaming answer does token by token: only that block is measured again, and only the rows after it move. A change of width is the one linear operation, because a width is what every height is a function of.

The zero value is an empty transcript at width zero. Its first Transcript.Stage establishes the width; subsequent appends reuse the last committed width. A Transcript must not be copied after first use: retained blocks and pending layout are one publication owner.

func (*Transcript) Append added in v0.0.2

func (t *Transcript) Append(b Block) BlockID

Append adds a block at the end, measures it, and returns its stable identity. Appending nil changes nothing and returns the next available identity.

Identities are never reused, and Append panics once every one has been issued. A session that wrapped would let a BlockID a caller is holding — a scroll anchor, a search result, a selection — name a different block than the one it was taken from, so the interface would jump somewhere the user never asked to go.

func (*Transcript) At added in v0.0.2

func (t *Transcript) At(row int) (id BlockID, offset int, ok bool)

At is the block covering a row, and how far into that block the row is.

The search is a bisection over the tops rather than a walk, because this is asked once per click, once per selection end, and once per frame for the top of the view. Deliberately retained content may be much taller than a screen.

func (*Transcript) Block added in v0.0.2

func (t *Transcript) Block(id BlockID) Block

Block is the live block with id, or nil when there is none.

func (*Transcript) Changed added in v0.0.2

func (t *Transcript) Changed(id BlockID)

Changed says the block at i has new content, and re-measures from there.

It has to be said rather than noticed. A block is an ordinary mutable object and nothing here is told when its text changes, so the alternative is measuring every block on every frame — which is the one thing this structure exists to avoid.

Everything before i keeps the height it had, because nothing before it moved.

func (*Transcript) Commit added in v0.0.2

func (t *Transcript) Commit(give func(b Block, rows int) bool) int

Commit gives the leading run of finished blocks to the terminal, in order, and reports how many went.

Why only the leading run

Text printed into a terminal's own output goes after what is already there, and there is no way to put something in front of it. So a block that finished while an earlier one is still being written has to wait: giving it over first would put the answer above the question.

Why this is one call

The alternative is a range to ask for and a range to record afterwards, and the second half of that pair is the one that gets forgotten — which prints the whole session again on the next frame. give is called with each block and its height, and returning false stops the run and leaves that block and everything after it for another time.

It is a one-way door

A committed block belongs to the terminal. It is no longer drawn, no longer re-wrapped when the window changes, and no longer selectable or searchable by this program — that is the trade printing makes, and it is why nothing is committed unless it is asked for. What it buys is that the output survives the program exiting, and that a session's memory stops growing.

func (*Transcript) EndRow added in v0.1.0

func (t *Transcript) EndRow() int

EndRow is the exclusive end of the transcript's live row range.

func (*Transcript) Extent added in v0.0.2

func (t *Transcript) Extent(id BlockID) (top, height int, ok bool)

Extent is the rows block i covers: the first, and how many.

It reports false for an identity that is not live, which is what a caller holding a reference from an earlier frame needs after a commit removes a prefix.

func (*Transcript) Finish added in v0.0.2

func (t *Transcript) Finish(id BlockID)

Finish says a block will not change again.

It is what makes a block eligible to be given to the terminal, and it is the caller's to say: a streaming answer is finished when whatever is streaming it says so, and nothing here can tell a pause from an ending.

func (*Transcript) Finished added in v0.0.2

func (t *Transcript) Finished(id BlockID) bool

Finished reports whether a block has been said to be finished.

func (*Transcript) FirstBlock added in v0.1.0

func (t *Transcript) FirstBlock() BlockID

FirstBlock is the ID of the first live block. When the transcript is empty it is the ID the next appended block will receive.

func (*Transcript) Height added in v0.0.2

func (t *Transcript) Height() int

Height is the height of the live blocks at the current width.

func (*Transcript) Last added in v0.0.2

func (t *Transcript) Last() Block

Last is the block most recently appended, or nil when the transcript is empty. It is the one a streaming answer is still arriving into.

func (*Transcript) Len added in v0.0.2

func (t *Transcript) Len() int

Len is how many live blocks the transcript holds.

func (*Transcript) Rows added in v0.0.2

func (t *Transcript) Rows(from, count int) []text.Row

Rows is what the transcript says over [from, from+count), one entry per row.

Rows belonging to a block that cannot project text come back empty and unjoined. The count is what was asked for, clamped to what exists, so a caller can index the result by row and get the row it meant.

func (*Transcript) Stage added in v0.1.0

func (t *Transcript) Stage(frame Frame, width int) TranscriptLayout

Stage lays the transcript out at width for a component frame.

A changed width is measured into private pending placement. The new row space becomes observable through Height, Extent, selection and search only when the complete Root frame commits. Calling Stage at the committed width reuses the existing placement without allocation.

func (*Transcript) StartRow added in v0.1.0

func (t *Transcript) StartRow() int

StartRow is the first row the transcript still owns. Earlier rows have been committed to the terminal and cannot be drawn, searched, selected, or rewrapped.

func (*Transcript) Visible added in v0.0.2

func (t *Transcript) Visible(from, rows int) (first, last BlockID)

Visible is the range of blocks that any of the rows [from, from+rows) touch.

The end is exclusive. An empty range comes back as two equal identities, which is what a viewport scrolled past everything gives.

func (*Transcript) Width added in v0.0.2

func (t *Transcript) Width() int

Width is the width every height in it was measured at.

type TranscriptLayout added in v0.1.0

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

TranscriptLayout is the immutable placement returned by Transcript.Stage for one component frame.

func (TranscriptLayout) Block added in v0.1.0

func (l TranscriptLayout) Block(id BlockID) Block

Block returns the live block with id.

func (TranscriptLayout) Draw added in v0.1.0

func (l TranscriptLayout) Draw(v grid.View, from int)

Draw writes the window of rows starting at from.

func (TranscriptLayout) EndRow added in v0.1.0

func (l TranscriptLayout) EndRow() int

EndRow is the exclusive end of this layout's live rows.

func (TranscriptLayout) Extent added in v0.1.0

func (l TranscriptLayout) Extent(id BlockID) (top, height int, ok bool)

Extent is the first row and height occupied by id.

func (TranscriptLayout) FirstBlock added in v0.1.0

func (l TranscriptLayout) FirstBlock() BlockID

FirstBlock is the identity of the first live block.

func (TranscriptLayout) Height added in v0.1.0

func (l TranscriptLayout) Height() int

Height is the number of live rows in this layout.

func (TranscriptLayout) StartRow added in v0.1.0

func (l TranscriptLayout) StartRow() int

StartRow is the first live row in this layout.

type Tree added in v0.0.2

type Tree[T any] struct {

	// Row draws one row. at is where it sits among the rows on screen and selected
	// says whether it is the one under the cursor. It runs during drawing and must be
	// an observationally pure projection; state changes belong in event handlers.
	Row func(v grid.View, at int, row Shown[T], selected bool)
	// Keys say which keystrokes produce which of the actions the tree answers to —
	// see [Tree.Do]. Nil reads through [DefaultTreeKeys].
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Tree is a list of items with items under them, which can be opened and closed.

The items are the caller's shape — see Node — and how a row looks is the caller's too: a tree that drew its own indentation would have decided what a branch mark is, which is a matter of taste and belongs a layer up. What is here is which branches are open, which row the cursor is on, and what the keys do.

The zero Tree shows nothing and answers nothing. A Tree must not be copied after first use: its owned hierarchy, branch identities, rows and matcher are one mutable owner.

func NewTree added in v0.3.0

func NewTree[T any](nodes ...Node[T]) *Tree[T]

NewTree constructs a tree from top-level nodes in display order.

func (*Tree[T]) Close added in v0.0.2

func (t *Tree[T]) Close(at int) bool

Close hides it again, and reports the same.

func (*Tree[T]) Current added in v0.0.2

func (t *Tree[T]) Current() (T, bool)

Current is the item under the cursor and whether there is one.

func (*Tree[T]) CurrentRow added in v0.0.2

func (t *Tree[T]) CurrentRow() (Shown[T], bool)

CurrentRow is the whole row under the cursor and whether there is one: what a caller asks when it needs more than the item — whether it can be opened, how deep it sits, where it came from.

func (*Tree[T]) Do added in v0.0.2

func (t *Tree[T]) Do(action keymap.Action) bool

Do runs one of the tree's actions by name, reporting whether it was one this tree knows. See Doer.

Everything about moving through the rows is the list's, because that is what it is. What is the tree's is opening and closing — and the two edges that make a tree feel like a tree: opening a leaf does nothing, and closing something already closed goes up to whatever it is under.

func (*Tree[T]) Draw added in v0.0.2

func (t *Tree[T]) Draw(v Frame)

Draw paints the rows that fit.

func (*Tree[T]) DrawRows added in v0.3.0

func (t *Tree[T]) DrawRows(v Frame, draw func(grid.View, int, Shown[T], bool))

DrawRows paints the rows that fit with draw.

Like List.DrawRows, it lets an appearance compose with the controller without replacing Row. Selection, scrolling and committed pointer geometry remain owned by the tree; only the appearance of this frame is supplied by the caller.

func (*Tree[T]) Focus added in v0.0.3

func (t *Tree[T]) Focus(has bool)

Focus takes the keyboard, or gives it up — see List.Focus, which is where the rows this is made of hold it.

func (*Tree[T]) Focused added in v0.0.3

func (t *Tree[T]) Focused() bool

Focused reports whether this tree has the keyboard.

func (*Tree[T]) Handle added in v0.0.2

func (t *Tree[T]) Handle(ev input.Event) bool

Handle answers keys, the wheel and a press, reporting whether it consumed the event.

func (*Tree[T]) Measure added in v0.0.2

func (t *Tree[T]) Measure(int) int

Measure is one row per row showing, which is what a container needs to decide how much room to give it.

func (*Tree[T]) Nodes added in v0.0.2

func (t *Tree[T]) Nodes() []Node[T]

Nodes returns a complete copy of the hierarchy.

func (*Tree[T]) Open added in v0.0.2

func (t *Tree[T]) Open(at int) bool

Open shows what is under the row at, and reports whether that changed anything: a leaf, or a branch that was already open, changes nothing.

func (*Tree[T]) Rows added in v0.0.2

func (t *Tree[T]) Rows() []Shown[T]

Rows are the rows the tree is showing, top to bottom, as a copy.

A copy because the visible-row buffer belongs to the tree and changes when nodes are replaced or branches open and close. The allocation happens only when somebody outside asks for a snapshot; drawing reads the owned buffer directly.

func (*Tree[T]) Scroll added in v0.0.2

func (t *Tree[T]) Scroll() *Scroll

Scroll exposes the position, for a scrollbar drawn beside the tree.

func (*Tree[T]) Select added in v0.0.2

func (t *Tree[T]) Select(at int)

Select moves the cursor to a row, clamped to what is showing.

func (*Tree[T]) Selected added in v0.0.2

func (t *Tree[T]) Selected() int

Selected is the row the cursor is on, or -1 when the tree is showing nothing.

func (*Tree[T]) SetNodes added in v0.3.0

func (t *Tree[T]) SetNodes(nodes []Node[T])

SetNodes replaces the hierarchy. The tree copies the entire node collection; the caller may reuse or change every input slice after the call. Item values remain caller-owned. Open branches follow their positions where those positions still name branches, and selection stays on the same visible row when possible.

A Node graph must be acyclic. Cyclic slice graphs are a programmer error and panic here instead of making an ownership copy run until the stack or heap is exhausted. Traversal itself is iterative, so valid depth is limited by available storage rather than the goroutine stack.

type Trigger

type Trigger struct {
	// Prefix begins the token — "@" for a file, "/" for a command, ":" for an emoji.
	Prefix string
	// AtStart limits it to the beginning of the line, which is what makes "/help" a
	// command and "and/or" not one.
	AtStart bool
}

Trigger is what opens a completion: the characters that begin a token, and where they count as beginning one.

type Viewport added in v0.0.2

type Viewport struct {

	// Keys say which keystrokes scroll — see [Scroll]. Nil reads through
	// [DefaultScrollKeys], and they are tried only after the content has declined the
	// keystroke, so content with arrow keys of its own keeps them.
	Keys *keymap.Map
	// contains filtered or unexported fields
}

Viewport shows a window onto content taller than the room there is for it.

There was a scroll position and something that draws a bar, and nothing that put content in a box and scrolled it. Every interface that wanted one wrote the same three things by hand: measure the content, keep an offset, and hand the content a view starting above the window so the rows off the top fall away.

That last part is the whole trick, and it is why this is so short. A view is already a clipped window onto a surface, and a widget drawn into one that begins above the box lays itself out at its full height and simply loses what is outside — so nothing has to be taught about being scrolled. The content does not know, and the cursor it places while it is off-screen is discarded rather than drawn in the wrong place.

What it is not

It does not scroll content that scrolls itself. A Transcript measures incrementally and keeps its own position, because a session's output is too tall to re-measure every frame; a field taller than its box scrolls to keep its own cursor in view. Both would fight a window that also had an opinion.

The zero Viewport is empty and shows nothing. A Viewport must not be copied after first use: its content focus, scroll and committed routing geometry are one mutable owner.

Example
// Content is drawn at its whole height into a view that begins above the box, so
// the rows off the top fall away and nothing has to be told it is scrolled.
window := headless.NewViewport(numbered(8))
showWidget(8, 3, window)
window.Scroll().By(4)
showWidget(8, 3, window)
Output:
|row 0   |
|row 1   |
|row 2   |
|row 4   |
|row 5   |
|row 6   |

func NewViewport added in v0.3.0

func NewViewport(content Sized) *Viewport

NewViewport constructs a window around content.

func (*Viewport) Content added in v0.0.2

func (p *Viewport) Content() Sized

Content returns what is shown through the window.

func (*Viewport) Do added in v0.0.2

func (p *Viewport) Do(action keymap.Action) bool

Do runs an action, the content's first and then the window's own. See Doer.

func (*Viewport) Draw added in v0.0.2

func (p *Viewport) Draw(v Frame)

Draw paints as much of the content as fits.

func (*Viewport) Focus added in v0.0.2

func (p *Viewport) Focus(has bool)

Focus takes the keyboard, or gives it up, and passes the news to the content. A window is a widget like any other, so one goes in a Container beside anything else.

func (*Viewport) Handle added in v0.0.2

func (p *Viewport) Handle(ev input.Event) bool

Handle scrolls, and gives the content whatever is not about scrolling.

The wheel is the window's: content that answered it as well would scroll twice as far as the reader asked. Everything else a pointer does belongs to the content, in the content's own coordinates, which here means the row it is over rather than the row on screen.

func (*Viewport) Measure added in v0.0.2

func (p *Viewport) Measure(across int) int

Measure is how tall the content wants to be, which is what a window inside a measured slot asks for: a window that is never scrolled is a window nobody notices.

func (*Viewport) Scroll added in v0.0.2

func (p *Viewport) Scroll() *Scroll

Scroll is the window's position, for a scrollbar drawn beside it.

func (*Viewport) SetContent added in v0.3.0

func (p *Viewport) SetContent(content Sized)

SetContent replaces what is shown and transfers keyboard ownership. The scroll position is preserved and clamped to the new content on its next layout.

type Widget

type Widget interface {
	Draw(frame Frame)
}

Widget draws itself into the space it is given.

The view is already positioned and clipped. A widget that draws outside it is not a bug that shows on screen — the drawing is simply discarded — which is what makes the box a boundary rather than a convention.

Jump to

Keyboard shortcuts

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