672 lines
23 KiB
Rust
672 lines
23 KiB
Rust
//! One desktop action registry shared by every presentation entry point.
|
|
|
|
use iced::keyboard::{self, key::Named};
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
pub enum UiAction {
|
|
About,
|
|
Settings,
|
|
InitializeStore,
|
|
NewFolder,
|
|
NewEntry,
|
|
OpenFolder,
|
|
OpenEntry,
|
|
Save,
|
|
CloseWindow,
|
|
Quit,
|
|
Undo,
|
|
Redo,
|
|
Cut,
|
|
CopyField,
|
|
CopyEditedField,
|
|
Paste,
|
|
Find,
|
|
CommandPalette,
|
|
TogglePaneFocus,
|
|
Refresh,
|
|
ReloadEntry,
|
|
EditEntry,
|
|
GeneratePassword,
|
|
ToggleReveal,
|
|
Lock,
|
|
Minimize,
|
|
Help,
|
|
}
|
|
|
|
impl UiAction {
|
|
pub const fn id(self) -> &'static str {
|
|
match self {
|
|
Self::About => "about",
|
|
Self::Settings => "settings",
|
|
Self::InitializeStore => "initialize-store",
|
|
Self::NewFolder => "new-folder",
|
|
Self::NewEntry => "new-entry",
|
|
Self::OpenFolder => "open-folder",
|
|
Self::OpenEntry => "open-entry",
|
|
Self::Save => "save",
|
|
Self::CloseWindow => "close-window",
|
|
Self::Quit => "quit",
|
|
Self::Undo => "undo",
|
|
Self::Redo => "redo",
|
|
Self::Cut => "cut",
|
|
Self::CopyField => "copy-field",
|
|
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",
|
|
Self::EditEntry => "edit-entry",
|
|
Self::GeneratePassword => "generate-password",
|
|
Self::ToggleReveal => "toggle-reveal",
|
|
Self::Lock => "lock",
|
|
Self::Minimize => "minimize",
|
|
Self::Help => "help",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
pub enum MenuGroup {
|
|
App,
|
|
File,
|
|
Edit,
|
|
View,
|
|
Entry,
|
|
Tools,
|
|
Window,
|
|
Help,
|
|
}
|
|
|
|
impl MenuGroup {
|
|
pub const ALL: [Self; 8] = [
|
|
Self::App,
|
|
Self::File,
|
|
Self::Edit,
|
|
Self::View,
|
|
Self::Entry,
|
|
Self::Tools,
|
|
Self::Window,
|
|
Self::Help,
|
|
];
|
|
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::App => "IronStorage",
|
|
Self::File => "File",
|
|
Self::Edit => "Edit",
|
|
Self::View => "View",
|
|
Self::Entry => "Entry",
|
|
Self::Tools => "Tools",
|
|
Self::Window => "Window",
|
|
Self::Help => "Help",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ActionContext {
|
|
pub storage_ready: bool,
|
|
pub tree_loading: bool,
|
|
pub unlocked: bool,
|
|
pub document_open: bool,
|
|
pub editing: bool,
|
|
pub dirty: bool,
|
|
pub saving: bool,
|
|
pub switching_vault: bool,
|
|
pub modal_open: bool,
|
|
pub focused_field: bool,
|
|
pub focused_sensitive: bool,
|
|
pub focused_generatable: bool,
|
|
pub entry_path: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub struct ActionSpec {
|
|
pub action: UiAction,
|
|
pub group: MenuGroup,
|
|
pub label: &'static str,
|
|
pub shortcut: Option<&'static str>,
|
|
}
|
|
|
|
pub const ACTIONS: &[ActionSpec] = &[
|
|
spec(UiAction::About, MenuGroup::App, "About IronStorage", None),
|
|
spec(UiAction::Settings, MenuGroup::App, "Settings…", Some("⌘,")),
|
|
spec(
|
|
UiAction::InitializeStore,
|
|
MenuGroup::File,
|
|
"Initialize Store…",
|
|
None,
|
|
),
|
|
spec(UiAction::NewFolder, MenuGroup::File, "New Folder…", None),
|
|
spec(
|
|
UiAction::Quit,
|
|
MenuGroup::App,
|
|
"Quit IronStorage",
|
|
Some("⌘Q"),
|
|
),
|
|
spec(UiAction::NewEntry, MenuGroup::File, "New Entry", Some("⌘N")),
|
|
spec(
|
|
UiAction::OpenFolder,
|
|
MenuGroup::File,
|
|
"Open Folder…",
|
|
Some("⌘O"),
|
|
),
|
|
spec(UiAction::OpenEntry, MenuGroup::Entry, "Open Entry", None),
|
|
spec(UiAction::Save, MenuGroup::File, "Save", Some("⌘S")),
|
|
spec(
|
|
UiAction::CloseWindow,
|
|
MenuGroup::File,
|
|
"Close Window",
|
|
Some("⌘W"),
|
|
),
|
|
spec(UiAction::Undo, MenuGroup::Edit, "Undo", Some("⌘Z")),
|
|
spec(UiAction::Redo, MenuGroup::Edit, "Redo", Some("⇧⌘Z")),
|
|
spec(UiAction::Cut, MenuGroup::Edit, "Cut", Some("⌘X")),
|
|
spec(UiAction::CopyField, MenuGroup::Entry, "Copy Field", None),
|
|
spec(
|
|
UiAction::CopyEditedField,
|
|
MenuGroup::Entry,
|
|
"Copy Edited Field",
|
|
None,
|
|
),
|
|
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,
|
|
"Next Pane",
|
|
Some("Tab"),
|
|
),
|
|
spec(UiAction::Refresh, MenuGroup::View, "Refresh", Some("⌘R")),
|
|
spec(
|
|
UiAction::ReloadEntry,
|
|
MenuGroup::Entry,
|
|
"Reload Entry",
|
|
None,
|
|
),
|
|
spec(UiAction::EditEntry, MenuGroup::Entry, "Edit Entry", None),
|
|
spec(
|
|
UiAction::GeneratePassword,
|
|
MenuGroup::Entry,
|
|
"Generate Password…",
|
|
None,
|
|
),
|
|
spec(
|
|
UiAction::ToggleReveal,
|
|
MenuGroup::Entry,
|
|
"Reveal or Hide Field",
|
|
None,
|
|
),
|
|
spec(UiAction::Lock, MenuGroup::Tools, "Lock", Some("⌘L")),
|
|
spec(
|
|
UiAction::Minimize,
|
|
MenuGroup::Window,
|
|
"Minimize",
|
|
Some("⌘M"),
|
|
),
|
|
spec(
|
|
UiAction::Help,
|
|
MenuGroup::Help,
|
|
"IronStorage Help",
|
|
Some("F1"),
|
|
),
|
|
];
|
|
|
|
const fn spec(
|
|
action: UiAction,
|
|
group: MenuGroup,
|
|
label: &'static str,
|
|
shortcut: Option<&'static str>,
|
|
) -> ActionSpec {
|
|
ActionSpec {
|
|
action,
|
|
group,
|
|
label,
|
|
shortcut,
|
|
}
|
|
}
|
|
|
|
pub fn spec_for(action: UiAction) -> &'static ActionSpec {
|
|
ACTIONS
|
|
.iter()
|
|
.find(|spec| spec.action == action)
|
|
.expect("every UI action is registered")
|
|
}
|
|
|
|
pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec> {
|
|
ACTIONS.iter().filter(move |spec| spec.group == group)
|
|
}
|
|
|
|
pub fn shortcut_label(action: UiAction) -> Option<String> {
|
|
let shortcut = spec_for(action).shortcut?;
|
|
if cfg!(target_os = "macos") {
|
|
Some(shortcut.to_owned())
|
|
} else {
|
|
Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
|
|
}
|
|
}
|
|
|
|
pub fn enabled(action: UiAction, context: ActionContext) -> bool {
|
|
match action {
|
|
UiAction::About | UiAction::Help => !context.modal_open,
|
|
UiAction::Settings => {
|
|
context.storage_ready
|
|
&& !context.saving
|
|
&& !context.switching_vault
|
|
&& !context.modal_open
|
|
}
|
|
UiAction::CommandPalette => !context.modal_open,
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
|
|
context.storage_ready
|
|
&& !context.dirty
|
|
&& !context.saving
|
|
&& !context.switching_vault
|
|
&& !context.modal_open
|
|
}
|
|
UiAction::OpenFolder => {
|
|
context.storage_ready && !context.saving && !context.switching_vault
|
|
}
|
|
UiAction::OpenEntry => {
|
|
context.storage_ready
|
|
&& context.entry_path
|
|
&& !context.saving
|
|
&& !context.switching_vault
|
|
}
|
|
UiAction::Save => {
|
|
context.unlocked
|
|
&& context.editing
|
|
&& context.dirty
|
|
&& !context.saving
|
|
&& !context.switching_vault
|
|
}
|
|
UiAction::CloseWindow | UiAction::Quit => !context.switching_vault,
|
|
UiAction::Minimize => true,
|
|
UiAction::Undo | UiAction::Redo | UiAction::Cut | UiAction::Paste | UiAction::Find => false,
|
|
UiAction::CopyField => {
|
|
context.unlocked
|
|
&& context.document_open
|
|
&& !context.editing
|
|
&& !context.switching_vault
|
|
&& context.focused_field
|
|
}
|
|
UiAction::CopyEditedField => {
|
|
context.unlocked && context.editing && !context.switching_vault && context.focused_field
|
|
}
|
|
UiAction::TogglePaneFocus => !context.modal_open,
|
|
UiAction::Refresh => {
|
|
context.storage_ready && !context.tree_loading && !context.switching_vault
|
|
}
|
|
UiAction::ReloadEntry => {
|
|
context.unlocked && context.document_open && !context.saving && !context.switching_vault
|
|
}
|
|
UiAction::EditEntry => {
|
|
context.unlocked
|
|
&& context.document_open
|
|
&& !context.editing
|
|
&& !context.switching_vault
|
|
}
|
|
UiAction::GeneratePassword => {
|
|
context.unlocked
|
|
&& context.editing
|
|
&& context.focused_generatable
|
|
&& !context.switching_vault
|
|
&& !context.modal_open
|
|
}
|
|
UiAction::ToggleReveal => {
|
|
context.unlocked
|
|
&& context.document_open
|
|
&& !context.switching_vault
|
|
&& context.focused_sensitive
|
|
}
|
|
UiAction::Lock => context.unlocked,
|
|
}
|
|
}
|
|
|
|
pub fn disabled_reason(action: UiAction, context: ActionContext) -> Option<&'static str> {
|
|
if enabled(action, context) {
|
|
return None;
|
|
}
|
|
Some(match action {
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
|
|
if !context.storage_ready =>
|
|
{
|
|
"Shared configuration is unavailable"
|
|
}
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.dirty => {
|
|
"Save or discard the current draft first"
|
|
}
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry if context.saving => {
|
|
"Wait for the active save"
|
|
}
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry
|
|
if context.switching_vault =>
|
|
{
|
|
"Wait for vault validation"
|
|
}
|
|
UiAction::InitializeStore | UiAction::NewFolder | UiAction::NewEntry => {
|
|
"Close the current screen first"
|
|
}
|
|
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::GeneratePassword if !context.unlocked => "Unlock an entry first",
|
|
UiAction::GeneratePassword if !context.editing => "Open the entry editor first",
|
|
UiAction::GeneratePassword if !context.focused_generatable => {
|
|
"Select a password or other generatable secret field first"
|
|
}
|
|
UiAction::GeneratePassword if context.switching_vault => "Wait for vault validation",
|
|
UiAction::GeneratePassword => "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",
|
|
UiAction::ToggleReveal => "Wait for vault validation",
|
|
UiAction::Lock => "The password store is already locked",
|
|
UiAction::CommandPalette => "Finish the current confirmation first",
|
|
UiAction::Settings if !context.storage_ready => "Shared configuration is unavailable",
|
|
UiAction::Settings if context.saving => "Wait for the active save",
|
|
UiAction::Settings if context.switching_vault => "Wait for vault validation",
|
|
UiAction::About | UiAction::Settings | UiAction::TogglePaneFocus | UiAction::Help => {
|
|
"Close the current screen first"
|
|
}
|
|
UiAction::Minimize => "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::InitializeStore => &["pass init", "recipients", "gpg id"],
|
|
UiAction::NewFolder => &["create folder", "directory", "nested recipients"],
|
|
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::GeneratePassword => &["random password", "replace password", "generate"],
|
|
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);
|
|
}
|
|
if key.as_ref() == keyboard::Key::Named(Named::F1) {
|
|
return Some(UiAction::Help);
|
|
}
|
|
if !modifiers.command() {
|
|
return None;
|
|
}
|
|
match key.as_ref() {
|
|
keyboard::Key::Character(",") => Some(UiAction::Settings),
|
|
keyboard::Key::Character("n" | "N") => Some(UiAction::NewEntry),
|
|
keyboard::Key::Character("o" | "O") => Some(UiAction::OpenFolder),
|
|
keyboard::Key::Character("s" | "S") => Some(UiAction::Save),
|
|
keyboard::Key::Character("w" | "W") => Some(UiAction::CloseWindow),
|
|
keyboard::Key::Character("q" | "Q") => Some(UiAction::Quit),
|
|
keyboard::Key::Character("z" | "Z") if modifiers.shift() => Some(UiAction::Redo),
|
|
keyboard::Key::Character("z" | "Z") => Some(UiAction::Undo),
|
|
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") => 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),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::BTreeSet;
|
|
|
|
use super::*;
|
|
|
|
fn context() -> ActionContext {
|
|
ActionContext {
|
|
storage_ready: true,
|
|
tree_loading: false,
|
|
unlocked: true,
|
|
document_open: true,
|
|
editing: true,
|
|
dirty: true,
|
|
saving: false,
|
|
switching_vault: false,
|
|
modal_open: false,
|
|
focused_field: true,
|
|
focused_sensitive: true,
|
|
focused_generatable: true,
|
|
entry_path: true,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn registry_is_complete_unique_and_grouped_for_every_platform_menu() {
|
|
let actions = ACTIONS
|
|
.iter()
|
|
.map(|spec| spec.action)
|
|
.collect::<BTreeSet<_>>();
|
|
assert_eq!(actions.len(), ACTIONS.len());
|
|
for group in MenuGroup::ALL {
|
|
assert!(actions_in(group).next().is_some(), "missing {group:?} menu");
|
|
}
|
|
for action in actions {
|
|
assert_eq!(spec_for(action).action, action);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn enablement_closes_authentication_dirty_and_loading_bypasses() {
|
|
let ready = context();
|
|
assert!(!enabled(UiAction::NewEntry, ready));
|
|
assert!(enabled(UiAction::Save, ready));
|
|
assert!(enabled(UiAction::CopyEditedField, ready));
|
|
assert!(enabled(UiAction::GeneratePassword, ready));
|
|
assert!(!enabled(UiAction::CopyField, ready));
|
|
assert!(enabled(UiAction::ToggleReveal, ready));
|
|
|
|
let viewing = ActionContext {
|
|
editing: false,
|
|
dirty: false,
|
|
..ready
|
|
};
|
|
assert!(enabled(UiAction::CopyField, viewing));
|
|
assert!(!enabled(UiAction::CopyEditedField, viewing));
|
|
assert!(enabled(UiAction::InitializeStore, viewing));
|
|
assert!(enabled(UiAction::NewFolder, viewing));
|
|
assert!(enabled(UiAction::NewEntry, viewing));
|
|
|
|
let locked = ActionContext {
|
|
unlocked: false,
|
|
..ready
|
|
};
|
|
for action in [
|
|
UiAction::Save,
|
|
UiAction::CopyField,
|
|
UiAction::CopyEditedField,
|
|
UiAction::ToggleReveal,
|
|
UiAction::GeneratePassword,
|
|
UiAction::Lock,
|
|
] {
|
|
assert!(!enabled(action, locked));
|
|
}
|
|
assert!(!enabled(
|
|
UiAction::Refresh,
|
|
ActionContext {
|
|
tree_loading: true,
|
|
..ready
|
|
}
|
|
));
|
|
assert!(!enabled(
|
|
UiAction::Save,
|
|
ActionContext {
|
|
dirty: false,
|
|
..ready
|
|
}
|
|
));
|
|
assert!(!enabled(
|
|
UiAction::OpenFolder,
|
|
ActionContext {
|
|
switching_vault: true,
|
|
..ready
|
|
}
|
|
));
|
|
let switching = ActionContext {
|
|
switching_vault: true,
|
|
..ready
|
|
};
|
|
for action in [
|
|
UiAction::Settings,
|
|
UiAction::Save,
|
|
UiAction::CloseWindow,
|
|
UiAction::Quit,
|
|
UiAction::CopyField,
|
|
UiAction::CopyEditedField,
|
|
UiAction::Refresh,
|
|
UiAction::ReloadEntry,
|
|
UiAction::EditEntry,
|
|
UiAction::GeneratePassword,
|
|
UiAction::ToggleReveal,
|
|
] {
|
|
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")
|
|
);
|
|
let modal = ActionContext {
|
|
modal_open: true,
|
|
..ready
|
|
};
|
|
for action in [
|
|
UiAction::About,
|
|
UiAction::Settings,
|
|
UiAction::TogglePaneFocus,
|
|
UiAction::Help,
|
|
] {
|
|
assert!(!enabled(action, modal), "{action:?}");
|
|
}
|
|
assert!(!enabled(
|
|
UiAction::Settings,
|
|
ActionContext {
|
|
storage_ready: false,
|
|
..ready
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn conventional_shortcuts_resolve_to_the_registered_actions() {
|
|
let primary = keyboard::Modifiers::COMMAND;
|
|
for (key, expected) in [
|
|
("o", UiAction::OpenFolder),
|
|
("s", UiAction::Save),
|
|
("f", UiAction::Find),
|
|
("k", UiAction::CommandPalette),
|
|
("n", UiAction::NewEntry),
|
|
("w", UiAction::CloseWindow),
|
|
("q", UiAction::Quit),
|
|
("x", UiAction::Cut),
|
|
("c", UiAction::CopyField),
|
|
("v", UiAction::Paste),
|
|
(",", UiAction::Settings),
|
|
] {
|
|
assert_eq!(
|
|
shortcut_action(&keyboard::Key::Character(key.into()), primary),
|
|
Some(expected)
|
|
);
|
|
}
|
|
assert_eq!(
|
|
shortcut_action(&keyboard::Key::Named(Named::F1), keyboard::Modifiers::NONE),
|
|
Some(UiAction::Help)
|
|
);
|
|
assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
|
|
}
|
|
}
|