pub struct Engine { /* private fields */ }Expand description
The terminal engine: pairs the vte parser with our state model.
Parser and Term are kept as separate fields because Parser::advance
borrows both the parser and the performer mutably at once — a single struct
owning both could not satisfy the borrow checker.
Implementations§
Source§impl Engine
impl Engine
Sourcepub fn new(cols: usize, rows: usize) -> Self
pub fn new(cols: usize, rows: usize) -> Self
A blank engine with a cols × rows screen and a default scrollback cap.
cols is widened to MIN_COLUMNS — a narrower screen cannot represent a
width-2 glyph, so the engine clamps rather than accepting a size it would
have three different answers for (#547).
Sourcepub fn with_scrollback(
cols: usize,
rows: usize,
scrollback_limit: usize,
) -> Self
pub fn with_scrollback( cols: usize, rows: usize, scrollback_limit: usize, ) -> Self
Like Engine::new but with an explicit scrollback line limit. cols is
clamped to MIN_COLUMNS the same way.
Sourcepub fn feed(&mut self, bytes: &[u8])
pub fn feed(&mut self, bytes: &[u8])
Push a slice of VT bytes. The caller owns the PTY/SSH/socket I/O — the engine only consumes the bytes it is handed.
The stream is UTF-8, and a lone 0x80..=0x9F byte is ill-formed input
rather than a C1 control (#847). So the 8-bit forms of the C1 controls are
not interpreted: 0x9B does not open a CSI, 0x9D an OSC, 0x90 a DCS, and
0x9C does not terminate a string — nor does C2 9C, the well-formed UTF-8
encoding of U+009C. Send the 7-bit forms, which every one of them has:
ESC [, ESC ], ESC P, ESC \.
This is a contract, not a gap, and the reason is that an OSC payload
legitimately carries 8-bit text. 0x9C is the last byte of 한 (ED 95 9C)
— all six occurrences of it in this repository’s recorded captures are
exactly that — so honouring it as ST in a byte-wise parser would cut a
title mid-character. xterm arrives at the same place from the other side:
under UTF-8 it maps an ill-formed byte to U+FFFD and ignores a properly
encoded C1, with both escape hatches (EXP_C2_CONTROLS, allowC1Printable)
off by default. Measurements and reference sites are in
docs/agents/reference-facts.md.
Two visible consequences, stated so they are not re-discovered as bugs: an
unrecognised 8-bit introducer leaves its payload to print as ordinary text,
and an OSC “closed” with 0x9C stays open, accumulating everything after it
until a BEL, ESC, CAN or SUB arrives.
Sourcepub fn resize(&mut self, cols: usize, rows: usize)
pub fn resize(&mut self, cols: usize, rows: usize)
Resize the screen to cols x rows. Rows that scroll off the top enter
scrollback; the whole screen is damaged.
The primary screen reflows; the alternate screen does not (#567). On the primary, soft-wrapped logical lines are re-split at the new width — scrollback included, since it is one buffer with the screen — so a long line keeps its tail instead of being truncated. Reflow is not gated on DECAWM: the wrap flag records that a row continues into the next one, which stays true after a re-split, and re-reading a momentary mode at resize time would decide the fate of history written under the opposite setting. The alt screen is re-fit only — rows are dropped or added to reach the new size and nothing re-wraps, because a full-screen application places its own lines and re-wrapping them would change what it drew.
What a consumer must redo afterwards. Query-derived state is invalidated and user-authored state is re-anchored: search highlights are dropped (re-run the search at the new width — a reflow moves match coordinates and can change the match set), while the selection is carried to its new coordinates for you.
Two pieces of application-written state, answered differently. The tab-stop table is extended, never rebuilt: a resize that changes only the row count — or one that changes nothing, since this call has no early return — leaves it exactly as the application set it, and a stop pushed outside a narrowed grid returns when the grid widens again (#849). The DECSTBM scroll region is reset to the full screen — but only when the geometry actually changed, since it is a range over the current screen and a resize to the size you already have redefines nothing. An application that set one re-sends it after a real resize; nothing tells it the region is gone, so a call that discarded one needlessly could never be repaired.
cols is widened to MIN_COLUMNS silently: a resize(1, rows) during
a pane drag yields a two-column screen with no error. Read the resulting
width back from Engine::grid or the frame header rather than assuming the
value passed here, and size the PTY from that same width (#547).
Sourcepub fn bracketed_paste(&self) -> bool
pub fn bracketed_paste(&self) -> bool
Whether bracketed-paste mode (DEC ?2004) is enabled. A consumer’s input encoder reads this to decide whether to wrap pasted text in markers.
Sourcepub fn encode_key(&self, ev: KeyEvent) -> Option<Vec<u8>>
pub fn encode_key(&self, ev: KeyEvent) -> Option<Vec<u8>>
Encode a key event to the bytes an application expects, honouring the
engine’s cursor-key mode (DECCKM). The inverse of Engine::feed — the
consumer hands a decoded key event and writes the bytes to its PTY.
Returns None for a key with no defined encoding.
Sourcepub fn encode_mouse(&self, ev: MouseEvent) -> Option<Vec<u8>>
pub fn encode_mouse(&self, ev: MouseEvent) -> Option<Vec<u8>>
Encode a mouse event using the engine’s active tracking mode + encoding.
Returns None when mouse reporting is off, or when the event is filtered
out by the mode (e.g. a bare move while only ?1000 is set).
Sourcepub fn encode_paste(&self, text: &str) -> Vec<u8> ⓘ
pub fn encode_paste(&self, text: &str) -> Vec<u8> ⓘ
Encode pasted text — wrapped in bracketed-paste markers when ?2004 is on, raw otherwise.
Sourcepub fn encode_focus(&self, focused: bool) -> Option<Vec<u8>>
pub fn encode_focus(&self, focused: bool) -> Option<Vec<u8>>
Encode a focus change (CSI I on focus-in, CSI O on focus-out), or
None when focus reporting (?1004) is off.
Sourcepub fn drain_events(&mut self) -> Vec<TermEvent>
pub fn drain_events(&mut self) -> Vec<TermEvent>
Take the consumer events accumulated since the last drain (title / bell /
cwd — see TermEvent), emptying the queue. The pull counterpart to a
callback: poll this alongside Engine::frame.
Sourcepub fn drain_replies(&mut self) -> Vec<u8> ⓘ
pub fn drain_replies(&mut self) -> Vec<u8> ⓘ
Take the reply bytes the engine produced for app queries (DA / DSR /
DECRQM) since the last drain — the consumer writes them straight back to
the PTY. The inbound-query counterpart to Engine::drain_events.
Sourcepub fn link_at(&self, row: usize, col: usize) -> Option<Hyperlink>
pub fn link_at(&self, row: usize, col: usize) -> Option<Hyperlink>
The OSC 8 hyperlink URI at screen (row, col) — the live grid, same
coordinates as Engine::grid’s cell(row, col) — or None if that cell
carries no declared link.
One call, not two, since #628. This returned a NonZeroU32 index that a
second method resolved against a buffer-wide pool; the pool is gone (it was never
reclaimed, and nothing interned across opens that a shared Arc does not), so
there is no index left to hand out.
Owned, not borrowed — a &str into the row’s map would be tied to &Engine,
so a hover handler could not keep it across the next Engine::feed. Measured:
the borrow reads at 0.75 ns but cannot be held at all, and the caller’s workaround
(copying the string) costs 62.6 ns against this handle’s 17.9 ns. See
Hyperlink.
Do not confuse this with a decoded Span’s links, which is a frame-local
index into that frame’s link_table and belongs to the wire, not to the engine.
The old two-call form invited exactly that mix-up and its doc-comment recommended
it: the two index spaces coincide only when a frame carries a single link.
Sourcepub fn underline_color_at(&self, row: usize, col: usize) -> Color
pub fn underline_color_at(&self, row: usize, col: usize) -> Color
The underline colour (SGR 58, #520) at screen (row, col) — same
coordinates as Engine::grid’s cell(row, col). A theme-agnostic
Color reference; Color::Default means the underline follows the
glyph’s foreground (the common case, and what a cell with no SGR 58 returns).
Like the hyperlink, the colour rides a per-row side table, not the 12-byte
Cell (#520).
Sourcepub fn viewport_link_at(&self, row: usize, col: usize) -> Option<Hyperlink>
pub fn viewport_link_at(&self, row: usize, col: usize) -> Option<Hyperlink>
The OSC 8 hyperlink URI at viewport (row, col) — the visible window
including scrollback at the current scroll, same coordinates as
Engine::viewport_line — or None. Mirror of Engine::link_at, including
its #628 note about the vanished index.
Sourcepub fn scrollback_len(&self) -> usize
pub fn scrollback_len(&self) -> usize
Number of lines currently held in scrollback history.
Sourcepub fn synchronized_output(&self) -> bool
pub fn synchronized_output(&self) -> bool
Whether the app has an open synchronized-output block (DEC ?2026):
it has asked that the next frame of output be painted atomically. The
engine only reports this — the consumer owns the paint-hold and the
spec-mandated timeout (a buggy app that never closes the block must not
freeze the screen forever, and the engine has no clock). Poll this after
feed; while it is true, defer applying frames, and apply once it
clears (or your own timeout fires). (#73)
Sourcepub fn color_scheme_updates(&self) -> bool
pub fn color_scheme_updates(&self) -> bool
Whether the app enabled color-scheme-update notifications (DEC ?2031).
The engine is theme-agnostic — it never knows the scheme. The consumer
answers a TermEvent::ColorSchemeQuery (from ?996) and, when its
scheme changes and this is true, sends an unsolicited notification, in
both cases by calling Engine::report_color_scheme (#85).
Sourcepub fn report_color_scheme(&mut self, dark: bool)
pub fn report_color_scheme(&mut self, dark: bool)
Report the current light/dark color scheme to the app as CSI ? 997 ; 1 n
(dark) / ; 2 n (light), drained via Engine::drain_replies. Call this
to answer a TermEvent::ColorSchemeQuery, or — guarded by
Engine::color_scheme_updates — when the scheme changes. The engine only
formats the bit you pass; it stores no scheme (#85).
Sourcepub fn report_background(&mut self, spec: &str, terminator: Terminator)
pub fn report_background(&mut self, spec: &str, terminator: Terminator)
Answer an OSC 11 QueryBackground event (#122): the consumer hands back
the current background spec (it owns the palette) and the engine queues
the OSC 11 reply for drain_replies. Theme-agnostic — the engine never
knows the colour, only formats the envelope.
Sourcepub fn report_foreground(&mut self, spec: &str, terminator: Terminator)
pub fn report_foreground(&mut self, spec: &str, terminator: Terminator)
Answer an OSC 10 QueryForeground event (#122): queue the OSC 10 reply
from the consumer-supplied spec. Theme-agnostic envelope-only.
Sourcepub fn report_cursor_color(&mut self, spec: &str, terminator: Terminator)
pub fn report_cursor_color(&mut self, spec: &str, terminator: Terminator)
Answer an OSC 12 QueryCursorColor event (#832): queue the OSC 12 reply
from the consumer-supplied spec. Theme-agnostic envelope-only, like its
foreground and background siblings.
Sourcepub fn report_palette_color(
&mut self,
index: u8,
spec: &str,
terminator: Terminator,
)
pub fn report_palette_color( &mut self, index: u8, spec: &str, terminator: Terminator, )
Answer an OSC 4 QueryPaletteColor event (#122): queue the OSC 4 reply for
index from the consumer-supplied spec. Theme-agnostic envelope-only.
Sourcepub fn report_clipboard(
&mut self,
target: ClipboardTarget,
text: &str,
terminator: Terminator,
)
pub fn report_clipboard( &mut self, target: ClipboardTarget, text: &str, terminator: Terminator, )
Answer an OSC 52 TermEvent::QueryClipboard event (#828): base64-encode
the consumer’s clipboard text into the OSC 52 reply envelope for
Engine::drain_replies.
The engine holds no clipboard — the text comes from the consumer, which owns it along with every policy about it. Not calling this is how a read is refused, independently of whether stores are honoured, and nothing is queued until you do.
Sourcepub fn win32_input_mode(&self) -> bool
pub fn win32_input_mode(&self) -> bool
Whether the app enabled win32-input-mode (DEC ?9001): it asked for
keys as raw Windows key-records. The engine only tracks the flag — encoding
the records (CSI Vk;Sc;Uc;Kd;Cs;Rc _) is a non-goal (raw passthrough, no
semantic conversion), so Engine::encode_key is unchanged. A ConPTY
consumer reads this to decide whether to emit the records itself (#86).
Sourcepub fn damage(&self) -> TermDamage
pub fn damage(&self) -> TermDamage
What changed since the last Engine::reset_damage — line ranges each
with a changed column span (see ADR-0003).
Sourcepub fn frame(&self) -> Frame
pub fn frame(&self) -> Frame
Build a serializable Frame of the current diff — the damaged spans
(or every row, when Full), the recorded scroll op, and a frame-local
grapheme side-table. Pass it to encode for the wire (see #6). Reading
a frame does not clear damage; call Engine::reset_damage on ack.
Sourcepub fn reset_damage(&mut self)
pub fn reset_damage(&mut self)
Clear accumulated damage after a frame is applied (the consumer’s ack).
Sourcepub fn mark_fully_damaged(&mut self)
pub fn mark_fully_damaged(&mut self)
Force the next Engine::frame to be a Full frame (every row), even if
little changed. The use case is reattach / late subscribe: a renderer
that connects after output has already been parsed needs the whole current
viewport once, then incremental diffs. Marks the screen fully damaged; the
next frame() reports FrameKind::Full.
Sourcepub fn scroll_delta(&self) -> Option<ScrollOp>
pub fn scroll_delta(&self) -> Option<ScrollOp>
The first-class scroll recorded since the last Engine::reset_damage,
if any — lets the renderer shift rows instead of redrawing them.
count is capped at the scroll region’s own height (#661). Repeated
scrolls of one region accumulate into a single op between acks, and a flood
accumulates far past the region: 32 KB of newlines in one Engine::feed is
enough. Shifting a region by more than its height already moves every source
row outside it, so the surplus names nothing a consumer can act on — while it
did overflow the i16 this value rides on the wire and arrive as a scroll in
the opposite direction. Suppressed entirely while the viewport is scrolled
up, since a content scroll must not shift a frozen view.
Sourcepub fn viewport_line(&self, i: usize) -> &[Cell]
pub fn viewport_line(&self, i: usize) -> &[Cell]
The cells of visible row i (0..rows) at the current scroll position.
Sourcepub fn scroll_up(&mut self, n: usize)
pub fn scroll_up(&mut self, n: usize)
Scroll the viewport up by n lines into scrollback history.
Sourcepub fn scroll_down(&mut self, n: usize)
pub fn scroll_down(&mut self, n: usize)
Scroll the viewport down by n lines toward the live screen.
Sourcepub fn scroll_to_bottom(&mut self)
pub fn scroll_to_bottom(&mut self)
Jump the viewport back to the live screen (follow the bottom).
Sourcepub fn selection_begin(
&mut self,
row: usize,
col: usize,
side: Side,
ty: SelectionType,
)
pub fn selection_begin( &mut self, row: usize, col: usize, side: Side, ty: SelectionType, )
Begin a selection of ty at viewport cell (row, col), on side of the
cell. Coordinates are viewport-relative (what a mouse event carries).
Sourcepub fn selection_extend(&mut self, row: usize, col: usize, side: Side)
pub fn selection_extend(&mut self, row: usize, col: usize, side: Side)
Extend the live selection to viewport cell (row, col), on side.
Sourcepub fn set_word_separators(&mut self, separators: &str)
pub fn set_word_separators(&mut self, separators: &str)
Replace the characters that end a word for SelectionType::Word — consumer
policy injected into a core mechanism (ADR-0017). Defaults to
DEFAULT_WORD_SEPARATORS. ' ' is forced in; see Term::set_word_separators
for why that floor is load-bearing rather than defensive.
Sourcepub fn word_separators(&self) -> &str
pub fn word_separators(&self) -> &str
The word-boundary set currently in force (including the forced ' ').
Sourcepub fn selection_clear(&mut self)
pub fn selection_clear(&mut self)
Clear the selection.
Sourcepub fn selection_range(&self) -> Vec<SelectionSpan>
pub fn selection_range(&self) -> Vec<SelectionSpan>
The selection projected onto the viewport: one inclusive-column span per visible row, for the renderer to highlight. Empty when nothing is selected or the selection is fully scrolled off-screen.
A span never ends inside a wide glyph (#454). An endpoint landing on
half of a width-2 pair takes the whole pair, so a highlight cannot split
a CJK glyph down the middle — which also means a span may be one column
wider than the columns the caller’s gesture named. On a Block
selection that widening is per row, so the rectangle’s rows can differ
in width. selection_text widens identically;
the two never disagree.
Sourcepub fn selection_text(&self) -> Option<String>
pub fn selection_text(&self) -> Option<String>
The selected text for copy (respects scrollback), or None if no
selection.
Widened onto whole wide-glyph pairs exactly as
selection_range is (#454) — a spacer extracts
as nothing, so a range ending inside a pair would copy text the
highlight does not show.
Sourcepub fn search(&self, query: &str) -> Vec<Match>
pub fn search(&self, query: &str) -> Vec<Match>
Literal search over the grid + scrollback, returning every match in
absolute buffer coordinates (top-to-bottom). Smart-case: a query with no
uppercase matches case-insensitively. The consumer drives next/prev by
walking the returned Vec and calling Engine::scroll_to_match.
Sourcepub fn search_with(&self, query: &str, opts: SearchOptions) -> Vec<Match>
pub fn search_with(&self, query: &str, opts: SearchOptions) -> Vec<Match>
Search with explicit SearchOptions — regex, whole-word, and a case-sensitivity override
beyond the literal + smart-case search (#314).
Sourcepub fn viewport_logical_lines(&self) -> Vec<LogicalLine>
pub fn viewport_logical_lines(&self) -> Vec<LogicalLine>
The viewport’s logical lines (#113/ADR-0017): each soft-wrap-joined line’s
text plus a per-char map to its viewport (row, col). The buffer-wide
mechanism for consumer-side URL detection — the consumer runs its own
regex / new URL() over the text and maps matches back through cells.
Also serves the a11y mirror (#119).
Sourcepub fn accessible_text(&self) -> String
pub fn accessible_text(&self) -> String
The whole buffer (scrollback + screen) as one text document for a
screen-reader accessible view (#150) — soft-wrap-joined, wide-spacers
skipped, trailing blanks trimmed at the logical end, \n between logical
lines. A query seam the consumer summons (frame mode: over IPC, like
selection_text); no wire-format change. On the
alt screen only the alt buffer is shown.
This is the document CommandLine::line indexes, which makes that last
sentence a pairing obligation rather than a detail (#743): ask both in the
same breath and keep them together, because a document line is meaningless
against a document sampled at another instant — and while the alt screen is up
the two queries are about different buffers entirely. See
Engine::command_lines.
Sourcepub fn scroll_to_match(&mut self, m: &Match)
pub fn scroll_to_match(&mut self, m: &Match)
Scroll the viewport so m is visible (next/prev navigation: the consumer
picks the match, the engine scrolls to it).
Sourcepub fn match_spans(&self, m: &Match) -> Vec<SelectionSpan>
pub fn match_spans(&self, m: &Match) -> Vec<SelectionSpan>
The match projected onto the viewport as inclusive-column spans per visible row, for the renderer to highlight.
A span never ends inside a wide glyph (#454), the same widening
selection_range applies. It matters more here,
because a Match may be one the caller assembled: an out-of-range
column is bounded onto the row’s last cell, which is a trailing spacer
whenever the row ends in a wide glyph — so without the widening a
highlight could be that glyph’s right half alone.
Sourcepub fn set_search_highlights(&mut self, matches: Vec<Match>)
pub fn set_search_highlights(&mut self, matches: Vec<Match>)
Set the search highlights the frame should carry (#108). The
consumer owns match navigation, so it hands the set to highlight back
here; Engine::frame then projects them onto the viewport overlay
alongside the selection. An empty vec clears the highlights.
Sourcepub fn set_active_search_highlight(&mut self, index: Option<usize>)
pub fn set_active_search_highlight(&mut self, index: Option<usize>)
Designate which member of the held highlight set is the active match
(#428) — the one next/prev navigation currently points at (that choice is
the consumer’s policy). Engine::frame projects it into the overlay’s
active_match group; it also stays in matches, and the renderer’s
highlight ranking resolves the overlap (#424). None or an out-of-range
index projects nothing. Passing a new set to
set_search_highlights resets the
designation, so re-designate after every hand-over.
Sourcepub fn set_active_search_match(&mut self, m: Option<Match>)
pub fn set_active_search_match(&mut self, m: Option<Match>)
Designate the active match by its absolute span (#436), independent of
the held highlight set — the past-cap path. A backend that caps its
hand-over (the documented 1000, xterm’s highlightLimit) can still give
the current match its active emphasis: xterm builds its active
decoration from the found result outside the capped list, and this is
that model. The span projects through the same wrap-aware viewport math
as any match; past the cap it paints the ACTIVE colour only (no plain
highlight underneath — honest about the cap). None clears. Same
lifecycle as the index form: reset on every
set_search_highlights hand-over and on
any coordinate-shifting invalidation (eviction, region scroll, reflow,
alt-screen swaps), so re-designate after each hand-over.
Sourcepub fn add_marker(&mut self, row: usize) -> MarkerId
pub fn add_marker(&mut self, row: usize) -> MarkerId
Register a decoration marker at viewport row, returning its stable id
(#118). The marker anchors the content currently on that row and tracks
it through scroll/eviction/reflow; Engine::frame reports its viewport
position while visible. Use the id to remove it or to match the
TermEvent::MarkerDisposed fired when its line leaves the buffer.
A buffer holds at most MAX_MARKERS live markers (#721) — the population is
also grown by the stream, through OSC 133 command marks, so it is bounded.
Past the cap the oldest marker is retired and announced through the same
MarkerDisposed event, so a consumer that already handles disposal needs no new
handling; a consumer that ignores it can leave a decoration bound to a dead id.
Sourcepub fn remove_marker(&mut self, id: MarkerId)
pub fn remove_marker(&mut self, id: MarkerId)
Remove a marker by id (#118), firing TermEvent::MarkerDisposed. A no-op
for an unknown or already-disposed id.
Sourcepub fn track_point(&mut self, line: usize, col: usize) -> TrackedId
pub fn track_point(&mut self, line: usize, col: usize) -> TrackedId
Track absolute buffer (line, col), returning a stable id (#691): the
engine keeps the position on the content that is there now, through
scrollback eviction, region scrolls and reflow.
This is what an absolute coordinate held outside the engine needs to stay
meaningful — a search anchor carrying an emphasis across a re-search is the
case it exists for. The engine renumbers this space (evicting the oldest
history line shifts every index down by one), and it renumbers it in the
consumer’s absence, so a remembered Match silently comes to name
different text.
Mechanism only: which position is worth remembering, and what to do once it
is gone, stay with the consumer (ADR-0017). Release it with
Engine::untrack_point — the engine cannot know when you are done.
The line is maintained; the column is carried, not tracked. In-row edits
(ICH / DCH) shift cells past a tracked column without moving it, so a point
on text that was pushed sideways names the wrong cell in that row. No
reference maintains a column here either — xterm’s markers carry none at
all, and ghostty’s pins are untouched by its insertChars/deleteChars —
so this is the convergent behaviour rather than an omission.
Sourcepub fn tracked_point(&self, id: TrackedId) -> Option<(usize, usize)>
pub fn tracked_point(&self, id: TrackedId) -> Option<(usize, usize)>
Where the point registered as id sits now, in the active screen’s
coordinates — or None (#691).
None covers three cases, and a caller does not need to tell them apart:
the content has left the buffer, the id is unknown or released, or the point
belongs to the other screen. The last one is not a limitation but the only
honest answer: the primary grid and the alt grid occupy the same absolute
indices, so a number alone cannot say which screen it means. All three say
do not move anything on account of this point.
An out-of-range coordinate is clamped rather than rejected, at both ends (ADR-0026 D2/D3): the line into the buffer’s range, the column to the grid width. That bound is applied here, at the read; a coordinate that was never in range to begin with is also resolved by a reflow (it maps to the top of the buffer), so “bounded once” holds for the site, not for the value.
Sourcepub fn untrack_point(&mut self, id: TrackedId)
pub fn untrack_point(&mut self, id: TrackedId)
Release a tracked point (#691). A no-op for an unknown or already-released id.
Sourcepub fn command_marks(&self) -> Vec<(MarkerId, usize, MarkerKind)>
pub fn command_marks(&self) -> Vec<(MarkerId, usize, MarkerKind)>
The OSC 133 shell-integration command marks in buffer order — (id, absolute line, kind) (#158). Excludes plain add_marker decorations.
The consumer pairs prompt/command/finished marks to drive prompt-to-prompt
navigation and command/exit announcements (#160); the engine only parses
the 133;A/B/C/D sequences and anchors the marks.
The answer is instantaneous — it describes the buffer it was asked of, and
nothing on it dates it (#742). Re-ask; never keep it and never rebase it.
The lines move on both of the axes MarkerIndex carries a scalar for:
scrollback eviction shifts every mark by the same amount, and a top-anchored
DECSTBM region shifts the marks below its margin once per output line — the
second inside a single Engine::feed, with no resize anywhere.
Why this is not shaped like its sibling. Engine::marker_index carries a
basis and an epoch because a consumer must hold its answer: it feeds an
overview ruler that has to be current in every frame, and re-pulling per frame
is the O(M)-per-frame payload ADR-0020 R3 exists to forbid. This query is
consumed when a user acts, so re-asking is the natural act — and here a
re-ask always answers, because this population’s frame of reference never
changes. That is the property the sibling lacks, and the reason it needed the
epoch rather than a reason this one does: an alt switch is one of the four
moves that epoch announces.
The lines are [scrollback ++ primary], always — including while the alt
screen is up. They do not name the active buffer. The two buffers occupy the
same absolute indices, so one integer from here and the same integer from
Engine::marker_index name different content, and neither tuple nor struct
says which. Engine::tracked_point meets that ambiguity and answers None
rather than a number (ADR-0026 D2/D3); it can, because it is asked about one
point of unknown origin. This query enumerates a population whose screen is
fixed by definition, so it answers — and states the screen here instead.
Consequently an empty answer means every mark was disposed and can mean nothing
else, where marker_index’s silence is ambiguous between that and “you are on
the other screen”.
A mark also dies when a whole row is blanked where it stands (#750). Until
then the only deaths were the buffer moving — eviction, a region rotate, a
reflow — and a clear left every mark on the screen alive over blank rows. ED
now retires the marks on each whole row it blanks, through the same
TermEvent::MarkerDisposed a consumer already handles, so this query going empty
after a clear is the ordinary meaning above and not a new one. EL and ECH
deliberately do not, whatever they blank: a line editor redraws its input line
with \r ESC[K on every keystroke, and the CommandStart of the command being
typed is on that row.
Sourcepub fn command_lines(&self) -> Vec<CommandLine>
pub fn command_lines(&self) -> Vec<CommandLine>
The executed shell commands recovered from OSC-133 marks, in buffer order
(#166) — the query behind screen-reader command navigation. Each
CommandLine carries the typed command text (prompt/output excluded via
the captured columns), its jump line (CommandStart), and the exit code.
This is a full-buffer query, wired to the frame-mode consumer over IPC like
Engine::accessible_text; the web side has no scrollback cells to derive
it (ADR-0017 — buffer-wide text is core’s).
The text and the exit are frozen when the stream reveals them; only the line
is derived (#750). CommandLine::command is captured at the 133;C that
closes the command — the instant it is complete and on screen — and
CommandLine::exit is written down when 133;D is parsed. Neither is
recoverable afterwards: re-reading the text through the recorded columns names
whatever now occupies those cells, which a plain overwrite, ICH, DCH and an
erase all arrange, and an exit code is in no cell at any time. A capture is
bounded at MAX_COMMAND_TEXT chars, truncated at a char boundary, for the
reason MAX_MARKERS exists: the stream chooses the distance between B and
C. CommandLine::line stays derived, because it is the half the anchor
fixups already maintain.
The answer is instantaneous — it describes the buffer it was asked of, and
nothing on it dates it (#743). Re-ask; never keep it past the document it
indexes, and never rebase it. Same discharge as Engine::command_marks and
for the same two reasons (ADR-0029 D3): the clock is a user action, so the ask
is the act; and this population’s frame of reference never flips, so a re-ask
always answers. Absence means the command is gone or that its output has not
started yet — both of which the next ask resolves. What absence never means is
“you are on the other screen”, which is the meaning no re-ask could undo.
Do not rebase by MarkerIndex::evicted_total. That dates the absolute
space. CommandLine::line is a document line, and the two spaces move
apart in both directions: an eviction that pops a soft-wrap continuation row
moves the absolute lines and not this one, and flipping a row’s wrap bit — which
ordinary output does — moves this one while the absolute lines and both of
MarkerIndex’s scalars stay put. They agree most of the time, which is what
makes rebasing look correct right up until it silently is not.
Ask Engine::accessible_text in the same breath, and only on the primary
screen. The lines index that document; while the alt screen is up it returns
the alt document instead, and these lines are indices into the primary one. If
the alt screen is taller than the held index — a full-screen TUI, which is the
normal case — the index still resolves, onto unrelated content, so a bounds
check does not save a caller here. The query keeps answering on the alt screen
deliberately: emptying it would give absence the one meaning a re-ask cannot
recover from, which is what the discharge above rests on. Pairing the two is the
caller’s, and this is where it is said.
Sourcepub fn marker_index(&self) -> MarkerIndex
pub fn marker_index(&self) -> MarkerIndex
Every live marker of the active buffer with its absolute buffer line, plus the basis that says how long the answer stays usable (#490).
The pull half of the marker surface. It shares Engine::command_lines’s
shape — the consumer asks once and keeps the answer, rather than being handed
every live marker inside every frame, which is O(M) payload per frame for a
quantity unrelated to what changed (ADR-0020 R3). It does not share its
coordinate: only the lines here are buffer-absolute and rebasable by the
evicted_total delta. CommandLine::line is a document line over
Engine::accessible_text, where soft-wrapped rows collapse — eviction moves it
by an amount no scalar on this surface expresses, so it is an answer to keep only
as long as the buffer it was asked of.
Ask again when MarkerIndex::epoch differs from the one you hold. Drop an
entry when its TermEvent::MarkerDisposed arrives, and append one when
TermEvent::MarkerCreated does — neither deliberately moves the epoch, so
neither costs a re-pull. Append it on the instant the event carries, not on the
newest frame’s: a feed can create a marker and then evict, and those are two
different origins (#737).
Adopt a birth only into the generation it names (#741). The event carries this
pull’s whole triple — line, basis, MarkerIndex::epoch — because the basis dates
only a uniform move. A reflow or a region rotate moves markers individually, so a
line dated to the generation before one is not stale by a delta; it is an answer
about a different buffer, and the re-pull the epoch already forces is what supplies
the marker instead. Compare generations for equality: the counter wraps.
Draining before you read the frame is then a cost preference, not a correctness
one. Reading the frame first leaves marker_count one ahead of an index that has
not been told yet, so a consumer comparing the two spends an O(M) re-pull
reconciling a fact this event delivered at O(1). Placement does not depend on the
order, on either axis.