use crate::overlay::{OverlayState, OverlayType};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OverlayInputResult {
Consumed,
CloseOverlay,
NotHandled,
}
pub struct OverlayInputManager;
impl OverlayInputManager {
pub fn handle_input(state: &OverlayState, key: &KeyEvent) -> OverlayInputResult {
let Some(active) = state.active() else {
return OverlayInputResult::NotHandled;
};
if let KeyCode::Char(c) = key.code {
if active.is_toggle_key(c, key.modifiers) {
return OverlayInputResult::CloseOverlay;
}
}
if key.code == KeyCode::Esc && active.closes_on_escape() {
if active.handles_escape_internally() {
return OverlayInputResult::NotHandled;
}
return OverlayInputResult::CloseOverlay;
}
if !key.modifiers.contains(KeyModifiers::CONTROL) {
if let KeyCode::Char(c) = key.code {
if let Some(other) = OverlayType::from_toggle_key(c, key.modifiers) {
if other != active {
return OverlayInputResult::Consumed;
}
}
}
}
OverlayInputResult::NotHandled
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::overlay::OverlayType;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
fn make_key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::empty(),
}
}
#[test]
fn test_no_overlay_not_handled() {
let state = OverlayState::default();
let key = make_key(KeyCode::Char('p'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &key),
OverlayInputResult::NotHandled
);
}
#[test]
fn test_escape_closes_overlay() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &esc),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn test_toggle_key_closes_overlay() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
let h = make_key(KeyCode::Char('h'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &h),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn test_other_overlay_keys_blocked() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
let backslash = make_key(KeyCode::Char('\\'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &backslash),
OverlayInputResult::Consumed
);
let p = make_key(KeyCode::Char('p'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &p),
OverlayInputResult::Consumed
);
}
#[test]
fn test_non_overlay_keys_pass_through() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
let c = make_key(KeyCode::Char('c'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &c),
OverlayInputResult::NotHandled
);
let space = make_key(KeyCode::Char(' '), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &space),
OverlayInputResult::NotHandled
);
}
#[test]
fn test_palette_editor_toggle_keys() {
let mut state = OverlayState::default();
state.open(OverlayType::PaletteEditor);
let p = make_key(KeyCode::Char('p'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &p),
OverlayInputResult::CloseOverlay
);
let slash = make_key(KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &slash),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn test_dashboard_toggle_keys() {
let mut state = OverlayState::default();
state.open(OverlayType::Dashboard);
let backslash = make_key(KeyCode::Char('\\'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &backslash),
OverlayInputResult::CloseOverlay
);
let pipe = make_key(KeyCode::Char('|'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &pipe),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn test_keyboard_hints_toggle_key() {
let mut state = OverlayState::default();
state.open(OverlayType::KeyboardHints);
let question = make_key(KeyCode::Char('?'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &question),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn test_ctrl_keys_for_config_overlays() {
let mut state = OverlayState::default();
state.open(OverlayType::ConfigBrowser);
let ctrl_l = make_key(KeyCode::Char('l'), KeyModifiers::CONTROL);
assert_eq!(
OverlayInputManager::handle_input(&state, &ctrl_l),
OverlayInputResult::CloseOverlay
);
let l = make_key(KeyCode::Char('l'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &l),
OverlayInputResult::NotHandled
);
}
#[test]
fn test_palette_editor_escape_delegated() {
let mut state = OverlayState::default();
state.open(OverlayType::PaletteEditor);
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &esc),
OverlayInputResult::NotHandled
);
}
#[test]
fn test_regular_overlay_escape_closes() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &esc),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn esc_closes_all_dismissable_overlays() {
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
for overlay in [
OverlayType::Controls,
OverlayType::Dashboard,
OverlayType::KeyboardHints,
OverlayType::ConfigBrowser,
OverlayType::ConfigSave,
OverlayType::DirtyGuard,
OverlayType::PresetComparison,
] {
let mut state = OverlayState::default();
state.open(overlay);
let result = OverlayInputManager::handle_input(&state, &esc);
assert_eq!(
result,
OverlayInputResult::CloseOverlay,
"{overlay:?} + Esc should return CloseOverlay"
);
}
}
#[test]
fn esc_does_not_close_pause_overlays() {
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
for overlay in [OverlayType::PauseBadge, OverlayType::PauseLogo] {
let mut state = OverlayState::default();
state.open(overlay);
let result = OverlayInputManager::handle_input(&state, &esc);
assert_ne!(
result,
OverlayInputResult::CloseOverlay,
"{overlay:?} + Esc must NOT return CloseOverlay"
);
}
}
#[test]
fn esc_on_palette_editor_is_delegated() {
let mut state = OverlayState::default();
state.open(OverlayType::PaletteEditor);
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &esc),
OverlayInputResult::NotHandled,
"PaletteEditor Esc must be delegated (NotHandled), not CloseOverlay"
);
}
#[test]
fn toggle_key_re_closes_controls() {
let mut state = OverlayState::default();
state.open(OverlayType::Controls);
for c in ['h', 'H'] {
let k = make_key(KeyCode::Char(c), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &k),
OverlayInputResult::CloseOverlay,
"'{c}' should re-close Controls"
);
}
}
#[test]
fn dashboard_dual_toggle_both_reclose() {
let mut state = OverlayState::default();
state.open(OverlayType::Dashboard);
for c in ['\\', '|'] {
let k = make_key(KeyCode::Char(c), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &k),
OverlayInputResult::CloseOverlay,
"'{c}' should re-close Dashboard"
);
}
}
#[test]
fn keyboard_hints_toggle_recloses() {
let mut state = OverlayState::default();
state.open(OverlayType::KeyboardHints);
let k = make_key(KeyCode::Char('?'), KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &k),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn config_browser_toggle_recloses_ctrl_only() {
let mut state = OverlayState::default();
state.open(OverlayType::ConfigBrowser);
for c in ['l', 'L', 'b', 'B'] {
let ctrl = make_key(KeyCode::Char(c), KeyModifiers::CONTROL);
assert_eq!(
OverlayInputManager::handle_input(&state, &ctrl),
OverlayInputResult::CloseOverlay,
"Ctrl+{c} should re-close ConfigBrowser"
);
let plain = make_key(KeyCode::Char(c), KeyModifiers::NONE);
assert_ne!(
OverlayInputManager::handle_input(&state, &plain),
OverlayInputResult::CloseOverlay,
"plain '{c}' must not re-close ConfigBrowser"
);
}
}
#[test]
fn config_save_toggle_recloses() {
let mut state = OverlayState::default();
state.open(OverlayType::ConfigSave);
let ctrl_s = make_key(KeyCode::Char('s'), KeyModifiers::CONTROL);
assert_eq!(
OverlayInputManager::handle_input(&state, &ctrl_s),
OverlayInputResult::CloseOverlay
);
}
#[test]
fn esc_with_no_overlay_is_not_handled() {
let state = OverlayState::default();
let esc = make_key(KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(
OverlayInputManager::handle_input(&state, &esc),
OverlayInputResult::NotHandled,
"Esc with no overlay open must be NotHandled (reaches sticky-error dismiss)"
);
}
#[test]
fn space_never_consumed_by_overlay_manager() {
let space = make_key(KeyCode::Char(' '), KeyModifiers::NONE);
for overlay in [
OverlayType::Controls,
OverlayType::Dashboard,
OverlayType::KeyboardHints,
] {
let mut state = OverlayState::default();
state.open(overlay);
let result = OverlayInputManager::handle_input(&state, &space);
assert_ne!(
result,
OverlayInputResult::Consumed,
"Space must never be Consumed ({overlay:?} open)"
);
}
}
}