From ac9ae7a154538c997dcb91b1ad7e0191cd4f13c7 Mon Sep 17 00:00:00 2001 From: Ross MacArthur Date: Sun, 31 Dec 2023 06:56:08 +0200 Subject: [PATCH] Support multiple modifier keys Alfred supports combinations of keys as modifiers. This adds support for `ctrl+shift` and similar combinations. --- crates/cli/src/main.rs | 1 - examples/hello-alfred/src/main.rs | 5 ++- src/lib.rs | 51 +++++++++++++++++++++++++++++-- tests/basic.rs | 5 ++- tests/testdata/all.golden | 8 ++--- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 274c9b9..c958c6c 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -58,7 +58,6 @@ fn init(manifest_dir: &Path, name: Option) -> Result<()> { // Add workflow/ to the gitignore file (if it exists) if let Ok(mut file) = fs::OpenOptions::new() - .write(true) .append(true) .open(manifest_dir.join(".gitignore")) { diff --git a/examples/hello-alfred/src/main.rs b/examples/hello-alfred/src/main.rs index a400830..98b3395 100644 --- a/examples/hello-alfred/src/main.rs +++ b/examples/hello-alfred/src/main.rs @@ -23,7 +23,10 @@ fn main() -> Result<(), Box> { .large_type_text("this text will be displayed with ⌘L") .modifier(Modifier::new(Key::Command).subtitle("⌘ changes the subtitle")) .modifier(Modifier::new(Key::Option).arg("/path/to/modified.jpg")) - .modifier(Modifier::new(Key::Control).icon(Icon::with_image("/path/to/file.png"))) + .modifier( + Modifier::new_multi([Key::Control, Key::Shift]) + .icon(Icon::with_image("/path/to/file.png")), + ) .modifier(Modifier::new(Key::Shift).valid(false)) .quicklook_url("https://example.com") .action(value!({ diff --git a/src/lib.rs b/src/lib.rs index 4ad247c..821d64a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,15 @@ pub enum Key { Function, } +/// A keyboard modifier combination. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(untagged)] +enum Keys { + One(Key), + #[serde(serialize_with = "serialize_many_keys")] + Many(Vec), +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum IconInner { /// Load an image from a path. @@ -146,7 +155,7 @@ struct Data { #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] pub struct Modifier { /// The modifier key. - key: Key, + key: Keys, /// The modifier data. data: Data, @@ -192,7 +201,7 @@ pub struct Item { /// Control how the modifier keys react. #[serde(rename = "mods", skip_serializing_if = "HashMap::is_empty")] - modifiers: HashMap, + modifiers: HashMap, /// Defines the copied or large type text for this item. #[serde(skip_serializing_if = "Option::is_none")] @@ -228,6 +237,26 @@ pub struct Output { // Implementations //////////////////////////////////////////////////////////////////////////////// +fn serialize_many_keys(duration: &[Key], s: S) -> Result +where + S: Serializer, +{ + let mut out = String::with_capacity(5 * duration.len()); + for (i, key) in duration.iter().enumerate() { + if i != 0 { + out.push('+'); + } + out.push_str(match key { + Key::Command => "cmd", + Key::Option => "alt", + Key::Control => "ctrl", + Key::Shift => "shift", + Key::Function => "fn", + }); + } + s.serialize_str(&out) +} + impl Serialize for Icon { fn serialize(&self, serializer: S) -> Result { match &self.0 { @@ -322,7 +351,23 @@ impl Modifier { #[must_use] pub fn new(key: Key) -> Self { Self { - key, + key: Keys::One(key), + data: Data::default(), + } + } + + /// Create a new modifier with multiple keys. + /// + /// # Examples + /// + /// ``` + /// # use powerpack::{Key, Modifier}; + /// let m = Modifier::multi([Key::Command, Key::Option]); + /// ``` + #[must_use] + pub fn new_multi(keys: impl IntoIterator) -> Self { + Self { + key: Keys::Many(keys.into_iter().collect()), data: Data::default(), } } diff --git a/tests/basic.rs b/tests/basic.rs index 1252048..03e75fd 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -32,7 +32,10 @@ fn all() { .large_type_text("this text will be displayed with ⌘L") .modifier(Modifier::new(Key::Command).subtitle("⌘ changes the subtitle")) .modifier(Modifier::new(Key::Option).arg("/path/to/modified.jpg")) - .modifier(Modifier::new(Key::Control).icon(Icon::with_image("/path/to/file.png"))) + .modifier( + Modifier::new_multi([Key::Control, Key::Shift]) + .icon(Icon::with_image("/path/to/file.png")), + ) .modifier(Modifier::new(Key::Shift).valid(false)) .quicklook_url("https://example.com") .action(value!({ diff --git a/tests/testdata/all.golden b/tests/testdata/all.golden index e52ecf4..93886c1 100644 --- a/tests/testdata/all.golden +++ b/tests/testdata/all.golden @@ -15,19 +15,19 @@ "autocomplete": "to this", "type": "file:skipcheck", "mods": { - "cmd": { - "subtitle": "⌘ changes the subtitle" - }, "alt": { "arg": "/path/to/modified.jpg" }, - "ctrl": { + "ctrl+shift": { "icon": { "path": "/path/to/file.png" } }, "shift": { "valid": false + }, + "cmd": { + "subtitle": "⌘ changes the subtitle" } }, "text": {