Add desktop search and entry mutation workflows

This commit is contained in:
2026-08-10 19:19:46 +02:00
parent bb650c2b5c
commit de5dea28ef
8 changed files with 1292 additions and 30 deletions

View File

@@ -21,12 +21,16 @@ pub enum UiAction {
CopyEditedField,
Paste,
Find,
SearchContents,
CommandPalette,
TogglePaneFocus,
Refresh,
ReloadEntry,
EditEntry,
GeneratePassword,
MoveEntry,
CopyEntry,
DeleteEntry,
ToggleReveal,
Lock,
Minimize,
@@ -53,12 +57,16 @@ impl UiAction {
Self::CopyEditedField => "copy-edited-field",
Self::Paste => "paste",
Self::Find => "find",
Self::SearchContents => "search-contents",
Self::CommandPalette => "command-palette",
Self::TogglePaneFocus => "toggle-pane-focus",
Self::Refresh => "refresh",
Self::ReloadEntry => "reload-entry",
Self::EditEntry => "edit-entry",
Self::GeneratePassword => "generate-password",
Self::MoveEntry => "move-entry",
Self::CopyEntry => "copy-entry",
Self::DeleteEntry => "delete-entry",
Self::ToggleReveal => "toggle-reveal",
Self::Lock => "lock",
Self::Minimize => "minimize",
@@ -120,6 +128,7 @@ pub struct ActionContext {
pub focused_sensitive: bool,
pub focused_generatable: bool,
pub entry_path: bool,
pub selected_object: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -173,6 +182,12 @@ pub const ACTIONS: &[ActionSpec] = &[
),
spec(UiAction::Paste, MenuGroup::Edit, "Paste", Some("⌘V")),
spec(UiAction::Find, MenuGroup::Edit, "Find", Some("⌘F")),
spec(
UiAction::SearchContents,
MenuGroup::Edit,
"Search Decrypted Contents…",
Some("⇧⌘F"),
),
spec(
UiAction::CommandPalette,
MenuGroup::View,
@@ -199,6 +214,14 @@ pub const ACTIONS: &[ActionSpec] = &[
"Generate Password…",
None,
),
spec(
UiAction::MoveEntry,
MenuGroup::Entry,
"Move or Rename…",
None,
),
spec(UiAction::CopyEntry, MenuGroup::Entry, "Copy Entry…", None),
spec(UiAction::DeleteEntry, MenuGroup::Entry, "Delete…", None),
spec(
UiAction::ToggleReveal,
MenuGroup::Entry,
@@ -289,7 +312,13 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
}
UiAction::CloseWindow | UiAction::Quit => !context.switching_vault,
UiAction::Minimize => true,
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste | UiAction::Find => false,
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste => false,
UiAction::Find | UiAction::SearchContents => {
context.storage_ready
&& !context.saving
&& !context.switching_vault
&& !context.modal_open
}
UiAction::CopyField => {
context.unlocked
&& context.document_open
@@ -320,6 +349,13 @@ pub fn enabled(action: UiAction, context: ActionContext) -> bool {
&& !context.switching_vault
&& !context.modal_open
}
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry => {
context.storage_ready
&& context.selected_object
&& !context.saving
&& !context.switching_vault
&& !context.modal_open
}
UiAction::ToggleReveal => {
context.unlocked
&& context.document_open
@@ -367,9 +403,17 @@ pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'sta
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 => {
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste => {
"Use the focused native text field"
}
UiAction::Find | UiAction::SearchContents if !context.storage_ready => {
"Shared configuration is unavailable"
}
UiAction::Find | UiAction::SearchContents if context.saving => "Wait for the active save",
UiAction::Find | UiAction::SearchContents if context.switching_vault => {
"Wait for vault validation"
}
UiAction::Find | UiAction::SearchContents => "Close the current screen first",
UiAction::CopyField | UiAction::CopyEditedField if !context.unlocked => {
"Unlock an entry first"
}
@@ -402,6 +446,27 @@ pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'sta
}
UiAction::GeneratePassword if context.switching_vault => "Wait for vault validation",
UiAction::GeneratePassword => "Close the current screen first",
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
if !context.storage_ready =>
{
"Shared configuration is unavailable"
}
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
if !context.selected_object =>
{
"Select an entry or folder first"
}
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry if context.saving => {
"Wait for the active save"
}
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry
if context.switching_vault =>
{
"Wait for vault validation"
}
UiAction::MoveEntry | UiAction::CopyEntry | UiAction::DeleteEntry => {
"Close the current screen first"
}
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",
@@ -436,12 +501,16 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
UiAction::CopyField | UiAction::CopyEditedField => &["copy value", "clipboard"],
UiAction::Paste => &["insert clipboard"],
UiAction::Find => &["search text"],
UiAction::SearchContents => &["grep", "decrypted search", "search passwords"],
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::GeneratePassword => &["random password", "replace password", "generate"],
UiAction::MoveEntry => &["rename", "mv", "move folder"],
UiAction::CopyEntry => &["duplicate entry", "copy folder", "pass cp"],
UiAction::DeleteEntry => &["remove", "rm", "delete folder"],
UiAction::ToggleReveal => &["show password", "hide password", "reveal field"],
UiAction::Lock => &["secure", "log out", "relock"],
UiAction::Minimize => &["hide window"],
@@ -471,6 +540,7 @@ pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> O
keyboard::Key::Character("x" | "X") => Some(UiAction::Cut),
keyboard::Key::Character("c" | "C") => Some(UiAction::CopyField),
keyboard::Key::Character("v" | "V") => Some(UiAction::Paste),
keyboard::Key::Character("f" | "F") if modifiers.shift() => Some(UiAction::SearchContents),
keyboard::Key::Character("f" | "F") => Some(UiAction::Find),
keyboard::Key::Character("k" | "K") => Some(UiAction::CommandPalette),
keyboard::Key::Character("r" | "R") => Some(UiAction::Refresh),
@@ -501,6 +571,7 @@ mod tests {
focused_sensitive: true,
focused_generatable: true,
entry_path: true,
selected_object: true,
}
}
@@ -554,6 +625,18 @@ mod tests {
] {
assert!(!enabled(action, locked));
}
for action in [
UiAction::Find,
UiAction::SearchContents,
UiAction::MoveEntry,
UiAction::CopyEntry,
UiAction::DeleteEntry,
] {
assert!(
enabled(action, locked),
"{action:?} can initiate authentication"
);
}
assert!(!enabled(
UiAction::Refresh,
ActionContext {
@@ -591,6 +674,11 @@ mod tests {
UiAction::EditEntry,
UiAction::GeneratePassword,
UiAction::ToggleReveal,
UiAction::Find,
UiAction::SearchContents,
UiAction::MoveEntry,
UiAction::CopyEntry,
UiAction::DeleteEntry,
] {
assert!(!enabled(action, switching), "{action:?}");
}
@@ -662,6 +750,13 @@ mod tests {
Some(expected)
);
}
assert_eq!(
shortcut_action(
&keyboard::Key::Character("f".into()),
primary | keyboard::Modifiers::SHIFT,
),
Some(UiAction::SearchContents)
);
assert_eq!(
shortcut_action(&keyboard::Key::Named(Named::F1), keyboard::Modifiers::NONE),
Some(UiAction::Help)