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:

#[cfg(feature = "insta")]
{
    insta::assert_snapshot!(t.screen());        // plain insta…
    termlens::assert_screen_snapshot!(t.screen()); // …or the bundled macro
}

Re-exports§

pub use insta;

Macros§

assert_screen_snapshot
Snapshot-assert anything that displays like a Screen.

Structs§

Bitmap
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
What an application copied with OSC 52, as observed at one snapshot.
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().
Screen
An immutable snapshot of the terminal screen.
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.

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).
DecodeError
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.
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.
Signal
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>.