use bevy_ecs::entity_disabling::Disabled;
use crossterm::event::KeyEventKind;
use super::*;
use crate::tests::{Delivered, key_events, press, type_text, typed, view_app};
use crate::view::TuiView;
use crate::{CompletionSource, Placement, TuiPanel};
use ratatui::layout::Constraint;
use rig_harness::commands::{AppCommandsExt, CommandArgs};
#[derive(Resource, Default)]
struct Heard(Vec<bool>);
fn heard(pressed: On<KeyPressed>, mut heard: ResMut<Heard>) {
heard.0.push(pressed.released);
}
fn heard_count(app: &App) -> usize {
app.world().resource::<Heard>().0.len()
}
#[test]
fn keys_read_as_they_are_written() {
for written in [
"ctrl+x ctrl+e",
"alt+shift+left",
"shift+tab",
"f5",
"ctrl+space",
"A",
] {
let keys: Result<Keys, String> = written.parse();
assert_eq!(keys.map(|keys| keys.to_string()).as_deref(), Ok(written));
}
assert_eq!(
"Ctrl+E".parse::<Keys>(),
Ok(Keys::from(KeyPress::ctrl('E')))
);
assert_eq!(
"shift+a".parse::<Keys>(),
Ok(Keys::from(KeyPress::key(KeyCode::Char('A'))))
);
for wrong in ["hyper+x", "ctrl+bogus", "", "f99"] {
assert!(wrong.parse::<Keys>().is_err(), "{wrong}");
}
}
#[test]
fn a_global_binding_shadows_the_inputs_and_a_taken_key_stays_until_disabled() {
let (mut app, _) = view_app();
app.init_resource::<Heard>();
let edit = || KeyBinding::new(KeyPress::ctrl('e'), "Edit in $EDITOR");
let first = app.add_key_binding(edit(), heard);
assert!(first.is_some());
type_text(&mut app, "ab");
press(&mut app, "ctrl+a ctrl+e x");
assert_eq!((heard_count(&app), typed(&app)), (1, "xab".to_owned()));
assert_eq!(app.add_key_binding(edit(), heard), None);
if let Some(first) = first {
app.world_mut().entity_mut(first).insert(Disabled);
}
assert!(app.add_key_binding(edit(), heard).is_some());
press(&mut app, "ctrl+e");
assert_eq!(heard_count(&app), 2);
}
#[test]
fn a_binding_reads_and_edits_the_input_before_it_is_sent() {
let (mut app, _) = view_app();
app.add_key_binding(
KeyBinding::new(KeyPress::alt('r'), "Ask for a review"),
|_: On<KeyPressed>, mut input: crate::MainInput| {
let draft = input.take();
input.set(format!("Review this: {draft}"));
input.set_cursor(0);
input.insert("Please. ");
},
);
type_text(&mut app, "the parser");
press(&mut app, "alt+r");
assert_eq!(typed(&app), "Please. Review this: the parser");
press(&mut app, "enter");
assert_eq!(
app.world().resource::<Delivered>().0,
["Please. Review this: the parser"]
);
}
#[test]
fn a_sequence_waits_for_its_last_key() {
let (mut app, _) = view_app();
app.init_resource::<Heard>();
let keys: Result<Keys, String> = "ctrl+x ctrl+e".parse();
let Ok(keys) = keys else {
return;
};
app.add_key_binding(KeyBinding::new(keys, "Edit in $EDITOR"), heard);
app.add_key_binding(KeyBinding::new(KeyPress::ctrl('x'), "Cut"), heard);
press(&mut app, "ctrl+x");
assert_eq!(heard_count(&app), 0);
press(&mut app, "ctrl+e");
assert_eq!(heard_count(&app), 1);
press(&mut app, "ctrl+x a");
assert_eq!((heard_count(&app), typed(&app)), (1, String::new()));
}
#[test]
fn a_focused_panel_takes_its_keys_and_esc_gives_them_back() {
let (mut app, _) = view_app();
app.init_resource::<Heard>();
let panel = TuiPanel::new(Placement::Top(Constraint::Length(3)));
let panel = app.world_mut().spawn(panel).id();
let up = KeyBinding::new(KeyPress::key(KeyCode::Up), "Select the file above");
app.add_key_binding(up.in_scope(KeyScope::Panel(panel)), heard);
app.world_mut().resource_mut::<KeyFocus>().0 = Some(panel);
press(&mut app, "up x");
assert_eq!((heard_count(&app), typed(&app)), (1, String::new()));
press(&mut app, "esc x up");
assert_eq!(app.world().resource::<KeyFocus>().0, None);
assert_eq!((heard_count(&app), typed(&app)), (1, "x".to_owned()));
app.world_mut().resource_mut::<KeyFocus>().0 = Some(panel);
app.world_mut().entity_mut(panel).remove::<TuiPanel>();
assert_eq!(app.world().resource::<KeyFocus>().0, None);
press(&mut app, "y");
assert_eq!(typed(&app), "xy");
let other = app.world_mut().spawn_empty().id();
app.world_mut().resource_mut::<KeyFocus>().0 = Some(other);
press(&mut app, "z");
assert_eq!(typed(&app), "xyz");
}
#[test]
fn a_held_key_is_heard_once_and_again_when_let_go() {
let (mut app, _) = view_app();
app.init_resource::<Heard>();
let talk = KeyBinding::new(KeyPress::ctrl(' '), "Hold to talk").with_releases();
app.add_key_binding(talk, heard);
for kind in [
KeyEventKind::Press,
KeyEventKind::Repeat,
KeyEventKind::Repeat,
KeyEventKind::Release,
] {
key_events(&mut app, "ctrl+space", kind);
}
assert_eq!(app.world().resource::<Heard>().0, [false, true]);
}
#[test]
fn a_plugins_binding_and_action_do_built_in_actions() {
let (mut app, _) = view_app();
let down = KeyBinding::new(KeyPress::ctrl('o'), "Down");
app.world_mut().spawn((down, InputAction::Down));
app.add_command("model", "Choose the model", |_: In<CommandArgs>| {});
let models = CompletionSource::arg("model").candidate("a/one", "");
app.world_mut().spawn(models.candidate("a/two", ""));
let selected = |app: &App| {
let view = app.world().resource::<TuiView>();
view.completion
.as_ref()
.map(|completion| completion.selected)
};
type_text(&mut app, "/model a/");
assert_eq!(selected(&app), Some(0));
app.update();
let state = app.world().resource::<crate::InputContext>();
let listed = state.completion().map(|open| (open.kind, open.items.len()));
assert_eq!(listed, Some((crate::CompletionKind::Arg(0), 2)));
press(&mut app, "ctrl+o");
assert_eq!(selected(&app), Some(1));
crate::tests::edit(&mut app, |input| input.set("ab\ncd"));
crate::tests::edit(&mut app, |input| input.set_cursor(1));
app.update();
press(&mut app, "ctrl+o");
assert_eq!(crate::tests::input(&app).cursor(), 4);
crate::tests::push(&mut app, crate::KeyInput::Action(InputAction::LineStart));
app.update();
assert_eq!(crate::tests::input(&app).cursor(), 3);
}