Skip to main content

openlogi_core/binding/
key_combo.rs

1//! Modifier + virtual-key chords for custom shortcuts and workflows.
2
3use serde::{Deserialize, Serialize};
4
5/// A modifier + virtual-key keystroke captured by the P1.3 recorder UI or
6/// hand-authored in `config.toml`.
7///
8/// `modifiers` is a bitmask of [`KeyCombo::MOD_CMD`] etc. so the wire format
9/// is a compact integer, not a string. `key_code` is the macOS virtual key
10/// (`kVK_*`); on Linux, `openlogi-inject` maps it to an evdev `KeyCode` when it
11/// synthesizes the chord.
12///
13/// `display` is purely for rendering — e.g. `"⌘⇧P"`. Callers regenerate it
14/// from the captured chord; we keep it in the struct so older configs
15/// continue to render the same label without re-deriving on every load.
16#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct KeyCombo {
18    /// Bitmask of [`Self::MOD_CMD`] etc.
19    pub modifiers: u8,
20    /// macOS virtual key code (`kVK_*`). 0 means "no key" — useful for
21    /// modifier-only placeholders that the recorder UI rejects. On Linux,
22    /// `openlogi-inject` translates this to an evdev `KeyCode`.
23    pub key_code: u16,
24    /// Pre-rendered chord label, e.g. `"⌘⇧P"`. Empty falls through to a
25    /// generated label at runtime.
26    #[serde(default)]
27    pub display: String,
28}
29
30impl KeyCombo {
31    /// Bit for the ⌘ Command modifier in [`Self::modifiers`].
32    pub const MOD_CMD: u8 = 1 << 0;
33    /// Bit for the ⇧ Shift modifier in [`Self::modifiers`].
34    pub const MOD_SHIFT: u8 = 1 << 1;
35    /// Bit for the ⌃ Control modifier in [`Self::modifiers`].
36    pub const MOD_CTRL: u8 = 1 << 2;
37    /// Bit for the ⌥ Option/Alt modifier in [`Self::modifiers`].
38    pub const MOD_OPTION: u8 = 1 << 3;
39
40    /// Build the human-readable label from the modifier bitmask + key code.
41    /// Falls back to `"⌘key 0xNN"` when the key code isn't one of the
42    /// commonly-recognised letters; the recorder UI usually overrides this
43    /// with its own derivation.
44    #[must_use]
45    pub fn rendered_label(&self) -> String {
46        if !self.display.is_empty() {
47            return self.display.clone();
48        }
49        let mut out = String::new();
50        if self.modifiers & Self::MOD_CTRL != 0 {
51            out.push('⌃');
52        }
53        if self.modifiers & Self::MOD_OPTION != 0 {
54            out.push('⌥');
55        }
56        if self.modifiers & Self::MOD_SHIFT != 0 {
57            out.push('⇧');
58        }
59        if self.modifiers & Self::MOD_CMD != 0 {
60            out.push('⌘');
61        }
62        match self.key_code {
63            0x00 => out.push('A'),
64            0x01 => out.push('S'),
65            0x02 => out.push('D'),
66            0x03 => out.push('F'),
67            0x06 => out.push('Z'),
68            0x07 => out.push('X'),
69            0x08 => out.push('C'),
70            0x09 => out.push('V'),
71            0x0B => out.push('B'),
72            0x0C => out.push('Q'),
73            0x0D => out.push('W'),
74            0x0E => out.push('E'),
75            0x0F => out.push('R'),
76            0x10 => out.push('Y'),
77            0x11 => out.push('T'),
78            0x20 => out.push('U'),
79            0x22 => out.push('I'),
80            0x1F => out.push('O'),
81            0x23 => out.push('P'),
82            _ => {
83                use std::fmt::Write as _;
84                let _ = write!(out, "key 0x{:02X}", self.key_code);
85            }
86        }
87        out
88    }
89}