Skip to content

Terminal Engine

Se.Cherkasov edited this page Aug 23, 2026 · 1 revision

Terminal engine

English · Русский

The emulator is written in commonMain — pure Kotlin over a cell grid, no platform terminal library — so desktop and Android render the same state.

Pipeline

flowchart LR
  CH[ShellChannel<br/>ssh · telnet · serial · mosh · pty] --> SES[TerminalSession<br/>coroutine loop, PTY size]
  SES --> EMU[TerminalEmulator<br/>grid, cursor, modes]
  EMU --> SNAP[render snapshot<br/>TermSnapshotRow]
  SNAP --> UI[TerminalScreenState → Compose]
  SES --> REC[SessionRecorder<br/>asciicast v2]
  EMU --> MARK[TerminalStepMark<br/>runbook step boundaries]
Loading

The grid

Type What it models
TermCell one cell: text (a String, so astral characters and combining sequences fit), style, width
TermWidth Single / Wide (leading cell of a CJK or emoji pair) / Continuation
TermStyle the full SGR attribute set; underline is a shape (4:x — double, curly, dotted, dashed) plus its own colour
TermColor Default, Indexed (xterm 0–255) or Rgb truecolor — UI-independent; the renderer maps it into the theme palette
TermRow cells plus a wrapped flag: true means auto-wrap (DECAWM) cut the line, an honest \n leaves it false
TermSnapshotRow an immutable row in a render snapshot; equal snapshots let an unchanged screen skip recomposition

Reflow

TerminalReflow is a pure function over row lists: soft-wrapped physical rows are joined into logical lines and re-split at the new width, the cursor following its text, with the bottom rows becoming the grid and the rest the scrollback (trimmed to the configured limit). Because it holds no emulator state it is tested directly as well as through resize tests.

Supporting pieces

Concern Where
SGR parsing SgrParser
Glyph metrics (for hit-testing and layout) CharMetrics
Selection and its fragments TerminalSelection
Scrollback search TerminalSearch
Mouse reporting TerminalMouse
Cross-session command history CommandHistory, TerminalHistoryStore
fish-style completion AutocompleteEngine, CommandSuggestions, FuzzyMatch
Input safety (what may be sent) SafeTerminalInput
Syntax highlighting terminal/highlight/

Recording

SessionRecorder writes asciicast v2: a JSON header line and one [time, "o", data] line per output chunk, so a recording plays in any asciinema player. The whole recording is held in memory and written once at the end — terminal output is untrusted and unbounded (cat a large file and it never stops), so maxEvents and maxBytes bound it and truncated says when a limit was hit rather than the app quietly eating memory. Recordings hold whatever the server printed, so they are only ever written when the user exports one.

Step marks

Runbooks need to know where a step's output starts and ends without printing protocol on the screen. TerminalStepMark defines a private OSC code, OSC 8375 (S,K in ASCII decimal):

  • OSC 8375 ; <token> ; BEL — the step's output starts here;
  • OSC 8375 ; <token> ; <exit code> BEL — the step finished with that status.

The emulator acts on a mark only for a step the client declared through expectStep, so an ordinary session parks nothing and a host echoing a token it read off the screen cannot close a capture window this client never opened. TerminalEchoFilter hides the echo of what the client typed. The runbook layer only builds the probes (runbook/RunbookMarker).

What a mark says is what the host's shell said: a compromised host can emit any status it likes. Runbooks are not a security boundary over a hostile host.

Rendering

ui/terminal/TerminalScreenState holds screen state for Compose, including scrollback search. Geometry derived from settings (font size → mouse hit-testing) is recomputed when the setting changes rather than cached forever, and nothing is allocated per recomposition.

Clone this wiki locally