use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::bindings::KeyBindings;
use super::exit::ExitState;
use super::types::{AppKeyAction, AppKeyResult, KeyCombo, KeyContext};
pub trait KeyHandler: Send + 'static {
fn handle_key(&mut self, key: KeyEvent, context: &KeyContext) -> AppKeyResult;
fn status_hint(&self) -> Option<String> {
None
}
fn bindings(&self) -> &KeyBindings;
}
pub struct DefaultKeyHandler {
bindings: KeyBindings,
exit_state: ExitState,
custom_bindings: Vec<(KeyCombo, Box<dyn Fn() -> AppKeyAction + Send + Sync>)>,
}
impl DefaultKeyHandler {
pub fn new(bindings: KeyBindings) -> Self {
Self {
bindings,
exit_state: ExitState::default(),
custom_bindings: Vec::new(),
}
}
pub fn bindings(&self) -> &KeyBindings {
&self.bindings
}
pub fn with_custom_binding<F>(mut self, combo: KeyCombo, action_fn: F) -> Self
where
F: Fn() -> AppKeyAction + Send + Sync + 'static,
{
self.custom_bindings.push((combo, Box::new(action_fn)));
self
}
fn is_exit_key(&self, key: &KeyEvent) -> bool {
KeyBindings::matches_any(&self.bindings.enter_exit_mode, key)
}
fn check_custom_binding(&self, key: &KeyEvent) -> Option<AppKeyAction> {
for (combo, action_fn) in &self.custom_bindings {
if combo.matches(key) {
return Some(action_fn());
}
}
None
}
}
impl Default for DefaultKeyHandler {
fn default() -> Self {
Self::new(KeyBindings::default())
}
}
impl KeyHandler for DefaultKeyHandler {
fn handle_key(&mut self, key: KeyEvent, context: &KeyContext) -> AppKeyResult {
if self.exit_state.is_expired() {
self.exit_state.reset();
}
if context.widget_blocking {
if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
return AppKeyResult::Action(AppKeyAction::Quit);
}
return AppKeyResult::NotHandled;
}
if context.is_processing {
if KeyBindings::matches_any(&self.bindings.interrupt, &key) {
return AppKeyResult::Action(AppKeyAction::Interrupt);
}
if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
return AppKeyResult::Action(AppKeyAction::Quit);
}
if self.is_exit_key(&key) {
if self.exit_state.is_awaiting() {
self.exit_state.reset();
return AppKeyResult::Action(AppKeyAction::RequestExit);
} else if context.input_empty {
self.exit_state = ExitState::awaiting_confirmation(
self.bindings.exit_timeout_secs,
);
return AppKeyResult::Handled;
}
}
return AppKeyResult::Handled;
}
if self.exit_state.is_awaiting() {
if self.is_exit_key(&key) {
self.exit_state.reset();
return AppKeyResult::Action(AppKeyAction::RequestExit);
}
self.exit_state.reset();
}
if let Some(action) = self.check_custom_binding(&key) {
return AppKeyResult::Action(action);
}
if KeyBindings::matches_any(&self.bindings.force_quit, &key) {
return AppKeyResult::Action(AppKeyAction::Quit);
}
if KeyBindings::matches_any(&self.bindings.quit, &key) && context.input_empty {
return AppKeyResult::Action(AppKeyAction::Quit);
}
if self.is_exit_key(&key) {
if context.input_empty {
self.exit_state = ExitState::awaiting_confirmation(
self.bindings.exit_timeout_secs,
);
return AppKeyResult::Handled;
}
return AppKeyResult::Action(AppKeyAction::DeleteCharAt);
}
if KeyBindings::matches_any(&self.bindings.submit, &key) {
return AppKeyResult::Action(AppKeyAction::Submit);
}
if KeyBindings::matches_any(&self.bindings.interrupt, &key) {
return AppKeyResult::Action(AppKeyAction::Interrupt);
}
if KeyBindings::matches_any(&self.bindings.move_up, &key) {
return AppKeyResult::Action(AppKeyAction::MoveUp);
}
if KeyBindings::matches_any(&self.bindings.move_down, &key) {
return AppKeyResult::Action(AppKeyAction::MoveDown);
}
if KeyBindings::matches_any(&self.bindings.move_left, &key) {
return AppKeyResult::Action(AppKeyAction::MoveLeft);
}
if KeyBindings::matches_any(&self.bindings.move_right, &key) {
return AppKeyResult::Action(AppKeyAction::MoveRight);
}
if KeyBindings::matches_any(&self.bindings.move_line_start, &key) {
return AppKeyResult::Action(AppKeyAction::MoveLineStart);
}
if KeyBindings::matches_any(&self.bindings.move_line_end, &key) {
return AppKeyResult::Action(AppKeyAction::MoveLineEnd);
}
if KeyBindings::matches_any(&self.bindings.delete_char_before, &key) {
return AppKeyResult::Action(AppKeyAction::DeleteCharBefore);
}
if KeyBindings::matches_any(&self.bindings.delete_char_at, &key) {
return AppKeyResult::Action(AppKeyAction::DeleteCharAt);
}
if KeyBindings::matches_any(&self.bindings.kill_line, &key) {
return AppKeyResult::Action(AppKeyAction::KillLine);
}
if KeyBindings::matches_any(&self.bindings.insert_newline, &key) {
return AppKeyResult::Action(AppKeyAction::InsertNewline);
}
if let KeyCode::Char(c) = key.code {
if key.modifiers.is_empty() || key.modifiers == KeyModifiers::SHIFT {
return AppKeyResult::Action(AppKeyAction::InsertChar(c));
}
}
AppKeyResult::NotHandled
}
fn status_hint(&self) -> Option<String> {
if self.exit_state.is_awaiting() {
Some("Press again to exit".to_string())
} else {
None
}
}
fn bindings(&self) -> &KeyBindings {
&self.bindings
}
}
pub struct ComposedKeyHandler<H: KeyHandler> {
inner: H,
pre_hooks: Vec<Box<dyn Fn(&KeyEvent, &KeyContext) -> Option<AppKeyResult> + Send>>,
}
impl<H: KeyHandler> ComposedKeyHandler<H> {
pub fn new(inner: H) -> Self {
Self {
inner,
pre_hooks: Vec::new(),
}
}
pub fn with_pre_hook<F>(mut self, hook: F) -> Self
where
F: Fn(&KeyEvent, &KeyContext) -> Option<AppKeyResult> + Send + 'static,
{
self.pre_hooks.push(Box::new(hook));
self
}
pub fn inner(&self) -> &H {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut H {
&mut self.inner
}
}
impl<H: KeyHandler> KeyHandler for ComposedKeyHandler<H> {
fn handle_key(&mut self, key: KeyEvent, context: &KeyContext) -> AppKeyResult {
for hook in &self.pre_hooks {
if let Some(result) = hook(&key, context) {
return result;
}
}
self.inner.handle_key(key, context)
}
fn status_hint(&self) -> Option<String> {
self.inner.status_hint()
}
fn bindings(&self) -> &KeyBindings {
self.inner.bindings()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_handler_force_quit_in_modal() {
let mut handler = DefaultKeyHandler::default();
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: true, };
let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
let result = handler.handle_key(key, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = handler.handle_key(esc, &context);
assert_eq!(result, AppKeyResult::NotHandled);
}
#[test]
fn test_emacs_handler_processing_mode() {
let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
let context = KeyContext {
input_empty: true,
is_processing: true, widget_blocking: false,
};
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = handler.handle_key(esc, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::Interrupt));
let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
let result = handler.handle_key(a, &context);
assert_eq!(result, AppKeyResult::Handled);
}
#[test]
fn test_emacs_handler_exit_mode() {
let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
let result = handler.handle_key(ctrl_d, &context);
assert_eq!(result, AppKeyResult::Handled);
assert!(handler.status_hint().is_some());
let result = handler.handle_key(ctrl_d, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::RequestExit));
assert!(handler.status_hint().is_none());
}
#[test]
fn test_minimal_handler_quit() {
let mut handler = DefaultKeyHandler::default(); let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = handler.handle_key(esc, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
}
#[test]
fn test_default_handler_char_input() {
let mut handler = DefaultKeyHandler::default();
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
let result = handler.handle_key(a, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('a')));
let shift_a = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT);
let result = handler.handle_key(shift_a, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('A')));
}
#[test]
fn test_exit_mode_cancelled_by_other_key() {
let mut handler = DefaultKeyHandler::new(KeyBindings::emacs());
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
let result = handler.handle_key(ctrl_d, &context);
assert_eq!(result, AppKeyResult::Handled);
assert!(handler.status_hint().is_some());
let a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
let result = handler.handle_key(a, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::InsertChar('a')));
assert!(handler.status_hint().is_none());
}
#[test]
fn test_custom_binding_basic() {
let mut handler = DefaultKeyHandler::new(KeyBindings::emacs())
.with_custom_binding(KeyCombo::ctrl('t'), || {
AppKeyAction::custom("toggle")
});
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_t = KeyEvent::new(KeyCode::Char('t'), KeyModifiers::CONTROL);
let result = handler.handle_key(ctrl_t, &context);
if let AppKeyResult::Action(AppKeyAction::Custom(any)) = result {
assert!(any.downcast_ref::<&str>().is_some());
} else {
panic!("Expected Custom action, got {:?}", result);
}
}
#[test]
fn test_custom_binding_overrides_standard() {
let mut handler = DefaultKeyHandler::new(KeyBindings::emacs())
.with_custom_binding(KeyCombo::ctrl('p'), || {
AppKeyAction::custom("custom_up")
});
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_p = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL);
let result = handler.handle_key(ctrl_p, &context);
if let AppKeyResult::Action(AppKeyAction::Custom(_)) = result {
} else {
panic!("Expected Custom action to override MoveUp, got {:?}", result);
}
}
#[test]
fn test_composed_handler_basic() {
let base = DefaultKeyHandler::new(KeyBindings::minimal());
let mut composed = ComposedKeyHandler::new(base);
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = composed.handle_key(esc, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
}
#[test]
fn test_composed_handler_pre_hook_intercepts() {
let base = DefaultKeyHandler::new(KeyBindings::minimal());
let mut composed = ComposedKeyHandler::new(base)
.with_pre_hook(|key, _ctx| {
if key.code == KeyCode::F(1) {
return Some(AppKeyResult::Action(AppKeyAction::custom("help")));
}
None
});
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let f1 = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
let result = composed.handle_key(f1, &context);
if let AppKeyResult::Action(AppKeyAction::Custom(_)) = result {
} else {
panic!("Expected hook to intercept F1, got {:?}", result);
}
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
let result = composed.handle_key(esc, &context);
assert_eq!(result, AppKeyResult::Action(AppKeyAction::Quit));
}
#[test]
fn test_composed_handler_multiple_hooks() {
let base = DefaultKeyHandler::new(KeyBindings::minimal());
let mut composed = ComposedKeyHandler::new(base)
.with_pre_hook(|key, _ctx| {
if key.code == KeyCode::F(1) {
return Some(AppKeyResult::Action(AppKeyAction::custom("first")));
}
None
})
.with_pre_hook(|key, _ctx| {
if key.code == KeyCode::F(2) {
return Some(AppKeyResult::Action(AppKeyAction::custom("second")));
}
if key.code == KeyCode::F(1) {
return Some(AppKeyResult::Action(AppKeyAction::custom("should_not_see")));
}
None
});
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let f1 = KeyEvent::new(KeyCode::F(1), KeyModifiers::NONE);
let result = composed.handle_key(f1, &context);
if let AppKeyResult::Action(AppKeyAction::Custom(any)) = result {
let s = any.downcast_ref::<&str>().unwrap();
assert_eq!(*s, "first");
} else {
panic!("Expected first hook to handle F1");
}
let f2 = KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE);
let result = composed.handle_key(f2, &context);
if let AppKeyResult::Action(AppKeyAction::Custom(any)) = result {
let s = any.downcast_ref::<&str>().unwrap();
assert_eq!(*s, "second");
} else {
panic!("Expected second hook to handle F2");
}
}
#[test]
fn test_composed_handler_status_hint() {
let mut base = DefaultKeyHandler::new(KeyBindings::emacs());
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
base.handle_key(ctrl_d, &context);
let composed = ComposedKeyHandler::new(base);
assert!(composed.status_hint().is_some());
assert!(composed.status_hint().unwrap().contains("exit"));
}
#[test]
fn test_composed_handler_inner_access() {
let base = DefaultKeyHandler::new(KeyBindings::emacs());
let mut composed = ComposedKeyHandler::new(base);
assert!(composed.inner().status_hint().is_none());
let context = KeyContext {
input_empty: true,
is_processing: false,
widget_blocking: false,
};
let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
composed.inner_mut().handle_key(ctrl_d, &context);
assert!(composed.inner().status_hint().is_some());
}
}