Expand description
Headless PTY test harness for CLI/TUI applications.
termlens spawns your program in a real pseudo-terminal, feeds its
output through a VT emulator into an in-memory screen grid, and
lets tests assert and snapshot on the rendered screen instead of raw
bytes — Playwright for the terminal.
- It is not an expect-style stream matcher (see
rexpect/expectrl). - It is not an SVG transcript generator for docs (see
term-transcript). - It is: real PTY + emulated screen + snapshot assertions.
§Example
use std::time::Duration;
use termlens::{Key, Terminal};
let mut t = Terminal::builder()
.size(80, 24)
.env("TERM", "xterm-256color") // the default; shown for completeness
.timeout(Duration::from_secs(10))
.args(["-c", r#"read line; echo "got: $line"; read quit"#])
.spawn("sh")?;
t.send_str("hello")?;
t.send(Key::Enter)?;
t.wait_until(|screen| screen.contains("got: hello"))?;
t.send(Key::Enter)?; // release `read quit`; the script finishes
let status = t.wait_exit()?;
assert!(status.success());Every wait_* call runs under a deadline — the builder’s
timeout (default 5s) or a per-call one
(wait_until_for and friends) — and a
timeout error embeds the screen so a CI log alone
shows what the application was displaying. A background reader thread
drains the PTY continuously — no output is lost between waits — and
answers the queries a real terminal answers, so capability-probing apps
run instead of hanging.
Where the application brackets its repaints in DEC 2026 synchronized
updates, wait_frame evaluates predicates only
on complete frames and returns the one it matched — never a torn
repaint, and each call observes a frame no earlier call did. Content
that scrolls off the top is retained as well, so
full_text answers “this reached the terminal”
without the test having to know which region currently holds it.
Input is mode-aware: mouse clicks,
pastes, modifier chords, and cursor keys
are encoded exactly as the application configured its terminal — and a
drag reports one motion per cell crossed, so an
application that acts along the path sees the path. Focus
events go the other way, reaching an application
that enabled mode 1004 so the unfocused branch of a UI can be driven at
all. The terminal’s out-of-band state — the window title, the
alternate-screen flag, the input modes, the last OSC 52
clipboard write, the
cursor shape the application asked for, and the
OSC 8 hyperlinks it emitted — is readable from every
Screen as plain accessors. Both of those last two leave the grid
identical: a bar cursor and a block cursor draw the same cells, and a
hyperlink’s label renders as ordinary text with its URL nowhere on the
screen, so a test asserting a link used to pass against an application
that emitted none.
Behaviour that leaves the screen identical is assertable too, which
no content predicate can manage: repaints counts
completed frames (so “one input became four repaints” is catchable),
bells counts BEL, and
graphics counts the inline images an application
transmitted — often to assert that it transmitted none.
frame_timings adds what each repaint cost,
so a suite can hold a performance line as well as a correctness one.
Needles are matched by what the terminal draws rather than by how it is
spelled: contains and find fold
both sides to NFC, so a needle typed in an editor finds text an
application normalized the other way. The grid keeps exactly the
codepoints the application sent.
With the default insta feature, snapshot-test whole screens — after
waiting for what the application paints and for the picture to hold
still, which snapshot_after does in one
call:
let screen = t.snapshot_after(|s| s.contains("Ready"))?;
#[cfg(feature = "insta")]
insta::assert_snapshot!(screen); // or termlens::assert_screen_snapshot!(screen)Testing a binary of your own package? bin! spawns
CARGO_BIN_EXE_<name> under the harness defaults — a fixed grid, a
cleared environment, a deadline — with builder calls after the name to
override any of them.
Re-exports§
pub use insta;insta
Macros§
- assert_
screen_ snapshot insta - Snapshot a terminal’s screen the way a TUI snapshot has to be taken:
settled, with its styles, through
insta::assert_snapshot!. - bin
- Spawn one of this package’s binaries under the harness defaults.
Structs§
- Bitmap
decode - The pixels one payload depicted.
- Cell
- One cell of the screen grid.
- Chord
- A modifier chord over a special key —
Ctrl-Right,Shift-Up,Alt-PageDown,Ctrl-Shift-F5. Build it from aKey: - Clipboard
- Read it from a snapshot via
Screen::clipboard. A toast on screen proves the copy path ran; this proves the payload, which is usually the behaviour actually under test. - Exit
Status - Exit status of the child process.
- Frame
Timing - What one completed repaint cost.
- Graphics
Payload - One inline image an application transmitted, as observed on the wire.
- Graphics
Seen - Inline graphics payloads the application transmitted, as observed at one snapshot.
- Link
- A hyperlink an application emitted with
OSC 8, as observed at one snapshot. - Mouse
Chord - A mouse button plus modifier keys —
MouseButton::Left.ctrl(). - Mouse
Modes - The set of mouse tracking modes an application has enabled and not yet
disabled — what it asked for, as distinct from the one protocol the
terminal reports in, which is
Screen::mouse_mode. - Recorder
- A recording in progress: every complete frame from
Terminal::recordon, timestamped, untilstop. - Recording
- What a
Recordercollected: complete frames with the time each ended, measured fromTerminal::record, oldest first. - Screen
- An immutable snapshot of the terminal screen.
- Screen
Diff - The difference between two screens. Built by
Screen::diff; render it with{}. - Screen
With Styles Screenrendered with its styles — seeScreen::with_styles.- Scroll
Chord - A wheel direction plus modifier keys —
Scroll::Up.ctrl(). - Style
- Visual attributes of a
Cell. - Terminal
- A program running inside a real PTY, observed through an emulated screen.
- Terminal
Builder - Configures and spawns a
Terminal. - Unsupported
- What an application copied with
OSC 52, as observed at one snapshot. - Unsupported
Iter - The iterator over an
Unsupportedview’s retained shapes.
Enums§
- Color
- A terminal color, as reported by the emulator.
- Cursor
Shape - The shape of the cursor an application asked its terminal for with
DECSCUSR(CSI Ps SP q). - Decode
Error decode - Why a payload could not be decoded.
- Error
- Errors returned by
Terminaloperations. - Graphics
- Inline-graphics support a test declares the simulated terminal has.
- Graphics
Action - What the application asked the terminal to do with an image.
- Graphics
Format - How the pixels in a payload are encoded.
- Graphics
Protocol - Which protocol carried a payload.
- Key
- A key press to send to the terminal.
- Location
- Where
Screen::locatefound a needle. - Mouse
Button - A mouse button, for
Terminal::click_withandTerminal::drag. - Mouse
Mode - Which mouse events the application asked its terminal to report.
- Scroll
- Scroll-wheel direction for
Terminal::scrollandTerminal::scroll_with. - Signal
Unix - A POSIX signal for
Terminal::signal: the graceful-shutdown set.
Traits§
- Input
- Anything
Terminal::sendcan send: aKeyor a modifierChord. Sealed — the set is fixed by the crate.
Type Aliases§
- Result
- Convenience alias for
std::result::Result<T, termlens::Error>.