Skip to main content

Crate termlens

Crate termlens 

Source
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_snapshotinsta
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§

Bitmapdecode
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 a Key:
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.
ExitStatus
Exit status of the child process.
FrameTiming
What one completed repaint cost.
GraphicsPayload
One inline image an application transmitted, as observed on the wire.
GraphicsSeen
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.
MouseChord
A mouse button plus modifier keys — MouseButton::Left.ctrl().
MouseModes
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::record on, timestamped, until stop.
Recording
What a Recorder collected: complete frames with the time each ended, measured from Terminal::record, oldest first.
Screen
An immutable snapshot of the terminal screen.
ScreenDiff
The difference between two screens. Built by Screen::diff; render it with {}.
ScreenWithStyles
Screen rendered with its styles — see Screen::with_styles.
ScrollChord
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.
TerminalBuilder
Configures and spawns a Terminal.
Unsupported
What an application copied with OSC 52, as observed at one snapshot.
UnsupportedIter
The iterator over an Unsupported view’s retained shapes.

Enums§

Color
A terminal color, as reported by the emulator.
CursorShape
The shape of the cursor an application asked its terminal for with DECSCUSR (CSI Ps SP q).
DecodeErrordecode
Why a payload could not be decoded.
Error
Errors returned by Terminal operations.
Graphics
Inline-graphics support a test declares the simulated terminal has.
GraphicsAction
What the application asked the terminal to do with an image.
GraphicsFormat
How the pixels in a payload are encoded.
GraphicsProtocol
Which protocol carried a payload.
Key
A key press to send to the terminal.
Location
Where Screen::locate found a needle.
MouseButton
A mouse button, for Terminal::click_with and Terminal::drag.
MouseMode
Which mouse events the application asked its terminal to report.
Scroll
Scroll-wheel direction for Terminal::scroll and Terminal::scroll_with.
SignalUnix
A POSIX signal for Terminal::signal: the graceful-shutdown set.

Traits§

Input
Anything Terminal::send can send: a Key or a modifier Chord. Sealed — the set is fixed by the crate.

Type Aliases§

Result
Convenience alias for std::result::Result<T, termlens::Error>.