program

package
v0.0.3 Latest Latest
Warning

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

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

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

View Source
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

func Run

func Run(ctx context.Context, cfg Config) (err error)

Run draws the interface until it is asked to stop, its input ends, or the terminal fails.

A cancelled context stops the program without being reported as a failure: being asked to stop is not one.

Types

type Component

type Component interface {
	grid.Drawer
	input.Handler
}

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)
	// Ground is the terminal's own two colours. A host that is not a terminal answers
	// the zero value, and so does a terminal nobody asked.
	//
	// It is here rather than in [Config] because it is a fact about the thing being
	// drawn on, and this is what stands for that thing. A test host that can say it
	// is light is a test that can check a look both ways round.
	Ground() grid.Ground
	// Wheel is what the terminal's wheel reports are worth. A host that is not a
	// terminal answers the zero value, which is the common arrangement.
	Wheel() input.Wheel
	// Keyboard is which Kitty keyboard enhancements are on, and whether the terminal
	// said. A host that cannot ask reports false.
	Keyboard() (input.KeyboardFlags, bool)
	// ReportDirectory tells the terminal where the program is working. A host that is
	// not a terminal does nothing and reports no error: there is nobody to tell.
	ReportDirectory(path string) error
	// Copy puts text on the system clipboard, reporting false for text it will not
	// carry.
	Copy(text string) bool
	// Paste asks for the system clipboard, whose contents arrive later on Events as
	// an [input.Paste]. A host that cannot read a clipboard does nothing.
	//
	// Copying and pasting are the host's because how they are done depends entirely
	// on what is at the other end. A terminal asks the terminal, over a protocol it
	// may refuse; a host somewhere else can shell out to the local tools and be
	// right for that case, which asking the terminal would not be.
	Paste()
	// Hand gives the terminal to something else and takes it back when run returns —
	// see [term.Terminal.Hand]. A host that is not a terminal has nothing to give
	// away and simply runs it.
	Hand(run func() error) error
	// SetTitle names the window, Bell asks for attention, and Notify asks for a
	// desktop notification. A host that is not a terminal has nobody to tell and does
	// nothing, which is the same answer ReportDirectory gives and for the same reason.
	SetTitle(s string)
	Bell()
	Notify(text string)
	// Graphics is how this host will take a picture, CellSize how many pixels a cell
	// is, and Transmit sends one — see [term.Terminal.Transmit]. A host that is not a
	// terminal shows no pictures and says so.
	Graphics() graphics.Protocol
	CellSize() (image.Point, bool)
	Transmit(png []byte) (graphics.Image, 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))

	// Append puts output onto the end of what was printed last rather than onto a
	// row of its own, which is what output arriving in pieces needs: a reply
	// streaming in three words at a time is one paragraph, not three rows.
	//
	// draw is given what is left of the open row and says whether there is more to
	// come. When there is, it is called again with a whole row, and again until it
	// says there is not — which is what lets a caller lay text out against room it
	// has no way of knowing until the loop tells it. A round with a whole row to
	// itself that draws nothing ends it, because asking again would never end.
	Append(draw func(v grid.View) (more bool))
}

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

	// Ground is what the terminal's own two colours are, as far as anyone knows.
	//
	// They are known when the terminal was asked and answered — see
	// [term.Options.Probe]. The background is the one a look is usually built on, and
	// [grid.RGB.Dark] turns it into the only question a theme has; a component given
	// no answer has to choose for itself, and there is no safe guess, because dark is
	// the commoner choice and light is the one that becomes unreadable when it is
	// guessed wrong.
	//
	// The colours are here rather than the conclusion because the conclusion is not
	// the only use for them. A component drawing a gradient, or floating a layer over
	// what is behind it, needs the numbers — and a lower layer that only ever answered
	// "dark?" would have decided for everyone above it. The frame already carries the
	// same answer for drawing's sake, which is why a widget with a view in hand asks
	// [grid.View.Ground] instead of coming back here.
	Ground() grid.Ground

	// Copy asks for text to be put on the system clipboard, reporting false for
	// text too large to carry — see [term.Terminal.Copy].
	//
	// Who does the copying is the host's business and not a component's. A terminal
	// asks the terminal, because over ssh or through a multiplexer that is the only
	// end of the connection the user is at; a host that knows better can do it
	// another way without anything above having to hear about it.
	Copy(text string) bool

	// Wheel is what this terminal's wheel reports are worth, which is not a constant:
	// terminals send between one and three of them for one notch, and there is no way
	// to tell from a report. A component holding a scroll passes it on once — see
	// [headless.Scroll.Wheel].
	Wheel() input.Wheel

	// Keyboard is which of the Kitty keyboard protocol's enhancements the terminal
	// actually turned on, and whether it said.
	//
	// A component that waits for a key to be let go needs it. Asking for the
	// enhancements is not the same as getting them: a terminal can accept unambiguous
	// key codes and give nothing for releases, and then every key is held for ever as
	// far as this program can tell. Nothing in the events distinguishes that from a
	// user who has not lifted a key, so a component that cannot ask has no way to
	// choose a different interaction — which is what this is for.
	Keyboard() (input.KeyboardFlags, bool)

	// ReportDirectory tells the terminal which directory the program is working in.
	//
	// It is what lets a terminal resolve the relative paths in a program's own output
	// — which is why [link.Link.Hyperlink] declines to make one a hyperlink and leaves
	// it to the terminal. A host that is not a terminal does nothing.
	ReportDirectory(path string) error

	// Paste asks for the system clipboard's contents.
	//
	// The answer arrives as an ordinary [input.Paste], which is what makes this
	// worth having: a component that already inserts what the user pasted needs
	// nothing further to insert what they copied somewhere else. It may never
	// arrive — most terminals refuse to be read — so nothing should wait on one.
	Paste()

	// Hand gives the terminal to something else — an editor, a pager, a shell — and
	// takes it back when run returns.
	//
	// It runs on the goroutine that owns the interface and does not return until run
	// does, which is the point rather than a limitation: nothing may draw while a
	// child owns the screen, and the simplest way to guarantee that is for the one
	// goroutine that draws to be inside this call. Everything posted meanwhile waits
	// its turn, and the interface is redrawn in full afterwards, because what a child
	// did to the screen is not knowable.
	//
	// An inline interface is left in the terminal first, with the cursor below it, so
	// the child starts on a line of its own and the block is not written over. It is
	// drawn again where the cursor ends up.
	//
	// Where a terminal cannot be handed over — see [term.Terminal.Hand] — this
	// reports [errors.ErrUnsupported] and run is not called.
	Hand(run func() error) error

	// SetTitle names the terminal's window — see [term.Terminal.SetTitle]. It is
	// where a program says what it is doing to somebody who is looking at another
	// window, which is the one thing an interface cannot say by drawing.
	SetTitle(s string)

	// Bell asks the terminal for the user's attention, and leaves it to the terminal
	// and the user to have agreed what that means.
	Bell()

	// Notify asks for a desktop notification, for the thing that finished while the
	// user was looking at something else. A terminal that does not implement it
	// ignores it and says nothing, so anything worth notifying about is worth saying
	// in the interface as well.
	Notify(text string)

	// Graphics is the richest way this terminal will take a picture, and CellSize how
	// many pixels one cell is — the two questions that have to be answered before an
	// interface can show one. A terminal that cannot show pictures reports
	// [graphics.None], and one that never said how big a cell is reports false, which
	// is not the same thing: the first cannot show a picture at all, and the second
	// cannot be told what shape to draw it.
	Graphics() graphics.Protocol
	CellSize() (image.Point, bool)

	// Transmit sends a picture to the terminal and returns the handle it now knows it
	// by, which is what puts one in a frame — see [grid.View.Paint].
	//
	// Once per picture rather than once per frame: what a frame does with it after
	// that is place it, which is a dozen bytes. A host that is not a terminal has
	// nowhere to send one and reports [errors.ErrUnsupported].
	Transmit(png []byte) (graphics.Image, error)

	// Suspend gives the terminal back and stops this process, the way Ctrl+Z does in
	// a shell, returning when it is continued.
	//
	// In raw mode the terminal does not do this for the user: Ctrl+Z arrives as a
	// keystroke like any other, so a program that wants the shell's behaviour binds
	// it to this. It is [Loop.Hand] around [term.Suspend], and the terminal is given
	// back before the process stops rather than after, or the user is left looking at
	// half an interface they cannot type into.
	Suspend() error
}

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

type Printable interface {
	grid.Drawer
	layout.Measurer
}

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.

Jump to

Keyboard shortcuts

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