Documentation
¶
Overview ¶
Package program runs a terminal interface.
It owns the terminal, the frame schedule, and the one goroutine that is allowed to touch the interface's state. It knows nothing about what the interface is for: what it drives is a Component, which draws itself and answers input, and everything a component needs from the program it asks for through a Loop.
The concurrency model, in full ¶
One goroutine draws and handles input. Anything that happens elsewhere — a request finishing, a file changing, a timer firing — reaches the interface by being posted to that goroutine with Loop.Post, and runs there. That is the whole of it, and it is why every widget below this package is an ordinary mutable object with no lock in it.
The program parks when there is nothing to do. It wakes for input, for posted work, and for the terminal reporting progress — never on a clock that runs regardless. A component that wants a clock starts one with Loop.Every, and an interface with nothing animating costs nothing.
The two places an interface can be ¶
A program either takes a screen of its own, which it gives back on the way out, or draws in the terminal's own screen as a block with the session's output above it. The second is what Config.Inline asks for, and it is the difference between a program the user enters and leaves and one that is part of their session: what an inline interface has finished with is printed with InlineLoop.Print and belongs to the terminal from then on — scrollable, selectable, and still there afterwards.
Index ¶
Constants ¶
const DefaultFrameRate = 16 * time.Millisecond
DefaultFrameRate is the fastest a program redraws. A terminal cannot usefully show more, and a stream of updates would otherwise ask for a frame each.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Component ¶
Component is an interface a program can run: it draws itself into the space it is given, and says whether it wants an event.
It is handed a view that is already positioned and clipped, so its coordinates are its own. An event it does not consume is dropped by the program — a component is the root of its own tree and there is nobody above it to pass one on to.
type Config ¶
type Config struct {
// Root builds the component to run on a screen of its own. It is given the loop
// first, so the component can hold it from the moment it exists.
Root func(Loop) Component
// Inline builds the component to run as a block in the terminal's own screen,
// with output that is finished printed above it. Its component is given an
// [InlineLoop], which is a [Loop] that can also print.
Inline func(InlineLoop) Component
// Terminal says which of the terminal's optional behaviours to ask for. Ignored
// when Host is set.
//
// AltScreen is the program's to decide rather than the caller's, because where
// frames go is the rendering model and not an input capability: it follows from
// which of Root and Inline was set. Asking for it alongside Inline is a
// contradiction and is reported as one.
Terminal term.Options
// Color says how much colour the terminal can show. The zero value, [grid.Auto],
// asks [term.DetectDepth] — which is the one thing in this library that reads
// its environment rather than making a request and letting it be ignored,
// because a truecolor sequence a terminal cannot read prints wrong rather than
// degrading.
//
// Setting it is how a program that already knows — from its own configuration,
// or because it is writing to something that is not a terminal at all — takes
// that decision back.
Color grid.Depth
// Host overrides where input comes from and frames go. Nil opens the real terminal
// and gives it back on the way out.
Host Host
// FrameRate caps how often the interface redraws. Zero uses [DefaultFrameRate].
FrameRate time.Duration
}
Config is what a program needs to run.
Exactly one of Root and Inline says what to run, and which one it is decides where the interface is drawn: Root takes a screen of its own, Inline draws in the terminal's own screen and prints finished output into its scrollback.
type Host ¶
type Host interface {
// Events is the input, closed when the input ends.
Events() <-chan input.Event
// Writer is where frames go.
Writer() *term.Writer
// Size is the terminal's size in cells.
Size() (w, h int, err error)
}
Host is where a program's input comes from and its frames go.
A program opens the real terminal unless it is given one of these. Being able to supply it is what lets an interface be driven and inspected in a test, with no terminal in sight.
type InlineLoop ¶
type InlineLoop interface {
Loop
// Print draws something that can size itself into the terminal's own output,
// above the interface, where it stays after the program exits.
//
// It is how a streaming interface says something final: the interface itself is
// what is still changing, and everything it has finished with belongs to the
// terminal.
//
// The width is the program's to know and not the caller's. Measuring happens
// here, on the goroutine that owns the interface, which is both why a caller
// does not have to remember how wide the last frame was and why measuring a
// widget from somewhere else — a mutable object, on another goroutine — is not
// something this can be made to do by accident.
Print(p Printable)
// PrintRows is the same for a caller that has already worked out the height:
// rows it composed by hand, or content whose shape it knows better than any
// measurement would. draw is given a view rows tall and as wide as the
// interface.
PrintRows(rows int, draw func(grid.View))
}
InlineLoop is what an inline program's component may ask of it: everything a Loop offers, and somewhere to put output that is finished.
It is a separate interface rather than two more methods on Loop because a program drawing on a screen of its own has nowhere to print: that screen has no scrollback, and output written above the interface would be scrolled away and gone. A component that means to print says so by asking for this, and a program that cannot offer it cannot be given such a component.
type Loop ¶
type Loop interface {
// Refresh asks for a frame.
Refresh()
// Post runs fn on the program's goroutine and asks for a frame afterwards.
//
// This is the only safe way to change what a component holds from anywhere else.
// Work posted after the program has stopped is dropped rather than run: there is
// nothing left to show it, and blocking the caller for ever would be worse.
Post(fn func())
// Every calls fn on the program's goroutine at an interval until the returned
// function is called, or until the program stops.
//
// It is how anything animated advances. Nothing ticks unless something asked for
// it, which is what lets an idle interface be silent.
Every(d time.Duration, fn func()) (stop func())
// Quit asks the program to stop. The program returns from Run once the frame in
// hand has been dealt with.
Quit()
}
Loop is what a component may ask of the program running it.
Every method is safe from any goroutine, which is the point: a component holds one of these and hands it to whatever fetches, watches or waits on its behalf, and the answers come back on the goroutine that owns the state.
type Printable ¶
Printable is something that can say how tall it is at a width and then draw itself into that space.
Both halves are named from below rather than invented here: grid.Drawer and layout.Measurer are what the substrate already calls these, and a loop that coined its own words for them would be a lower layer taking its vocabulary from an upper one. It is the same reason components' own Sized is built from the same two — not because either copied the other, but because both are spelled in the language underneath them, which is what lets everything in components satisfy this without an adapter and without the loop knowing components exists.