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