Skip to main content

openlogi_core/config/
key_trigger.rs

1//! Keyboard key triggers and the global keyboard-bindings section.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::binding::Action;
8
9/// Detectable modifier state for a keyboard trigger. A leaf-level duplicate of
10/// `openlogi_hook::KeyModifiers` — core must not depend on hook, so the four
11/// bools are mirrored here and converted at the agent boundary (which depends
12/// on both crates). `Fn` is absent: firmware-internal, unusable as a trigger
13/// (function-key-remapper spec, Appendix A).
14#[derive(
15    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
16)]
17#[expect(
18    clippy::struct_excessive_bools,
19    reason = "four independent modifier flags mirrored from the OS hook"
20)]
21pub struct KeyModifiers {
22    /// Shift held.
23    pub shift: bool,
24    /// Control held.
25    pub control: bool,
26    /// Option/Alt held.
27    pub option: bool,
28    /// Command held.
29    pub command: bool,
30}
31
32impl KeyModifiers {
33    /// True when no modifiers are held.
34    #[must_use]
35    pub fn is_empty(&self) -> bool {
36        !self.shift && !self.control && !self.option && !self.command
37    }
38}
39
40/// A keyboard trigger: a keycode plus an optional modifier mask. The parse
41/// format is `[mod+]+key`, e.g. `"f1"`, `"shift+cmd+f5"`. Modifier names:
42/// `shift`, `control` (alias `ctrl`), `option` (alias `alt`), `command`
43/// (alias `cmd`). Key names: `esc`, `f1`..`f19` (macOS virtual keycodes).
44///
45/// Serializes as its string form (via `Display`) so it can be a TOML map key:
46/// `[keyboard.bindings]` keys are `"f1"`, `"shift+f2"`, etc.
47#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
48pub struct KeyTrigger {
49    /// Platform virtual keycode (macOS `kVK_*`).
50    pub keycode: u16,
51    /// Modifier mask that must also be held.
52    pub modifiers: KeyModifiers,
53}
54
55impl std::fmt::Display for KeyTrigger {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        let mut parts: Vec<&str> = Vec::new();
58        let m = &self.modifiers;
59        if m.shift {
60            parts.push("shift");
61        }
62        if m.control {
63            parts.push("control");
64        }
65        if m.option {
66            parts.push("option");
67        }
68        if m.command {
69            parts.push("command");
70        }
71        parts.push(keycode_to_name(self.keycode).ok_or(std::fmt::Error)?);
72        write!(f, "{}", parts.join("+"))
73    }
74}
75
76/// Reverse lookup for the parse table — needed so `Display` can render a
77/// parsed trigger back to its canonical name.
78fn keycode_to_name(code: u16) -> Option<&'static str> {
79    Some(match code {
80        0x35 => "esc",
81        0x7A => "f1",
82        0x78 => "f2",
83        0x63 => "f3",
84        0x76 => "f4",
85        0x60 => "f5",
86        0x61 => "f6",
87        0x62 => "f7",
88        0x64 => "f8",
89        0x65 => "f9",
90        0x6D => "f10",
91        0x67 => "f11",
92        0x6F => "f12",
93        0x69 => "f13",
94        0x6B => "f14",
95        0x71 => "f15",
96        0x6A => "f16",
97        0x40 => "f17",
98        0x4F => "f18",
99        0x50 => "f19",
100        _ => return None,
101    })
102}
103
104// String-form serde so KeyTrigger can be a TOML map key.
105impl Serialize for KeyTrigger {
106    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
107        s.collect_str(self)
108    }
109}
110impl<'de> Deserialize<'de> for KeyTrigger {
111    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
112        let s = String::deserialize(d)?;
113        s.parse().map_err(serde::de::Error::custom)
114    }
115}
116
117/// Error returned by [`KeyTrigger`]'s `FromStr` impl.
118#[derive(Debug)]
119pub struct ParseTriggerError(pub String);
120impl std::fmt::Display for ParseTriggerError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        write!(f, "invalid key trigger: {}", self.0)
123    }
124}
125impl std::error::Error for ParseTriggerError {}
126
127impl std::str::FromStr for KeyTrigger {
128    type Err = ParseTriggerError;
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        let mut mods = KeyModifiers::default();
131        let parts: Vec<&str> = s.split('+').map(str::trim).collect();
132        if parts.is_empty() || parts.iter().any(|p| p.is_empty()) {
133            return Err(ParseTriggerError("empty segment".into()));
134        }
135        // All but the last segment must be modifiers; the last is the key.
136        let (mod_parts, key_part) = parts.split_at(parts.len() - 1);
137        for part in mod_parts {
138            match part.to_ascii_lowercase().as_str() {
139                "shift" => mods.shift = true,
140                "control" | "ctrl" => mods.control = true,
141                "option" | "alt" => mods.option = true,
142                "command" | "cmd" => mods.command = true,
143                other => return Err(ParseTriggerError(format!("unknown modifier '{other}'"))),
144            }
145        }
146        let keycode = match key_part[0].to_ascii_lowercase().as_str() {
147            "esc" => 0x35,
148            "f1" => 0x7A,
149            "f2" => 0x78,
150            "f3" => 0x63,
151            "f4" => 0x76,
152            "f5" => 0x60,
153            "f6" => 0x61,
154            "f7" => 0x62,
155            "f8" => 0x64,
156            "f9" => 0x65,
157            "f10" => 0x6D,
158            "f11" => 0x67,
159            "f12" => 0x6F,
160            "f13" => 0x69,
161            "f14" => 0x6B,
162            "f15" => 0x71,
163            "f16" => 0x6A,
164            "f17" => 0x40,
165            "f18" => 0x4F,
166            "f19" => 0x50,
167            other => return Err(ParseTriggerError(format!("unknown key '{other}'"))),
168        };
169        Ok(KeyTrigger {
170            keycode,
171            modifiers: mods,
172        })
173    }
174}
175
176/// The top-level `[keyboard]` table. Bindings are keyed by [`KeyTrigger`].
177#[derive(Debug, Clone, Default, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct KeyboardConfig {
180    /// Function-key trigger → action map for the remapper.
181    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
182    pub bindings: BTreeMap<KeyTrigger, Action>,
183}