Implement the desktop command palette
This commit is contained in:
@@ -19,6 +19,7 @@ pub enum UiAction {
|
||||
CopyEditedField,
|
||||
Paste,
|
||||
Find,
|
||||
CommandPalette,
|
||||
TogglePaneFocus,
|
||||
Refresh,
|
||||
ReloadEntry,
|
||||
@@ -47,6 +48,7 @@ impl UiAction {
|
||||
Self::CopyEditedField => "copy-edited-field",
|
||||
Self::Paste => "paste",
|
||||
Self::Find => "find",
|
||||
Self::CommandPalette => "command-palette",
|
||||
Self::TogglePaneFocus => "toggle-pane-focus",
|
||||
Self::Refresh => "refresh",
|
||||
Self::ReloadEntry => "reload-entry",
|
||||
@@ -107,6 +109,7 @@ pub struct ActionContext {
|
||||
pub dirty: bool,
|
||||
pub saving: bool,
|
||||
pub switching_vault: bool,
|
||||
pub modal_open: bool,
|
||||
pub focused_field: bool,
|
||||
pub focused_sensitive: bool,
|
||||
pub entry_path: bool,
|
||||
@@ -156,6 +159,12 @@ pub const ACTIONS: &[ActionSpec] = &[
|
||||
),
|
||||
spec(UiAction::Paste, MenuGroup::Edit, "Paste", Some("⌘V")),
|
||||
spec(UiAction::Find, MenuGroup::Edit, "Find", Some("⌘F")),
|
||||
spec(
|
||||
UiAction::CommandPalette,
|
||||
MenuGroup::View,
|
||||
"Command Palette…",
|
||||
Some("⌘K"),
|
||||
),
|
||||
spec(
|
||||
UiAction::TogglePaneFocus,
|
||||
MenuGroup::View,
|
||||
@@ -228,6 +237,7 @@ pub fn shortcut_label(action: UiAction) -> Option<String> {
|
||||
pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
||||
match action {
|
||||
UiAction::About | UiAction::Settings | UiAction::Help => true,
|
||||
UiAction::CommandPalette => !context.modal_open,
|
||||
// Entry creation is not valid until the dedicated workflow exists.
|
||||
UiAction::NewEntry => false,
|
||||
UiAction::OpenFolder => {
|
||||
@@ -282,6 +292,95 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'static str> {
|
||||
if enabled(action, context) {
|
||||
return None;
|
||||
}
|
||||
Some(match action {
|
||||
UiAction::NewEntry => "Entry creation is not available yet",
|
||||
UiAction::OpenFolder if !context.storage_ready => "Shared configuration is unavailable",
|
||||
UiAction::OpenFolder if context.saving => "Wait for the active save",
|
||||
UiAction::OpenFolder => "Wait for vault validation",
|
||||
UiAction::OpenEntry if !context.storage_ready => "Shared configuration is unavailable",
|
||||
UiAction::OpenEntry if !context.entry_path => "Enter or select an entry path",
|
||||
UiAction::OpenEntry if context.saving => "Wait for the active save",
|
||||
UiAction::OpenEntry => "Wait for vault validation",
|
||||
UiAction::Save if !context.unlocked => "Unlock the current entry first",
|
||||
UiAction::Save if !context.editing => "Open an entry editor first",
|
||||
UiAction::Save if !context.dirty => "The editor has no changes",
|
||||
UiAction::Save if context.saving => "A save is already running",
|
||||
UiAction::Save => "Wait for vault validation",
|
||||
UiAction::CloseWindow | UiAction::Quit => "Wait for vault validation",
|
||||
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste | UiAction::Find => {
|
||||
"Use the focused native text field"
|
||||
}
|
||||
UiAction::CopyField | UiAction::CopyEditedField if !context.unlocked => {
|
||||
"Unlock an entry first"
|
||||
}
|
||||
UiAction::CopyField | UiAction::CopyEditedField if !context.document_open => {
|
||||
"Open an entry first"
|
||||
}
|
||||
UiAction::CopyField | UiAction::CopyEditedField if !context.focused_field => {
|
||||
"Select a field first"
|
||||
}
|
||||
UiAction::CopyField | UiAction::CopyEditedField if context.switching_vault => {
|
||||
"Wait for vault validation"
|
||||
}
|
||||
UiAction::CopyField => "Use Copy Edited Field while editing",
|
||||
UiAction::CopyEditedField => "Open the entry editor first",
|
||||
UiAction::Refresh if !context.storage_ready => "Shared configuration is unavailable",
|
||||
UiAction::Refresh if context.tree_loading => "A refresh is already running",
|
||||
UiAction::Refresh => "Wait for vault validation",
|
||||
UiAction::ReloadEntry if !context.unlocked => "Unlock an entry first",
|
||||
UiAction::ReloadEntry if !context.document_open => "Open an entry first",
|
||||
UiAction::ReloadEntry if context.saving => "Wait for the active save",
|
||||
UiAction::ReloadEntry => "Wait for vault validation",
|
||||
UiAction::EditEntry if !context.unlocked => "Unlock an entry first",
|
||||
UiAction::EditEntry if !context.document_open => "Open an entry first",
|
||||
UiAction::EditEntry if context.editing => "The entry editor is already open",
|
||||
UiAction::EditEntry => "Wait for vault validation",
|
||||
UiAction::ToggleReveal if !context.unlocked => "Unlock an entry first",
|
||||
UiAction::ToggleReveal if !context.document_open => "Open an entry first",
|
||||
UiAction::ToggleReveal if !context.focused_sensitive => "Select a sensitive field first",
|
||||
UiAction::ToggleReveal => "Wait for vault validation",
|
||||
UiAction::Lock => "The password store is already locked",
|
||||
UiAction::CommandPalette => "Finish the current confirmation first",
|
||||
UiAction::About
|
||||
| UiAction::Settings
|
||||
| UiAction::TogglePaneFocus
|
||||
| UiAction::Minimize
|
||||
| UiAction::Help => "Action is unavailable",
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn aliases(action: UiAction) -> &'static [&'static str] {
|
||||
match action {
|
||||
UiAction::About => &["version", "license", "credits"],
|
||||
UiAction::Settings => &["preferences", "configuration", "config"],
|
||||
UiAction::NewEntry => &["insert", "add password", "create entry"],
|
||||
UiAction::OpenFolder => &["open vault", "open store", "choose folder"],
|
||||
UiAction::OpenEntry => &["show entry", "view password"],
|
||||
UiAction::Save => &["write", "save entry"],
|
||||
UiAction::CloseWindow => &["close"],
|
||||
UiAction::Quit => &["exit"],
|
||||
UiAction::Undo => &["revert edit"],
|
||||
UiAction::Redo => &["repeat edit"],
|
||||
UiAction::Cut => &["remove selection"],
|
||||
UiAction::CopyField | UiAction::CopyEditedField => &["copy value", "clipboard"],
|
||||
UiAction::Paste => &["insert clipboard"],
|
||||
UiAction::Find => &["search text"],
|
||||
UiAction::CommandPalette => &["commands", "actions", "search commands"],
|
||||
UiAction::TogglePaneFocus => &["next pane", "switch pane", "focus"],
|
||||
UiAction::Refresh => &["reload vault", "refresh tree"],
|
||||
UiAction::ReloadEntry => &["revert entry", "refresh entry"],
|
||||
UiAction::EditEntry => &["modify entry"],
|
||||
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
|
||||
UiAction::Lock => &["secure", "log out", "relock"],
|
||||
UiAction::Minimize => &["hide window"],
|
||||
UiAction::Help => &["shortcuts", "documentation"],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<UiAction> {
|
||||
if key.as_ref() == keyboard::Key::Named(Named::Tab) && modifiers.is_empty() {
|
||||
return Some(UiAction::TogglePaneFocus);
|
||||
@@ -305,6 +404,7 @@ pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> O
|
||||
keyboard::Key::Character("c" | "C") => Some(UiAction::CopyField),
|
||||
keyboard::Key::Character("v" | "V") => Some(UiAction::Paste),
|
||||
keyboard::Key::Character("f" | "F") => Some(UiAction::Find),
|
||||
keyboard::Key::Character("k" | "K") => Some(UiAction::CommandPalette),
|
||||
keyboard::Key::Character("r" | "R") => Some(UiAction::Refresh),
|
||||
keyboard::Key::Character("l" | "L") => Some(UiAction::Lock),
|
||||
keyboard::Key::Character("m" | "M") => Some(UiAction::Minimize),
|
||||
@@ -328,6 +428,7 @@ mod tests {
|
||||
dirty: true,
|
||||
saving: false,
|
||||
switching_vault: false,
|
||||
modal_open: false,
|
||||
focused_field: true,
|
||||
focused_sensitive: true,
|
||||
entry_path: true,
|
||||
@@ -417,6 +518,32 @@ mod tests {
|
||||
] {
|
||||
assert!(!enabled(action, switching), "{action:?}");
|
||||
}
|
||||
for spec in ACTIONS {
|
||||
if !enabled(spec.action, ready) {
|
||||
assert!(
|
||||
disabled_reason(spec.action, ready).is_some(),
|
||||
"{:?}",
|
||||
spec.action
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(!enabled(
|
||||
UiAction::CommandPalette,
|
||||
ActionContext {
|
||||
modal_open: true,
|
||||
..ready
|
||||
}
|
||||
));
|
||||
assert_eq!(
|
||||
disabled_reason(
|
||||
UiAction::CommandPalette,
|
||||
ActionContext {
|
||||
modal_open: true,
|
||||
..ready
|
||||
}
|
||||
),
|
||||
Some("Finish the current confirmation first")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -426,6 +553,7 @@ mod tests {
|
||||
("o", UiAction::OpenFolder),
|
||||
("s", UiAction::Save),
|
||||
("f", UiAction::Find),
|
||||
("k", UiAction::CommandPalette),
|
||||
("n", UiAction::NewEntry),
|
||||
("w", UiAction::CloseWindow),
|
||||
("q", UiAction::Quit),
|
||||
|
||||
Reference in New Issue
Block a user