Documentation
¶
Overview ¶
Package screen is the terminal renderer core for the interactive session: line-array components, a diffing painter, and scrollback-native output.
The model is deliberately small. A component renders itself to a slice of pre-styled lines at a width; components compose by concatenation and overlays composite over the base lines before painting. The painter owns only a small live region at the bottom of the terminal (the in-flight output, status line, and composer) and repaints it by diffing pre-wrapped line arrays, rewriting only the rows that changed. Finalized content is printed above the live region as ordinary terminal lines, so native scrollback, mouse selection, copy, and terminal search all keep working on everything the session has already said.
Every paint is wrapped in synchronized output (DEC private mode 2026), so a repaint is one atomic frame and never tears under fast streaming. Terminals that do not implement the mode ignore the markers and still receive each frame as a single buffered write.
The renderer is pure with respect to the terminal: it reads nothing back and keeps no absolute cursor position, only the shape of the live region it last painted. That keeps the whole core testable as bytes in, bytes out, with no terminal required.
Index ¶
- func Center(frameWidth, frameHeight, boxWidth, boxHeight int) (x, y int)
- func Overlay(base, over []string, x, y int) []string
- func Truncate(s string, width int) string
- func Width(s string) int
- func Wrap(s string, width int) string
- type AltPainter
- type Component
- type Painter
- type Scheduler
- type Surface
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Center ¶
Center returns the x, y position that centers a box of the given size within a frame of the given size, clamped to the top-left when the box is larger than the frame.
func Overlay ¶
Overlay composites over onto base at column x, row y (both zero-based) and returns the resulting lines. It is how dialogs, pickers, and panels appear: the base component renders as usual, the overlay is spliced over it before the frame is painted, and dismissing the overlay simply stops splicing. Nothing about the base changes, so overlays never corrupt underlying state.
The splice is width-aware (grapheme clusters, escape sequences), and the overlay content is isolated from the base row's styling with an SGR reset on both sides. The base row's own styling does not resume after the overlay: re-opening an arbitrary interrupted style run is not worth the complexity when overlays are opaque boxes that cover the styled content anyway.
Rows and columns beyond the base are padded with blanks, so an overlay can extend past the base's bottom edge (a dialog taller than the live region grows the frame rather than being clipped).
func Truncate ¶
Truncate cuts s to at most width cells, preserving escape sequences and never splitting a grapheme cluster.
Types ¶
type AltPainter ¶
type AltPainter struct {
// contains filtered or unexported fields
}
AltPainter renders the session into the terminal's alternate screen: a fixed-size viewport that it repaints in full each frame with absolute cursor addressing. It is the fallback for emulators where the inline Painter's scroll-region insertion is unsafe (Zellij-class multiplexers), so it cannot lean on the terminal's own scrollback the way Painter does; instead it keeps a bounded transcript of its own and always draws the tail, the newest output with the composer beneath it. Entering and leaving the alternate screen is the term package's job, symmetric with the other terminal modes; this type only draws inside it.
Like Painter it is pure with respect to the terminal (it reads nothing back) and every write is one synchronized frame, so a repaint never tears. Write errors are sticky: after the first failure every later operation is a no-op and Err reports the cause.
func NewAltPainter ¶
func NewAltPainter(w io.Writer, width, height int) *AltPainter
NewAltPainter builds an alternate-screen painter over w for a viewport of the given size. The caller has already entered the alternate screen (through the term package), so the buffer starts blank; the first paint fills it.
func (*AltPainter) Close ¶
func (p *AltPainter) Close()
Close releases the painter. The alternate screen and everything drawn in it is discarded when the caller leaves it (the term package's teardown), so there is nothing to leave behind; Close exists to satisfy the Surface contract and is safe to call once.
func (*AltPainter) Err ¶
func (p *AltPainter) Err() error
Err returns the first write error the painter hit, or nil.
func (*AltPainter) Insert ¶
func (p *AltPainter) Insert(finalized, frame []string)
Insert commits finalized into the painter's own scrollback above the live region, then shows frame. Unlike the inline painter the finalized lines do not become native terminal output (the alternate screen has no scrollback): they join the retained transcript and scroll off the top as newer output arrives, bounded by maxAltHistory.
func (*AltPainter) Live ¶
func (p *AltPainter) Live() []string
Live returns the live-region rows currently composed. It exists for tests and diagnostics; callers must not mutate the returned slice.
func (*AltPainter) Paint ¶
func (p *AltPainter) Paint(frame []string)
Paint shows frame as the live region and repaints the viewport, writing only the rows that changed since the last frame.
func (*AltPainter) Repaint ¶
func (p *AltPainter) Repaint(frame []string)
Repaint shows frame and redraws the whole viewport from scratch, discarding the diff base. It is the recovery path after a resize or after output from outside the painter.
func (*AltPainter) Resize ¶
func (p *AltPainter) Resize(width, height int)
Resize records a new viewport size. The previously drawn rows can no longer be trusted as a diff base after the terminal reflowed them, so the next paint clears the viewport and redraws it in full.
type Component ¶
Component is anything that can render itself as lines at a given width. Each returned string is one terminal row, already styled and already wrapped: the painter treats every line as exactly one row and hard-guards overflow by truncation, so a component that wants wrapping does it here.
type Painter ¶
type Painter struct {
// contains filtered or unexported fields
}
Painter owns the live region: the last few rows of the terminal, repainted in place as the session streams. It tracks only the lines it last painted and the terminal size; it never queries the terminal. The cursor contract is simple and holds across every operation: the cursor rests at column one of the last live row (or at column one of the row where the live region will begin, while the region is empty).
All movement is relative (cursor up and down from that resting row), never absolute. Relative movement stays correct when the terminal scrolls, because the cursor and the live rows scroll together; absolute addressing would not.
Each operation is accumulated into one buffer and flushed as a single write wrapped in synchronized output, so the terminal applies it as one atomic frame. Write errors are sticky: after the first failure every later operation is a no-op and Err reports the cause, so a torn-down terminal does not produce a cascade of secondary failures mid-exit.
func NewPainter ¶
NewPainter builds a painter over w for a terminal of the given size. The painter assumes the cursor starts at column one of an otherwise unused row (a fresh prompt line); the first Paint draws the live region from there.
func (*Painter) Close ¶
func (p *Painter) Close()
Close finalizes the session's last frame: the live region's current content is left in place as ordinary scrollback and the cursor moves to a fresh line below it, where the shell prompt will appear. The painter is unusable afterwards except for Err.
func (*Painter) Insert ¶
Insert commits finalized lines into the terminal's own scrollback above the live region, then paints frame as the new live region. The finalized lines become ordinary terminal output: they are never repainted again, and native scrolling, selection, and search work on them. Long finalized lines are left to the terminal to wrap, matching how any other program's output behaves in scrollback; only live rows are hard-truncated, because the painter's row arithmetic depends on one live line per row.
func (*Painter) Live ¶
Live returns the lines currently painted in the live region. It exists for tests and diagnostics; callers must not mutate the returned slice.
func (*Painter) Paint ¶
Paint diffs frame against the last painted live region and rewrites only the rows that changed. Painting an identical frame writes nothing.
func (*Painter) Repaint ¶
Repaint clears the live region and redraws frame from scratch, without diffing. It is the recovery path after anything that invalidates the diff base: a resize (the terminal rewrapped the old rows) or output from outside the painter (a child process wrote to the terminal).
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler coalesces repaint requests into frames at a bounded rate. Every producer (a streaming token, a spinner tick, a keystroke echo) calls Request whenever anything changes; the scheduler guarantees the paint function runs soon after the first request and then at most once per interval, however many requests arrive in between. Fast streaming therefore costs one repaint per frame, not one per token, and the renderer can never fall behind its input.
Time comes from the injected clock.Timing, never the wall clock, keeping frame pacing on the same deterministic footing as the rest of the runtime: production wires clock.System, tests drive a clock.Manual and observe exact coalescing behavior with no sleeps.
func NewScheduler ¶
NewScheduler starts a scheduler that invokes paint on its own goroutine. The interval is the minimum time between the start of one paint and the next; requests inside the interval coalesce into at most one trailing paint. Stop must be called to release the goroutine.
type Surface ¶
type Surface interface {
// Resize records a new terminal size; the next paint re-renders at it.
Resize(width, height int)
// Paint shows frame as the live region, writing only what changed.
Paint(frame []string)
// Insert commits finalized above the live region, then shows frame.
Insert(finalized, frame []string)
// Repaint redraws frame from scratch, discarding any stale diff base.
Repaint(frame []string)
// Close finalizes the session's last frame and releases the surface.
Close()
// Err returns the first write error the surface hit, or nil.
Err() error
}
Surface is the render target the shell drives: it turns frames into terminal writes and owns whatever region of the terminal the session paints. Two implementations back it. Painter renders inline and commits finalized lines to the terminal's own scrollback, so native scrolling, selection, and search keep working on the whole transcript; it is the default and the full-fidelity path. AltPainter renders in the alternate screen for emulators where the inline path's scroll-region insertion is unsafe (Zellij-class multiplexers): it keeps its own bounded scrollback and repaints a full-screen viewport.
Both share the same contract, so the shell drives either without knowing which it holds. Every method is a no-op after the first write error, which Err reports.