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
- type Anchors
- type RowID
- type Screen
- func (s *Screen) ClearDirty()
- func (s *Screen) CursorLine() string
- func (s *Screen) Evicted() uint64
- func (s *Screen) Render() (text string, burstStart int, anchors Anchors)
- func (s *Screen) Repainted() bool
- func (s *Screen) Resize(width, height int)
- func (s *Screen) RowShows(id RowID, text string) bool
- func (s *Screen) RowState(id RowID) (present bool, blank bool)
- func (s *Screen) Size() (width, height int)
- func (s *Screen) Text() string
- func (s *Screen) TextAndBurst() (text string, burstStart int)
- func (s *Screen) UniqueRowShowing(text string) (RowID, string, bool)
- func (s *Screen) VisibleRowLine(index int) string
- func (s *Screen) VisibleRowOf(text string) (index int, line string, found bool)
- func (s *Screen) VisibleText() string
- func (s *Screen) Write(data []byte) (int, error)
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
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) Render ¶
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 ¶
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 ¶
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.
func (*Screen) RowState ¶
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. RowState reports whether the named row is present on the visible grid, and whether its content is currently blank.
func (*Screen) Text ¶
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 ¶
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 ¶
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
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
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 ¶
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.