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 ¶
- Constants
- func DefaultActivationKeys() *keymap.Map
- func DefaultCompletionKeys() *keymap.Map
- func DefaultConfirmKeys() *keymap.Map
- func DefaultContainerKeys() *keymap.Map
- func DefaultEditorKeys() *keymap.Map
- func DefaultFormKeys() *keymap.Map
- func DefaultListKeys() *keymap.Map
- func DefaultMultiSelectKeys() *keymap.Map
- func DefaultScrollKeys() *keymap.Map
- func DefaultSettingsKeys() *keymap.Map
- func DefaultSliderKeys() *keymap.Map
- func DefaultStackKeys() *keymap.Map
- func DefaultTabsKeys() *keymap.Map
- func DefaultTreeKeys() *keymap.Map
- type Accessor
- type Backdrop
- type Block
- type BlockID
- type Candidate
- type Caret
- type Clicks
- type Clipboard
- type Closer
- type Command
- type Commands
- type Completion
- func (c *Completion) Current() (Candidate, bool)
- func (c *Completion) Dismiss()
- func (c *Completion) Do(action keymap.Action) bool
- func (c *Completion) Draw(v Frame)
- func (c *Completion) Handle(ev input.Event) bool
- func (c *Completion) Measure(int) int
- func (c *Completion) Offer(t Token, candidates []Candidate)
- func (c *Completion) Open() bool
- func (c *Completion) Token() (Token, bool)
- func (c *Completion) Width() int
- type Confirm
- func (c *Confirm) Answer() bool
- func (c *Confirm) Ask() string
- func (c *Confirm) Do(action keymap.Action) bool
- func (c *Confirm) Draw(v Frame)
- func (f *Confirm) Error() error
- func (c *Confirm) Focus(has bool)
- func (c *Confirm) Handle(ev input.Event) bool
- func (c *Confirm) Measure(int) int
- func (c *Confirm) Prompt() string
- func (c *Confirm) Reply(said string) error
- func (c *Confirm) Say(yes bool)
- func (c *Confirm) Validate() error
- type Container
- func (c *Container) Add(items ...Item) *Container
- func (c *Container) Do(action keymap.Action) bool
- func (c *Container) Draw(v Frame)
- func (c *Container) Focus(has bool)
- func (c *Container) FocusNext() bool
- func (c *Container) FocusPrev() bool
- func (c *Container) Focused() Widget
- func (c *Container) Give(index int) bool
- func (c *Container) Handle(ev input.Event) bool
- func (c *Container) Items() []Item
- func (c *Container) Len() int
- func (c *Container) Measure(across int) int
- func (c *Container) Set(items ...Item)
- type Dialog
- func (d *Dialog) Content() *DialogContent
- func (d *Dialog) Description() string
- func (d *Dialog) Dismiss()
- func (d *Dialog) Open() bool
- func (d *Dialog) Semantics() SemanticNode
- func (d *Dialog) SetDescription(description string)
- func (d *Dialog) SetTitle(title string)
- func (d *Dialog) Show()
- func (d *Dialog) Sync() bool
- func (d *Dialog) Title() string
- func (d *Dialog) Trigger(label string, of Widget) *DialogTrigger
- type DialogConfig
- type DialogContent
- func (c *DialogContent) Backdrop(view grid.View)
- func (c *DialogContent) Closed()
- func (c *DialogContent) Draw(frame Frame)
- func (c *DialogContent) Focus(has bool)
- func (c *DialogContent) Handle(event input.Event) bool
- func (c *DialogContent) Insists() bool
- func (c *DialogContent) Place(space image.Point) layout.Placement
- func (c *DialogContent) Semantics() SemanticNode
- type DialogTrigger
- func (t *DialogTrigger) Appearance() Widget
- func (t *DialogTrigger) Do(action keymap.Action) bool
- func (t *DialogTrigger) Draw(frame Frame)
- func (t *DialogTrigger) Focus(has bool)
- func (t *DialogTrigger) Handle(event input.Event) bool
- func (t *DialogTrigger) Measure(across int) int
- func (t *DialogTrigger) Semantics() SemanticNode
- func (t *DialogTrigger) SetAppearance(appearance Widget)
- type Doer
- type Editor
- func (e *Editor) Anchor()
- func (e *Editor) At(x, y, width int) (Caret, bool)
- func (e *Editor) Clear()
- func (e *Editor) Copy() bool
- func (e *Editor) Cursor() (line, col int)
- func (e *Editor) Cut() bool
- func (e *Editor) DeleteBack()
- func (e *Editor) DeleteForward()
- func (e *Editor) DeleteSelection() bool
- func (e *Editor) DeleteWordBack()
- func (e *Editor) Do(action keymap.Action) bool
- func (e *Editor) Draw(frame Frame)
- func (e *Editor) DrawWith(frame Frame, look Look)
- func (e *Editor) ElementAt(line, col int) (Element, bool)
- func (e *Editor) Elements() []Element
- func (e *Editor) Empty() bool
- func (e *Editor) Focus(has bool)
- func (e *Editor) Handle(ev input.Event) bool
- func (e *Editor) Insert(s string)
- func (e *Editor) InsertElement(kind ElementKind, body string) Element
- func (e *Editor) InsertRune(r rune)
- func (e *Editor) KillToEnd()
- func (e *Editor) KillToStart()
- func (e *Editor) Mask() string
- func (e *Editor) Measure(width int) int
- func (e *Editor) MoveDown()
- func (e *Editor) MoveLeft()
- func (e *Editor) MoveLineEnd()
- func (e *Editor) MoveLineStart()
- func (e *Editor) MoveRight()
- func (e *Editor) MoveUp()
- func (e *Editor) MoveWordLeft()
- func (e *Editor) MoveWordRight()
- func (e *Editor) Newline()
- func (e *Editor) Paste() bool
- func (e *Editor) Redo()
- func (e *Editor) RemoveElement(id uint64) bool
- func (e *Editor) Replace(start, end int, s string)
- func (e *Editor) Revision() uint64
- func (e *Editor) Scroll() *Scroll
- func (e *Editor) SelectAll()
- func (e *Editor) SelectNone()
- func (e *Editor) Selected() string
- func (e *Editor) Selection() (start, end Caret, ok bool)
- func (e *Editor) SelectionSpans(width int) []RowSpan
- func (e *Editor) SetCursor(line, col int)
- func (e *Editor) SetMask(mask string)
- func (e *Editor) SetSingleLine(enabled bool)
- func (e *Editor) SetText(s string)
- func (e *Editor) SingleLine() bool
- func (e *Editor) Spans(from, to Caret, width int) []RowSpan
- func (e *Editor) Text() string
- func (e *Editor) Undo()
- func (e *Editor) Yank()
- func (e *Editor) YankPop()
- type Element
- type ElementKind
- type Field
- type Filter
- func (f *Filter[T]) Current() (T, bool)
- func (f *Filter[T]) Do(action keymap.Action) bool
- func (f *Filter[T]) Draw(v Frame)
- func (f *Filter[T]) Focus(has bool)
- func (f *Filter[T]) Focused() bool
- func (f *Filter[T]) Handle(ev input.Event) bool
- func (f *Filter[T]) Items() []T
- func (f *Filter[T]) Len() int
- func (f *Filter[T]) Matched() int
- func (f *Filter[T]) Measure(int) int
- func (f *Filter[T]) Pattern() string
- func (f *Filter[T]) Scroll() *Scroll
- func (f *Filter[T]) Select(at int)
- func (f *Filter[T]) Selected() int
- func (f *Filter[T]) SetItems(items []T, text func(item T) string)
- func (f *Filter[T]) SetPattern(pattern string)
- type Focusable
- type Form
- func (f *Form) Add(fields ...Field) *Form
- func (f *Form) Cancel()
- func (f *Form) Do(action keymap.Action) bool
- func (f *Form) Draw(v Frame)
- func (f *Form) DrawWith(v Frame, look Look)
- func (f *Form) Error() error
- func (f *Form) Fields() []Field
- func (f *Form) Focus(has bool)
- func (f *Form) Focused() Field
- func (f *Form) Handle(ev input.Event) bool
- func (f *Form) Measure(across int) int
- func (f *Form) Set(fields ...Field)
- func (f *Form) Submit() bool
- type Found
- type Frame
- type History
- func (h *History) Add(line string)
- func (h *History) At(n int) (string, bool)
- func (h *History) Back(current string) (string, bool)
- func (h *History) Cancel() (string, bool)
- func (h *History) Forward() (string, bool)
- func (h *History) Len() int
- func (h *History) Limit() int
- func (h *History) Recall(query string) []Recalled
- func (h *History) SetLimit(n int)
- func (h *History) Walking() bool
- type Insistent
- type Interactive
- type Item
- type LayerID
- type List
- func (l *List[T]) At(index int) (T, bool)
- func (l *List[T]) Current() (T, bool)
- func (l *List[T]) Do(action keymap.Action) bool
- func (l *List[T]) Draw(v Frame)
- func (l *List[T]) DrawRows(v Frame, draw func(grid.View, int, T, bool))
- func (l *List[T]) Focus(has bool)
- func (l *List[T]) Focused() bool
- func (l *List[T]) Handle(ev input.Event) bool
- func (l *List[T]) Items() []T
- func (l *List[T]) Len() int
- func (l *List[T]) Measure(int) int
- func (l *List[T]) Move(n int)
- func (l *List[T]) Scroll() *Scroll
- func (l *List[T]) Select(i int)
- func (l *List[T]) Selected() int
- func (l *List[T]) SetItems(items []T)
- type Look
- type Match
- type Modal
- type MultiSelect
- func (m *MultiSelect[T]) Ask() string
- func (m *MultiSelect[T]) Do(action keymap.Action) bool
- func (m *MultiSelect[T]) Draw(v Frame)
- func (f *MultiSelect) Error() error
- func (m *MultiSelect[T]) Focus(has bool)
- func (m *MultiSelect[T]) Handle(ev input.Event) bool
- func (m *MultiSelect[T]) Limit() int
- func (m *MultiSelect[T]) Measure(int) int
- func (m *MultiSelect[T]) Options() []Option[T]
- func (m *MultiSelect[T]) Prompt() string
- func (m *MultiSelect[T]) Reply(said string) error
- func (m *MultiSelect[T]) SetLimit(limit int)
- func (m *MultiSelect[T]) SetOptions(options []Option[T])
- func (m *MultiSelect[T]) Taken() []T
- func (m *MultiSelect[T]) Toggle() bool
- func (m *MultiSelect[T]) Validate() error
- type Node
- type Option
- type Pinned
- type Point
- type Pointer
- func (p *Pointer) Claim(region image.Rectangle)
- func (p *Pointer) Clicked(region image.Rectangle, button input.Button) bool
- func (p *Pointer) Handle(ev input.Event) bool
- func (p *Pointer) Left()
- func (p *Pointer) Over(region image.Rectangle) bool
- func (p *Pointer) Position() (image.Point, bool)
- func (p *Pointer) Pressing(region image.Rectangle) bool
- type PointerRegion
- type Recalled
- type Result
- type Role
- type Root
- type RowGutter
- type RowSpan
- type Scroll
- func (s *Scroll) AtBottom() bool
- func (s *Scroll) By(rows int)
- func (s *Scroll) Discard(rows int)
- func (s *Scroll) Do(action keymap.Action) bool
- func (s *Scroll) Handle(ev input.Event, keys *keymap.Map) bool
- func (s *Scroll) Offset() int
- func (s *Scroll) Pages(n int)
- func (s *Scroll) Reveal(first, last int)
- func (s *Scroll) Stage(frame Frame, total, window int) ScrollLayout
- func (s *Scroll) ToBottom()
- func (s *Scroll) ToTop()
- func (s *Scroll) Wheel(w input.Wheel)
- type ScrollLayout
- type Search
- type Select
- func (s *Select[T]) Ask() string
- func (s *Select[T]) Chosen() (Option[T], bool)
- func (s *Select[T]) Do(action keymap.Action) bool
- func (s *Select[T]) Draw(v Frame)
- func (f *Select) Error() error
- func (s *Select[T]) Focus(has bool)
- func (s *Select[T]) Handle(ev input.Event) bool
- func (s *Select[T]) Measure(int) int
- func (s *Select[T]) Options() []Option[T]
- func (s *Select[T]) Prompt() string
- func (s *Select[T]) Reply(said string) error
- func (s *Select[T]) SetOptions(options []Option[T])
- func (s *Select[T]) Validate() error
- type Selection
- func (s *Selection) Active() bool
- func (s *Selection) Begin(p Point)
- func (s *Selection) Clear()
- func (s *Selection) Covers(row, col int) bool
- func (s *Selection) DiscardBefore(row int)
- func (s *Selection) Done()
- func (s *Selection) Dragging() bool
- func (s *Selection) Extend(p Point)
- func (s *Selection) Range() (start, end Point)
- func (s *Selection) SelectLine(t *Transcript, p Point) bool
- func (s *Selection) SelectWord(t *Transcript, p Point) bool
- func (s *Selection) Text(t *Transcript) string
- type Semantic
- type SemanticNode
- type SemanticState
- type Settings
- type Shown
- type Sized
- type Slider
- func (s *Slider) Bounds() (minimum, maximum int)
- func (s *Slider) Do(action keymap.Action) bool
- func (s *Slider) Focus(has bool)
- func (s *Slider) Focused() bool
- func (s *Slider) Handle(event input.Event) bool
- func (s *Slider) Label() string
- func (s *Slider) Move(steps int) bool
- func (s *Slider) Position(cells int) int
- func (s *Slider) Semantics() SemanticNode
- func (s *Slider) Set(value int) bool
- func (s *Slider) SetBounds(minimum, maximum int)
- func (s *Slider) SetLabel(label string)
- func (s *Slider) SetStep(step int)
- func (s *Slider) Stage(frame Frame, track image.Rectangle)
- func (s *Slider) Step() int
- func (s *Slider) Sync() bool
- func (s *Slider) Value() int
- type SliderConfig
- type Snapshot
- type Span
- type Stack
- func (s *Stack) Area() (image.Rectangle, bool)
- func (s *Stack) Base() Widget
- func (s *Stack) Clear()
- func (s *Stack) Contains(id LayerID) bool
- func (s *Stack) Depth() int
- func (s *Stack) Do(action keymap.Action) bool
- func (s *Stack) Draw(v Frame)
- func (s *Stack) Focus(has bool)
- func (s *Stack) Handle(ev input.Event) bool
- func (s *Stack) Pop() bool
- func (s *Stack) Push(m Modal) LayerID
- func (s *Stack) Remove(id LayerID) bool
- func (s *Stack) SetBase(base Widget)
- func (s *Stack) Top() Modal
- type Static
- type Sticky
- type Tab
- type Table
- type Tabs
- func (t *Tabs) At(index int) (Tab, bool)
- func (t *Tabs) Current() (Tab, bool)
- func (t *Tabs) Do(action keymap.Action) bool
- func (t *Tabs) Draw(frame Frame)
- func (t *Tabs) Focus(has bool)
- func (t *Tabs) Handle(event input.Event) bool
- func (t *Tabs) Items() []Tab
- func (t *Tabs) Len() int
- func (t *Tabs) Measure(across int) int
- func (t *Tabs) Move(n int) bool
- func (t *Tabs) Select(at int)
- func (t *Tabs) Selected() int
- func (t *Tabs) Semantics() SemanticNode
- func (t *Tabs) Set(items ...Tab)
- func (t *Tabs) Sync() bool
- type TabsConfig
- type Text
- func (t *Text) Ask() string
- func (t *Text) Do(action keymap.Action) bool
- func (t *Text) Draw(v Frame)
- func (t *Text) Editor() *Editor
- func (f *Text) Error() error
- func (t *Text) Focus(has bool)
- func (t *Text) Handle(ev input.Event) bool
- func (t *Text) Measure(int) int
- func (t *Text) Prompt() string
- func (t *Text) Reply(said string) error
- func (t *Text) Validate() error
- type TextProjector
- type Token
- type Transcript
- func (t *Transcript) Append(b Block) BlockID
- func (t *Transcript) At(row int) (id BlockID, offset int, ok bool)
- func (t *Transcript) Block(id BlockID) Block
- func (t *Transcript) Changed(id BlockID)
- func (t *Transcript) Commit(give func(b Block, rows int) bool) int
- func (t *Transcript) EndRow() int
- func (t *Transcript) Extent(id BlockID) (top, height int, ok bool)
- func (t *Transcript) Finish(id BlockID)
- func (t *Transcript) Finished(id BlockID) bool
- func (t *Transcript) FirstBlock() BlockID
- func (t *Transcript) Height() int
- func (t *Transcript) Last() Block
- func (t *Transcript) Len() int
- func (t *Transcript) Rows(from, count int) []text.Row
- func (t *Transcript) Stage(frame Frame, width int) TranscriptLayout
- func (t *Transcript) StartRow() int
- func (t *Transcript) Visible(from, rows int) (first, last BlockID)
- func (t *Transcript) Width() int
- type TranscriptLayout
- func (l TranscriptLayout) Block(id BlockID) Block
- func (l TranscriptLayout) Draw(v grid.View, from int)
- func (l TranscriptLayout) EndRow() int
- func (l TranscriptLayout) Extent(id BlockID) (top, height int, ok bool)
- func (l TranscriptLayout) FirstBlock() BlockID
- func (l TranscriptLayout) Height() int
- func (l TranscriptLayout) StartRow() int
- type Tree
- func (t *Tree[T]) Close(at int) bool
- func (t *Tree[T]) Current() (T, bool)
- func (t *Tree[T]) CurrentRow() (Shown[T], bool)
- func (t *Tree[T]) Do(action keymap.Action) bool
- func (t *Tree[T]) Draw(v Frame)
- func (t *Tree[T]) DrawRows(v Frame, draw func(grid.View, int, Shown[T], bool))
- func (t *Tree[T]) Focus(has bool)
- func (t *Tree[T]) Focused() bool
- func (t *Tree[T]) Handle(ev input.Event) bool
- func (t *Tree[T]) Measure(int) int
- func (t *Tree[T]) Nodes() []Node[T]
- func (t *Tree[T]) Open(at int) bool
- func (t *Tree[T]) Rows() []Shown[T]
- func (t *Tree[T]) Scroll() *Scroll
- func (t *Tree[T]) Select(at int)
- func (t *Tree[T]) Selected() int
- func (t *Tree[T]) SetNodes(nodes []Node[T])
- type Trigger
- type Viewport
- func (p *Viewport) Content() Sized
- func (p *Viewport) Do(action keymap.Action) bool
- func (p *Viewport) Draw(v Frame)
- func (p *Viewport) Focus(has bool)
- func (p *Viewport) Handle(ev input.Event) bool
- func (p *Viewport) Measure(across int) int
- func (p *Viewport) Scroll() *Scroll
- func (p *Viewport) SetContent(content Sized)
- type Widget
Examples ¶
Constants ¶
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.
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.
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.
const ( Accept keymap.Action = "accept" Dismiss keymap.Action = "dismiss" )
The actions a Completion answers to, on top of the list movement inside it.
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.
const ( NextTab keymap.Action = "next-tab" PrevTab keymap.Action = "prev-tab" )
The actions a Tabs answers to.
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.
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.
const ( FocusNext keymap.Action = "focus-next" FocusPrev keymap.Action = "focus-prev" )
The actions a Container answers to: which of its children has the keyboard.
const Activate keymap.Action = "activate"
Activate invokes a button-like control such as a DialogTrigger.
const Close keymap.Action = "close"
Close is what dismisses the top layer of a Stack.
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.
const DefaultHistoryLimit = 1000
DefaultHistoryLimit is how many entries a history keeps when it is not told.
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
DefaultActivationKeys are the two conventional ways to invoke a focused control.
func DefaultCompletionKeys ¶
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
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
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 ¶
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
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 ¶
DefaultListKeys are the keystrokes a terminal list is expected to answer.
func DefaultMultiSelectKeys ¶ added in v0.0.2
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 ¶
DefaultScrollKeys are the keystrokes a terminal reader is expected to answer.
func DefaultSettingsKeys ¶ added in v0.3.0
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
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
DefaultStackKeys are the keystrokes a layered interface is expected to answer.
func DefaultTabsKeys ¶ added in v0.0.2
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
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.
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
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.
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
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.
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
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
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]) Lookup ¶ added in v0.0.2
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.
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) 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) 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) 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
Focus takes the keyboard or gives it up, and checks the answer on the way out.
func (*Confirm) Handle ¶ added in v0.0.2
Handle answers the field, by key or by pressing one of the two answers.
func (*Confirm) Measure ¶ added in v0.0.2
Measure is the label, the two answers on one row, and the problem if there is one.
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
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
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
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
Draw arranges the children and draws each into the room it got.
func (*Container) Focus ¶ added in v0.0.2
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
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
FocusPrev moves the keyboard to the previous child that will take it.
func (*Container) Focused ¶ added in v0.0.2
Focused is the child with the keyboard, or nil when no child will take it.
func (*Container) Give ¶ added in v0.0.2
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
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
Items returns a copy of the children in arrangement and keyboard order.
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
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) 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
SetDescription changes the dialog's semantic description.
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
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.
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
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
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) Copy ¶ added in v0.0.2
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 ¶
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
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
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
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) DrawWith ¶ added in v0.6.0
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
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
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) Focus ¶ added in v0.0.2
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 ¶
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 ¶
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) 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
Mask reports what each text cluster is drawn as. Empty means text is shown.
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
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) RemoveElement ¶ added in v0.0.2
RemoveElement deletes an element's text and forgets it, reporting whether it was there to remove.
func (*Editor) Replace ¶
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
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) 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
Selected is the selected text, or empty when nothing is selected.
func (*Editor) Selection ¶ added in v0.0.2
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
SelectionSpans is where the selection is, or nothing when there is none.
func (*Editor) SetCursor ¶ added in v0.0.2
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
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
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 ¶
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
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
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.
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.
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
Current is the item under the cursor and whether there is one.
func (*Filter[T]) Focus ¶ added in v0.0.3
Focus takes the keyboard, or gives it up — see List.Focus.
func (*Filter[T]) Handle ¶ added in v0.0.2
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]) Measure ¶ added in v0.0.2
Measure is one row per match. Match state is rebuilt by semantic operations, so measuring only observes the last complete projection.
func (*Filter[T]) Scroll ¶ added in v0.0.2
Scroll exposes the position, for a scrollbar drawn beside the list.
func (*Filter[T]) Select ¶ added in v0.0.2
Select moves the cursor to one of the matches, clamped to them.
func (*Filter[T]) Selected ¶ added in v0.0.2
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
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
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
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
Add appends fields and returns the form, so a form can be built in one expression.
func (*Form) Draw ¶ added in v0.0.2
Draw dresses the fields with Look and lays them out down the region.
func (*Form) DrawWith ¶ added in v0.3.0
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) Focus ¶ added in v0.0.2
Focus takes the keyboard, or gives it up, and passes the news to the field that has it.
func (*Form) Handle ¶ added in v0.0.2
Handle gives the event to the field with the keyboard, then to the form itself.
func (*Form) Measure ¶ added in v0.0.2
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
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.
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
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.
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
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
At is the entry n steps back from the end, one being the newest.
func (*History) Back ¶ added in v0.0.2
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
Cancel abandons a walk and reports the draft it began with, if there was one.
func (*History) Forward ¶ added in v0.0.2
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) Recall ¶ added in v0.0.2
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
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.
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 ¶
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]) Do ¶ added in v0.0.2
Do runs one of the list's actions by name, reporting whether it was one this list knows. See Doer.
func (*List[T]) DrawRows ¶ added in v0.3.0
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
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]) Handle ¶
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]) Measure ¶
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]) 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
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 ¶
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 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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 (*Root) Content ¶ added in v0.13.0
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
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
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
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
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
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) By ¶
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
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
Do runs one of the scroll's actions by name, reporting whether it was one a scroll knows. See Doer.
func (*Scroll) Handle ¶
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 ¶
Offset is how many rows are hidden above the window, which is what a scrollbar and a hit test both want.
func (*Scroll) Pages ¶
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
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) ToTop ¶
func (s *Scroll) ToTop()
ToTop shows the start of the content and stops following.
func (*Scroll) Wheel ¶ added in v0.0.2
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 ¶ added in v0.0.2
type Search struct {
// contains filtered or unexported fields
}
Search scans a transcript's text off the interface's goroutine.
Why it is not simply a function ¶
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.
What crosses the goroutine boundary ¶
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
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]) Chosen ¶ added in v0.0.2
Chosen is the option under the cursor, and whether there is one.
func (*Select[T]) Draw ¶ added in v0.0.2
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
Focus takes the keyboard or gives it up, and checks the choice on the way out.
func (*Select[T]) Measure ¶ added in v0.0.2
Measure is the label, the options within their cap, and the problem if there is one.
func (*Select[T]) SetOptions ¶ added in v0.2.0
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.
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) Begin ¶ added in v0.0.2
Begin starts a selection at p and marks a drag in progress.
func (*Selection) Covers ¶ added in v0.0.2
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
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
Dragging reports whether the pointer is still down and the far end still moving.
func (*Selection) Extend ¶ added in v0.0.2
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
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
Do navigates the list or applies a value action to its selected item.
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 ¶
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) Handle ¶ added in v0.3.0
Handle applies bound keys and a left-button drag to the value.
func (*Slider) Move ¶ added in v0.3.0
Move changes the value by a number of steps, saturating at either bound.
func (*Slider) Position ¶ added in v0.3.0
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
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
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) SetStep ¶ added in v0.3.0
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
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
Step returns how much one increase or decrease changes the value. The zero Slider uses one.
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.
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
NewStack constructs a stack over base. The zero Stack has no base and is ready.
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) Do ¶ added in v0.0.2
Do runs one of the stack's actions by name, reporting whether it was one a stack knows. See Doer.
func (*Stack) Draw ¶
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
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 ¶
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 ¶
Pop removes the top layer and reports whether there was one. The keyboard goes back to whatever was underneath.
func (*Stack) Push ¶
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
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.
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.
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) 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
Blocks returns a copy of the pinnable identities in transcript order.
func (*Sticky) DiscardBefore ¶ added in v0.1.0
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.
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
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
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
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
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) Current ¶ added in v0.0.2
Current is the pane that is showing, and whether there is one.
func (*Tabs) Do ¶ added in v0.0.2
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) Focus ¶ added in v0.0.2
Focus takes or releases keyboard ownership and settles it on the selected pane.
func (*Tabs) Handle ¶ added in v0.0.2
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
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) Select ¶ added in v0.0.2
Select shows a pane, clamped to the parts present, and transfers focus with it.
func (*Tabs) Selected ¶ added in v0.0.2
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) Sync ¶ added in v0.1.0
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
Ask is the label, with the placeholder as the hint it already is.
func (*Text) Draw ¶ added in v0.0.2
Draw paints the label, the field and whatever was wrong with the answer.
func (*Text) Editor ¶ added in v0.0.2
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
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
Handle passes input to the field and keeps the value in step with it.
func (*Text) Measure ¶ added in v0.0.2
Measure is the label, a row of text, and the problem with it if there is one.
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 ¶
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 (*Tree[T]) Current ¶ added in v0.0.2
Current is the item under the cursor and whether there is one.
func (*Tree[T]) CurrentRow ¶ added in v0.0.2
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
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]) DrawRows ¶ added in v0.3.0
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
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]) Handle ¶ added in v0.0.2
Handle answers keys, the wheel and a press, reporting whether it consumed the event.
func (*Tree[T]) Measure ¶ added in v0.0.2
Measure is one row per row showing, which is what a container needs to decide how much room to give it.
func (*Tree[T]) Open ¶ added in v0.0.2
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
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
Scroll exposes the position, for a scrollbar drawn beside the tree.
func (*Tree[T]) Select ¶ added in v0.0.2
Select moves the cursor to a row, clamped to what is showing.
func (*Tree[T]) Selected ¶ added in v0.0.2
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
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
NewViewport constructs a window around content.
func (*Viewport) Do ¶ added in v0.0.2
Do runs an action, the content's first and then the window's own. See Doer.
func (*Viewport) Focus ¶ added in v0.0.2
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
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
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
Scroll is the window's position, for a scrollbar drawn beside it.
func (*Viewport) SetContent ¶ added in v0.3.0
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.
Source Files
¶
- commands.go
- completion.go
- container.go
- dialog.go
- editor.go
- editor_element.go
- editor_history.go
- editor_input.go
- editor_kill.go
- editor_layout.go
- editor_line.go
- editor_select.go
- editor_spans.go
- fields.go
- filter.go
- focus.go
- form.go
- frame.go
- headless.go
- history.go
- identity.go
- index.go
- keys.go
- list.go
- modal.go
- ownership.go
- pointer.go
- pointer_region.go
- scroll.go
- search.go
- selection.go
- semantics.go
- settings.go
- slider.go
- spoken.go
- state.go
- sticky.go
- table.go
- tabs.go
- transcript.go
- tree.go
- viewport.go