screen

package
v0.8.19 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package screen renders a terminal byte stream into the grid of cells a person would actually see.

Relayer's detection has always normalized a byte stream: escape sequences are stripped and the surviving bytes kept in write order. That is exact for an agent that only appends, and wrong for one that repaints. The cursor movements that say WHERE each fragment lands are discarded, and the erases that say what is no longer on screen are discarded with them — so a question the agent has already withdrawn is still matchable, and a question painted into a frame is concatenated in write order instead of landing inside it.

The parser is deliberately TOTAL: every CSI, OSC, DCS, SOS, PM and APC sequence is recognised and consumed, even the ones the screen does nothing with. Acting on a small set is safe; failing to RECOGNISE a sequence is not, because its bytes would then be printed as text — and an unrecognised erase leaves stale cells live, which is the exact failure this package exists to remove. The recognition comes from github.com/charmbracelet/x/ansi, already in the module graph by way of bubbletea, so the riskiest part is not hand-written here.

Index

Constants

View Source
const (
	MinWidth      = 2
	MinHeight     = 1
	MaxWidth      = 1000
	MaxHeight     = 500
	MaxScrollback = 512
)

Bounds on what one screen may hold, so a hostile or broken agent cannot make Relayer allocate without limit.

Variables

This section is empty.

Functions

This section is empty.

Types

type Anchors

type Anchors struct {
	// contains filtered or unexported fields
}

Anchors translates a byte offset in the text one render produced into the row that painted it.

It is a value, rebuilt whole by each render and never mutated afterwards, so a caller that copies it — the Codex adapter probes by copying its whole detection state — shares a reading of the past, never a handle on the live screen.

func (Anchors) RowAt

func (a Anchors) RowAt(offset int) (RowID, bool)

RowAt names the row that painted the byte at offset. It reports false for an offset outside the text the anchors were built from, which is the only honest answer: a coordinate invented for an unknown offset would be a coordinate pointing at the wrong question.

func (Anchors) VisibleStart

func (a Anchors) VisibleStart() int

VisibleStart returns the byte offset where the visible grid begins in the rendered text.

type RowID

type RowID uint64

RowID names one grid line for as long as it exists. The zero value names no row, which is what a caller holding no coordinate has.

type Screen

type Screen struct {
	// contains filtered or unexported fields
}

Screen is one terminal's visible grid plus a bounded scrollback. It is not safe for concurrent use; the caller owns the lock, as Processor already does.

func New

func New(width, height int) *Screen

New creates a screen. A width or height outside the supported range is clamped rather than rejected: a caller that has not measured its terminal yet still needs somewhere to put output.

func (*Screen) ClearDirty

func (s *Screen) ClearDirty()

ClearDirty forgets which rows the last write touched, so the next one starts its own burst.

func (*Screen) CursorLine

func (s *Screen) CursorLine() string

CursorLine reports the logical line the cursor sits on, which is where an agent that has stopped to ask leaves its question.

func (*Screen) Evicted added in v0.3.0

func (s *Screen) Evicted() uint64

Evicted counts, monotonically, every time a line MOVED out of the visible grid or was dropped from it: scrolling either way, inserting or deleting lines, switching screens, resizing.

It exists to tell two textually identical situations apart. A question that is no longer on the grid because the agent ERASED it is a question that is no longer asked. A question that is no longer on the grid because it SCROLLED out of view may still be waiting for an answer, and treating it as withdrawn would stop the operator being asked at all. Two equal readings across a pair of writes prove that nothing left the view in between, so a disappearance in that interval can only be an erase or a rewrite in place.

It is deliberately not scrolledOff. That one gives a line an absolute coordinate, so it is only maintained where such a coordinate means something — the main screen, no scroll region — and it stands still while a DECSTBM region or an alternate screen moves content out of view. This counter answers the cruder question "did anything leave?" and is incremented unconditionally where lines actually move, so there is no state to keep in agreement with anything else.

func (*Screen) OnAlternate added in v0.8.8

func (s *Screen) OnAlternate() bool

OnAlternate reports whether the alternate screen is shown: a full-screen program has the grid, and the primary one is parked until it exits.

func (*Screen) Render

func (s *Screen) Render() (text string, burstStart int, anchors Anchors)

Render is TextAndBurst plus the map from that text back to the rows.

Detection finds a question at a byte offset. Where that question IS on the screen is knowledge this render already has and used to throw away, leaving the caller to search the grid for the matched text later — which finds the wrong row as soon as two rows carry the same fragment, and "y/n" is everywhere.

func (*Screen) Repainted

func (s *Screen) Repainted() bool

Repainted reports whether the agent has ever done something an appended byte stream cannot represent. It is false for an agent that only prints and advances, which is what makes the rendered screen safe to adopt selectively.

func (*Screen) Resize

func (s *Screen) Resize(width, height int)

Resize adapts the grid to a new terminal size, keeping what fits.

