termlens is 0.x, and every minor is allowed to break a consumer. 1.0 is
not a feature list — it is three decisions written down with the
measurement that decided each, plus a statement of which public items
the promise covers. A 1.0 tag requires all three sections below to be
filled in (RELEASING.md says so), and each was allowed to
come out no: an honest no is a 1.0 that means something.
Decided in 0.10 (September 2026). The issue each was decided in links back here.
The question. Does termlens run on Windows, and if so which of
wait_until, wait_frame and the query responder are supported there?
The measurement. tests/conpty_probe.rs sends twenty-one escape
sequences through a ConPTY (portable-pty 0.9, no passthrough mode) and
reports, per sequence, what the master read. Six came out verbatim. The
rest ConPTY eats (DA1, OSC 10/11, XTGETTCAP, DECRQM, mouse and focus mode
sets, kitty and sixel payloads, HTS/TBC), rewrites equivalently (SGR
reordered, OSC 2 as OSC 0, DEC graphics as UTF-8, a tab as CUF, link ids
of its own) or reorders: a DEC 2026 bracket closes before the content it
wrapped. It also sends every child a preamble (CSI 6 n, ?9001h,
?1004h, a title, ?25h) and does not start the child until the 6n is
answered, and a child's exit is not EOF on the pipe. The mechanics are in
DESIGN.md §1.
The decision. Windows is a supported platform for what survives the
re-render — the grid (text, cells, styles, cursor, wide characters, box
drawing, title, links by URL, clipboard, bracketed paste, cursor shape,
bell, the alternate screen), resize, typed input, wait_until,
wait_stable, snapshot_after, wait_exit, bin! — and is documented
as Unix-only for what does not: wait_frame, frame_timings, record,
graphics, the responder's outbound claims (Graphics, background_rgb,
foreground_rgb, cell_size), mouse_mode(s), focus_events, link
ids, Terminal::signal, non-UTF-8 bytes. Each test the platform cannot
honour is #[cfg_attr(windows, ignore = "<the probe row>")]. The
windows-latest leg runs the whole suite on every pull request and is a
required check; the windows workflow re-runs the probe on demand. The
LIMITATIONS.md carries the user-facing list. A change to what
ConPTY does is a change to this section, not a bug in termlens.
The question. Does the emulator behind the Emulator trait stay
vt100, or move to wezterm-term or alacritty_terminal — which would
retire the second parser that recovers blink, conceal and strikethrough,
and give mouse modes and grapheme clusters a stronger footing?
The measurement. BACKENDS.md holds the comparison:
attribute coverage, grapheme handling, mouse-mode fidelity, reflow,
dependency weight, MSRV, licence, publishability and maintenance cadence
of the three, against the seven methods the trait needs. The numbers that
decided it: wezterm-term is not on crates.io (a git dependency cannot be
published, and deny.toml refuses it); alacritty_terminal tracks
conceal and strikethrough but not blink, so a swap keeps one carrier
attribute in a shadow or a fork anyway, and brings 27 crates (an event
loop, polling, rustix, signal-hook — its tty half is not optional)
against vt100's 9, under a single Apache-2.0 licence and an MSRV that
moves with the Alacritty release train; and the shadow's measured cost is
noise (260 ms with, 263 ms without, for 40,000 lines through 80x24).
The decision. vt100 stays, and the shadow stays on purpose: two
parsers over one stream is a mechanism whose soundness is argued from
vt100's own code (attributes never influence geometry) and checked by a
debug assertion on every snapshot. What the backend does not do, termlens
does in front of it — character sets, tab stops, insert mode, the
unsupported-sequence record — on the same staged stream, which is why the
shadow keeps its shape. The trait remains the swap point, and BACKENDS.md
names what would reopen the question: upstream vt100 gaining the three
attributes (the shadow deletes), or a candidate that is published, tracks
all three, and can be driven headless without an event loop.
The question. What is a scrolled-off row: text, or cells with styles? The masked-password assertion — the one the shadow parser exists for — silently degraded to text the moment the screen filled.
The measurement. With history retained as cells, 20,000 lines
through an 80x24 screen cost about 90 ms against 40 ms text-only (release
build, tests/styled_history.rs's ignored probe). The cost lands where a
suite feels it: every read that scrolls captures cells, and a snapshot
pays one refcount per retained row.
The decision. History is text by default and cells on request:
TerminalBuilder::scrollback_styles(true) retains the styled rows, the
shadow parser keeps the same history and is read in lockstep so the three
recovered attributes come along, and Screen::scrollback_cell and
styled_scrollback read them back. Screen::locate answers the
addressing half — grid or history, and honest that a history column is
the row's as captured, since history is not reflowed (the reason is
Terminal::resize's: a record exists, and the cost is what decides it).
Off by default because a suite that never scrolls should not pay, and
because the one assertion that needs it can ask.
Once the three sections above stand, 1.0 promises semver over:
- every
pubitem exported fromlib.rswithout a feature gate: theTerminal/TerminalBuildersurface,Screenand its accessors,Cell,Style,Color, the mode enums,Error(#[non_exhaustive], so variants may be added),bin!; - the snapshot text format of DESIGN.md §3, which
Screen::parsereads back — a snapshot file recorded under 1.0 stays valid; - the coordinate conventions:
(row, col)for cells,(cols, rows)for geometry.
Behind features, versioned with the feature's dependency rather than with
termlens: insta (the macro follows insta), decode, regex, serde
(the JSON shape follows the types' derives).
Documented as heuristics, promised to keep their shape and not their
timing: wait_idle and wait_stable (a quiet window is a judgement),
snapshot_after's 100 ms settle, and the frame-history and record
budgets, whose defaults may move.
Not covered: the Emulator trait and everything under emu/ (internal),
the exact text of error messages (they carry screens; the prefixes in the
skill's failure table are kept stable, the rest is prose), the fixtures,
and termlens-cli's output format beyond its exit codes.