use std::cell::RefCell;
use std::rc::Rc;
use abstracttui::app::anchored::{Completion, CompletionCandidate};
use abstracttui::app::{App, Driver, RunConfig};
use abstracttui::base::Size;
use abstracttui::prelude::*;
use abstracttui::term::Capabilities;
use abstracttui::testing::CaptureTerm;
use abstracttui::ui::text;
const W: i32 = 44;
const H: i32 = 12;
fn config() -> RunConfig {
RunConfig {
caps: Some(Capabilities::with(|c| {
c.truecolor = true;
c.colors_256 = true;
})),
enter: None,
probe: false,
..RunConfig::default()
}
}
fn settle(driver: &mut Driver, app: &mut App, term: &mut CaptureTerm) {
for _ in 0..64 {
let turn = driver.turn(app, term).expect("turn");
if turn.idle {
return;
}
}
panic!("loop failed to settle within 64 turns");
}
fn composer_app(app: &mut App) -> (TextAreaState, Rc<RefCell<Vec<String>>>) {
let overlays = app.overlays();
let submitted: Rc<RefCell<Vec<String>>> = Default::default();
let s2 = submitted.clone();
let holder: Rc<RefCell<Option<TextAreaState>>> = Default::default();
let h2 = holder.clone();
app.mount(move |cx| {
let t = use_theme(cx).get().tokens;
let state = TextAreaState::new(cx);
*h2.borrow_mut() = Some(state.clone());
let submit_state = state.clone();
let composer = TextArea::new()
.state(&state)
.rows(1, 3)
.placeholder("message")
.on_submit(move |v| {
s2.borrow_mut().push(v.to_string());
submit_state.push_history(v);
submit_state.clear();
})
.element(cx, &t)
.autofocus()
.build();
let wrapped = Completion::new()
.trigger('/', |q| {
["help", "theme", "clear", "quit"]
.iter()
.filter(|c| c.starts_with(q))
.map(|c| {
CompletionCandidate::new(format!("/{c}"), format!("/{c} ")).detail("cmd")
})
.collect()
})
.trigger('@', |q| {
["alice", "bob"]
.iter()
.filter(|c| c.starts_with(q))
.map(|c| CompletionCandidate::new(format!("@{c}"), format!("@{c} ")))
.collect()
})
.max_visible(4)
.attach(cx, &overlays, &state, composer);
Element::new()
.style(LayoutStyle::column())
.child(text("== transcript chrome =="))
.child(
Element::new()
.style(LayoutStyle::column().grow(1.0))
.child(text("pane row alpha"))
.child(text("pane row beta"))
.child(text("pane row gamma"))
.build(),
)
.child(wrapped)
.child(text(" status: ready"))
.build()
})
.expect("mount");
let state = holder.borrow().clone().expect("state");
(state, submitted)
}
fn screen_lines(term: &CaptureTerm) -> Vec<String> {
term.screen()
.to_text()
.lines()
.map(str::to_string)
.collect()
}
#[test]
fn submit_vs_newline_chords_on_both_wires_and_history_recall() {
let mut app = App::new(Size::new(W, H));
let (state, submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"hi");
term.push_input(b"\x1b[13;2u"); term.push_input(b"there");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "hi\nthere");
term.push_input(b"\x1b\r"); term.push_input(b"end");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "hi\nthere\nend");
term.push_input(b"\n");
term.push_input(b"tail");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "hi\nthere\nend\ntail");
let lines = screen_lines(&term);
assert!(lines.iter().any(|l| l.contains("there")));
assert!(lines.iter().any(|l| l.contains("end")));
assert!(lines.iter().any(|l| l.contains("tail")));
term.push_input(b"\r"); settle(&mut driver, &mut app, &mut term);
assert_eq!(
*submitted.borrow(),
vec!["hi\nthere\nend\ntail".to_string()]
);
assert_eq!(state.text(), "", "submit handler cleared the buffer");
term.push_input(b"\x1b[A");
settle(&mut driver, &mut app, &mut term);
assert_eq!(
state.text(),
"hi\nthere\nend\ntail",
"Up recalled the entry"
);
term.push_input(b"\x1b[B");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "", "the (empty) draft returned");
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
#[test]
fn bracketed_paste_inserts_multiline_and_never_submits() {
let mut app = App::new(Size::new(W, H));
let (state, submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b[200~first line\r\nsecond line\x1b[201~");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "first line\nsecond line");
assert!(submitted.borrow().is_empty(), "paste never submits");
let lines = screen_lines(&term);
assert!(lines.iter().any(|l| l.contains("first line")));
assert!(lines.iter().any(|l| l.contains("second line")));
}
#[test]
fn completion_dropdown_full_round_trip_with_damage_containment() {
let mut app = App::new(Size::new(W, H));
let (state, submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
let before_open = screen_lines(&term);
assert!(
before_open.iter().any(|l| l.contains("pane row beta")),
"content pane visible before the dropdown"
);
term.push_input(b"/");
settle(&mut driver, &mut app, &mut term);
let open_bytes = term.take_bytes().len();
let with_panel = screen_lines(&term);
let help_row = with_panel
.iter()
.position(|l| l.contains("/help"))
.expect("dropdown visible");
let composer_row = with_panel
.iter()
.position(|l| l.contains('▐'))
.expect("composer frame");
assert!(help_row < composer_row, "panel sits above the composer");
assert!(
with_panel.iter().any(|l| l.contains("/quit")),
"all four candidates offered: {with_panel:?}"
);
let panel_rows: Vec<usize> = with_panel
.iter()
.enumerate()
.filter(|(_, l)| l.contains('/'))
.map(|(i, _)| i)
.collect();
term.push_input(b"\x1b[B"); let turn = driver.turn(&mut app, &mut term).expect("turn");
assert!(turn.rendered);
settle(&mut driver, &mut app, &mut term);
let nav_bytes = term.take_bytes();
let after_nav = screen_lines(&term);
for (i, (before, after)) in with_panel.iter().zip(&after_nav).enumerate() {
if !panel_rows.contains(&i) {
assert_eq!(before, after, "row {i} outside the panel changed");
}
}
let nav_text = String::from_utf8_lossy(&nav_bytes);
assert!(
!nav_text.contains("chrome") && !nav_text.contains("status"),
"static chrome must not re-emit: {nav_text:?}"
);
assert!(
nav_bytes.len() < open_bytes,
"highlight flip ({} bytes) cheaper than panel open ({} bytes)",
nav_bytes.len(),
open_bytes
);
println!(
"measured: panel-open frame {} bytes; highlight-move frame {} bytes",
open_bytes,
nav_bytes.len()
);
term.push_input(b"\r");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "/theme ");
assert!(submitted.borrow().is_empty(), "accept is not a submit");
let after_accept = screen_lines(&term);
assert!(
!after_accept.iter().any(|l| l.contains("/help")),
"dropdown closed"
);
assert!(
after_accept.iter().any(|l| l.contains("pane row beta")),
"vacated region repainted from below"
);
assert!(
after_accept.iter().any(|l| l.contains("/theme")),
"accepted text in the composer"
);
term.push_input(b"x"); term.push_input(b" /q");
settle(&mut driver, &mut app, &mut term);
assert!(
screen_lines(&term).iter().any(|l| l.contains("/quit")),
"fresh trigger reopened"
);
term.push_input(b"\x1b[27u"); settle(&mut driver, &mut app, &mut term);
assert!(
!screen_lines(&term).iter().any(|l| l.contains("/quit")),
"Escape dismissed"
);
term.push_input(b"u");
settle(&mut driver, &mut app, &mut term);
assert!(
!screen_lines(&term).iter().any(|l| l.contains("/quit")),
"same token stays muted"
);
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
fn sgr_click_bytes(x: i32, y: i32) -> (Vec<u8>, Vec<u8>) {
(
format!("\x1b[<0;{};{}M", x + 1, y + 1).into_bytes(),
format!("\x1b[<0;{};{}m", x + 1, y + 1).into_bytes(),
)
}
#[test]
fn mouse_click_accepts_a_candidate_through_the_wire() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"@");
settle(&mut driver, &mut app, &mut term);
let lines = screen_lines(&term);
let bob_row = lines
.iter()
.position(|l| l.contains("@bob"))
.expect("mention dropdown open") as i32;
let bob_col = lines[bob_row as usize].find("@bob").unwrap() as i32;
let (press, release) = sgr_click_bytes(bob_col, bob_row);
term.push_input(&press);
term.push_input(&release);
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "@bob ", "click accepted the row");
assert!(
!screen_lines(&term).iter().any(|l| l.contains("@alice")),
"dropdown closed after the click"
);
}
#[test]
fn autofocused_composer_paints_placeholder_beside_caret_on_screen() {
let size = Size::new(W, H);
let mut app = App::new(size);
app.mount(move |cx| {
let t = use_theme(cx).get().tokens;
let composer = TextArea::new()
.placeholder("describe a task")
.placeholder_while_focused(true)
.rows(1, 3)
.element(cx, &t)
.autofocus()
.build();
Element::new()
.style(LayoutStyle::column())
.child(text("== chrome =="))
.child(composer)
.build()
})
.expect("mount");
let mut term = CaptureTerm::new(size);
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
let tokens = current_theme().tokens;
let row = 1; let hint_row = screen_lines(&term)[row as usize].clone();
assert!(
hint_row.contains("describe a task"),
"autofocused composer must paint its hint: {hint_row:?}"
);
let caret = term.screen().cell(1, row).expect("caret cell");
assert_eq!(
caret.paint.bg,
Some(tokens.cursor),
"caret bg is the cursor token"
);
let hint = term.screen().cell(2, row).expect("hint cell");
assert_eq!(hint.ch(), 'd');
assert_eq!(
hint.paint.fg,
Some(tokens.text_faint),
"hint ink is text_faint"
);
term.push_input(b"h");
settle(&mut driver, &mut app, &mut term);
let typed_row = screen_lines(&term)[row as usize].clone();
assert!(
!typed_row.contains("describe a task"),
"typing hides the focused placeholder: {typed_row:?}"
);
assert_eq!(term.screen().cell(1, row).expect("cell").ch(), 'h');
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
fn at(needle: &str) -> usize {
"alpha beta gamma".find(needle).expect("fixture substring")
}
#[test]
fn word_motion_arrives_on_every_terminal_spelling() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("gamma"), "ESC b: back one word");
term.push_input(b"\x1b[1;5D");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("beta"), "CSI 1;5D: back one word");
term.push_input(b"\x1b[1;3C");
settle(&mut driver, &mut app, &mut term);
let after_beta = at("beta") + "beta".len();
assert_eq!(state.caret_byte(), after_beta, "CSI 1;3C: forward one word");
term.push_input(b"\x1bf");
settle(&mut driver, &mut app, &mut term);
assert_eq!(
state.caret_byte(),
"alpha beta gamma".len(),
"ESC f: forward one word"
);
assert_eq!(state.text(), "alpha beta gamma", "motion never edits");
}
#[test]
fn delete_word_arrives_on_every_terminal_spelling() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b\x7f");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha beta ", "ESC DEL: rub out one word");
term.push_input(b"\x17");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha ", "Ctrl+W: rub out one word");
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb"); term.push_input(b"\x1bb"); settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("beta"));
term.push_input(b"\x1bd");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha gamma", "ESC d: delete word forward");
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb");
term.push_input(b"\x1bb");
term.push_input(b"\x1b[3;5~");
settle(&mut driver, &mut app, &mut term);
assert_eq!(
state.text(),
"alpha gamma",
"CSI 3;5~: delete word forward"
);
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
#[test]
fn word_chords_work_on_the_kitty_wire_too() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b[1;3D"); settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("gamma"), "CSI 1;3D");
term.push_input(b"\x1b[98;3u"); settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("beta"), "kitty Alt+b");
term.push_input(b"\x1b[127;3u"); settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "beta gamma", "kitty Alt+Backspace rubs out");
}
#[test]
fn shift_word_chords_extend_the_selection() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b[1;4D"); settle(&mut driver, &mut app, &mut term);
term.push_input(b"Z");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha beta Z", "word selection was replaced");
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1b[1;6D");
term.push_input(b"Q");
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha beta Q");
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}
#[test]
fn navigation_chords_match_codex_defaults() {
let mut app = App::new(Size::new(W, H));
let (state, _submitted) = composer_app(&mut app);
let mut term = CaptureTerm::new(Size::new(W, H));
let mut driver = Driver::new(&mut app, &mut term, config()).expect("driver");
settle(&mut driver, &mut app, &mut term);
for (label, bytes) in [
("alt+b", &b"\x1bb"[..]),
("alt+left", &b"\x1b[1;3D"[..]),
("ctrl+left", &b"\x1b[1;5D"[..]),
] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), at("gamma"), "move_word_left {label}");
}
for (label, bytes) in [
("alt+f", &b"\x1bf"[..]),
("alt+right", &b"\x1b[1;3C"[..]),
("ctrl+right", &b"\x1b[1;5C"[..]),
] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb"); term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(
state.caret_byte(),
"alpha beta gamma".len(),
"move_word_right {label}"
);
}
for (label, bytes) in [("home", &b"\x1b[H"[..]), ("ctrl+a", &b"\x01"[..])] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.caret_byte(), 0, "move_line_start {label}");
}
for (label, bytes) in [("end", &b"\x1b[F"[..]), ("ctrl+e", &b"\x05"[..])] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb");
term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(
state.caret_byte(),
"alpha beta gamma".len(),
"move_line_end {label}"
);
}
for (label, bytes) in [
("alt+backspace", &b"\x1b\x7f"[..]),
("ctrl+backspace", &b"\x1b[127;5u"[..]),
("ctrl+w", &b"\x17"[..]),
] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha beta ", "delete_backward_word {label}");
}
for (label, bytes) in [
("alt+delete", &b"\x1b[3;3~"[..]),
("ctrl+delete", &b"\x1b[3;5~"[..]),
("alt+d", &b"\x1bd"[..]),
] {
state.set_text("alpha beta gamma");
settle(&mut driver, &mut app, &mut term);
term.push_input(b"\x1bb");
term.push_input(b"\x1bb"); term.push_input(bytes);
settle(&mut driver, &mut app, &mut term);
assert_eq!(state.text(), "alpha gamma", "delete_forward_word {label}");
}
driver.finish(&mut term).expect("leave");
assert_eq!(term.screen().unknown_seq_count(), 0, "all bytes modeled");
}