Skip to main content

justerm_core/
lib.rs

1// The crate-level docs and the compiled usage example both live in README.md,
2// pulled in here so the published crates.io front page and this doctest are one
3// source: the `cargo test` doc pass compiles the README's `rust` block, so the
4// published usage snippet cannot drift from the real API (#483, and the #473
5// rule that a shipped usage snippet must compile against the real types).
6#![doc = include_str!("../README.md")]
7
8mod cell;
9mod color;
10mod cursor;
11mod damage;
12mod event;
13mod grapheme;
14mod grid;
15mod input;
16mod logical;
17mod search;
18mod selection;
19mod serialize;
20mod term;
21
22pub use cell::{Cell, CellFlags};
23pub use color::Color;
24pub use cursor::{Cursor, CursorShape, Pen};
25pub use damage::{LineDamage, ScrollOp, TermDamage};
26pub use event::TermEvent;
27pub use grid::{Grid, Row};
28pub use input::{
29    Key, KeyAction, KeyEvent, KeypadKey, Modifiers, MouseAction, MouseButton, MouseEvent,
30    MouseEvents,
31};
32pub use logical::LogicalLine;
33pub use search::{Match, SearchOptions, is_valid_regex};
34pub use selection::{SelectionSpan, SelectionType, Side};
35pub use serialize::{
36    CELL_RECORD_LEN, DecodeError, Frame, FrameKind, MarkerId, MarkerKind, MarkerLine,
37    MarkerPosition, Overlay, Span, WIRE_VERSION, decode, encode, encode_cell_record, encode_color,
38};
39
40pub use term::{CommandLine, MIN_COLUMNS, Term};
41
42use vte::Parser;
43
44/// The terminal engine: pairs the `vte` parser with our state model.
45///
46/// `Parser` and `Term` are kept as separate fields because `Parser::advance`
47/// borrows both the parser and the performer mutably at once — a single struct
48/// owning both could not satisfy the borrow checker.
49pub struct Engine {
50    parser: Parser,
51    term: Term,
52}
53
54impl Engine {
55    /// A blank engine with a `cols` × `rows` screen and a default scrollback cap.
56    ///
57    /// `cols` is widened to [`MIN_COLUMNS`] — a narrower screen cannot represent a
58    /// width-2 glyph, so the engine clamps rather than accepting a size it would
59    /// have three different answers for (#547).
60    pub fn new(cols: usize, rows: usize) -> Self {
61        Engine {
62            parser: Parser::new(),
63            term: Term::new(cols, rows),
64        }
65    }
66
67    /// Like [`Engine::new`] but with an explicit scrollback line limit. `cols` is
68    /// clamped to [`MIN_COLUMNS`] the same way.
69    pub fn with_scrollback(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
70        Engine {
71            parser: Parser::new(),
72            term: Term::with_scrollback(cols, rows, scrollback_limit),
73        }
74    }
75
76    /// Push a slice of VT bytes. The caller owns the PTY/SSH/socket I/O — the
77    /// engine only consumes the bytes it is handed.
78    pub fn feed(&mut self, bytes: &[u8]) {
79        self.parser.advance(&mut self.term, bytes);
80    }
81
82    /// Resize the screen to `cols` x `rows`. Rows that scroll off the top enter
83    /// scrollback; the whole screen is damaged. (Soft-wrap reflow lands in #7.)
84    ///
85    /// `cols` is widened to [`MIN_COLUMNS`] **silently**: a `resize(1, rows)` during
86    /// a pane drag yields a two-column screen with no error. Read the resulting
87    /// width back from [`Engine::grid`] or the frame header rather than assuming the
88    /// value passed here, and size the PTY from that same width (#547).
89    pub fn resize(&mut self, cols: usize, rows: usize) {
90        self.term.resize(cols, rows);
91    }
92
93    /// The current screen grid.
94    pub fn grid(&self) -> &Grid {
95        self.term.grid()
96    }
97
98    /// The current cursor (position, pending-wrap, pen).
99    pub fn cursor(&self) -> &Cursor {
100        self.term.cursor()
101    }
102
103    /// Whether bracketed-paste mode (DEC ?2004) is enabled. A consumer's input
104    /// encoder reads this to decide whether to wrap pasted text in markers.
105    pub fn bracketed_paste(&self) -> bool {
106        self.term.bracketed_paste()
107    }
108
109    /// Encode a key event to the bytes an application expects, honouring the
110    /// engine's cursor-key mode (DECCKM). The inverse of [`Engine::feed`] — the
111    /// consumer hands a decoded key event and writes the bytes to its PTY.
112    /// Returns `None` for a key with no defined encoding.
113    pub fn encode_key(&self, ev: KeyEvent) -> Option<Vec<u8>> {
114        self.term.encode_key(ev)
115    }
116
117    /// Encode a mouse event using the engine's active tracking mode + encoding.
118    /// Returns `None` when mouse reporting is off, or when the event is filtered
119    /// out by the mode (e.g. a bare move while only ?1000 is set).
120    pub fn encode_mouse(&self, ev: MouseEvent) -> Option<Vec<u8>> {
121        self.term.encode_mouse(ev)
122    }
123
124    /// Encode pasted text — wrapped in bracketed-paste markers when ?2004 is on,
125    /// raw otherwise.
126    pub fn encode_paste(&self, text: &str) -> Vec<u8> {
127        self.term.encode_paste(text)
128    }
129
130    /// Encode a focus change (`CSI I` on focus-in, `CSI O` on focus-out), or
131    /// `None` when focus reporting (?1004) is off.
132    pub fn encode_focus(&self, focused: bool) -> Option<Vec<u8>> {
133        self.term.encode_focus(focused)
134    }
135
136    /// Take the consumer events accumulated since the last drain (title / bell /
137    /// cwd — see [`TermEvent`]), emptying the queue. The pull counterpart to a
138    /// callback: poll this alongside [`Engine::frame`].
139    pub fn drain_events(&mut self) -> Vec<TermEvent> {
140        self.term.drain_events()
141    }
142
143    /// Take the reply bytes the engine produced for app queries (DA / DSR /
144    /// DECRQM) since the last drain — the consumer writes them straight back to
145    /// the PTY. The inbound-query counterpart to [`Engine::drain_events`].
146    pub fn drain_replies(&mut self) -> Vec<u8> {
147        self.term.drain_replies()
148    }
149
150    /// The OSC 8 hyperlink index at **screen** `(row, col)` — the live grid, same
151    /// coordinates as [`Engine::grid`]'s `cell(row, col)` — or `None`. Combining
152    /// and links no longer ride on the [`Cell`] (#45/#46); read the
153    /// index here, then resolve it with [`Engine::hyperlink`].
154    pub fn link_at(&self, row: usize, col: usize) -> Option<core::num::NonZeroU32> {
155        self.term.screen_link_at(row, col)
156    }
157
158    /// The underline colour (SGR 58, #520) at **screen** `(row, col)` — same
159    /// coordinates as [`Engine::grid`]'s `cell(row, col)`. A theme-agnostic
160    /// [`Color`] reference; [`Color::Default`] means the underline follows the
161    /// glyph's foreground (the common case, and what a cell with no SGR 58 returns).
162    /// Like the hyperlink, the colour rides a per-row side table, not the 12-byte
163    /// [`Cell`] (#520).
164    pub fn underline_color_at(&self, row: usize, col: usize) -> Color {
165        self.term.screen_underline_color_at(row, col)
166    }
167
168    /// The OSC 8 hyperlink index at **viewport** `(row, col)` — the visible
169    /// window including scrollback at the current scroll, same coordinates as
170    /// [`Engine::viewport_line`] — or `None`.
171    pub fn viewport_link_at(&self, row: usize, col: usize) -> Option<core::num::NonZeroU32> {
172        self.term.viewport_link_at(row, col)
173    }
174
175    /// Resolve a hyperlink index (from [`Engine::link_at`] /
176    /// [`Engine::viewport_link_at`], or a decoded `Span`'s `links`) to its URI,
177    /// to make a cell clickable.
178    pub fn hyperlink(&self, link: core::num::NonZeroU32) -> Option<&str> {
179        self.term.hyperlink(link)
180    }
181
182    /// Number of lines currently held in scrollback history.
183    pub fn scrollback_len(&self) -> usize {
184        self.term.scrollback_len()
185    }
186
187    /// Whether the app has an open **synchronized-output** block (DEC `?2026`):
188    /// it has asked that the next frame of output be painted atomically. The
189    /// engine only *reports* this — **the consumer owns the paint-hold and the
190    /// spec-mandated timeout** (a buggy app that never closes the block must not
191    /// freeze the screen forever, and the engine has no clock). Poll this after
192    /// `feed`; while it is `true`, defer applying frames, and apply once it
193    /// clears (or your own timeout fires). (#73)
194    pub fn synchronized_output(&self) -> bool {
195        self.term.synchronized_output()
196    }
197
198    /// Whether the app enabled color-scheme-update notifications (DEC `?2031`).
199    /// The engine is theme-agnostic — it never knows the scheme. The consumer
200    /// answers a [`TermEvent::ColorSchemeQuery`] (from `?996`) and, when its
201    /// scheme changes *and* this is `true`, sends an unsolicited notification, in
202    /// both cases by calling [`Engine::report_color_scheme`] (#85).
203    pub fn color_scheme_updates(&self) -> bool {
204        self.term.color_scheme_updates()
205    }
206
207    /// Report the current light/dark color scheme to the app as `CSI ? 997 ; 1 n`
208    /// (dark) / `; 2 n` (light), drained via [`Engine::drain_replies`]. Call this
209    /// to answer a [`TermEvent::ColorSchemeQuery`], or — guarded by
210    /// [`Engine::color_scheme_updates`] — when the scheme changes. The engine only
211    /// formats the bit you pass; it stores no scheme (#85).
212    pub fn report_color_scheme(&mut self, dark: bool) {
213        self.term.report_color_scheme(dark);
214    }
215
216    /// Answer an OSC 11 `QueryBackground` event (#122): the consumer hands back
217    /// the current background spec (it owns the palette) and the engine queues
218    /// the OSC 11 reply for `drain_replies`. Theme-agnostic — the engine never
219    /// knows the colour, only formats the envelope.
220    pub fn report_background(&mut self, spec: &str) {
221        self.term.report_background(spec);
222    }
223
224    /// Answer an OSC 10 `QueryForeground` event (#122): queue the OSC 10 reply
225    /// from the consumer-supplied spec. Theme-agnostic envelope-only.
226    pub fn report_foreground(&mut self, spec: &str) {
227        self.term.report_foreground(spec);
228    }
229
230    /// Answer an OSC 4 `QueryPaletteColor` event (#122): queue the OSC 4 reply for
231    /// `index` from the consumer-supplied spec. Theme-agnostic envelope-only.
232    pub fn report_palette_color(&mut self, index: u8, spec: &str) {
233        self.term.report_palette_color(index, spec);
234    }
235
236    /// Whether the app enabled **win32-input-mode** (DEC `?9001`): it asked for
237    /// keys as raw Windows key-records. The engine only tracks the flag — encoding
238    /// the records (`CSI Vk;Sc;Uc;Kd;Cs;Rc _`) is a non-goal (raw passthrough, no
239    /// semantic conversion), so [`Engine::encode_key`] is unchanged. A ConPTY
240    /// consumer reads this to decide whether to emit the records itself (#86).
241    pub fn win32_input_mode(&self) -> bool {
242        self.term.win32_input_mode()
243    }
244
245    /// What changed since the last [`Engine::reset_damage`] — line ranges each
246    /// with a changed column span (see ADR-0003).
247    pub fn damage(&self) -> TermDamage {
248        self.term.damage()
249    }
250
251    /// Build a serializable [`Frame`] of the current diff — the damaged spans
252    /// (or every row, when `Full`), the recorded scroll op, and a frame-local
253    /// grapheme side-table. Pass it to [`encode`] for the wire (see #6). Reading
254    /// a frame does not clear damage; call [`Engine::reset_damage`] on ack.
255    pub fn frame(&self) -> Frame {
256        self.term.frame()
257    }
258
259    /// Clear accumulated damage after a frame is applied (the consumer's ack).
260    pub fn reset_damage(&mut self) {
261        self.term.reset_damage();
262    }
263
264    /// Force the next [`Engine::frame`] to be a `Full` frame (every row), even if
265    /// little changed. The use case is **reattach / late subscribe**: a renderer
266    /// that connects after output has already been parsed needs the whole current
267    /// viewport once, then incremental diffs. Marks the screen fully damaged; the
268    /// next `frame()` reports `FrameKind::Full`.
269    pub fn mark_fully_damaged(&mut self) {
270        self.term.mark_fully_damaged();
271    }
272
273    /// The first-class scroll recorded since the last [`Engine::reset_damage`],
274    /// if any — lets the renderer shift rows instead of redrawing them.
275    pub fn scroll_delta(&self) -> Option<ScrollOp> {
276        self.term.scroll_delta()
277    }
278
279    /// The cells of visible row `i` (0..rows) at the current scroll position.
280    pub fn viewport_line(&self, i: usize) -> &[Cell] {
281        self.term.viewport_line(i)
282    }
283
284    /// Scroll the viewport up by `n` lines into scrollback history.
285    pub fn scroll_up(&mut self, n: usize) {
286        self.term.scroll_up(n);
287    }
288
289    /// Scroll the viewport down by `n` lines toward the live screen.
290    pub fn scroll_down(&mut self, n: usize) {
291        self.term.scroll_down(n);
292    }
293
294    /// Jump the viewport back to the live screen (follow the bottom).
295    pub fn scroll_to_bottom(&mut self) {
296        self.term.scroll_to_bottom();
297    }
298
299    /// Begin a selection of `ty` at viewport cell `(row, col)`, on `side` of the
300    /// cell. Coordinates are viewport-relative (what a mouse event carries).
301    pub fn selection_begin(&mut self, row: usize, col: usize, side: Side, ty: SelectionType) {
302        self.term.selection_begin(row, col, side, ty);
303    }
304
305    /// Extend the live selection to viewport cell `(row, col)`, on `side`.
306    pub fn selection_extend(&mut self, row: usize, col: usize, side: Side) {
307        self.term.selection_extend(row, col, side);
308    }
309
310    /// Clear the selection.
311    pub fn selection_clear(&mut self) {
312        self.term.selection_clear();
313    }
314
315    /// The selection projected onto the viewport: one inclusive-column span per
316    /// visible row, for the renderer to highlight. Empty when nothing is
317    /// selected or the selection is fully scrolled off-screen.
318    pub fn selection_range(&self) -> Vec<SelectionSpan> {
319        self.term.selection_range()
320    }
321
322    /// The selected text for copy (respects scrollback), or `None` if no
323    /// selection.
324    pub fn selection_text(&self) -> Option<String> {
325        self.term.selection_text()
326    }
327
328    /// Literal search over the grid + scrollback, returning every match in
329    /// absolute buffer coordinates (top-to-bottom). Smart-case: a query with no
330    /// uppercase matches case-insensitively. The consumer drives next/prev by
331    /// walking the returned `Vec` and calling [`Engine::scroll_to_match`].
332    pub fn search(&self, query: &str) -> Vec<Match> {
333        self.term.search(query)
334    }
335
336    /// Search with explicit [`SearchOptions`] — regex, whole-word, and a case-sensitivity override
337    /// beyond the literal + smart-case [`search`](Self::search) (#314).
338    pub fn search_with(&self, query: &str, opts: SearchOptions) -> Vec<Match> {
339        self.term.search_with(query, opts)
340    }
341
342    /// The viewport's logical lines (#113/ADR-0017): each soft-wrap-joined line's
343    /// text plus a per-char map to its viewport `(row, col)`. The buffer-wide
344    /// mechanism for consumer-side URL detection — the consumer runs its own
345    /// regex / `new URL()` over the text and maps matches back through `cells`.
346    /// Also serves the a11y mirror (#119).
347    pub fn viewport_logical_lines(&self) -> Vec<LogicalLine> {
348        self.term.viewport_logical_lines()
349    }
350
351    /// The whole buffer (scrollback + screen) as one text document for a
352    /// screen-reader accessible view (#150) — soft-wrap-joined, wide-spacers
353    /// skipped, trailing blanks trimmed at the logical end, `\n` between logical
354    /// lines. A query seam the consumer summons (frame mode: over IPC, like
355    /// [`selection_text`](Self::selection_text)); no wire-format change. On the
356    /// alt screen only the alt buffer is shown.
357    pub fn accessible_text(&self) -> String {
358        self.term.accessible_text()
359    }
360
361    /// Scroll the viewport so `m` is visible (next/prev navigation: the consumer
362    /// picks the match, the engine scrolls to it).
363    pub fn scroll_to_match(&mut self, m: &Match) {
364        self.term.search_scroll_to(m);
365    }
366
367    /// The match projected onto the viewport as inclusive-column spans per
368    /// visible row, for the renderer to highlight.
369    pub fn match_spans(&self, m: &Match) -> Vec<SelectionSpan> {
370        self.term.match_spans(m)
371    }
372
373    /// Set the search highlights the frame should carry (#108). The
374    /// consumer owns match navigation, so it hands the set to highlight back
375    /// here; [`Engine::frame`] then projects them onto the viewport overlay
376    /// alongside the selection. An empty vec clears the highlights.
377    pub fn set_search_highlights(&mut self, matches: Vec<Match>) {
378        self.term.set_search_highlights(matches);
379    }
380
381    /// Designate which member of the held highlight set is the *active* match
382    /// (#428) — the one next/prev navigation currently points at (that choice is
383    /// the consumer's policy). [`Engine::frame`] projects it into the overlay's
384    /// `active_match` group; it also stays in `matches`, and the renderer's
385    /// highlight ranking resolves the overlap (#424). `None` or an out-of-range
386    /// index projects nothing. Passing a new set to
387    /// [`set_search_highlights`](Self::set_search_highlights) resets the
388    /// designation, so re-designate after every hand-over.
389    pub fn set_active_search_highlight(&mut self, index: Option<usize>) {
390        self.term.set_active_search_highlight(index);
391    }
392
393    /// Designate the *active* match by its absolute span (#436), independent of
394    /// the held highlight set — the past-cap path. A backend that caps its
395    /// hand-over (the documented 1000, xterm's `highlightLimit`) can still give
396    /// the current match its active emphasis: xterm builds its active
397    /// decoration from the found result *outside* the capped list, and this is
398    /// that model. The span projects through the same wrap-aware viewport math
399    /// as any match; past the cap it paints the ACTIVE colour only (no plain
400    /// highlight underneath — honest about the cap). `None` clears. Same
401    /// lifecycle as the index form: reset on every
402    /// [`set_search_highlights`](Self::set_search_highlights) hand-over and on
403    /// any coordinate-shifting invalidation (eviction, region scroll, reflow,
404    /// alt-screen swaps), so re-designate after each hand-over.
405    pub fn set_active_search_match(&mut self, m: Option<Match>) {
406        self.term.set_active_search_match(m);
407    }
408
409    /// Register a decoration marker at viewport `row`, returning its stable id
410    /// (#118). The marker anchors the content currently on that row and tracks
411    /// it through scroll/eviction/reflow; [`Engine::frame`] reports its viewport
412    /// position while visible. Use the id to remove it or to match the
413    /// `TermEvent::MarkerDisposed` fired when its line leaves the buffer.
414    pub fn add_marker(&mut self, row: usize) -> MarkerId {
415        self.term.add_marker(row)
416    }
417
418    /// Remove a marker by id (#118), firing `TermEvent::MarkerDisposed`. A no-op
419    /// for an unknown or already-disposed id.
420    pub fn remove_marker(&mut self, id: MarkerId) {
421        self.term.remove_marker(id);
422    }
423
424    /// The OSC 133 shell-integration command marks in buffer order — `(id,
425    /// absolute line, kind)` (#158). Excludes plain `add_marker` decorations.
426    /// The consumer pairs prompt/command/finished marks to drive prompt-to-prompt
427    /// navigation and command/exit announcements (#160); the engine only parses
428    /// the `133;A/B/C/D` sequences and anchors the marks.
429    pub fn command_marks(&self) -> Vec<(MarkerId, usize, MarkerKind)> {
430        self.term.command_marks()
431    }
432
433    /// The executed shell commands recovered from OSC-133 marks, in buffer order
434    /// (#166) — the query behind screen-reader command navigation. Each
435    /// [`CommandLine`] carries the typed command text (prompt/output excluded via
436    /// the captured columns), its jump line (CommandStart), and the exit code.
437    /// This is a full-buffer query, wired to the frame-mode consumer over IPC like
438    /// [`Engine::accessible_text`]; the web side has no scrollback cells to derive
439    /// it (ADR-0017 — buffer-wide text is core's).
440    pub fn command_lines(&self) -> Vec<CommandLine> {
441        self.term.command_lines()
442    }
443}