use super::keybindings::{KeyBinding, KeyCombo, ModeRow};
use crate::app::update::action::Action;
pub fn resolve(combo: &KeyCombo, bindings: &[KeyBinding]) -> Option<Action> {
bindings
.iter()
.filter(|kb| !matches!(kb.action, Action::None))
.find(|kb| kb.combos.contains(combo))
.map(|kb| kb.action.clone())
}
pub fn resolve_mode(combo: &KeyCombo, rows: &[ModeRow]) -> Option<Action> {
for row in rows {
for eb in row.bindings {
if !matches!(eb.action, Action::None) && eb.combos.contains(combo) {
return Some(eb.action.clone());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::update::input::keybindings::{Key, KeyCombo};
static QUIT_COMBOS: &[KeyCombo] = &[KeyCombo::plain(Key::Char('q'))];
static HELP_COMBOS: &[KeyCombo] = &[KeyCombo::plain(Key::Char('?'))];
static J_COMBOS: &[KeyCombo] = &[KeyCombo::plain(Key::Char('j'))];
static EMPTY_COMBOS: &[KeyCombo] = &[];
fn quit_binding() -> KeyBinding {
KeyBinding {
key_short: "q",
key: "q",
desc_short: "Quit",
description: "Quit",
action: Action::Quit,
combos: QUIT_COMBOS,
}
}
fn none_display_binding() -> KeyBinding {
KeyBinding {
key_short: "j/k",
key: "j/k",
desc_short: "Nav",
description: "Navigate",
action: Action::None,
combos: J_COMBOS, }
}
fn help_binding() -> KeyBinding {
KeyBinding {
key_short: "?",
key: "?",
desc_short: "Help",
description: "Help",
action: Action::OpenHelp,
combos: HELP_COMBOS,
}
}
fn empty_combos_binding() -> KeyBinding {
KeyBinding {
key_short: "q",
key: "q",
desc_short: "Quit",
description: "Quit",
action: Action::Quit,
combos: EMPTY_COMBOS,
}
}
#[test]
fn resolves_matching_combo() {
let bindings = [quit_binding()];
let result = resolve(&KeyCombo::plain(Key::Char('q')), &bindings);
assert!(matches!(result, Some(Action::Quit)));
}
#[test]
fn resolves_no_match_to_none() {
let bindings = [quit_binding()];
let result = resolve(&KeyCombo::plain(Key::Char('x')), &bindings);
assert!(result.is_none());
}
#[test]
fn skips_display_only_none_entries() {
let none_j = none_display_binding();
let quit = quit_binding();
let bindings = [none_j, quit];
assert!(resolve(&KeyCombo::plain(Key::Char('j')), &bindings).is_none());
assert!(matches!(
resolve(&KeyCombo::plain(Key::Char('q')), &bindings),
Some(Action::Quit)
));
}
#[test]
fn first_matching_non_none_entry_is_resolved() {
let quit = quit_binding();
let help = help_binding();
let bindings = [quit, help];
let result = resolve(&KeyCombo::plain(Key::Char('q')), &bindings);
assert!(matches!(result, Some(Action::Quit)));
}
#[test]
fn empty_combos_entry_never_matches() {
let bindings = [empty_combos_binding()];
let result = resolve(&KeyCombo::plain(Key::Char('q')), &bindings);
assert!(result.is_none());
}
mod resolve_mode_tests {
use super::*;
use crate::app::update::input::keybindings::{
CONNECTION_ERROR_ROWS, HELP_ROWS, TABLE_PICKER_ROWS,
};
#[test]
fn empty_rows_returns_none() {
let result = resolve_mode(&KeyCombo::plain(Key::Char('q')), &[]);
assert!(result.is_none());
}
#[test]
fn matches_binding_in_rows() {
let result = resolve_mode(&KeyCombo::plain(Key::Esc), HELP_ROWS);
assert!(matches!(result, Some(Action::CloseHelp)));
}
#[test]
fn mode_returns_none_for_no_match() {
let result = resolve_mode(&KeyCombo::plain(Key::F(12)), HELP_ROWS);
assert!(result.is_none());
}
#[test]
fn first_matching_binding_wins() {
let result = resolve_mode(&KeyCombo::plain(Key::Esc), CONNECTION_ERROR_ROWS);
assert!(matches!(result, Some(Action::CloseConnectionError)));
}
#[test]
fn unrelated_row_does_not_block_later_match() {
let result = resolve_mode(&KeyCombo::plain(Key::Enter), TABLE_PICKER_ROWS);
assert!(matches!(result, Some(Action::ConfirmSelection)));
}
}
}