1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! 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};
//!
//! # fn main() -> termlens::Result<()> {
//! 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());
//! # Ok(())
//! # }
//! ```
//!
//! Every `wait_*` call runs under a deadline — the builder's
//! [`timeout`](TerminalBuilder::timeout) (default 5s) or a per-call one
//! ([`wait_until_for`](Terminal::wait_until_for) and friends) — and a
//! timeout error [embeds the screen](Error::Timeout) 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`](Terminal::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`](Screen::full_text) answers "this reached the terminal"
//! without the test having to know which region currently holds it.
//!
//! Input is mode-aware: [mouse clicks](Terminal::click),
//! [pastes](Terminal::paste), modifier [chords](Chord), and cursor keys
//! are encoded exactly as the application configured its terminal — and a
//! [drag](Terminal::drag) reports one motion per cell crossed, so an
//! application that acts along the path sees the path. [Focus
//! events](Terminal::focus_out) 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](Screen::clipboard) write, the
//! [cursor shape](Screen::cursor_shape) the application asked for, and the
//! `OSC 8` [hyperlinks](Screen::links) 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`](Screen::repaints) counts
//! completed frames (so "one input became four repaints" is catchable),
//! [`bells`](Screen::bells) counts `BEL`, and
//! [`graphics`](Screen::graphics) counts the inline images an application
//! transmitted — often to assert that it transmitted *none*.
//! [`frame_timings`](Terminal::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`](Screen::contains) and [`find`](Screen::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:
//!
//! ```no_run
//! # fn main() -> termlens::Result<()> {
//! # let mut t = termlens::Terminal::builder().spawn("true")?;
//! #[cfg(feature = "insta")]
//! {
//! insta::assert_snapshot!(t.screen()); // plain insta…
//! termlens::assert_screen_snapshot!(t.screen()); // …or the bundled macro
//! }
//! # Ok(())
//! # }
//! ```
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use Signal;
pub use ;
/// Re-export of [`insta`](https://insta.rs) (feature `insta`, on by
/// default), so [`assert_screen_snapshot!`] always agrees with the `insta`
/// version doing the snapshotting.
pub use insta;
/// Snapshot-assert anything that displays like a [`Screen`].
///
/// Sugar for [`insta::assert_snapshot!`] through the re-exported `insta`;
/// accepts the same optional inline-snapshot form:
///
/// ```no_run
/// # fn main() -> termlens::Result<()> {
/// # let t = termlens::Terminal::builder().spawn("true")?;
/// #[cfg(feature = "insta")]
/// {
/// termlens::assert_screen_snapshot!(t.screen());
/// }
/// # Ok(())
/// # }
/// ```