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}