diff options
| author | Joe Carstairs <me@joeac.net> | 2025-08-13 07:50:21 +0000 |
|---|---|---|
| committer | joeac <me@joeac.net> | 2025-08-13 07:50:21 +0000 |
| commit | ee3cd780e21260257b401c3f5cbababd9e27a15a (patch) | |
| tree | b0571649a1a1a7644dbc50183cc4c3a29966a963 /schist_desktop_gui/src/shortcut.rs | |
| parent | 49342e8b553d5c92f1c043b3b0ce18bdf1c2cd44 (diff) | |
task-073
Co-authored-by: Joe Carstairs <jcarstairs@scottlogic.com>
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/1
Co-authored-by: Joe Carstairs <me@joeac.net>
Co-committed-by: Joe Carstairs <me@joeac.net>
Diffstat (limited to 'schist_desktop_gui/src/shortcut.rs')
| -rw-r--r-- | schist_desktop_gui/src/shortcut.rs | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/schist_desktop_gui/src/shortcut.rs b/schist_desktop_gui/src/shortcut.rs new file mode 100644 index 0000000..f979f4e --- /dev/null +++ b/schist_desktop_gui/src/shortcut.rs @@ -0,0 +1,339 @@ +// borrowed from https://github.com/squidowl/halloy/tree/main/src/shortcut.rs#L230 + +use iced::keyboard::{self, key}; +use std::hash::Hash; +use std::str::FromStr; +use std::{fmt, ops}; + +use crate::gui::screens::main_screen::View; + +pub fn shortcut(key_bind: KeyBind, command: Command) -> Shortcut { + Shortcut { key_bind, command } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Shortcut { + key_bind: KeyBind, + command: Command, +} + +impl Shortcut { + pub fn execute(&self, key_bind: &KeyBind) -> Option<Command> { + (self.key_bind == *key_bind).then_some(self.command) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Command { + GotoNextItem, + GotoPrevItem, + GotoNextView, + GotoPrevView, + GotoView(View), +} + +macro_rules! default { + ($name:ident, $k:tt) => { + pub fn $name() -> KeyBind { + KeyBind { + key_code: KeyCode(iced::keyboard::Key::Named(iced::keyboard::key::Named::$k)), + modifiers: Modifiers::default(), + } + } + }; + ($name:ident, $k:literal, $m:expr) => { + pub fn $name() -> KeyBind { + KeyBind { + key_code: KeyCode(iced::keyboard::Key::Character($k.into())), + modifiers: $m, + } + } + }; + ($name:ident, $k:tt, $m:expr) => { + pub fn $name() -> KeyBind { + KeyBind { + key_code: KeyCode(iced::keyboard::Key::Named(iced::keyboard::key::Named::$k)), + modifiers: $m, + } + } + }; +} + +#[derive(Debug, Clone, Eq, Ord, PartialOrd)] +pub struct KeyBind { + key_code: KeyCode, + modifiers: Modifiers, +} + +impl fmt::Display for KeyBind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.modifiers, self.key_code) + } +} + +impl PartialEq for KeyBind { + fn eq(&self, other: &Self) -> bool { + if self.modifiers != other.modifiers { + return false; + } + + match (&self.key_code.0, &other.key_code.0) { + // SHIFT modifier effects if this comes across as `a` or `A`, but + // we explicitly define / check modifiers so it doesn't matter if + // user defined it as `a` or `A` in their keymap + (keyboard::Key::Character(a), keyboard::Key::Character(b)) => { + a.to_lowercase() == b.to_lowercase() + } + (a, b) => a == b, + } + } +} + +impl Hash for KeyBind { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.key_code.hash(state); + self.modifiers.hash(state); + } +} + +// For defaults check the platform specific defaults: +// macOS: https://support.apple.com/en-us/102650 +// Windows: https://support.microsoft.com/en-us/windows/keyboard-shortcuts-in-windows-dcc61a57-8ff0-cffe-9796-cb9706c75eec +// Linux FreeDesktop (not ready yet): https://wiki.freedesktop.org/www/Specifications/default-keys-spec/ +// Linux - KDE: https://docs.kde.org/stable5/en/khelpcenter/fundamentals/kbd.html +// Linux - Gnome: https://help.gnome.org/users/gnome-help/stable/keyboard-nav.html.en + +impl KeyBind { + default!(goto_next_item, "j", Modifiers::EMPTY); + default!(goto_prev_item, "k", Modifiers::EMPTY); + default!(goto_next_view, "j", Modifiers::CTRL); + default!(goto_prev_view, "k", Modifiers::CTRL); + default!(goto_view_1, "1", Modifiers::CTRL); + default!(goto_view_2, "2", Modifiers::CTRL); + default!(goto_view_3, "3", Modifiers::CTRL); +} + +impl From<(keyboard::Key, keyboard::Modifiers)> for KeyBind { + fn from((key_code, modifiers): (keyboard::Key, keyboard::Modifiers)) -> Self { + Self { + key_code: KeyCode(key_code), + modifiers: Modifiers(modifiers), + } + } +} + +#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone)] +pub struct KeyCode(keyboard::Key); + +#[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy, Default)] +pub struct Modifiers(keyboard::Modifiers); + +impl Modifiers { + const CTRL: Modifiers = Modifiers(keyboard::Modifiers::CTRL); + const EMPTY: Modifiers = Modifiers(keyboard::Modifiers::empty()); +} + +impl From<keyboard::Modifiers> for Modifiers { + fn from(modifiers: keyboard::Modifiers) -> Self { + Self(modifiers) + } +} + +impl ops::BitOr for Modifiers { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} + +impl fmt::Display for Modifiers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut mods = vec![]; + let inner = self.0; + + if inner.contains(keyboard::Modifiers::SHIFT) { + mods.push("Shift"); + } + if inner.contains(keyboard::Modifiers::CTRL) { + if cfg!(target_os = "macos") { + // macOS: ⌃ + mods.push("\u{2303}"); + } else { + mods.push("Ctrl"); + } + } + if inner.contains(keyboard::Modifiers::ALT) { + if cfg!(target_os = "macos") { + // macOS: ⌥ + mods.push("\u{2325}"); + } else { + mods.push("Alt"); + } + } + if inner.contains(keyboard::Modifiers::LOGO) { + // macOS: ⌘ + mods.push("\u{2318}"); + } + + if mods.is_empty() { + write!(f, "") + } else { + write!(f, "{}", mods.join(" ")) + } + } +} + +impl fmt::Display for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let key = match self.0.clone() { + key::Key::Named(name) => { + let named = match name { + key::Named::F1 => "F1", + key::Named::F2 => "F2", + key::Named::F3 => "F3", + key::Named::F4 => "F4", + key::Named::F5 => "F5", + key::Named::F6 => "F6", + key::Named::F7 => "F7", + key::Named::F8 => "F8", + key::Named::F9 => "F9", + key::Named::F10 => "F10", + key::Named::F11 => "F11", + key::Named::F12 => "F12", + key::Named::F13 => "F13", + key::Named::F14 => "F14", + key::Named::F15 => "F15", + key::Named::F16 => "F16", + key::Named::F17 => "F17", + key::Named::F18 => "F18", + key::Named::F19 => "F19", + key::Named::F20 => "F20", + key::Named::F21 => "F21", + key::Named::F22 => "F22", + key::Named::F23 => "F23", + key::Named::F24 => "F24", + key::Named::Home => "Home", + key::Named::Delete => "Delete", + key::Named::End => "End", + key::Named::PageDown => "PageDown", + key::Named::PageUp => "PageUp", + key::Named::ArrowLeft => "←", + key::Named::ArrowUp => "↑", + key::Named::ArrowRight => "→", + key::Named::ArrowDown => "↓", + key::Named::Backspace => "Backspace", + key::Named::Enter => "Enter", + key::Named::Space => "Space", + key::Named::NumLock => "NumLock", + key::Named::Alt => "Alt", + key::Named::Tab => "Tab", + key::Named::Pause => "Pause", + key::Named::Insert => "Insert", + key::Named::Cut => "Cut", + key::Named::Paste => "Paste", + key::Named::Copy => "Copy", + key::Named::AudioVolumeDown => "VolumeDown", + key::Named::AudioVolumeUp => "VolumeUp", + key::Named::Shift => "Shift", + key::Named::Control => "Control", + key::Named::AudioVolumeMute => "Mute", + key::Named::MediaStop => "MediaStop", + key::Named::MediaPause => "MediaPause", + key::Named::MediaTrackNext => "MediaTrackNext", + key::Named::MediaTrackPrevious => "MediaTrackPrev", + _ => "", + }; + + named.to_string() + } + key::Key::Character(c) => c.to_uppercase(), + key::Key::Unidentified => String::new(), + }; + + write!(f, "{key}") + } +} + +impl FromStr for KeyCode { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(Self(match s.to_ascii_lowercase().as_str() { + "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "a" | "b" | "c" | "d" + | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" + | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "`" | "-" | "=" | "[" | "]" + | "\\" | ";" | "'" | "," | "." | "/" => keyboard::Key::Character(s.into()), + "escape" | "esc" => keyboard::Key::Named(key::Named::Escape), + "f1" => keyboard::Key::Named(key::Named::F1), + "f2" => keyboard::Key::Named(key::Named::F2), + "f3" => keyboard::Key::Named(key::Named::F3), + "f4" => keyboard::Key::Named(key::Named::F4), + "f5" => keyboard::Key::Named(key::Named::F5), + "f6" => keyboard::Key::Named(key::Named::F6), + "f7" => keyboard::Key::Named(key::Named::F7), + "f8" => keyboard::Key::Named(key::Named::F8), + "f9" => keyboard::Key::Named(key::Named::F9), + "f10" => keyboard::Key::Named(key::Named::F10), + "f11" => keyboard::Key::Named(key::Named::F11), + "f12" => keyboard::Key::Named(key::Named::F12), + "f13" => keyboard::Key::Named(key::Named::F13), + "f14" => keyboard::Key::Named(key::Named::F14), + "f15" => keyboard::Key::Named(key::Named::F15), + "f16" => keyboard::Key::Named(key::Named::F16), + "f17" => keyboard::Key::Named(key::Named::F17), + "f18" => keyboard::Key::Named(key::Named::F18), + "f19" => keyboard::Key::Named(key::Named::F19), + "f20" => keyboard::Key::Named(key::Named::F20), + "f21" => keyboard::Key::Named(key::Named::F21), + "f22" => keyboard::Key::Named(key::Named::F22), + "f23" => keyboard::Key::Named(key::Named::F23), + "f24" => keyboard::Key::Named(key::Named::F24), + "home" => keyboard::Key::Named(key::Named::Home), + "delete" => keyboard::Key::Named(key::Named::Delete), + "end" => keyboard::Key::Named(key::Named::End), + "pagedown" => keyboard::Key::Named(key::Named::PageDown), + "pageup" => keyboard::Key::Named(key::Named::PageUp), + "left" => keyboard::Key::Named(key::Named::ArrowLeft), + "up" => keyboard::Key::Named(key::Named::ArrowUp), + "right" => keyboard::Key::Named(key::Named::ArrowRight), + "down" => keyboard::Key::Named(key::Named::ArrowDown), + "backspace" => keyboard::Key::Named(key::Named::Backspace), + "enter" => keyboard::Key::Named(key::Named::Enter), + "space" => keyboard::Key::Named(key::Named::Space), + "numlock" => keyboard::Key::Named(key::Named::NumLock), + "alt" => keyboard::Key::Named(key::Named::Alt), + "tab" => keyboard::Key::Named(key::Named::Tab), + "pause" => keyboard::Key::Named(key::Named::Pause), + "insert" => keyboard::Key::Named(key::Named::Insert), + "cut" => keyboard::Key::Named(key::Named::Cut), + "paste" => keyboard::Key::Named(key::Named::Paste), + "copy" => keyboard::Key::Named(key::Named::Copy), + "volumedown" => keyboard::Key::Named(key::Named::AudioVolumeDown), + "volumeup" => keyboard::Key::Named(key::Named::AudioVolumeUp), + "shift" => keyboard::Key::Named(key::Named::Shift), + "control" => keyboard::Key::Named(key::Named::Control), + "mute" => keyboard::Key::Named(key::Named::AudioVolumeMute), + "mediastop" => keyboard::Key::Named(key::Named::MediaStop), + "mediapause" => keyboard::Key::Named(key::Named::MediaPause), + "mediatracknext" => keyboard::Key::Named(key::Named::MediaTrackNext), + "mediatrackprev" => keyboard::Key::Named(key::Named::MediaTrackPrevious), + _ => return Err(anyhow::format_err!("Invalid key code: {}", s)), + })) + } +} + +impl FromStr for Modifiers { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result<Self> { + Ok(Self(match s.to_lowercase().as_str() { + "shift" => keyboard::Modifiers::SHIFT, + "ctrl" => keyboard::Modifiers::CTRL, + "alt" | "option" | "opt" => keyboard::Modifiers::ALT, + "cmd" | "command" => keyboard::Modifiers::COMMAND, + "logo" | "super" | "windows" => keyboard::Modifiers::LOGO, + _ => return Err(anyhow::format_err!("Invalid error: {}", s)), + })) + } +} |
