Implement the password-store tree sidebar

This commit is contained in:
Hermes Agent
2026-08-10 06:07:05 +00:00
parent 42e6a82b2d
commit 91f58bae3a
7 changed files with 1097 additions and 114 deletions

View File

@@ -11,6 +11,19 @@ pub enum Action {
Command,
Cancel,
Lock,
Refresh,
Next,
Previous,
PageDown,
PageUp,
First,
Last,
Parent,
Child,
Activate,
Filter,
NextMatch,
PreviousMatch,
FocusNext,
FocusPrevious,
}
@@ -27,7 +40,7 @@ pub struct ActionSpec {
pub action: Action,
pub label: &'static str,
pub command: &'static str,
pub binding: KeyBinding,
pub bindings: &'static [KeyBinding],
modes: &'static [Mode],
}
@@ -51,82 +64,163 @@ const ALL: &[Mode] = &[
Mode::Locked,
];
macro_rules! keys {
($(($code:expr, $modifiers:expr, $display:expr)),+ $(,)?) => {
&[$(KeyBinding { code: $code, modifiers: $modifiers, display: $display }),+]
};
}
pub static ACTIONS: &[ActionSpec] = &[
ActionSpec {
action: Action::Quit,
label: "quit",
command: "quit",
binding: KeyBinding {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::NONE,
display: "q",
},
bindings: keys!((KeyCode::Char('q'), KeyModifiers::NONE, "q")),
modes: BROWSER_LIKE,
},
ActionSpec {
action: Action::Help,
label: "help",
command: "help",
binding: KeyBinding {
code: KeyCode::Char('?'),
modifiers: KeyModifiers::NONE,
display: "?",
},
bindings: keys!((KeyCode::Char('?'), KeyModifiers::NONE, "?")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Command,
label: "command",
command: "command",
binding: KeyBinding {
code: KeyCode::Char(':'),
modifiers: KeyModifiers::NONE,
display: ":",
},
bindings: keys!((KeyCode::Char(':'), KeyModifiers::NONE, ":")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Cancel,
label: "back",
command: "cancel",
binding: KeyBinding {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE,
display: "Esc",
},
bindings: keys!((KeyCode::Esc, KeyModifiers::NONE, "Esc")),
modes: OVERLAYS,
},
ActionSpec {
action: Action::Lock,
label: "lock",
command: "lock",
binding: KeyBinding {
code: KeyCode::Char('l'),
modifiers: KeyModifiers::CONTROL,
display: "C-l",
},
bindings: keys!((KeyCode::Char('l'), KeyModifiers::CONTROL, "C-l")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::Refresh,
label: "refresh",
command: "refresh",
bindings: keys!((KeyCode::Char('r'), KeyModifiers::NONE, "r")),
modes: BROWSER_LIKE,
},
ActionSpec {
action: Action::Next,
label: "next",
command: "next",
bindings: keys!(
(KeyCode::Char('j'), KeyModifiers::NONE, "j"),
(KeyCode::Down, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Previous,
label: "previous",
command: "previous",
bindings: keys!(
(KeyCode::Char('k'), KeyModifiers::NONE, "k"),
(KeyCode::Up, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PageDown,
label: "page down",
command: "page-down",
bindings: keys!((KeyCode::PageDown, KeyModifiers::NONE, "PgDn")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PageUp,
label: "page up",
command: "page-up",
bindings: keys!((KeyCode::PageUp, KeyModifiers::NONE, "PgUp")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::First,
label: "first",
command: "first",
bindings: keys!((KeyCode::Home, KeyModifiers::NONE, "Home")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Last,
label: "last",
command: "last",
bindings: keys!((KeyCode::End, KeyModifiers::NONE, "End")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Parent,
label: "parent",
command: "parent",
bindings: keys!(
(KeyCode::Char('h'), KeyModifiers::NONE, "h"),
(KeyCode::Left, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Child,
label: "child",
command: "child",
bindings: keys!(
(KeyCode::Char('l'), KeyModifiers::NONE, "l"),
(KeyCode::Right, KeyModifiers::NONE, ""),
),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Activate,
label: "open/toggle",
command: "open",
bindings: keys!((KeyCode::Enter, KeyModifiers::NONE, "Enter")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::Filter,
label: "filter",
command: "filter",
bindings: keys!((KeyCode::Char('/'), KeyModifiers::NONE, "/")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::NextMatch,
label: "next match",
command: "next-match",
bindings: keys!((KeyCode::Char('n'), KeyModifiers::NONE, "n")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::PreviousMatch,
label: "previous match",
command: "previous-match",
bindings: keys!((KeyCode::Char('N'), KeyModifiers::SHIFT, "N")),
modes: &[Mode::Browser],
},
ActionSpec {
action: Action::FocusNext,
label: "next pane",
command: "focus-next",
binding: KeyBinding {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
display: "Tab",
},
bindings: keys!((KeyCode::Tab, KeyModifiers::NONE, "Tab")),
modes: UNLOCKED,
},
ActionSpec {
action: Action::FocusPrevious,
label: "previous pane",
command: "focus-previous",
binding: KeyBinding {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
display: "S-Tab",
},
bindings: keys!((KeyCode::BackTab, KeyModifiers::SHIFT, "S-Tab")),
modes: UNLOCKED,
},
];
@@ -136,8 +230,10 @@ pub fn resolve_key(mode: Mode, code: KeyCode, modifiers: KeyModifiers) -> Option
.iter()
.find(|spec| {
spec.is_available(mode)
&& spec.binding.code == code
&& spec.binding.modifiers == modifiers
&& spec
.bindings
.iter()
.any(|binding| binding.code == code && binding.modifiers == modifiers)
})
.map(|spec| spec.action)
}
@@ -160,7 +256,11 @@ mod tests {
assert_ne!(action.command, other.command);
for mode in ALL {
if action.is_available(*mode) && other.is_available(*mode) {
assert_ne!(action.binding, other.binding);
for binding in action.bindings {
for other_binding in other.bindings {
assert_ne!(binding, other_binding);
}
}
}
}
}
@@ -171,10 +271,12 @@ mod tests {
fn resolution_and_help_consume_the_same_registry() {
for mode in ALL {
for spec in available_actions(*mode) {
assert_eq!(
resolve_key(*mode, spec.binding.code, spec.binding.modifiers),
Some(spec.action)
);
for binding in spec.bindings {
assert_eq!(
resolve_key(*mode, binding.code, binding.modifiers),
Some(spec.action)
);
}
}
}
assert_eq!(