use std::time::Duration;
use termlens::{CursorShape, Key, MouseMode, MouseModes, Screen, Terminal};
mod common;
fn emit(steps: &[&str]) -> termlens::Result<Terminal> {
common::spawn_emit(Terminal::builder().timeout(Duration::from_secs(10)), steps)
}
#[test]
#[cfg_attr(
windows,
ignore = "ConPTY does not forward the mode sets an application sends, so mouse and focus modes are never observed (#149)"
)]
fn screen_reports_title_alternate_screen_and_input_modes() -> termlens::Result<()> {
let mut t = emit(&[
"--raw",
r"\e]0;termlens state\x07",
"--raw",
r"\e[?1049h\e[?2004h\e[?1h\e[?1002h",
"modes: on",
"--wait",
"--raw",
r"\e]2;phase two\e\\",
"--raw",
r"\e[?1002l\e[?1l\e[?2004l\e[?1049l",
"modes: off",
"--wait",
])?;
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 = emit(&[
"ready", "--wait", "--csi", "5 q", " insert", "--wait",
"--csi", "2 q", " normal", "--wait",
])?;
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]
#[cfg_attr(
windows,
ignore = "ConPTY forwards an OSC 8 link with an id of its own choosing (#149)"
)]
fn an_osc8_hyperlink_is_observable_and_a_missing_one_is_not() -> termlens::Result<()> {
fn run(steps: &[&str]) -> termlens::Result<Screen> {
let mut t = emit(steps)?;
t.wait_until(|s| s.contains("see docs here"))?;
let screen = t.screen();
assert!(t.wait_exit()?.success());
Ok(screen)
}
let linked = run(&[
"--raw",
r"see \e]8;;https://example.invalid/a\e\\docs\e]8;;\e\\ here\n",
])?;
let plain = run(&["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 = emit(&[
"--raw",
r"\e]8;;http://a/\e\\LABEL one\n",
"--wait",
"--raw",
r"\e]8;;\e\\\e]8;;http://b/\e\\X two\n",
"--wait",
])?;
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]
#[cfg_attr(windows, ignore = "ConPTY does not forward DECSTR (#149)")]
fn a_soft_reset_returns_the_modes_a_screen_can_observe() -> termlens::Result<()> {
let mut t = emit(&[
"--raw",
r"\e[?1049h\e[?1h\e[?2004h\e[?1000h\e[?1006h\e[?1004h\e[?25l\e[5 q",
"set",
"--wait",
"--csi",
"!p",
" reset",
"--wait",
])?;
t.wait_until(|s| {
s.contains("set")
&& s.alternate_screen()
&& s.application_cursor()
&& s.bracketed_paste()
&& s.mouse_mode() == MouseMode::PressRelease
&& s.focus_events()
&& !s.cursor().2
&& s.cursor_shape() == CursorShape::Bar
})?;
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("reset"))?;
let s = t.screen();
assert!(
s.alternate_screen(),
"the alternate screen is left alone: {s}"
);
assert!(!s.application_cursor(), "{s}");
assert!(!s.bracketed_paste(), "{s}");
assert_eq!(s.mouse_mode(), MouseMode::None, "{s}");
assert!(!s.focus_events(), "{s}");
assert!(s.cursor().2, "DECTCEM: the cursor is visible again: {s}");
assert_eq!(s.cursor_shape(), CursorShape::Default, "{s}");
assert_eq!(s.cursor_blink(), None, "{s}");
t.send(Key::Enter)?;
assert!(t.wait_exit()?.success());
Ok(())
}
#[test]
#[cfg_attr(
windows,
ignore = "ConPTY does not forward the mode sets an application sends, so mouse and focus modes are never observed (#149)"
)]
fn mouse_mode_reports_the_exact_tracking_mode() -> termlens::Result<()> {
let mut t = emit(&[
"--raw",
r"\e[?9h9\n",
"--wait",
"--raw",
r"\e[?9l\e[?1000h1000\n",
"--wait",
"--raw",
r"\e[?1000l\e[?1003h1003\n",
"--wait",
])?;
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]
#[cfg_attr(
windows,
ignore = "ConPTY does not forward the mode sets an application sends, so mouse and focus modes are never observed (#149)"
)]
fn mouse_modes_reports_the_set_the_application_asked_for() -> termlens::Result<()> {
let mut t = emit(&[
"--raw",
r"\e[?1000h\e[?1002h\e[?1003h\e[?1006h",
"all three\n",
"--wait",
"--csi",
"?1003l",
"minus 1003\n",
"--wait",
"--raw",
r"\e[?1002l\e[?1000l",
"none\n",
"--wait",
])?;
let set = |modes: &[MouseMode]| -> Vec<MouseMode> { modes.to_vec() };
t.wait_until(|s| {
s.contains("all three")
&& s.mouse_mode() == MouseMode::AnyMotion
&& s.mouse_modes().iter().collect::<Vec<_>>()
== set(&[
MouseMode::PressRelease,
MouseMode::ButtonMotion,
MouseMode::AnyMotion,
])
})?;
let s = t.screen();
assert!(
s.mouse_modes().contains(MouseMode::ButtonMotion),
"{:?}",
s.mouse_modes()
);
assert!(
!s.mouse_modes().contains(MouseMode::Press),
"{:?}",
s.mouse_modes()
);
assert_eq!(s.mouse_modes().len(), 3);
t.send(Key::Enter)?;
t.wait_until(|s| {
s.contains("minus 1003")
&& s.mouse_mode() == MouseMode::None
&& s.mouse_modes().iter().collect::<Vec<_>>()
== set(&[MouseMode::PressRelease, MouseMode::ButtonMotion])
})?;
t.send(Key::Enter)?;
t.wait_until(|s| s.contains("none") && s.mouse_modes().is_empty())?;
assert_eq!(t.screen().mouse_modes(), MouseModes::default());
assert!(t.screen().mouse_modes().contains(MouseMode::None));
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 = common::spawn_emit(
Terminal::builder().timeout(Duration::from_secs(5)),
&[
"--raw",
r"\e]52;c;V2lyZSB1cCB0aGUgUFRZIHJlYWRlcg==\x07",
"copied to clipboard",
"--wait",
],
)?;
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 = common::spawn_emit(
Terminal::builder().timeout(Duration::from_secs(5)),
&["--raw", r"\e]52;p;not~valid~base64\x07", "done", "--wait"],
)?;
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(())
}