use std::time::Duration;
use termlens::{CursorShape, Key, MouseMode, Screen, Terminal};
#[test]
fn screen_reports_title_alternate_screen_and_input_modes() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(10))
.args([
"-c",
concat!(
r"printf '\033]0;termlens state\007'; ",
r"printf '\033[?1049h\033[?2004h\033[?1h\033[?1002h'; ",
r"printf 'modes: on'; ",
r"read _; ",
r"printf '\033]2;phase two\033\\'; ",
r"printf '\033[?1002l\033[?1l\033[?2004l\033[?1049l'; ",
r"printf 'modes: off'; ",
r"read _",
),
])
.spawn("sh")?;
t.wait_until(|s| {
s.contains("modes: on")
&& s.title() == "termlens state"
&& s.alternate_screen()
&& s.bracketed_paste()
&& s.application_cursor()
&& s.mouse_mode() == MouseMode::ButtonMotion
})?;
t.send(Key::Enter)?;
t.wait_until(|s| {
s.contains("modes: off")
&& s.title() == "phase two" && !s.alternate_screen()
&& !s.bracketed_paste()
&& !s.application_cursor()
&& s.mouse_mode() == MouseMode::None
})?;
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn screen_reports_the_cursor_shape_the_application_asked_for() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(10))
.args([
"-c",
concat!(
r"printf 'ready'; read _; ",
r"printf '\033[5 q'; printf ' insert'; read _; ",
r"printf '\033[2 q'; printf ' normal'; read _",
),
])
.spawn("sh")?;
t.wait_until(|s| s.contains("ready"))?;
let before = t.screen();
assert_eq!(before.cursor_shape(), CursorShape::Default);
assert_eq!(before.cursor_blink(), None);
t.send(Key::Enter)?;
t.wait_until(|s| {
s.contains("insert")
&& s.cursor_shape() == CursorShape::Bar
&& s.cursor_blink() == Some(true)
})?;
t.send(Key::Enter)?;
t.wait_until(|s| {
s.contains("normal")
&& s.cursor_shape() == CursorShape::Block
&& s.cursor_blink() == Some(false)
})?;
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
let after = t.screen();
assert_eq!(after.cursor_shape(), CursorShape::Block);
assert_eq!(after.cursor_blink(), Some(false));
Ok(())
}
#[test]
fn an_osc8_hyperlink_is_observable_and_a_missing_one_is_not() -> termlens::Result<()> {
fn run(script: &str) -> termlens::Result<Screen> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(10))
.args(["-c", script])
.spawn("sh")?;
t.wait_until(|s| s.contains("see docs here"))?;
let screen = t.screen();
assert!(t.wait_exit()?.success());
Ok(screen)
}
let linked =
run(r"printf 'see \033]8;;https://example.invalid/a\033\\docs\033]8;;\033\\ here\n'")?;
let plain = run(r"printf 'see docs here\n'")?;
assert_eq!(linked.text(), plain.text());
assert_eq!(linked.row_text(0).trim_end(), "see docs here");
assert!(!linked.text().contains("example.invalid"));
assert!(plain.links().is_empty());
let link = &linked.links()[0];
assert_eq!(linked.links().len(), 1);
assert_eq!(link.uri(), "https://example.invalid/a");
assert_eq!(link.label(), Some("docs"));
assert!(link.closed());
assert_eq!(link.id(), None);
assert_ne!(link.uri(), "https://example.invalid/b");
Ok(())
}
#[test]
fn a_snapshot_keeps_its_own_view_of_the_links() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(10))
.args([
"-c",
concat!(
r"printf '\033]8;;http://a/\033\\LABEL one\n'; read _; ",
r"printf '\033]8;;\033\\\033]8;;http://b/\033\\X two\n'; read _",
),
])
.spawn("sh")?;
t.wait_until(|s| s.contains("one"))?;
let early = t.screen();
assert_eq!(early.links().len(), 1);
assert!(
!early.links()[0].closed(),
"the span is open at this instant"
);
assert_eq!(early.links()[0].label(), None, "and has no final label yet");
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("two") && s.links().len() == 2)?;
let later = t.screen();
assert!(later.links()[0].closed());
assert_eq!(later.links()[0].uri(), "http://a/");
assert_eq!(later.links()[1].uri(), "http://b/");
assert_eq!(early.links().len(), 1, "an earlier snapshot grew a link");
assert!(
!early.links()[0].closed(),
"an earlier snapshot saw the span close after the fact"
);
assert_eq!(early.links()[0].label(), None);
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn mouse_mode_reports_the_exact_tracking_mode() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(10))
.args([
"-c",
concat!(
r"printf '\033[?9h9\n'; read _; ",
r"printf '\033[?9l\033[?1000h1000\n'; read _; ",
r"printf '\033[?1000l\033[?1003h1003\n'; read _",
),
])
.spawn("sh")?;
t.wait_until(|s| s.contains("9") && s.mouse_mode() == MouseMode::Press)?;
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("1000") && s.mouse_mode() == MouseMode::PressRelease)?;
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("1003") && s.mouse_mode() == MouseMode::AnyMotion)?;
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn a_clipboard_write_is_observable_with_its_payload() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(5))
.args([
"-c",
concat!(
r"printf '\033]52;c;V2lyZSB1cCB0aGUgUFRZIHJlYWRlcg==\007'; ",
r"printf 'copied to clipboard'; read guard"
),
])
.spawn("/bin/sh")?;
t.wait_until(|s| {
s.clipboard()
.is_some_and(|c| c.text() == Some("Wire up the PTY reader"))
})?;
let s = t.screen();
let clip = s.clipboard().expect("the write was captured");
assert_eq!(clip.text(), Some("Wire up the PTY reader"));
assert_eq!(clip.targets(), "c");
assert!(
!s.contains("V2lyZSB1cCB0"),
"the escape leaked into the grid:\n{s}"
);
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
fn an_unreadable_clipboard_payload_is_reported_as_such() -> termlens::Result<()> {
let mut t = Terminal::builder()
.timeout(Duration::from_secs(5))
.args([
"-c",
r"printf '\033]52;p;not~valid~base64\007'; printf 'done'; read guard",
])
.spawn("/bin/sh")?;
t.wait_until(|s| s.contains("done"))?;
let s = t.screen();
let clip = s.clipboard().expect("a write was still observed");
assert_eq!(clip.text(), None);
assert_eq!(clip.targets(), "p");
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}