A resize to the size already in use does nothing. It has to: rebuilding the rows marks every one of them as touched, so a caller that resizes on each render — which is what a terminal interface does — would report the whole screen as new work on every write, and the actionable region would never narrow.

While a full-screen program has the alternate screen, only that screen is resized. The parked primary one is resized once, to the size then in force, when the program exits; see switchScreen. That is what ConPTY does, and ConPTY renders every Windows session: it leaves the primary buffer alone while the alternate one is shown, so a window dragged smaller and back while an editor was open gives the primary screen back exactly as it was, and ConPTY repaints it that way. Following every step of the drag shifted the parked rows up on the shrink, pushed the top ones into the history, and did not pull them back on the grow: the answered question came back on a row the memory did not know, and was asked again. On Unix Relayer is the terminal, and giving the primary screen back as it was left is as good an answer as any.

It also keeps row identities unique. The parked screen mints its new rows from the counter it was parked with, which the alternate screen has been using since. A pane that more than doubled its height while a full-screen agent had it — four agents to one in the desktop grid, a small pane maximised — gave a row of the program's grid and a parked row one name. RowParked then answered yes for a live row, and an answer given there was kept for the rest of the program: the same dialog asked again later on that row was never offered, and the agent waited on a question nobody was shown.

func (*Screen) RowParked added in v0.8.8

func (s *Screen) RowParked(id RowID) bool

RowParked reports whether the named row belongs to the primary screen, parked while the alternate one is shown.

Such a row is on no visible grid, so RowState and RowShows answer as they do for a row that is gone. It is not gone: the primary screen comes back unchanged when the program exits, and a caller that took the row's absence for its end forgot what it knew about a line that is about to be shown again exactly as it was.

func (*Screen) RowShows

func (s *Screen) RowShows(id RowID, text string) bool

RowShows reports whether the named row is still on the visible grid and still carries text.

The pair is the point. The row alone would answer yes to a line the agent rewrote with something else; the text alone would answer yes to the same words on another line. A row that scrolled away, that was erased, or that now says something different answers false — which is how a caller learns that what it remembered about that question no longer holds.

func (*Screen) RowState

func (s *Screen) RowState(id RowID) (present bool, blank bool)

RowState reports whether the named row is present on the visible grid, and whether its content is currently blank.

func (*Screen) Size

func (s *Screen) Size() (width, height int)

Size reports the current grid dimensions.

func (*Screen) Text

func (s *Screen) Text() string

Text renders the screen as the operator sees it: scrollback first, then the live grid, with rows joined where they wrapped so a sentence broken by the right margin is one line again. Trailing blank rows are dropped, because a mostly empty screen is not the same as a screen full of blank lines.

func (*Screen) TextAndBurst

func (s *Screen) TextAndBurst() (text string, burstStart int)

TextAndBurst renders the screen and reports where in that text the rows this write touched begin.

On a byte stream "what the agent just wrote" is a range of offsets. On a grid it is a set of rows, and a repaint touches them out of order — a frame drawn top to bottom then filled in the middle changes row 1, 3 and 2 in that order. The offset returned is that of the EARLIEST touched row, so the region is contiguous and conservative: it can include a row the write did not touch, never exclude one it did. Excluding is the unsafe direction, because a question in an excluded row is a question nobody is shown.

A burst offset of len(text) means this write changed nothing that survives on screen.

func (*Screen) UniqueRowShowing

func (s *Screen) UniqueRowShowing(text string) (RowID, string, bool)

UniqueRowShowing names the only visible row whose logical line contains text, and reports false when none does or when more than one does.

It is the last resort for a caller holding no coordinate at all — an occurrence rebuilt from a snapshot, which crossed a process boundary where a screen coordinate has no meaning. Ambiguity is refused rather than guessed: picking one of two identical lines is how a memory latches onto the wrong question in the first place.

func (*Screen) VisibleRowLine added in v0.3.0

func (s *Screen) VisibleRowLine(index int) string

VisibleRowLine returns the logical line that begins at a visible row, joined across wrapped rows, or the empty string if the row is off the grid.

func (*Screen) VisibleRowOf added in v0.3.0

func (s *Screen) VisibleRowOf(text string) (index int, line string, found bool)

VisibleRowOf reports the visible row at which the logical line containing text begins, and that line as it is currently serialised.

The last such line, not the first: when a screen shows the same question twice the live one is the lower. The index is a position on the visible grid, not an absolute coordinate, and it is only meaningful for as long as nothing leaves the grid — which is what Evicted is for.

func (*Screen) VisibleText

func (s *Screen) VisibleText() string

VisibleText renders only the live grid, without scrollback.

Detection reads the scrollback too, because a question can legitimately have scrolled just above the fold. But "is this question still on screen?" must be answered by the screen alone: history keeps a question findable long after the agent stopped showing it, and a memory released by text-matching would then never be released at all.

func (*Screen) Write

func (s *Screen) Write(data []byte) (int, error)

Write feeds raw terminal bytes, escape sequences included.

Jump to

Keyboard shortcuts

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