Documentation
¶
Overview ¶
Package kit is a default appearance for the behaviour in github.com/Tangerg/oolong/components/headless.
It is a default and not a destination. Everything here is an answer to a question headless deliberately refuses — what a border is made of, what a spinner looks like, which grey is muted text — and every one of those answers is a matter of taste that a real product eventually disagrees with. When that happens, the way out is to stop importing this package and keep the one below it, not to fight this one. Nothing in headless imports kit, so that door is always open.
What that buys ¶
An interface can be assembled in a few lines and look deliberate, which is what makes the library possible to evaluate. A library whose shortest working example is two hundred lines of style assignment is one nobody gets far enough into to judge.
The palette ¶
Theme names roles rather than colours: a widget asks for the style of a border or of muted text, never for a particular grey. That is what lets a whole interface change palette in one place, and what stops the same grey from being chosen twice with two slightly different values.
A theme is a value that widgets are given, not a global they reach for. A global palette cannot be varied per pane, and a test cannot pin one.
How a widget is dressed ¶
There is one way, and it is a field:
kit.Transcript{Content: session, Theme: theme, Glyphs: glyphs}
A widget takes a Theme, and a Glyphs as well if it draws furniture — a border, a marker, a rule — because which characters a terminal can draw is a fact about the terminal and not a matter of taste. Neither has a default: a widget given no theme draws in the terminal's own colours, and one given no glyphs draws no furniture, because guessing either would be guessing about a terminal nobody has asked.
No widget carries styles of its own. Every part of a widget has a fixed role in a look — a frame is a border, a pinned header sits on a surface, the match being stepped to is the accent — so a field for each would be a field with one sensible value and a hundred ways to be inconsistent. A caller who wants one of them different passes a theme with that role changed:
quiet := theme quiet.Selection = quiet.Sunken
The exception is text. Label, Paragraph, and the label on Entry take a github.com/Tangerg/oolong/core/grid.Style, because the same words are a heading in one place and a warning in another, and which they are here is the caller's to say and nothing a theme can work out.
Index ¶
- Constants
- func Ask(form *headless.Form, in io.Reader, out io.Writer) error
- type Bar
- type BarChart
- type Border
- type Box
- type Cell
- type Code
- type Column
- type Composer
- type Dialog
- type DialogConfig
- type DialogPanel
- func (d *DialogPanel) Backdrop(v grid.View)
- func (d *DialogPanel) Body() headless.Widget
- func (d *DialogPanel) Draw(v headless.Frame)
- func (d *DialogPanel) Focus(has bool)
- func (d *DialogPanel) Handle(ev input.Event) bool
- func (d *DialogPanel) Place(image.Point) layout.Placement
- func (d *DialogPanel) SetBody(body headless.Widget)
- type Diff
- type DiffConfig
- type Entry
- type Form
- type FormConfig
- type Glyphs
- type Help
- type Image
- type Label
- type LineNumbers
- type LinkConfig
- type Overlay
- type Palette
- type Panel
- type PanelConfig
- type Paragraph
- func (p *Paragraph) Draw(v grid.View)
- func (p *Paragraph) Lines() []text.Line
- func (p *Paragraph) LinkAt(x, y, width int) (link.Link, bool)
- func (p *Paragraph) Measure(width int) int
- func (p *Paragraph) Rows(width int) []text.Row
- func (p *Paragraph) SetLinks(config LinkConfig)
- func (p *Paragraph) SetText(lines []text.Line)
- type Printer
- type Progress
- type Scrim
- type Scrollbar
- type Settings
- type SettingsConfig
- type Slider
- type SliderConfig
- type Sparkline
- type Spinner
- type Status
- type Table
- type TableLayout
- type Tabs
- type TabsConfig
- type Theme
- type Transcript
- type Tree
- type TreeConfig
Examples ¶
Constants ¶
const DefaultComposerRows = 8
DefaultComposerRows is how tall a composer grows before it starts scrolling: enough for a paragraph, few enough to leave the transcript visible above it.
Variables ¶
This section is empty.
Functions ¶
func Ask ¶ added in v0.0.2
Ask puts a form one question at a time, in words.
It is the same form answered without a screen: for somebody reading with a screen reader, for a program whose output is a pipe, and for a script that would rather say "good" than press the down arrow twice. Every field asks what it is asking and says what is wrong with an answer, so what is here is only the conversation: write the question, read the line, say what was wrong, ask again.
A field that has no words for itself stops the conversation rather than being skipped. Skipping it would collect a form with an answer missing and submit it anyway, which is the one outcome worse than refusing.
The form is submitted at the end, and what it says about the answers together is reported as an error. Nothing here draws, so a caller wanting both this and a screen builds the same form twice and uses whichever the terminal turned out to be.
Types ¶
type Bar ¶ added in v0.15.0
Bar is one category in a BarChart. Text is the caller's formatted value; an empty Text reserves no value column. A non-empty value is drawn in full or omitted when the chart is too narrow, never clipped into a different-looking value.
type BarChart ¶ added in v0.15.0
BarChart compares non-negative category values as horizontal bars.
It is passive presentation, not a selection controller. Bars remain caller-owned. Maximum fixes the full extent when positive and finite; any other value derives it from the largest finite positive bar. Values at or below zero draw an empty track, values beyond the maximum fill it, and non-finite values draw empty rather than corrupting the scale.
type Border ¶
type Border struct {
Top, Bottom, Left, Right string
TopLeft, TopRight string
BottomLeft, BottomRight string
}
Border is the set of characters a box is drawn with. The zero Border draws no lines, which is what a box that only pads its content wants.
type Box ¶
type Box struct {
// Theme is the look. A box's parts each have a fixed role in one — the frame is
// a border, the interior is a surface, the title is a heading — so there is
// nothing here for a caller to choose between and no style of its own to set.
Theme Theme
// Glyphs are the characters the frame is drawn with, which is a fact about the
// terminal rather than about the look: a box drawn with line characters on a
// terminal that cannot show them is a box drawn in mojibake.
Glyphs Glyphs
// Border is which frame to draw. The zero value takes the rounded one from the
// glyph set, which reads as a panel; [Box.Bare] draws none.
Border Border
// Padding is held clear inside the border.
Padding layout.Inset
// Bare draws no frame at all, for a box that only pads and fills.
Bare bool
// Title sits in the top border, indented one column so the corner reads as a
// corner.
Title string
TitleAlign layout.Align
// Footer sits in the bottom border, on the same terms as the title.
}
Box frames and pads a region.
It is not a container: it does not own a child. A caller draws the box and then draws into the view the box hands back, which keeps the box out of the question of what goes inside it and lets the same box frame a widget, a string, or nothing. A focusable child that must participate in routing inside the frame belongs in a Panel, which owns that narrower live composition explicitly.
Example ¶
inner := kit.Box{Glyphs: kit.Unicode(), Title: "Plan"}
show(14, 3, func(v grid.View) {
kit.Label{Text: "step one"}.Draw(inner.Draw(v))
})
Output: |╭─Plan───────╮| |│step one │| |╰────────────╯|
func (Box) Draw ¶
Draw paints the frame and returns the region left for content, so the common use reads as one step:
inner := box.Draw(v) content.Draw(inner)
type Cell ¶ added in v0.3.0
type Cell struct {
Preferred int
// Paint runs during drawing and must only paint its view; it must not mutate
// application state, publish output, or start work.
Paint func(view grid.View, base grid.Style)
}
Cell is one table cell's intrinsic width and drawing behaviour.
Keeping the two together is what makes content-fitted columns trustworthy: the value measured is the value later drawn. Preferred is a request rather than a reservation; a narrow table may still give the cell less room.
func LabelCell ¶ added in v0.3.0
LabelCell adapts a Label into a measured table cell.
The row's base style is merged under the label's style, so a selection or band is retained unless the label deliberately replaces it.
type Code ¶ added in v0.3.0
type Code struct {
// Gutter draws beside the source. Nil gives every column to the code.
Gutter headless.RowGutter
// contains filtered or unexported fields
}
Code is a passive block of styled source text.
Styling is supplied as text.Line values, which keeps syntax choice and the optional highlight module outside the component dependency graph. Code owns only the assembly every caller otherwise repeats: wrapping, optional line numbers and copying. A viewport can make a tall block interactive through headless.Static.
type Column ¶
type Column struct {
Title string
Align layout.Align
// Size is how the shared layout allocator sizes this column. Use layout.Fixed
// for an exact width, layout.Flex for a share of what remains, and
// layout.Measured to fit the widest title or cell. The zero value defaults to
// layout.Flex(1), so an unsized column participates in the remaining space.
Size layout.Sizing
}
Column is one column of a Table.
type Composer ¶
type Composer struct {
Theme Theme
// Prompt marks the first row of the field. Empty draws no marker and gives the
// columns back to the text.
Prompt string
// Hints are the actions drawn under the field, in the order they matter. An action
// with nothing bound to it is not shown, so an empty row costs nothing to ask for.
// Bindings come from [Composer.Editor], keeping input and the hint row on one map.
Hints []keymap.Action
// MaxRows caps how tall the field grows before it scrolls. Zero uses
// [DefaultComposerRows].
MaxRows int
// contains filtered or unexported fields
}
Composer is the thing the user types into: a prompt marker, a field that grows with what is in it, and a row of hints under it.
It is the block at the bottom of a streaming interface, and the reason this package exists — assembling one out of headless.Editor, a marker, a placeholder and a hint row is the same forty lines in every program, and forty lines is enough to stop someone finding out whether the library is any good.
Enter is left alone, as headless.Editor leaves it: whether it sends or breaks the line is the interface's decision and not a widget's. Composer.Editor is the one behavior surface for reading, replacing and clearing what was typed.
The zero value is ready. A Composer must not be copied after first use: its editor, pointer routing and presentation cache are one mutable owner.
Example ¶
// One table for what the field does and what the program does. The hint row reads
// the keystroke back out of it rather than being told it a second time.
keys := headless.DefaultEditorKeys()
keys.Bind("send", input.Chord{Code: input.Enter})
c := kit.Composer{
Prompt: "› ",
Hints: []keymap.Action{"send"},
}
c.Editor().Placeholder = "Ask something"
c.Editor().Keys = keys
showWidget(28, c.Measure(28), &c)
Output: |› Ask something | |enter send |
Example (Typing) ¶
var c kit.Composer
c.Prompt = "› "
for _, r := range "hello" {
c.Handle(input.Key{Code: input.Character, Rune: r})
}
fmt.Println(c.Editor().Text())
showWidget(12, 1, &c)
Output: hello |› hello |
func (*Composer) Editor ¶
Editor is the field itself. Its Placeholder, Keys, Clipboard and editing options configure composer input directly. Text, cursor, history and completion operations all remain on that owner instead of acquiring a partial second facade on Composer.
func (*Composer) Focus ¶ added in v0.0.2
Focus takes the keyboard, or gives it up, and passes the news to the field. A composer without it draws no cursor — see headless.Focusable.
func (*Composer) Handle ¶
Handle passes input to the field.
A mouse event is translated into the field's own box first, which is something only whatever drew the composer can do: the field is inset by the marker and the position on screen means nothing to it. The width is remembered from the last frame, because a click can only be about a frame that has already been drawn.
type Dialog ¶
type Dialog struct {
// contains filtered or unexported fields
}
Dialog is the polished composition of a headless.Dialog controller and its appearance part.
The common call site needs only a stack, theme, glyphs, title and body. Controller and Panel expose the two parts without allowing them to be replaced independently.
func NewDialog ¶ added in v0.1.0
func NewDialog(config DialogConfig) *Dialog
NewDialog constructs one dialog with kit defaults from config.
Example (Controlled) ¶
package main
import (
"fmt"
"github.com/Tangerg/oolong/components/headless"
"github.com/Tangerg/oolong/components/kit"
)
func main() {
var open bool
dialog := kit.NewDialog(kit.DialogConfig{
Stack: &headless.Stack{}, Open: headless.Bind(&open), Glyphs: kit.ASCII(),
Title: "Confirm", Body: headless.Static{Of: kit.Label{Text: "Continue?"}},
})
dialog.Controller().Show()
fmt.Println(open)
open = false
dialog.Controller().Sync()
fmt.Println(dialog.Controller().Open())
}
Output: true false
func (*Dialog) Controller ¶ added in v0.1.0
Controller returns the headless dialog that owns open state and semantics.
func (*Dialog) Panel ¶ added in v0.1.0
func (d *Dialog) Panel() *DialogPanel
Panel returns the appearance part installed in the dialog controller.
func (*Dialog) Semantics ¶ added in v0.1.0
func (d *Dialog) Semantics() headless.SemanticNode
Semantics returns the underlying structural semantic projection.
type DialogConfig ¶ added in v0.11.0
type DialogConfig struct {
// Stack owns modal ordering and is required.
Stack *headless.Stack
// Open is optional caller-owned state. Nil starts locally closed.
Open headless.Accessor[bool]
// Theme and Glyphs define the panel appearance.
Theme Theme
Glyphs Glyphs
// Title and Description are copied semantic text; Title also labels the border.
Title string
Description string
// Body is the optional live content inside the panel.
Body headless.Widget
// Where places the panel, and Border selects its frame style.
Where layout.Placement
Border Border
// Keys supplies the map used to draw Hints. Hints is copied.
Keys *keymap.Map
Hints []keymap.Action
}
DialogConfig is the complete construction state of Dialog.
Stack is required. A nil Open gives the underlying controller local ownership; setting it gives ownership to the caller without choosing a second constructor. The remaining fields configure the semantic dialog and its kit appearance together, so the title, panel and controller cannot be assembled out of step.
type DialogPanel ¶ added in v0.1.0
type DialogPanel struct {
Theme Theme
// Where the dialog goes. The zero value centres it and fills what the margin
// leaves, which is what a dialog with a lot in it wants.
Where layout.Placement
// Glyphs are the characters the frame is drawn with. See [Box.Glyphs].
Glyphs Glyphs
// Border draws the frame. The zero value takes the rounded one from the glyph
// set, which reads as a panel.
Border Border
// Keys is where the hints' keystrokes are read from, and Hints are the actions to
// show along the bottom border, where they do not cost a row. An action with
// nothing bound to it is not shown.
Keys *keymap.Map
Hints []keymap.Action
// contains filtered or unexported fields
}
DialogPanel is the kit appearance part of a headless.Dialog.
The behavior — open state, focus restoration, escape and outside-click policy — is owned below by the controller and stack. This part owns only border, palette, placement and body composition. A DialogPanel must not be copied after first use: body focus and committed pointer routing are one appearance owner.
func (*DialogPanel) Backdrop ¶ added in v0.1.0
func (d *DialogPanel) Backdrop(v grid.View)
Backdrop shades what the dialog covers.
It is a separate step from Draw because a layer is handed a view of its own area and nothing else — which is what stops it drawing outside its box, and what makes reaching the space behind it something it has to ask for.
What it paints is the theme's, not the dialog's. Dimming is part of a look and varies with it — a light interface takes less of it than a dark one — so it is held where the rest of the look is, and a dialog given no theme dims nothing.
func (*DialogPanel) Body ¶ added in v0.1.0
func (d *DialogPanel) Body() headless.Widget
Body returns what is inside the frame.
func (*DialogPanel) Draw ¶ added in v0.1.0
func (d *DialogPanel) Draw(v headless.Frame)
Draw paints the frame and the body.
func (*DialogPanel) Focus ¶ added in v0.1.0
func (d *DialogPanel) Focus(has bool)
Focus passes the keyboard to the body, if the body can hold it.
A stack hands the keyboard to the layer on top and expects the layer to pass it on. Without this the news stops at the frame: the dialog is the layer, so a form inside one would never be told it is being typed at, and a field would draw no caret while taking every keystroke.
func (*DialogPanel) Handle ¶ added in v0.1.0
func (d *DialogPanel) Handle(ev input.Event) bool
Handle passes the event to the body, if the body answers input at all.
func (*DialogPanel) Place ¶ added in v0.1.0
func (d *DialogPanel) Place(image.Point) layout.Placement
Place is where the dialog goes, which is what headless.Stack asks.
func (*DialogPanel) SetBody ¶ added in v0.3.0
func (d *DialogPanel) SetBody(body headless.Widget)
SetBody replaces what is inside the frame and transfers keyboard ownership. A body that answers input receives it after the stack chooses this layer.
type Diff ¶ added in v0.0.2
type Diff struct {
// contains filtered or unexported fields
}
Diff draws a change to a text.
It takes hunks rather than lines because a change of three lines in a file of two thousand is a view nobody reads unless the two thousand are left out — and the break between one hunk and the next is how the reader is told they were. diff.Hunks is what makes them.
It is a passive headless.Block. A change too tall for its pane becomes live viewport content through headless.Static, without giving finished content an interaction lifecycle of its own.
Diff owns its hunks and appearance. Mutations go through its methods so the wrapped representation measured and drawn in one frame cannot describe different inputs. Copies detach their private layout on the next layout or mutation.
Example ¶
before := strings.Split("keep\nold\nkeep", "\n")
after := strings.Split("keep\nnew\nkeep", "\n")
view := kit.NewDiff(kit.DiffConfig{
Theme: kit.Dark(), Glyphs: kit.ASCII(),
Hunks: diff.Between(before, after).Hunks(1), Numbers: true,
})
show(16, view.Measure(16), view.Draw)
Output: |1 1 keep | |2 -old | | 2 +new | |3 3 keep |
func NewDiff ¶ added in v0.8.0
func NewDiff(config DiffConfig) *Diff
NewDiff returns a diff that owns a copy of config.Hunks.
func (*Diff) Measure ¶ added in v0.0.2
Measure reports the physical rows needed at width. Long lines wrap through the same layout Draw consumes, so measurement cannot promise rows drawing truncates.
func (*Diff) SetGlyphs ¶ added in v0.8.0
SetGlyphs changes the characters used between hunks and beside continuation rows.
func (*Diff) SetHunks ¶ added in v0.8.0
SetHunks replaces the parts of the change worth showing. The input and its text are copied, so changing or releasing the source cannot change or retain the component.
func (*Diff) SetNumbers ¶ added in v0.13.0
SetNumbers controls whether each line's number in both texts appears down the left. Without numbers only the marks remain, which is what a narrow pane has room for.
type DiffConfig ¶ added in v0.13.0
DiffConfig is the complete initial state of a Diff. Its zero value constructs an empty, unnumbered diff with the package's zero appearance.
type Entry ¶ added in v0.13.0
type Entry struct {
Theme Theme
// Label names the source or role of the body. Empty draws no label and no gutter.
Label string
// LabelStyle overlays Theme.Muted. The zero value keeps the ordinary receding
// label; a caller that gives a product role visual emphasis supplies that role's
// style explicitly.
LabelStyle grid.Style
// Body is the text. Newlines are line breaks; everything else wraps.
Body string
// Trailing is how many blank rows follow, so consecutive entries do not run
// together. Zero uses one, which is what separates without doubling the spacing.
Trailing int
// contains filtered or unexported fields
}
Entry is one finished, labelled piece of text, ready for a transcript or the terminal's own scrollback.
It is the stable half of a streaming interface. A recent entry may remain in a selectable transcript while interaction is worth its cost; an older one may be committed to the terminal, where it survives the program. The same passive Block works in both places and is measured before it is drawn because publication needs a row count:
e := &kit.Entry{Theme: th, Label: "build", Body: line}
output.Print(e)
It owns no interaction lifecycle or routing state. Its transcript or printer decides how long it remains live. Entry is a pointer only because wrapping is memoised between Measure and Draw; that private cache is presentation state.
Entry knows only labelled text. Whether a label denotes a person, process, log source or anything else is application grammar and remains with the caller.
Example ¶
m := kit.Entry{Label: "build", Body: "what changed"}
show(20, m.Measure(20), m.Draw)
Output: |build | | what changed | | |
type Form ¶ added in v0.0.2
type Form struct {
// Theme is the look, and Glyphs the characters the marks beside a choice are drawn
// with.
Theme Theme
Glyphs Glyphs
// Title sits above the fields. Empty draws none and costs no row.
Title string
// Hints are the controller actions to show under the fields. Their keystrokes
// always come from the controller's own map, so help cannot disagree with input.
// An action with nothing bound to it is not shown.
Hints []keymap.Action
// contains filtered or unexported fields
}
Form dresses a headless.Form: a title, the fields, and a row of hints under them.
The fields draw themselves, because a field is generic over what it holds and nothing here could name every kind of one — see headless.Look. So this projects the theme as the handful of roles a field has, without changing the controller's own look. A Form must not be copied after first use: its controller and committed pointer routing are one dressed component owner.
Example ¶
// A theme becomes the handful of roles a field draws itself in, and a glyph set
// becomes the marks beside a choice. That is the whole of dressing a form.
var name, model string
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), Placeholder: "who?"},
modelField,
)
keys := headless.DefaultFormKeys()
form.Keys = keys
view := kit.NewForm(kit.FormConfig{
Theme: kit.Dark(), Glyphs: kit.ASCII(), Controller: form, Title: "New session",
Hints: []keymap.Action{headless.Submit},
})
showWidget(22, view.Measure(22), view)
Output: |New session | |Name | |who? | |Model | |x fast | |- good | |enter submit |
func NewForm ¶ added in v0.3.0
func NewForm(config FormConfig) *Form
NewForm dresses one headless form without creating a second behavior surface.
FormConfig.Controller is required; a nil one is a programmer error and panics here. Supplying a default would be this package creating the behavior surface it exists not to own, and the caller would then hold a form whose fields answer to state they cannot reach.
func (*Form) Controller ¶ added in v0.3.0
Controller returns the headless form that owns fields and submission behavior.
func (*Form) Focus ¶ added in v0.0.3
Focus passes the keyboard to the form — see headless.Form.Focus.
func (*Form) Handle ¶ added in v0.0.3
Handle gives the event to the form, with a press moved up by the rows this drew above it.
That translation is the whole reason this answers events at all. A field told a press was two rows further down than it was is a field that puts the caret in the wrong place, and it is the sort of mistake that only shows as "clicking the second field selects the first".
type FormConfig ¶ added in v0.11.0
type FormConfig struct {
// Theme and Glyphs define field roles and choice marks.
Theme Theme
Glyphs Glyphs
// Controller owns fields, keys, validation and submission and is required.
Controller *headless.Form
// Title is copied. Empty omits the heading row.
Title string
// Hints is copied and resolved through Controller.Keys.
Hints []keymap.Action
}
FormConfig is the complete construction state of Form.
Controller is required and remains the sole owner of fields, keys, validation and submission. A nil Controller.Keys reads through the standard form map for both behavior and visible hints; dressing the form does not mutate its configuration.
type Glyphs ¶ added in v0.0.2
type Glyphs struct {
// The lines a box is drawn with, rounded and square.
Horizontal, Vertical string
TopLeft, TopRight, BottomLeft, BottomRight string
RoundTopLeft, RoundTopRight string
RoundBottomLeft, RoundBottomRight string
// Ellipsis stands for text that did not fit.
Ellipsis string
// Bullet marks an item in a list, and Marker the one under the cursor.
Bullet, Marker string
// Taken and Free are the two states of a choice: one that has been made, and one
// that is still on offer. They are a pair and are drawn in the same column, so
// they have to be the same width or the labels beside them do not line up.
Taken, Free string
// ScrollTrack and ScrollThumb are the two halves of a scrollbar.
ScrollTrack, ScrollThumb string
// SliderTrack is the line a bounded value moves over and SliderThumb is its current
// position. They are separate from a scrollbar because one is a control and the
// other reports a viewport.
SliderTrack, SliderThumb string
// BarFull and BarEmpty are the two halves of a progress bar, and BarSteps are the
// pieces of a cell it is part way through — from the narrowest to the widest, in
// order, with as many of them as the set has. A set with none draws the last cell
// whole or not at all, which is what an eight-step bar degrades to and not a
// separate design.
BarFull, BarEmpty string
BarSteps []string
// Sparkline is the ordered set of sample heights, from lowest to highest.
Sparkline []string
// Expanded and Collapsed are the two states of a branch in a tree: one showing
// what is under it, and one inviting the reader to look. Like Taken and Free they
// are drawn in the same column and have to be the same width.
Expanded, Collapsed string
// Ascending and Descending mark the column a table is sorted by.
Ascending, Descending string
// Spinner is the frames of a busy indicator, in order.
Spinner []string
}
Glyphs are the characters a look draws its furniture with.
Why they are gathered ¶
Not every terminal can draw them. A terminal running in a locale that is not UTF-8 shows a box-drawing character as a question mark or as two bytes of mojibake, and a panel drawn in mojibake is worse than a panel drawn in dashes. There is no way to ask a terminal about this, so the answer is the locale — which is a fact about the environment, and therefore an argument rather than something read here.
Gathering them is what makes one answer possible. Scattered through the widgets, each would need its own fallback and the set would be inconsistent the first time somebody added a glyph and forgot one.
The zero value draws nothing, which is not useful: Unicode and ASCII are the two sets, and GlyphsFor picks between them.
func ASCII ¶ added in v0.0.2
func ASCII() Glyphs
ASCII is the set for a terminal that cannot, and for output that is going somewhere other than a terminal at all.
It is not a transliteration of the other set. A rounded corner has no ASCII equivalent and is drawn square; an ellipsis is three stops, which is wider, and the widgets that use it measure rather than assume. The point is that it is readable, not that it looks the same.
func GlyphsFor ¶ added in v0.0.2
GlyphsFor picks the set a terminal in this locale can draw.
The test is the locale, because there is no other. A terminal cannot be asked whether it will render a box-drawing character, and the one thing that reliably decides it is whether the environment says UTF-8: outside it, a multi-byte glyph arrives as bytes the terminal draws one at a time.
The resolved locale is passed rather than an environment lookup because choosing which environment belongs to a terminal is a transport concern. This appearance package only interprets the fact it was given.
func Unicode ¶ added in v0.0.2
func Unicode() Glyphs
Unicode is the set for a terminal that can draw them, which is nearly all of them.
type Help ¶
type Help struct {
// Theme is the look. Each part of a hint has a fixed role in one — the key is
// the thing to press, what it does is a note about it — so there is nothing here
// to choose between.
Theme Theme
// Keys is where the keystrokes come from.
Keys *keymap.Map
// Show are the actions to show, in the order they matter. Hiding one is leaving it
// out: an action that works and that nobody needs told about is simply not listed.
//
// Each identifier is shown as words by this appearance layer; the key map carries
// only identity and binding policy.
Show []keymap.Action
// Separator sits between hints. Empty uses two spaces, which separates without
// adding another thing to look at.
Separator string
}
Help is a row of key hints.
The keystrokes are read out of the same map the handlers read, so a hint cannot disagree with what the key does: there is one place a key is bound, and this asks it. An action with nothing bound to it is not shown, because a hint nobody can press is worse than no hint.
type Image ¶ added in v0.0.3
type Image struct {
// Of is the picture, as the terminal knows it. The zero value is no picture,
// which draws the alternative text.
Of graphics.Image
// Cell is how many pixels one terminal cell is, as reported by the host.
// The zero value is a terminal that never said, which draws the alternative text:
// a picture scaled by an invented cell size is a picture the wrong shape.
Cell image.Point
// MaxRows caps how tall the picture may be, so one from somewhere else cannot
// take the whole screen. Zero is a cap of eight rows, which is about a third of a
// terminal and small enough to read around.
MaxRows int
// Alt is what to say where the picture cannot be shown.
Alt string
// Align is where a picture narrower than its space sits.
Align layout.Align
// Theme is the look of the alternative text, which is text present for reference
// rather than for reading.
Theme Theme
}
Image shows a picture the terminal has already been given.
The sending happens once in the host adapter, which hands back the handle that goes in Image.Of. This value places it: it works out how many cells the picture should have at the width it is drawn in, keeps the room for it, and lets the frame do the rest.
When there is no picture to show ¶
Which is often. A terminal may not show pictures at all, may not say how big a cell is, or may be an interface being written to a file. All three end in the same place, and Image.Alt is what is drawn there — the alternative text a document gave, which is exactly what it was written for.
Whether the terminal can is not this widget's to ask: it is one question at startup and the answer is a program's, so a caller that passes a handle it never obtained gets the alternative and no harm.
func (Image) Draw ¶ added in v0.0.3
Draw keeps the room the picture needs, or writes the alternative text.
type Label ¶
type Label struct {
Text string
Style grid.Style
Align layout.Align
// Ellipsis marks a truncation. Empty means truncate silently, which is right for
// a value the user can see in full elsewhere and wrong for prose.
Ellipsis string
}
Label is one line of text that does not wrap.
Text too wide for its space is truncated rather than folded, because a label is used where exactly one row is available — a header, a status field, a table cell — and a label that grew to two rows would push whatever is below it off the screen.
type LineNumbers ¶ added in v0.3.0
type LineNumbers struct {
Style grid.Style
// First is the number of source line one. Zero uses one, which is how a whole
// document is numbered; a snippet beginning later sets the actual first line.
First int
// Separator is drawn after the number column. Empty draws none.
Separator string
// Gap is the clear space after Separator. Zero uses one column.
Gap int
}
LineNumbers draws the logical source line beside visual rows.
Continuation rows are deliberately blank: a number names a logical line, not every row the current width happened to wrap it into. It implements headless.RowGutter, so the same value dresses an editor and a passive Code block without either component learning the other's layout.
func (LineNumbers) Draw ¶ added in v0.3.0
func (n LineNumbers) Draw(view grid.View, rows []text.Row)
Draw paints one number for each logical line beginning in rows.
func (LineNumbers) Width ¶ added in v0.3.0
func (n LineNumbers) Width(lines int) int
Width is the stable gutter width for this many logical lines.
type LinkConfig ¶ added in v0.11.0
LinkConfig says which destinations a Paragraph detects.
Enabled finds URLs and file paths. Exists additionally decides whether ambiguous bare names such as "main.go" are files; nil leaves those names as prose. The callback runs only from Paragraph.SetLinks and Paragraph.SetText, never from Measure, Draw or Paragraph.LinkAt, so it may consult the filesystem without turning frame projection into I/O.
The zero value disables detection.
type Overlay ¶
type Overlay struct {
layout.Placement
// Theme is the look, of which an overlay uses exactly one part: the scrim it
// paints over what it covers, so the eye goes to the layer and it is obvious that
// what is behind is not the thing to act on. A zero theme covers nothing.
Theme Theme
}
Overlay floats one thing over another.
It is placement and dimming, not a container: it works out where a layer goes and hands back the view to draw it in. What goes inside is the caller's, which is what lets the same overlay carry a dialog, a completion list, or a single line of warning.
Where the layer goes is layout.Placement's answer and nothing is added to it here but the part that has an appearance. A hit test that needs the same rectangle a frame later asks the placement, which is why that lives a layer down and holds no styling of its own.
type Palette ¶ added in v0.0.2
type Palette struct {
// Found is what to show, best first, as the registry ranked it.
Found []headless.Found
// Selected is the index under the cursor.
Selected int
// Theme is the look. Every part of a row has a fixed role in one — a name is
// text, the row under the cursor is the selection, a matched letter is the accent
// — so there is nothing here to choose between.
Theme Theme
// Glyphs are the characters the marker is drawn with, which is a fact about the
// terminal rather than about the look.
Glyphs Glyphs
// Empty is what to say when nothing matched. Empty says so in words, because a
// blank space reads as a bug rather than as an answer.
Empty string
}
Palette draws a list of commands a query matched, with the matched characters picked out.
Picking them out is most of the value. A list of names that all look equally like the query tells a user nothing about why any of them is there, and with subsequence matching the reason is often several letters apart — showing which letters matched is what makes "ns" finding "new-session" obvious instead of surprising.
type Panel ¶ added in v0.1.1
type Panel struct {
// Box is the panel's frame, fill, padding, title, and footer. Replacing it changes
// appearance and interior geometry, never the child's behavior.
Box Box
// contains filtered or unexported fields
}
Panel puts a framed surface around one focusable widget.
It is the live counterpart to Box. Box is deliberately only chrome: it can frame text, a widget, or nothing, and therefore owns no child or interaction lifecycle. A Panel is deliberately narrower. Its child can take the keyboard, so the panel can preserve that capability, translate pointer coordinates through the frame, and participate in a headless.Container without pretending a passive child is focusable.
The zero Panel has no child and draws the zero Box. A Panel must not be copied after first use: its child focus and committed pointer routing are one mutable owner.
func NewPanel ¶ added in v0.1.1
func NewPanel(config PanelConfig) *Panel
NewPanel returns a panel with config's frame and content.
func (*Panel) Handle ¶ added in v0.1.1
Handle passes keys to the child and translates pointer events into the interior's coordinates. A press over the frame itself is declined.
type PanelConfig ¶ added in v0.13.0
PanelConfig is the complete initial state of a Panel. Box owns appearance and interior geometry; Content owns the interaction lifecycle inside it. The zero value constructs an empty panel.
type Paragraph ¶
type Paragraph struct {
// Indent is held clear on the left of every row, including continuations, so a
// wrapped paragraph reads as one block rather than as several.
Indent int
// MaxRows caps the height. Zero means no cap; a cap replaces the last row it
// keeps with one ending in an ellipsis.
MaxRows int
// contains filtered or unexported fields
}
Paragraph is text that wraps to the width it is given.
Its height is not known until its width is, which is the whole reason a passive headless.Block measures itself: publication and a headless.Static viewport adapter both have to ask before they can decide how much room to give. Copies detach their private wrap on the next layout or text change.
func NewParagraph ¶
NewParagraph is a paragraph of one plain styled string. Its newlines are line breaks.
func (*Paragraph) LinkAt ¶ added in v0.0.2
LinkAt is the complete destination at a position when the paragraph is laid out at width, and whether there is one. File line and column information is retained rather than flattened into the target string.
Width is explicit because a passive Block has no committed routing lifecycle. The answer is a pure projection of the same wrapped rows Draw uses, so measuring or drawing cannot publish hidden hit-test state.
func (*Paragraph) Measure ¶
Measure is how many rows the paragraph needs at this width. A width no larger than Indent is measured with one text column: there is nothing to draw yet, but reporting no content would let a parent collapse the paragraph permanently.
func (*Paragraph) Rows ¶ added in v0.0.2
Rows is what the paragraph says, one entry per drawn row, so a selection over it can be copied.
Each row carries what the wrap consumed at the break above it, which is why the wrap records where every row came from. Between words it swallowed a space, and splitting a word too long for the row swallowed nothing; neither is recoverable from the rows afterwards, and guessing wrong either runs two words together or breaks a word in half.
func (*Paragraph) SetLinks ¶ added in v0.11.0
func (p *Paragraph) SetLinks(config LinkConfig)
SetLinks replaces link-detection policy and detects the current logical lines. Detection belongs to this explicit semantic operation rather than layout: changing terminal width can then reflow and draw the paragraph without repeating a caller's filesystem work or any other effect hidden behind Exists.
type Printer ¶ added in v0.0.2
type Printer interface {
// Print draws content into the terminal's own output, above the interface, where
// it stays after the program exits.
Print(content grid.Drawable)
}
Printer is somewhere finished output can be put permanently.
It is declared here because Commit is its consumer. Concrete output transports can satisfy it without this package depending on them.
type Progress ¶ added in v0.0.2
type Progress struct {
// Theme is the look: the bar is the thing the interface is about, its track is
// structure to skip, and the numbers beside it are for reference.
Theme Theme
// Glyphs are what the bar is drawn with. A progress bar is furniture, so one
// given no glyphs draws nothing — see the package documentation.
Glyphs Glyphs
// Done and Total are the work: how much of it is finished, and how much there is
// altogether. A total of zero draws an empty bar rather than a full one, which is
// the honest reading of "nothing to do yet".
Done, Total int
// Label goes before the bar, truncated to fit. It is what the work is, and it
// belongs here rather than above because a bar with no name is a bar nobody can
// act on when two of them are on screen.
Label string
// Percent writes the fraction after the bar as a number. The field is omitted
// when it cannot fit in full; clipping a percentage could display another,
// plausible percentage.
Percent bool
}
Progress shows how far along something with a total is.
It is the other half of Spinner, and the two are told apart by one question: is there a total? Work with one gets a bar, which says how much is left; work without one gets a spinner, which can only say that something is happening. A bar drawn for work whose size is unknown has to invent a number, and the number it invents is the one the user plans around.
It holds nothing. The counts are the caller's, because they are already somewhere — a byte count, a file count, a test count — and a widget that kept its own copy would be a second place for them to be wrong.
Example ¶
// Work with a total gets a bar; work without one gets a Spinner. The cell the bar
// ends in is drawn as a fraction of itself, so it moves by eighths of a column
// rather than in jumps of a whole one.
p := kit.Progress{
Glyphs: kit.Unicode(),
Label: "fetching",
Done: 3,
Total: 8,
Percent: true,
}
show(28, p.Measure(28), p.Draw)
Output: |fetching █████▎░░░░░░░░ 38%|
func (Progress) Draw ¶ added in v0.0.2
Draw writes the label, the bar and the percentage into the first row of v.
func (Progress) Finished ¶ added in v0.0.2
Finished reports whether the work is over, which is what a caller asks before deciding whether the bar is still worth showing.
type Scrim ¶ added in v0.0.2
type Scrim struct {
// Color is what the sheet is made of, and Opacity how much of it shows, from 0
// for nothing to 1 for paint.
Color grid.Color
Opacity float64
}
Scrim is a translucent sheet of colour, painted over a region to make it recede.
The zero value covers nothing, so a widget that was given no theme dims nothing — which is the right answer, because dimming toward a colour nobody chose is a guess about what the interface looks like.
func (Scrim) Over ¶ added in v0.0.2
Over paints the scrim across everything the view covers.
Where a cell's colour is the terminal's own it is resolved first, which needs the terminal to have said what that is — see grid.View.Blend. Nothing else here has to know that, which is why this is one line and lives at the bottom of the layer rather than in every widget that floats something.
type Scrollbar ¶
type Scrollbar struct {
// Total is how many rows of content there are, Window how many are shown, and
// Offset how many are above the window.
Total, Window, Offset int
// Theme is the look: the track is structure the eye should skip, and the thumb
// is where you are.
Theme Theme
// Glyphs supplies the track and thumb. A zero set draws nothing, following the
// same coverage rule as every other piece of kit furniture.
Glyphs Glyphs
}
Scrollbar shows where a window sits in something taller than itself.
It is drawn, not interacted with. Scrolling is the business of whatever owns the content and the keys; this only says where you are, which is the part a user cannot work out for themselves.
type Settings ¶ added in v0.3.0
type Settings[T any] struct { Theme Theme // Label and Value are the two columns shown for an item. Nil returns an empty // column. Both run during measurement and drawing and must be observationally // pure; Change is the event-side mutation boundary. Label func(T) string Value func(T) string // ValueWidth caps the fitted value column. Zero leaves it uncapped. ValueWidth int // contains filtered or unexported fields }
Settings dresses a browsable list of application-owned values.
headless.Settings owns selection, scrolling and action routing. Label and Value are projections supplied by the application, so this component can display any setting without naming a product schema or storing a second copy of it. The value column is fitted to its widest cell and the label receives the remaining width.
func NewSettings ¶ added in v0.3.0
func NewSettings[T any](config SettingsConfig[T]) *Settings[T]
NewSettings builds a settings list around one application-owned item slice.
func (*Settings[T]) Controller ¶ added in v0.3.0
Controller returns the headless settings list that owns selection and actions.
func (*Settings[T]) Draw ¶ added in v0.3.0
Draw paints the visible rows with one shared table layout.
type SettingsConfig ¶ added in v0.11.0
type SettingsConfig[T any] struct { // Theme defines row and value appearance. Theme Theme // Items are copied into the constructed controller. Items []T // Label and Value project the two visible columns. Nil produces an empty column. // They run during measurement and drawing and must be observationally pure. Label func(T) string Value func(T) string // Change applies a value action. Nil makes values read-only. Change func(index int, item T, action keymap.Action) bool // Keys browse rows; EditKeys change the selected value. Nil uses the respective defaults. Keys *keymap.Map EditKeys *keymap.Map // Wrap moves row selection across the ends. Wrap bool // ValueWidth caps the right column. Zero leaves it uncapped. ValueWidth int }
SettingsConfig is the complete construction state of Settings.
Keys browse the list and EditKeys change the selected value. Keeping both in this value makes their different responsibilities explicit without hiding either in a positional function argument.
type Slider ¶ added in v0.3.0
type Slider struct {
// Theme and Glyphs choose the appearance of the track and thumb.
Theme Theme
Glyphs Glyphs
// Format turns the integer value into its right-hand label. Nil uses strconv.Itoa;
// returning an empty string omits the value and leaves that room to the track. A
// non-empty value is likewise omitted when it cannot fit in full, because clipped
// digits can read as another value. Format runs during drawing and must be an
// observationally pure projection.
Format func(int) string
// contains filtered or unexported fields
}
Slider is the polished one-row appearance of a headless.Slider.
Its headless controller owns the value and every interaction. This layer formats the label and value, paints the track, and stages exactly the rectangle it painted so pointer routing and appearance cannot disagree.
func NewSlider ¶ added in v0.3.0
func NewSlider(config SliderConfig) *Slider
NewSlider constructs the kit appearance and its sole headless controller.
Example (Controlled) ¶
package main
import (
"fmt"
"github.com/Tangerg/oolong/components/headless"
"github.com/Tangerg/oolong/components/kit"
)
func main() {
value := 2
slider := kit.NewSlider(kit.SliderConfig{
Glyphs: kit.ASCII(), Value: headless.Bind(&value),
Minimum: 1, Maximum: 8, Label: "workers",
})
slider.Controller().Move(2)
fmt.Println(value)
value = 8
slider.Controller().Sync()
fmt.Println(slider.Controller().Value())
}
Output: 4 8
func (*Slider) Controller ¶ added in v0.3.0
Controller returns the headless slider that owns value and interaction state.
func (*Slider) Semantics ¶ added in v0.3.0
func (s *Slider) Semantics() headless.SemanticNode
Semantics forwards the controller's structural meaning.
type SliderConfig ¶ added in v0.11.0
type SliderConfig struct {
// Theme and Glyphs define the track appearance.
Theme Theme
Glyphs Glyphs
// Value is optional caller-owned state. Nil starts local state at Minimum.
Value headless.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 at the left of the track and in semantics.
Label string
// Keys maps slider actions. Nil uses the headless defaults.
Keys *keymap.Map
// Format renders the value at the right. Nil uses strconv.Itoa. It runs during
// drawing and must be observationally pure.
Format func(int) string
}
SliderConfig is the complete construction state of Slider.
Value chooses state ownership without selecting a second constructor. Nil starts a locally owned value at Minimum. Format is optional and has the same zero meaning as Slider.Format.
type Sparkline ¶ added in v0.15.0
type Sparkline struct {
Theme Theme
Glyphs Glyphs
Values []float64
// Minimum and Maximum fix the vertical domain when both are finite and
// Maximum is greater than Minimum. Any other pair derives the domain from the
// visible samples, which makes the zero value the automatic mode.
Minimum, Maximum float64
}
Sparkline is a one-row shape of the most recent numeric samples.
It is passive presentation: Values remain caller-owned and may be replaced between frames. When more samples exist than columns, the newest ones are shown. Their finite minimum and maximum define the visible scale; a constant non-zero series is measured against zero so its magnitude remains visible. NaN and infinities leave their columns blank rather than inventing a height.
type Spinner ¶
type Spinner struct {
// Theme is the look: the glyph is what the interface is waiting on, and the
// label is a note about it.
Theme Theme
// Glyphs supplies the coverage-aware frames. A set with no frames draws the label
// alone, following the package-wide rule that zero glyphs invent no furniture.
Glyphs Glyphs
// Label follows the glyph, if there is room.
Label string
// contains filtered or unexported fields
}
Spinner shows that something is happening.
It holds a frame number rather than a clock. The caller decides when time passes, which is what lets a test step it deterministically and what keeps an idle UI from waking up to animate something nobody is waiting for.
type Status ¶
type Status struct {
Theme Theme
Glyphs Glyphs
// Doing is what is happening, in the present tense — "thinking", "reading
// main.go", "running tests".
Doing string
// Elapsed is shown after the label, receding. Empty shows nothing, which is right
// until something has taken long enough to be worth reporting.
Elapsed string
// contains filtered or unexported fields
}
Status is the line that says something is happening: a spinner, what it is doing, and how long it has been at it.
It holds no clock. The caller advances it with Status.Tick, which is what lets a test step it deterministically and what keeps an idle interface from waking up to animate something nobody is waiting for.
type Table ¶
type Table struct {
// Theme is the look. A column title is a heading and there is nothing else here
// with a fixed role: what a row looks like is data, not a look, which is why
// RowStyle is a function of the row.
Theme Theme
Columns []Column
// Rows is how many rows there are.
Rows int
// Cell returns one cell for a row and column. The value carries both the width a
// fitted column measures and the painter given its final box.
//
// The plain-text case is:
//
// Cell: func(row, col int) kit.Cell {
// return kit.LabelCell(kit.Label{Text: data[row][col],
// Align: columns[col].Align, Ellipsis: "…"})
// }
//
// A custom cell uses a Cell literal. Its painter receives the row's base style —
// a band or selection — because replacing the cells over a filled row without it
// would erase the band. Cell and Paint are projection callbacks: they may run
// repeatedly during measurement and drawing and must be observationally pure.
Cell func(row, column int) Cell
// Gap is the space between columns. Zero uses one column, which is the least
// that still reads as two columns rather than one.
Gap int
// Header draws the column titles in the first row.
Header bool
// Sorted says which column the rows are in the order of and which way round, when
// they are in an order at all, so the header can mark it — the only way a reader
// can tell an order from a coincidence.
//
// It is a function because that is the shape the answer already has: a table with
// a cursor answers exactly this, so wiring the two together is
// Sorted: rows.Sorted. And because the zero value of a column number is a column,
// which would mark the first one on every table nobody sorted.
// Sorted may run during drawing and must only observe ordering state.
Sorted func() (column int, descending, ok bool)
// Glyphs are the marks beside a sorted column's title. A table given none marks
// nothing, which is the rule the whole package keeps.
Glyphs Glyphs
// RowStyle styles a whole row, for banding or for a selection. It is a pure
// projection callback and may run during drawing.
RowStyle func(row int) grid.Style
}
Table lays out rows of cells in columns.
It is layout, not storage: the caller says how many rows there are and what a cell contains, and the table works out where each one goes. That keeps the row data wherever it already lives instead of copied into a widget, and it means a cell can be as elaborate as its own draw function likes.
func (Table) Layout ¶ added in v0.3.0
func (t Table) Layout(width int) TableLayout
Layout measures the columns and fixes their boxes at width.
type TableLayout ¶ added in v0.3.0
type TableLayout struct {
// contains filtered or unexported fields
}
TableLayout is one table's column geometry at one width.
Computing it once matters for content-fitted columns: finding the widest cell is linear in the rows, and drawing every row must not repeat that scan. A composed table also keeps this value as its committed hit-test geometry.
func (TableLayout) Cells ¶ added in v0.3.0
Cells draws one row using this layout. Base is the row's band or selection and is handed to every cell for the reason Table.Cell gives.
func (TableLayout) ColumnAt ¶ added in v0.3.0
func (l TableLayout) ColumnAt(x int) (int, bool)
ColumnAt reports which column contains x in this layout. A press in a gap is in neither. Keeping the accepted frame's layout makes a heading click answer about the same boxes the reader saw.
func (TableLayout) Titles ¶ added in v0.3.0
func (l TableLayout) Titles(v grid.View)
Titles draws the headings using this layout. A table with a cursor keeps the layout for its visible rows and for committed heading hit tests, so all three use the same boxes.
func (TableLayout) Widths ¶ added in v0.3.0
func (l TableLayout) Widths() []int
Widths returns the column widths. The caller owns the result.
type Tabs ¶ added in v0.0.2
type Tabs struct {
// Theme is the look: the name of the pane showing is the thing the interface is
// about, and the rest are there for reference.
Theme Theme
// Glyphs draw the rule under the strip. A tabs given none draws no rule, which is
// the rule the whole package keeps.
Glyphs Glyphs
// Rule draws a line under the strip, which is what makes a strip of names read as
// tabs rather than as a row of words.
Rule bool
// contains filtered or unexported fields
}
Tabs draws a strip of names above the pane that is showing.
The strip is the part headless.Tabs refuses to have an opinion about — where it goes, what marks the one selected, whether there is a rule under it — and this is one answer to it. A press on a name selects that pane; everything else is offered to the pane, in the pane's own coordinates.
A Tabs value must not be copied after first use: its controller, committed strip geometry and pane pointer routing are one dressed component owner.
func NewTabs ¶ added in v0.1.0
func NewTabs(config TabsConfig) *Tabs
NewTabs constructs the kit's finished tab strip and its sole headless controller.
Example (Controlled) ¶
package main
import (
"fmt"
"github.com/Tangerg/oolong/components/headless"
"github.com/Tangerg/oolong/components/kit"
)
func main() {
selected := 1
tabs := kit.NewTabs(kit.TabsConfig{
Glyphs: kit.ASCII(), Selection: headless.Bind(&selected),
Items: []headless.Tab{{Title: "one"}, {Title: "two"}},
})
fmt.Println(tabs.Controller().Selected())
tabs.Controller().Select(0)
fmt.Println(selected)
selected = 1
tabs.Controller().Sync()
fmt.Println(tabs.Controller().Selected())
}
Output: 1 0 1
func (*Tabs) At ¶ added in v0.0.2
At is which tab a column of the strip belongs to, and whether it belongs to one: the room between two names is in neither.
func (*Tabs) Controller ¶ added in v0.3.0
Controller returns the headless tabs that own selection and pane behavior.
func (*Tabs) Draw ¶ added in v0.0.2
Draw paints the strip, the rule under it, and the pane in what is left.
func (*Tabs) Focus ¶ added in v0.0.3
Focus passes the keyboard to the panes — see headless.Tabs.Focus — so that a dressed strip is a widget a container can hold like any other.
func (*Tabs) Handle ¶ added in v0.0.2
Handle sends a press on the strip to the tab it landed on, and everything else to the pane — moved up by the rows the strip took, so the pane is handed a position in its own box.
That translation is the whole reason this type answers events at all. A pane told a press was two rows further down than it was is a pane that answers the wrong click, and it is the sort of mistake that only shows up as "the second row selects the first".
type TabsConfig ¶ added in v0.11.0
type TabsConfig struct {
// Theme and Glyphs define the strip appearance.
Theme Theme
Glyphs Glyphs
// Items are copied into the constructed headless controller.
Items []headless.Tab
// Selection is optional caller-owned state. Nil keeps state local.
Selection headless.Accessor[int]
// Keys maps tab actions. Nil uses the headless defaults.
Keys *keymap.Map
// NoWrap stops movement at the ends. NoRule hides the separator below the strip.
NoWrap, NoRule bool
}
TabsConfig is the complete construction state of Tabs.
Selection chooses state ownership in the same configuration that chooses content and appearance. Nil keeps selection local; a non-nil accessor gives ownership to the caller. NoRule suppresses the rule under the tab strip; its zero value keeps the kit default.
type Theme ¶
type Theme struct {
// Text is body text, and Muted is text present for reference rather than for
// reading — timestamps, paths, counts.
Text grid.Style
Muted grid.Style
// Subtle is quieter than muted: structure the eye should skip unless it is
// looking for it.
Subtle grid.Style
// Strong is emphasis within body text.
Strong grid.Style
// Heading titles a pane or a section.
Heading grid.Style
// Accent marks the thing the interface is about: the active item, the key in a
// hint, the prompt marker.
Accent grid.Style
// Success, Warning, Danger and Info are outcomes. They are the only colours in
// the interface that carry meaning on their own, which is why there are exactly
// four of them.
Success grid.Style
Warning grid.Style
Danger grid.Style
Info grid.Style
// Border draws a frame, and Divider a line between things inside one.
Border grid.Style
Divider grid.Style
// Selection is the row under the cursor.
Selection grid.Style
// Surface is a pane's background, and Sunken a well inside one — a tool's output,
// a code block.
Surface grid.Style
Sunken grid.Style
// Added and Removed are the two halves of a diff. Nothing else in the interface
// uses green and red on a background, so a diff is recognisable at a glance.
Added grid.Style
Removed grid.Style
// Context is a diff line that did not change.
Context grid.Style
// Scrim is what a layer paints over what it covers, so the eye goes to the layer
// and it is obvious that what is behind it is not the thing to act on.
//
// It is the one entry that is not a style, because receding is not a colour: it
// is mixing with whatever is already there. A style could only have set one
// colour over everything, which erases the interface underneath instead of
// dimming it.
Scrim Scrim
}
Theme is the palette an interface is drawn with.
The fields are styles rather than colours because a role often carries more than a colour: muted text is dim as well as grey, and a heading is bold. A widget that had to remember to add the dimming would be a widget that sometimes forgot.
func Dark ¶
func Dark() Theme
Dark is the default theme: neutral charcoal surfaces with bright accents used sparingly. Keeping the foundation achromatic lets status colours carry meaning without tinting every piece of surrounding chrome.
func Light ¶
func Light() Theme
Light turns the same hierarchy over for a light background. Its accents are deepened rather than merely reused so their contrast and relative emphasis stay aligned with the dark theme.
func Suited ¶ added in v0.0.2
Suited returns a theme fitted to the surrounding terminal colours.
The terminal's own foreground remains the body colour. Neutral structure is mixed from the terminal's foreground and background, so panes remain part of the user's terminal rather than looking like a fixed rectangle laid over it. Accent and outcome colours come from Dark or Light, selected by the background.
Both colours are needed to fit a theme. If either is unknown, Suited returns the corresponding built-in theme unchanged; guessing a missing colour can erase text.
func (Theme) Look ¶ added in v0.0.2
Look is what a headless widget that draws itself is dressed with — see headless.Look — built from this palette and a glyph set.
It is here rather than in each widget that needs one because it is one mapping: the answer is a heading, a label is strong, a hint is subtle, the row under the keyboard is the selection. A second copy of it somewhere else would be a second place for a form and the editor inside it to disagree about what a placeholder looks like.
type Transcript ¶ added in v0.0.2
type Transcript struct {
// Content is what to draw.
Content *headless.Transcript
// Scroll is where in it the window sits.
Scroll *headless.Scroll
// Keys maps scrolling actions. Nil reads through
// [headless.DefaultScrollKeys]. It is used only when Scroll is non-nil.
Keys *keymap.Map
// Selection picks out cells the user dragged over. Nil selects nothing.
Selection *headless.Selection
// Sticky pins a header above the window. Nil pins nothing.
Sticky *headless.Sticky
// Matches are highlighted where they fall inside the window, and Current is the
// index of the one being stepped to, which is drawn differently. Matches must be
// in row order and non-overlapping; [headless.Result.Matches] already has that
// shape. The order is what lets drawing depend on the visible window rather than
// on the age of the session. A Current outside the matches means none is current.
Matches []headless.Match
Current int
// Theme is the look. Every part of a transcript has a fixed role in one — a
// selection is the selection, the match being stepped to is the accent, a pinned
// header sits on a surface — so there is nothing here to choose between.
//
// A caller who wants one of them different passes a theme with that role
// changed, which is one line and keeps the look in the one place a look lives:
//
// quiet := theme
// quiet.Selection = quiet.Sunken
Theme Theme
// Glyphs are the characters the rule under a pinned header is drawn with, which
// is a fact about the terminal rather than about the look.
Glyphs Glyphs
// contains filtered or unexported fields
}
Transcript draws a session's output: the window of it that fits, the header pinned above, and whatever is selected or found picked out.
It owns no content. The blocks, where the view is scrolled to, what is selected and what a search turned up are all the caller's, held in the headless types that answer those questions — this decides what they look like and nothing else.
A Transcript must not be copied after first use: its committed window and captured selection gesture are one appearance owner even though the content remains external.
func (*Transcript) Commit ¶ added in v0.0.2
func (t *Transcript) Commit(p Printer, limit int) int
Commit gives the transcript's finished leading blocks to a printer, which is what puts them in the terminal's own output for good.
It is here rather than on the transcript because printing needs a width to draw at and the transcript does not have one until it has been laid out. The rest of the rule — leading, in order, once each — is headless.Transcript.Commit's, and this only supplies the drawing.
Any output sink with the small Printer method set can receive committed blocks:
view.Commit(output, 0)
Nothing is committed unless this is called. A block given to the terminal is no longer selectable, searchable, or re-wrapped when the window changes, so the choice is the program's and is made block by block with headless.Transcript.Finish.
limit is the most blocks to transfer. Zero transfers every finished block; a positive limit lets an application retain a recent window and publish only its excess stable prefix. A negative limit is a programmer error and panics: zero already means "all of them", and committing is irreversible, so reading a negative limit as either none or all would be a guess about output the caller cannot take back. A nil printer or content transfers nothing and reports zero.
func (*Transcript) Draw ¶ added in v0.0.2
func (t *Transcript) Draw(v headless.Frame)
Draw fills v with as much of the transcript as fits.
func (*Transcript) Handle ¶ added in v0.0.2
func (t *Transcript) Handle(event input.Event) bool
Handle answers scrolling and selection over the transcript, reporting whether it took the event.
A press starts a selection, a drag moves its far end, a second press in the same place takes the word and a third takes the row. That is what selecting text means everywhere, and it took five pieces wired together by hand until this: a selection, a click counter, the word rule, the scroll offset, and the translation from a position on screen into a row of the transcript.
The last of those is why it lives here rather than on the selection. A point on screen means nothing without knowing where the transcript was drawn and how far it is scrolled, and this is the thing that drew it.
The clicks are counted from the time the event arrived with, which the terminal's reader stamped on it. A caller feeding events it made up itself gets single clicks, because there is nothing to tell one press from another by.
The position is in the transcript's own coordinates, like everything else a widget is handed. Whoever drew it is responsible for that, which for anything inside a headless.Container is the container.
type Tree ¶ added in v0.0.2
type Tree[T any] struct { // Text is what a row says. A tree given none draws nothing but its marks, which // is what an item that cannot be read as text comes to. Text runs during drawing // and must be an observationally pure projection. Text func(item T) string // Theme is the look, and Glyphs are the marks beside a branch. A tree given no // glyphs draws no marks, and its rows are then told apart by their indent alone. Theme Theme Glyphs Glyphs // Indent is how many columns a level is worth. Zero uses two. Indent int // contains filtered or unexported fields }
Tree draws the rows of a headless.Tree with an indent and a mark on what can be opened.
It is one answer to the question that behaviour refuses — what a branch looks like — and the answer is: as far in as it is deep, a mark that turns over when it opens, and the row under the cursor drawn as a selection.
func NewTree ¶ added in v0.3.0
func NewTree[T any](config TreeConfig[T]) *Tree[T]
NewTree dresses one headless tree with the kit appearance.
TreeConfig.Controller is required; a nil one is a programmer error and panics here. Supplying a default would be this package creating the behavior surface it exists not to own, and the caller would then hold a tree whose selection and expansion answer to state they cannot reach.
func (*Tree[T]) Controller ¶ added in v0.3.0
Controller returns the headless tree that owns hierarchy and selection state.
func (*Tree[T]) Handle ¶ added in v0.0.3
Handle passes the event to the tree — see headless.Tree.Handle.
This and the two below are here so that a dressed tree is a widget like any other: something that can go in a container, take the keyboard from it, and answer what the container hands it. A look that could only be drawn would have to be wired up by hand wherever it was used.
type TreeConfig ¶ added in v0.11.0
type TreeConfig[T any] struct { // Theme and Glyphs define row and branch appearance. Theme Theme Glyphs Glyphs // Controller owns hierarchy, selection and interaction and is required. Controller *headless.Tree[T] // Text projects an item into its row label. Nil produces an empty label. It runs // during drawing and must be observationally pure. Text func(item T) string // Indent is the columns per depth. Zero means two. Indent int }
TreeConfig is the complete construction state of Tree. Controller is required; Text may be nil when the appearance needs only branch marks and indentation.