Complete desktop parity and security audit (#42)
This commit is contained in:
@@ -2,6 +2,32 @@
|
||||
|
||||
use iced::keyboard::{self, key::Named};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum DesktopPlatform {
|
||||
MacOs,
|
||||
Linux,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl DesktopPlatform {
|
||||
pub const fn current() -> Self {
|
||||
if cfg!(target_os = "macos") {
|
||||
Self::MacOs
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
const fn primary_modifier(self) -> keyboard::Modifiers {
|
||||
match self {
|
||||
Self::MacOs => keyboard::Modifiers::LOGO,
|
||||
Self::Linux | Self::Windows => keyboard::Modifiers::CTRL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub enum UiAction {
|
||||
About,
|
||||
@@ -314,8 +340,12 @@ pub fn actions_in(group: MenuGroup) -> impl Iterator<Item = &'static ActionSpec>
|
||||
}
|
||||
|
||||
pub fn shortcut_label(action: UiAction) -> Option<String> {
|
||||
shortcut_label_for(action, DesktopPlatform::current())
|
||||
}
|
||||
|
||||
pub fn shortcut_label_for(action: UiAction, platform: DesktopPlatform) -> Option<String> {
|
||||
let shortcut = spec_for(action).shortcut?;
|
||||
if cfg!(target_os = "macos") {
|
||||
if platform == DesktopPlatform::MacOs {
|
||||
Some(shortcut.to_owned())
|
||||
} else {
|
||||
Some(shortcut.replace('⇧', "Shift+").replace('⌘', "Ctrl+"))
|
||||
@@ -685,13 +715,21 @@ pub const fn aliases(action: UiAction) -> &'static [&'static str] {
|
||||
}
|
||||
|
||||
pub fn shortcut_action(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<UiAction> {
|
||||
shortcut_action_for(DesktopPlatform::current(), key, modifiers)
|
||||
}
|
||||
|
||||
pub fn shortcut_action_for(
|
||||
platform: DesktopPlatform,
|
||||
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() {
|
||||
if !modifiers.contains(platform.primary_modifier()) {
|
||||
return None;
|
||||
}
|
||||
match key.as_ref() {
|
||||
@@ -940,36 +978,62 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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),
|
||||
fn conventional_shortcuts_resolve_on_every_supported_platform() {
|
||||
for (platform, primary, label) in [
|
||||
(DesktopPlatform::MacOs, keyboard::Modifiers::LOGO, "⌘S"),
|
||||
(DesktopPlatform::Linux, keyboard::Modifiers::CTRL, "Ctrl+S"),
|
||||
(
|
||||
DesktopPlatform::Windows,
|
||||
keyboard::Modifiers::CTRL,
|
||||
"Ctrl+S",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
shortcut_action(&keyboard::Key::Character(key.into()), primary),
|
||||
Some(expected)
|
||||
shortcut_label_for(UiAction::Save, platform).as_deref(),
|
||||
Some(label)
|
||||
);
|
||||
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_for(platform, &keyboard::Key::Character(key.into()), primary,),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
shortcut_action_for(
|
||||
platform,
|
||||
&keyboard::Key::Character("f".into()),
|
||||
primary | keyboard::Modifiers::SHIFT,
|
||||
),
|
||||
Some(UiAction::SearchContents)
|
||||
);
|
||||
assert_eq!(
|
||||
shortcut_action_for(
|
||||
platform,
|
||||
&keyboard::Key::Named(Named::F1),
|
||||
keyboard::Modifiers::NONE,
|
||||
),
|
||||
Some(UiAction::Help)
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
shortcut_action(
|
||||
&keyboard::Key::Character("f".into()),
|
||||
primary | keyboard::Modifiers::SHIFT,
|
||||
shortcut_action_for(
|
||||
DesktopPlatform::MacOs,
|
||||
&keyboard::Key::Character("s".into()),
|
||||
keyboard::Modifiers::CTRL,
|
||||
),
|
||||
Some(UiAction::SearchContents)
|
||||
);
|
||||
assert_eq!(
|
||||
shortcut_action(&keyboard::Key::Named(Named::F1), keyboard::Modifiers::NONE),
|
||||
Some(UiAction::Help)
|
||||
None
|
||||
);
|
||||
assert_eq!(shortcut_label(UiAction::Help).as_deref(), Some("F1"));
|
||||
}
|
||||
|
||||
@@ -852,14 +852,18 @@ fn main() -> iced::Result {
|
||||
.title(ironstorage::PRODUCT_NAME)
|
||||
.subscription(App::subscription)
|
||||
.exit_on_close_request(false)
|
||||
.window(window::Settings {
|
||||
size: Size::new(1_080.0, 720.0),
|
||||
min_size: Some(Size::new(720.0, 480.0)),
|
||||
..window::Settings::default()
|
||||
})
|
||||
.window(window_settings())
|
||||
.run()
|
||||
}
|
||||
|
||||
fn window_settings() -> window::Settings {
|
||||
window::Settings {
|
||||
size: Size::new(1_080.0, 720.0),
|
||||
min_size: Some(Size::new(720.0, 480.0)),
|
||||
..window::Settings::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let mut app = Self {
|
||||
@@ -3345,11 +3349,8 @@ impl App {
|
||||
.min_size(220)
|
||||
.on_resize(8, Message::PaneResized);
|
||||
|
||||
let shortcut = if cfg!(target_os = "macos") {
|
||||
"⌘K"
|
||||
} else {
|
||||
"Ctrl+K"
|
||||
};
|
||||
let shortcut = action::shortcut_label(UiAction::CommandPalette)
|
||||
.expect("the command palette has a registered shortcut");
|
||||
let command_input = text_input(
|
||||
&format!("Search commands ({shortcut})"),
|
||||
self.palette.query(),
|
||||
@@ -5034,6 +5035,89 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn desktop_coverage_matrix_contains_every_registered_action() {
|
||||
let matrix = include_str!("../../../docs/desktop-audit.md");
|
||||
for spec in action::ACTIONS {
|
||||
assert!(
|
||||
matrix.contains(&format!("`{}`", spec.action.id())),
|
||||
"desktop parity matrix is missing {}",
|
||||
spec.action.id()
|
||||
);
|
||||
}
|
||||
for required_surface in [
|
||||
"Base pass",
|
||||
"Pass OTP",
|
||||
"Embedded Git",
|
||||
"Configuration and lock",
|
||||
"macOS",
|
||||
"Linux",
|
||||
"Windows",
|
||||
] {
|
||||
assert!(
|
||||
matrix.contains(required_surface),
|
||||
"missing {required_surface}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_desktop_sources_preserve_security_and_architecture_boundaries() {
|
||||
let sources = [
|
||||
("action.rs", include_str!("action.rs")),
|
||||
("editor.rs", include_str!("editor.rs")),
|
||||
("folder_picker.rs", include_str!("folder_picker.rs")),
|
||||
("main.rs", include_str!("main.rs")),
|
||||
("native_menu.rs", include_str!("native_menu.rs")),
|
||||
("navigation.rs", include_str!("navigation.rs")),
|
||||
("palette.rs", include_str!("palette.rs")),
|
||||
];
|
||||
for (name, source) in sources {
|
||||
let production = source
|
||||
.split("#[cfg(test)]\nmod tests")
|
||||
.next()
|
||||
.unwrap_or(source);
|
||||
let forbidden_tokens = [
|
||||
["std::", "process"].concat(),
|
||||
["process", "::Command"].concat(),
|
||||
["Command", "::new("].concat(),
|
||||
["Repository", "::open"].concat(),
|
||||
["GitRepository", "::"].concat(),
|
||||
["OtpUri", "::parse"].concat(),
|
||||
["qrcode", "::QrCode"].concat(),
|
||||
["fs::", "write"].concat(),
|
||||
["File", "::create"].concat(),
|
||||
["OpenOptions", "::new"].concat(),
|
||||
["println", "!("].concat(),
|
||||
["eprintln", "!("].concat(),
|
||||
["dbg", "!("].concat(),
|
||||
["log", "::info"].concat(),
|
||||
["log", "::debug"].concat(),
|
||||
["log", "::error"].concat(),
|
||||
["tracing", "::info"].concat(),
|
||||
["tracing", "::debug"].concat(),
|
||||
["tracing", "::error"].concat(),
|
||||
["http", "://"].concat(),
|
||||
["unsafe", " {"].concat(),
|
||||
];
|
||||
for forbidden in &forbidden_tokens {
|
||||
assert!(
|
||||
!production.contains(forbidden),
|
||||
"{name} crosses the desktop architecture boundary with {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_contract_keeps_both_scrollable_panes_usable_at_narrow_size() {
|
||||
let settings = window_settings();
|
||||
assert_eq!(settings.size, Size::new(1_080.0, 720.0));
|
||||
assert_eq!(settings.min_size, Some(Size::new(720.0, 480.0)));
|
||||
let app = test_app(None);
|
||||
let _view = app.view();
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct ManualClock(Arc<Mutex<Duration>>);
|
||||
|
||||
|
||||
@@ -160,3 +160,35 @@ fn accelerator(action: UiAction) -> Option<Accelerator> {
|
||||
};
|
||||
Some(Accelerator::new(Some(modifiers), code))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn native_menu_uses_standard_macos_accelerators() {
|
||||
for (action, modifiers, code) in [
|
||||
(UiAction::Settings, Modifiers::SUPER, Code::Comma),
|
||||
(UiAction::NewEntry, Modifiers::SUPER, Code::KeyN),
|
||||
(UiAction::OpenFolder, Modifiers::SUPER, Code::KeyO),
|
||||
(UiAction::Save, Modifiers::SUPER, Code::KeyS),
|
||||
(UiAction::CloseWindow, Modifiers::SUPER, Code::KeyW),
|
||||
(UiAction::Quit, Modifiers::SUPER, Code::KeyQ),
|
||||
(UiAction::Find, Modifiers::SUPER, Code::KeyF),
|
||||
(
|
||||
UiAction::SearchContents,
|
||||
Modifiers::SUPER | Modifiers::SHIFT,
|
||||
Code::KeyF,
|
||||
),
|
||||
(UiAction::CommandPalette, Modifiers::SUPER, Code::KeyK),
|
||||
(UiAction::Refresh, Modifiers::SUPER, Code::KeyR),
|
||||
(UiAction::Lock, Modifiers::SUPER, Code::KeyL),
|
||||
(UiAction::Help, Modifiers::empty(), Code::F1),
|
||||
] {
|
||||
let accelerator = accelerator(action).expect("registered native accelerator");
|
||||
assert_eq!(accelerator.modifiers(), modifiers, "{action:?}");
|
||||
assert_eq!(accelerator.key(), code, "{action:?}");
|
||||
}
|
||||
assert!(accelerator(UiAction::GeneratePassword).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user