use justerm_core::{Engine, TermEvent};
const CLOSED_LOOP: &[u8] = include_bytes!("fixtures/vim_closed_loop.raw");
const OPEN_LOOP: &[(&str, &[u8])] = &[
("vim_redraw", include_bytes!("fixtures/vim_redraw.raw")),
(
"vim_title_stack",
include_bytes!("fixtures/vim_title_stack.raw"),
),
(
"alt_resize_vim.pre",
include_bytes!("fixtures/alt_resize_vim.pre.raw"),
),
(
"tmux_clipboard",
include_bytes!("fixtures/tmux_clipboard.raw"),
),
];
fn da2_version() -> u32 {
let v = env!("CARGO_PKG_VERSION").split(['-', '+']).next().unwrap();
let mut p = v.split('.').map(|c| c.parse::<u32>().unwrap_or(0));
p.next().unwrap_or(0) * 10_000 + p.next().unwrap_or(0) * 100 + p.next().unwrap_or(0)
}
fn count(haystack: &[u8], needle: &[u8]) -> usize {
haystack
.windows(needle.len())
.filter(|w| *w == needle)
.count()
}
fn xtgettcap_names(stream: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let mut i = 0;
while i + 4 < stream.len() {
if &stream[i..i + 4] == b"\x1bP+q" {
let start = i + 4;
let mut j = start;
while j < stream.len() && stream[j].is_ascii_hexdigit() {
j += 1;
}
let name: String = stream[start..j]
.chunks(2)
.filter_map(|p| u8::from_str_radix(std::str::from_utf8(p).ok()?, 16).ok())
.map(|b| b as char)
.collect();
out.push(name);
i = j;
} else {
i += 1;
}
}
out.sort();
out.dedup();
out
}
#[test]
fn the_ten_xtgettcap_questions_appear_only_once_the_loop_is_closed() {
assert_eq!(
xtgettcap_names(CLOSED_LOOP),
vec!["#2", "#4", "%i", "*7", "Co", "k1", "kd", "kl", "kr", "ku"],
);
for (name, stream) in OPEN_LOOP {
assert_eq!(
count(stream, b"\x1bP+q"),
0,
"{name} is an open-loop capture and must not contain XTGETTCAP",
);
assert!(
count(stream, b"\x1b[>c") > 0,
"{name} must still ask DA2, or it is not the control this test needs",
);
}
}
#[test]
fn replaying_it_produces_the_da2_answer_that_gated_the_burst() {
let mut engine = Engine::new(80, 24);
engine.feed(CLOSED_LOOP);
let replies = engine.drain_replies();
let expected = format!("\x1b[>1;{};0c", da2_version());
assert_eq!(
count(&replies, expected.as_bytes()),
1,
"expected exactly one DA2 answer {expected:?}, got {:?}",
String::from_utf8_lossy(&replies),
);
}
#[test]
fn the_two_cursor_reports_answer_vims_two_probes() {
let mut engine = Engine::new(80, 24);
engine.feed(CLOSED_LOOP);
let replies = engine.drain_replies();
let reports: Vec<Vec<u8>> = replies
.split(|b| *b == 0x1b)
.filter(|s| s.starts_with(b"[") && s.ends_with(b"R"))
.map(|s| s.to_vec())
.collect();
assert_eq!(
reports,
vec![b"[2;2R".to_vec(), b"[3;1R".to_vec()],
"the cursor reports are the probe answers, not just two different strings",
);
}
#[test]
fn the_engine_still_answers_none_of_the_ten() {
let mut engine = Engine::new(80, 24);
engine.feed(CLOSED_LOOP);
let replies = engine.drain_replies();
assert_eq!(
count(&replies, b"\x1bP"),
0,
"a DCS reply appeared — XTGETTCAP may now be answered; re-read this test",
);
}
#[test]
fn the_colour_queries_arrive_as_events_for_a_consumer_to_answer() {
let mut engine = Engine::new(80, 24);
engine.feed(CLOSED_LOOP);
let events = engine.drain_events();
let queries = events
.iter()
.filter(|e| {
matches!(
e,
TermEvent::QueryForeground { .. } | TermEvent::QueryBackground { .. }
)
})
.count();
assert_eq!(queries, 2, "vim asks OSC 10 and OSC 11 once each here");
assert_eq!(
count(&engine.drain_replies(), b"\x1b]1"),
0,
"the engine must not answer a colour query itself",
);
}