use crossterm::event::KeyCode;
pub trait KeyHandler {
fn handle_key(&mut self, key: &KeyCode) -> bool {
if self.should_quit(key) {
return false; }
if self.in_modal() {
return self.handle_modal_key(key);
}
if self.has_confirm() {
return self.handle_confirm_key(key);
}
if self.has_popup() {
return self.handle_popup_key(key);
}
self.handle_view_key(key)
}
fn should_quit(&self, key: &KeyCode) -> bool;
fn in_modal(&self) -> bool {
false
}
fn handle_modal_key(&mut self, _key: &KeyCode) -> bool {
true
}
fn has_confirm(&self) -> bool {
false
}
fn handle_confirm_key(&mut self, _key: &KeyCode) -> bool {
true
}
fn has_popup(&self) -> bool {
false
}
fn handle_popup_key(&mut self, _key: &KeyCode) -> bool {
true
}
fn handle_view_key(&mut self, key: &KeyCode) -> bool;
}
pub mod common {
use crossterm::event::KeyCode;
pub fn is_nav_key(key: &KeyCode) -> bool {
matches!(
key,
KeyCode::Char('j')
| KeyCode::Char('k')
| KeyCode::Up
| KeyCode::Down
| KeyCode::Char('g')
| KeyCode::Char('G')
)
}
pub fn is_horizontal_nav(key: &KeyCode) -> bool {
matches!(
key,
KeyCode::Char('h') | KeyCode::Char('l') | KeyCode::Left | KeyCode::Right
)
}
pub fn is_confirm_key(key: &KeyCode) -> bool {
matches!(
key,
KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter
)
}
pub fn is_cancel_key(key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc)
}
pub fn is_quit_key(key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('q') | KeyCode::Char('Q'))
}
pub fn is_help_key(key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('?'))
}
pub fn is_refresh_key(key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('r') | KeyCode::Char('R'))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestApp {
in_modal: bool,
has_confirm: bool,
has_popup: bool,
#[allow(dead_code)]
quit: bool,
last_key: Option<KeyCode>,
layer_handled: String,
}
impl TestApp {
fn new() -> Self {
Self {
in_modal: false,
has_confirm: false,
has_popup: false,
quit: false,
last_key: None,
layer_handled: String::new(),
}
}
}
impl KeyHandler for TestApp {
fn should_quit(&self, key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('q'))
}
fn in_modal(&self) -> bool {
self.in_modal
}
fn handle_modal_key(&mut self, key: &KeyCode) -> bool {
self.last_key = Some(*key);
self.layer_handled = "modal".to_string();
true
}
fn has_confirm(&self) -> bool {
self.has_confirm
}
fn handle_confirm_key(&mut self, key: &KeyCode) -> bool {
self.last_key = Some(*key);
self.layer_handled = "confirm".to_string();
true
}
fn has_popup(&self) -> bool {
self.has_popup
}
fn handle_popup_key(&mut self, key: &KeyCode) -> bool {
self.last_key = Some(*key);
self.layer_handled = "popup".to_string();
true
}
fn handle_view_key(&mut self, key: &KeyCode) -> bool {
self.last_key = Some(*key);
self.layer_handled = "view".to_string();
true
}
}
#[test]
fn test_quit_returns_false() {
let mut app = TestApp::new();
assert!(!app.handle_key(&KeyCode::Char('q')));
}
#[test]
fn test_non_quit_key_returns_true() {
let mut app = TestApp::new();
assert!(app.handle_key(&KeyCode::Char('j')));
}
#[test]
fn test_modal_priority_over_confirm() {
let mut app = TestApp::new();
app.in_modal = true;
app.has_confirm = true;
app.has_popup = true;
app.handle_key(&KeyCode::Char('j'));
assert_eq!(app.layer_handled, "modal");
}
#[test]
fn test_confirm_priority_over_popup() {
let mut app = TestApp::new();
app.has_confirm = true;
app.has_popup = true;
app.handle_key(&KeyCode::Char('y'));
assert_eq!(app.layer_handled, "confirm");
}
#[test]
fn test_popup_priority_over_view() {
let mut app = TestApp::new();
app.has_popup = true;
app.handle_key(&KeyCode::Char('x'));
assert_eq!(app.layer_handled, "popup");
}
#[test]
fn test_view_when_no_layers() {
let mut app = TestApp::new();
app.handle_key(&KeyCode::Char('j'));
assert_eq!(app.layer_handled, "view");
assert_eq!(app.last_key, Some(KeyCode::Char('j')));
}
#[test]
fn test_quit_takes_priority_over_modal() {
let mut app = TestApp::new();
app.in_modal = true;
let result = app.handle_key(&KeyCode::Char('q'));
assert!(!result);
}
struct MinimalApp;
impl KeyHandler for MinimalApp {
fn should_quit(&self, key: &KeyCode) -> bool {
matches!(key, KeyCode::Char('q'))
}
fn handle_view_key(&mut self, _key: &KeyCode) -> bool {
true
}
}
#[test]
fn test_default_in_modal() {
let app = MinimalApp;
assert!(!app.in_modal());
}
#[test]
fn test_default_has_confirm() {
let app = MinimalApp;
assert!(!app.has_confirm());
}
#[test]
fn test_default_has_popup() {
let app = MinimalApp;
assert!(!app.has_popup());
}
#[test]
fn test_default_handle_modal_key() {
let mut app = MinimalApp;
assert!(app.handle_modal_key(&KeyCode::Char('x')));
}
#[test]
fn test_default_handle_confirm_key() {
let mut app = MinimalApp;
assert!(app.handle_confirm_key(&KeyCode::Enter));
}
#[test]
fn test_default_handle_popup_key() {
let mut app = MinimalApp;
assert!(app.handle_popup_key(&KeyCode::Esc));
}
mod common_tests {
use super::common::*;
use crossterm::event::KeyCode;
#[test]
fn test_is_nav_key_j() {
assert!(is_nav_key(&KeyCode::Char('j')));
}
#[test]
fn test_is_nav_key_k() {
assert!(is_nav_key(&KeyCode::Char('k')));
}
#[test]
fn test_is_nav_key_up() {
assert!(is_nav_key(&KeyCode::Up));
}
#[test]
fn test_is_nav_key_down() {
assert!(is_nav_key(&KeyCode::Down));
}
#[test]
fn test_is_nav_key_g() {
assert!(is_nav_key(&KeyCode::Char('g')));
}
#[test]
fn test_is_nav_key_upper_g() {
assert!(is_nav_key(&KeyCode::Char('G')));
}
#[test]
fn test_is_nav_key_false() {
assert!(!is_nav_key(&KeyCode::Char('a')));
assert!(!is_nav_key(&KeyCode::Enter));
}
#[test]
fn test_is_horizontal_nav_h() {
assert!(is_horizontal_nav(&KeyCode::Char('h')));
}
#[test]
fn test_is_horizontal_nav_l() {
assert!(is_horizontal_nav(&KeyCode::Char('l')));
}
#[test]
fn test_is_horizontal_nav_left() {
assert!(is_horizontal_nav(&KeyCode::Left));
}
#[test]
fn test_is_horizontal_nav_right() {
assert!(is_horizontal_nav(&KeyCode::Right));
}
#[test]
fn test_is_horizontal_nav_false() {
assert!(!is_horizontal_nav(&KeyCode::Char('j')));
assert!(!is_horizontal_nav(&KeyCode::Up));
}
#[test]
fn test_is_confirm_key_y_lower() {
assert!(is_confirm_key(&KeyCode::Char('y')));
}
#[test]
fn test_is_confirm_key_y_upper() {
assert!(is_confirm_key(&KeyCode::Char('Y')));
}
#[test]
fn test_is_confirm_key_enter() {
assert!(is_confirm_key(&KeyCode::Enter));
}
#[test]
fn test_is_confirm_key_false() {
assert!(!is_confirm_key(&KeyCode::Char('n')));
assert!(!is_confirm_key(&KeyCode::Esc));
}
#[test]
fn test_is_cancel_key_n_lower() {
assert!(is_cancel_key(&KeyCode::Char('n')));
}
#[test]
fn test_is_cancel_key_n_upper() {
assert!(is_cancel_key(&KeyCode::Char('N')));
}
#[test]
fn test_is_cancel_key_esc() {
assert!(is_cancel_key(&KeyCode::Esc));
}
#[test]
fn test_is_cancel_key_false() {
assert!(!is_cancel_key(&KeyCode::Char('y')));
assert!(!is_cancel_key(&KeyCode::Enter));
}
#[test]
fn test_is_quit_key_q_lower() {
assert!(is_quit_key(&KeyCode::Char('q')));
}
#[test]
fn test_is_quit_key_q_upper() {
assert!(is_quit_key(&KeyCode::Char('Q')));
}
#[test]
fn test_is_quit_key_false() {
assert!(!is_quit_key(&KeyCode::Char('x')));
assert!(!is_quit_key(&KeyCode::Esc));
}
#[test]
fn test_is_help_key_question() {
assert!(is_help_key(&KeyCode::Char('?')));
}
#[test]
fn test_is_help_key_false() {
assert!(!is_help_key(&KeyCode::Char('h')));
assert!(!is_help_key(&KeyCode::F(1)));
}
#[test]
fn test_is_refresh_key_r_lower() {
assert!(is_refresh_key(&KeyCode::Char('r')));
}
#[test]
fn test_is_refresh_key_r_upper() {
assert!(is_refresh_key(&KeyCode::Char('R')));
}
#[test]
fn test_is_refresh_key_false() {
assert!(!is_refresh_key(&KeyCode::F(5)));
assert!(!is_refresh_key(&KeyCode::Char('f')));
}
}
}