mod action;
mod catalog;
mod lookup;
pub(crate) use self::action::Action;
#[allow(unused_imports)]
pub(crate) use self::catalog::{
COMMAND_DOMAINS, COMMANDS, CommandContext, CommandDomain, CommandLifecycle, CommandSpec,
DETAIL_COMMANDS, KeySequence,
};
#[cfg(test)]
pub(crate) use self::catalog::{DUE_SORT_REASON, PROJECT_PATH_FLOW_REASON};
#[allow(unused_imports)]
pub(crate) use self::lookup::{
CommandCompletion, CommandLookup, ShortcutLookup, command_cycle_options, complete_command,
key_label, lookup_command, matching_commands, prefix_hint_commands, resolve_shortcut,
resolve_shortcut_for, resolve_shortcut_in, shortcut_label,
};
#[cfg(test)]
fn implemented_action_is_handled(action: Action) -> bool {
matches!(
action,
Action::Quit
| Action::MoveDown
| Action::MoveUp
| Action::MoveLeft
| Action::MoveRight
| Action::PreviousItem
| Action::NextItem
| Action::First
| Action::Last
| Action::ToggleFocus
| Action::ToggleSidebar
| Action::ToggleDetail
| Action::GoBack
| Action::ToggleHelp
| Action::BeginSearch
| Action::BeginCommand
| Action::Refresh
| Action::SetOrder(_)
| Action::ReverseSort
| Action::SetStatus(_)
| Action::SetPriority(_)
| Action::CyclePriority(_)
| Action::CopyShortRef
| Action::CopyDurableRef
| Action::BeginEditTitle
| Action::BeginEditDescription
| Action::BeginEditProject
| Action::BeginEditPriority
| Action::BeginEditLabels
| Action::Delete
| Action::Restore
| Action::ToggleEpicExpanded
| Action::DetachEpicChild
| Action::PromoteEpicChild
| Action::BeginStatusPicker
| Action::BeginRenameProject
| Action::BeginDeleteProject
| Action::BeginAddTask
| Action::BeginAddNote
| Action::BeginAddProject
| Action::BeginAddLabel
| Action::BeginFilterLabel
| Action::BeginFilterPriority
| Action::BeginScopeProject
| Action::BeginSwitchWorkspace
| Action::ClearFilters
| Action::ToggleDeletedFilter
| Action::ShowView(_)
| Action::ShowWorkspaceScope
| Action::BeginConflictList
| Action::ShowConflictDetails
| Action::NextConflict
| Action::PreviousConflict
| Action::AcceptConflictLocal
| Action::AcceptConflictRemote
| Action::BeginManualConflictMerge
| Action::ShowConfigStatus
| Action::ShowConfigInfo
| Action::ShowConfigPaths
| Action::ShowDatabaseStats
| Action::BeginConfigInit
| Action::BeginAddDependency
| Action::BeginRemoveDependency
| Action::Undo
| Action::ToggleMarkSelected
| Action::ToggleMarkAllInView
| Action::ClearMarks
)
}
#[cfg(test)]
mod tests {
use crossterm::event::KeyCode;
use super::*;
#[test]
fn maps_navigation_keys() {
assert_eq!(
Action::from_normal_key(KeyCode::Char('j')),
Action::MoveDown
);
assert_eq!(Action::from_normal_key(KeyCode::Down), Action::MoveDown);
assert_eq!(Action::from_normal_key(KeyCode::Char('k')), Action::MoveUp);
assert_eq!(Action::from_normal_key(KeyCode::Up), Action::MoveUp);
}
#[test]
fn search_mode_captures_text_keys() {
assert_eq!(
Action::from_search_key(KeyCode::Char('q')),
Action::SearchChar('q')
);
assert_eq!(
Action::from_search_key(KeyCode::Enter),
Action::AcceptSearch
);
assert_eq!(Action::from_search_key(KeyCode::Esc), Action::CancelSearch);
}
#[test]
fn normal_escape_cancels_overlay() {
assert_eq!(Action::from_normal_key(KeyCode::Esc), Action::CancelOverlay);
}
#[test]
fn maps_command_panel_key() {
assert_eq!(
Action::from_normal_key(KeyCode::Char(':')),
Action::BeginCommand
);
}
#[test]
fn command_mode_captures_text_keys() {
assert_eq!(
Action::from_command_key(KeyCode::Char('q')),
Action::CommandChar('q')
);
assert_eq!(
Action::from_command_key(KeyCode::Enter),
Action::AcceptCommand
);
assert_eq!(
Action::from_command_key(KeyCode::Esc),
Action::CancelCommand
);
assert_eq!(
Action::from_command_key(KeyCode::Backspace),
Action::BackspaceCommand
);
}
#[test]
fn lookup_command_finds_exact_name() {
assert_eq!(lookup_command("quit"), CommandLookup::Found(Action::Quit));
}
#[test]
fn lookup_command_finds_unique_prefix() {
assert_eq!(lookup_command("ref"), CommandLookup::Found(Action::Refresh));
}
#[test]
fn lookup_command_finds_prefixed_suffix() {
assert_eq!(
lookup_command(":todo"),
CommandLookup::Found(Action::SetStatus("todo"))
);
}
#[test]
fn lookup_command_ignores_dashes() {
assert_eq!(
lookup_command(":statusin"),
CommandLookup::Found(Action::SetStatus("inbox"))
);
}
#[test]
fn lookup_command_preserves_suffix_ambiguity() {
assert_eq!(lookup_command(":done"), CommandLookup::Ambiguous);
}
#[test]
fn complete_command_fills_unique_match() {
assert_eq!(
complete_command(":todo"),
CommandCompletion::Completed("status-todo".to_string())
);
}
#[test]
fn complete_command_fills_dashless_match() {
assert_eq!(
complete_command(":statusin"),
CommandCompletion::Completed("status-inbox".to_string())
);
}
#[test]
fn complete_command_reports_unchanged_for_ambiguous_match() {
assert_eq!(complete_command("stat"), CommandCompletion::Unchanged);
}
#[test]
fn command_cycle_options_keeps_lower_ranked_visible_matches() {
assert_eq!(
command_cycle_options("r"),
vec![
"refresh",
"restore",
"rename-project",
"remove-project-path",
"remove-dependency",
"move-right",
"view-recent",
"copy-ref",
"order-reverse",
"conflict-use-remote"
]
);
}
#[test]
fn command_cycle_options_limits_exact_match_to_exact_commands() {
assert_eq!(command_cycle_options("todo"), vec!["status-todo"]);
}
#[test]
fn complete_command_reports_unchanged_when_fully_extended() {
assert_eq!(complete_command("status-"), CommandCompletion::Unchanged);
}
#[test]
fn lookup_command_reports_ambiguous_prefix() {
assert_eq!(lookup_command("s"), CommandLookup::Ambiguous);
}
#[test]
fn lookup_command_reports_empty_input() {
assert_eq!(lookup_command(""), CommandLookup::Empty);
assert_eq!(lookup_command(" "), CommandLookup::Empty);
}
#[test]
fn lookup_command_reports_missing_input() {
assert_eq!(lookup_command("zzzz"), CommandLookup::Missing);
}
#[test]
fn resolves_single_key_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('q')]),
ShortcutLookup::Found(Action::Quit)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('j')]),
ShortcutLookup::Found(Action::MoveDown)
);
}
#[test]
fn resolves_multi_key_sequences_from_catalog() {
let commands = [CommandSpec::implemented(
"test-sequence",
"test sequence",
"Test",
&[KeySequence {
codes: &[KeyCode::Char('a'), KeyCode::Char('t')],
label: "a t",
}],
Action::BeginSearch,
)];
assert_eq!(
resolve_shortcut_in(&commands, &[KeyCode::Char('a')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut_in(&commands, &[KeyCode::Char('a'), KeyCode::Char('t')]),
ShortcutLookup::Found(Action::BeginSearch)
);
}
#[test]
fn resolves_exact_prefix_ambiguity() {
let commands = [
CommandSpec::implemented(
"single-g",
"single g",
"Test",
&[KeySequence {
codes: &[KeyCode::Char('g')],
label: "g",
}],
Action::First,
),
CommandSpec::implemented(
"double-g",
"double g",
"Test",
&[KeySequence {
codes: &[KeyCode::Char('g'), KeyCode::Char('g')],
label: "g g",
}],
Action::Last,
),
];
assert_eq!(
resolve_shortcut_in(&commands, &[KeyCode::Char('g')]),
ShortcutLookup::Ambiguous(Action::First)
);
}
#[test]
fn resolves_duplicate_exact_sequences_as_ambiguous() {
let commands = [
CommandSpec::implemented(
"first-q",
"first q",
"Test",
&[KeySequence {
codes: &[KeyCode::Char('q')],
label: "q",
}],
Action::Quit,
),
CommandSpec::implemented(
"second-q",
"second q",
"Test",
&[KeySequence {
codes: &[KeyCode::Char('q')],
label: "q",
}],
Action::Refresh,
),
];
assert_eq!(
resolve_shortcut_in(&commands, &[KeyCode::Char('q')]),
ShortcutLookup::Ambiguous(Action::Quit)
);
}
#[test]
fn resolver_reports_missing_and_empty_inputs() {
assert_eq!(resolve_shortcut(&[]), ShortcutLookup::Missing);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('!')]),
ShortcutLookup::Missing
);
}
#[test]
fn resolves_undo_shortcut() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('u')]),
ShortcutLookup::Found(Action::Undo)
);
assert_eq!(lookup_command("undo"), CommandLookup::Found(Action::Undo));
}
#[test]
fn resolves_phase_prefix_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('g')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('g'), KeyCode::Char('g')]),
ShortcutLookup::Found(Action::First)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('a')]),
ShortcutLookup::Found(Action::SetStatus("active"))
);
}
#[test]
fn current_single_key_shortcuts_match_catalog() {
for command in COMMANDS {
for key in command.keys {
if key.codes.len() != 1 {
continue;
}
assert_eq!(
Action::from_normal_key(key.codes[0]),
command.action,
"shortcut {} for :{} resolved incorrectly",
key.label,
command.name
);
}
}
assert_eq!(Action::from_normal_key(KeyCode::Esc), Action::CancelOverlay);
assert_eq!(Action::from_normal_key(KeyCode::Char('u')), Action::Undo);
assert_eq!(Action::from_normal_key(KeyCode::Char('z')), Action::None);
assert_eq!(Action::from_normal_key(KeyCode::Char('g')), Action::None);
assert_eq!(Action::from_normal_key(KeyCode::Char('t')), Action::None);
assert_eq!(Action::from_normal_key(KeyCode::Char('v')), Action::None);
}
#[test]
fn catalog_lifecycle_matches_action_state() {
for command in COMMANDS {
match (command.lifecycle, command.action) {
(CommandLifecycle::Implemented, action) => {
assert!(
implemented_action_is_handled(action),
"implemented :{} is not handled",
command.name
);
}
(
CommandLifecycle::Planned { reason },
Action::Planned {
name,
reason: action_reason,
},
) => {
assert_eq!(name, command.name);
assert_eq!(reason, action_reason);
assert!(
!reason.trim().is_empty(),
":{} planned reason is empty",
command.name
);
}
(
CommandLifecycle::Disabled { reason },
Action::Disabled {
name,
reason: action_reason,
},
) => {
assert_eq!(name, command.name);
assert_eq!(reason, action_reason);
assert!(
!reason.trim().is_empty(),
":{} disabled reason is empty",
command.name
);
}
_ => panic!("lifecycle/action mismatch for :{}", command.name),
}
}
}
#[test]
fn implemented_detail_actions_are_handled() {
for command in CommandContext::Detail.commands() {
assert!(
implemented_action_is_handled(command.action),
"implemented detail command :{} is not handled",
command.name
);
}
}
#[test]
fn command_contexts_reject_duplicate_exact_shortcuts() {
for context in [CommandContext::Normal, CommandContext::Detail] {
let mut seen: Vec<(&[KeyCode], &str, &str)> = Vec::new();
for command in context.commands() {
for key in command.keys {
if let Some((_, other_command, other_label)) =
seen.iter().find(|(codes, _, _)| *codes == key.codes)
{
panic!(
"duplicate shortcut {} for :{} conflicts with {} for :{}",
key.label, command.name, other_label, other_command
);
}
seen.push((key.codes, command.name, key.label));
}
}
}
}
#[test]
fn detail_context_resolves_detail_shortcuts() {
assert_eq!(
resolve_shortcut_for(CommandContext::Detail, &[KeyCode::Char('e')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut_for(CommandContext::Detail, &[KeyCode::Char('s')]),
ShortcutLookup::Found(Action::BeginStatusPicker)
);
assert_eq!(
resolve_shortcut_for(CommandContext::Detail, &[KeyCode::Char('t')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('t'), KeyCode::Char('e')]
),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('e'), KeyCode::Char('t')]
),
ShortcutLookup::Found(Action::BeginEditTitle)
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('t')]
),
ShortcutLookup::Found(Action::BeginEditTitle)
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('e'), KeyCode::Char('j')]
),
ShortcutLookup::Found(Action::BeginEditProject)
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('e'), KeyCode::Char('p')]
),
ShortcutLookup::Found(Action::BeginEditPriority)
);
assert_eq!(
resolve_shortcut_for(
CommandContext::Detail,
&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('l')]
),
ShortcutLookup::Found(Action::BeginEditLabels)
);
}
#[test]
fn command_domains_cover_catalog_sections() {
let mut covered = Vec::new();
for domain in catalog::COMMAND_DOMAINS {
let commands = domain.commands();
assert!(
!commands.is_empty(),
"empty command domain {}",
domain.section
);
assert!(
commands
.iter()
.all(|command| command.section == domain.section),
"domain {} contains another section",
domain.section
);
covered.extend(commands.into_iter().map(|command| command.name));
}
for command in COMMANDS {
assert!(
covered.contains(&command.name),
":{} domain is not registered",
command.name
);
}
}
#[test]
fn resolves_project_shortcuts() {
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('p')]),
ShortcutLookup::Prefix
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('p'), KeyCode::Char('a')]),
ShortcutLookup::Found(Action::BeginAddProject)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('p'), KeyCode::Char('r')]),
ShortcutLookup::Found(Action::BeginRenameProject)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('p'), KeyCode::Char('D')]),
ShortcutLookup::Found(Action::BeginDeleteProject)
));
}
#[test]
fn resolves_authoring_shortcuts() {
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t')]),
ShortcutLookup::Prefix
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('n')]),
ShortcutLookup::Found(Action::BeginAddTask)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('a')]),
ShortcutLookup::Found(Action::BeginAddTask)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('N')]),
ShortcutLookup::Found(Action::BeginAddNote)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('n')]),
ShortcutLookup::Found(Action::BeginAddNote)
));
}
#[test]
fn project_path_commands_remain_planned_with_explicit_reason() {
for name in ["add-project-path", "remove-project-path"] {
let command = COMMANDS
.iter()
.find(|command| command.name == name)
.unwrap_or_else(|| panic!("missing command :{name}"));
assert_eq!(
command.lifecycle,
CommandLifecycle::Planned {
reason: PROJECT_PATH_FLOW_REASON
}
);
}
}
#[test]
fn required_action_families_are_present() {
for name in [
"add-task",
"edit-title",
"status-picker",
"copy-ref",
"copy-id",
"status-active",
"scope-project",
"scope-all",
"order-due",
"order-priority",
"order-reverse",
"conflict-list",
"add-project",
"rename-project",
"delete-project",
"config-show",
"config-status",
"config-paths",
"database-stats",
"config-init",
"add-dependency",
"remove-dependency",
] {
assert!(
COMMANDS.iter().any(|command| command.name == name),
"missing required command :{name}"
);
}
}
#[test]
fn resolves_task_editing_shortcuts() {
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e')]),
ShortcutLookup::Prefix
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e'), KeyCode::Char('t')]),
ShortcutLookup::Found(Action::BeginEditTitle)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::BeginEditDescription)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e'), KeyCode::Char('j')]),
ShortcutLookup::Found(Action::BeginEditProject)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::BeginEditPriority)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('e'), KeyCode::Char('l')]),
ShortcutLookup::Found(Action::BeginEditLabels)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e')]),
ShortcutLookup::Prefix
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('t')]),
ShortcutLookup::Found(Action::BeginEditTitle)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::BeginEditDescription)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('j')]),
ShortcutLookup::Found(Action::BeginEditProject)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::BeginEditPriority)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('e'), KeyCode::Char('l')]),
ShortcutLookup::Found(Action::BeginEditLabels)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('c')]),
ShortcutLookup::Prefix
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('c'), KeyCode::Char('t')]),
ShortcutLookup::Found(Action::ToggleEpicExpanded)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('c'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::DetachEpicChild)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('c'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::PromoteEpicChild)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('y')]),
ShortcutLookup::Found(Action::CopyShortRef)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('Y')]),
ShortcutLookup::Found(Action::CopyDurableRef)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('U')]),
ShortcutLookup::Found(Action::BeginRemoveDependency)
));
assert!(matches!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('B')]),
ShortcutLookup::Found(Action::BeginAddDependency)
));
}
#[test]
fn resolves_exact_priority_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('0')]),
ShortcutLookup::Found(Action::SetPriority("none"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('l')]),
ShortcutLookup::Found(Action::SetPriority("low"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('m')]),
ShortcutLookup::Found(Action::SetPriority("medium"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('h')]),
ShortcutLookup::Found(Action::SetPriority("high"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('u')]),
ShortcutLookup::Found(Action::SetPriority("urgent"))
);
}
#[test]
fn status_shortcuts_resolve_through_task_prefix() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('s')]),
ShortcutLookup::Found(Action::BeginStatusPicker)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('d')]),
ShortcutLookup::Found(Action::SetStatus("done"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('x')]),
ShortcutLookup::Found(Action::SetStatus("canceled"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('i')]),
ShortcutLookup::Found(Action::SetStatus("inbox"))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('t'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::SetStatus("done"))
);
}
#[test]
fn resolves_filter_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('f'), KeyCode::Char('l')]),
ShortcutLookup::Found(Action::BeginFilterLabel)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('f'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::BeginFilterPriority)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('f'), KeyCode::Char('c')]),
ShortcutLookup::Found(Action::ClearFilters)
);
}
#[test]
fn resolves_view_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('v')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('v'), KeyCode::Char('q')]),
ShortcutLookup::Found(Action::ShowView(crate::tui::store::TaskView::Queue))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('v'), KeyCode::Char('e')]),
ShortcutLookup::Found(Action::ShowView(crate::tui::store::TaskView::Epics))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('v'), KeyCode::Char('r')]),
ShortcutLookup::Found(Action::ShowView(crate::tui::store::TaskView::RecentActions))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('v'), KeyCode::Char('s')]),
ShortcutLookup::Found(Action::ShowView(crate::tui::store::TaskView::Search))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('g'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::BeginScopeProject)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('g'), KeyCode::Char('a')]),
ShortcutLookup::Found(Action::ShowWorkspaceScope)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('g'), KeyCode::Char('w')]),
ShortcutLookup::Found(Action::BeginSwitchWorkspace)
);
}
#[test]
fn resolves_conflict_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('l')]),
ShortcutLookup::Found(Action::BeginConflictList)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('s')]),
ShortcutLookup::Found(Action::ShowConflictDetails)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('n')]),
ShortcutLookup::Found(Action::NextConflict)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::PreviousConflict)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('a')]),
ShortcutLookup::Found(Action::AcceptConflictLocal)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('r')]),
ShortcutLookup::Found(Action::AcceptConflictRemote)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('c'), KeyCode::Char('m')]),
ShortcutLookup::Found(Action::BeginManualConflictMerge)
);
}
#[test]
fn resolves_config_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('?')]),
ShortcutLookup::Found(Action::ToggleHelp)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('h')]),
ShortcutLookup::Found(Action::MoveLeft)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C')]),
ShortcutLookup::Prefix
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C'), KeyCode::Char('s')]),
ShortcutLookup::Found(Action::ShowConfigStatus)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C'), KeyCode::Char('c')]),
ShortcutLookup::Found(Action::ShowConfigInfo)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::ShowConfigPaths)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C'), KeyCode::Char('D')]),
ShortcutLookup::Found(Action::ShowDatabaseStats)
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('C'), KeyCode::Char('i')]),
ShortcutLookup::Found(Action::BeginConfigInit)
);
}
#[test]
fn non_executing_lifecycle_shortcuts_resolve_to_catalog_action() {
for command in COMMANDS {
if !matches!(command.lifecycle, CommandLifecycle::Implemented) {
for key in command.keys {
assert_eq!(
resolve_shortcut(key.codes),
ShortcutLookup::Found(command.action),
"shortcut {} for :{} resolved incorrectly",
key.label,
command.name
);
}
}
}
}
#[test]
fn non_executing_lifecycle_commands_resolve_to_catalog_action() {
for command in COMMANDS {
if !matches!(command.lifecycle, CommandLifecycle::Implemented) {
assert_eq!(
lookup_command(command.name),
CommandLookup::Found(command.action)
);
}
}
}
#[test]
fn resolves_order_shortcuts() {
assert_eq!(
resolve_shortcut(&[KeyCode::Char('o'), KeyCode::Char('d')]),
ShortcutLookup::Found(Action::Disabled {
name: "order-due",
reason: DUE_SORT_REASON,
})
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('o'), KeyCode::Char('c')]),
ShortcutLookup::Found(Action::SetOrder(crate::tui::store::TaskOrder::Created))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('o'), KeyCode::Char('p')]),
ShortcutLookup::Found(Action::SetOrder(crate::tui::store::TaskOrder::Priority))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('o'), KeyCode::Char('u')]),
ShortcutLookup::Found(Action::SetOrder(crate::tui::store::TaskOrder::Updated))
);
assert_eq!(
resolve_shortcut(&[KeyCode::Char('o'), KeyCode::Char('r')]),
ShortcutLookup::Found(Action::ReverseSort)
);
}
}