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};
6use thiserror::Error;
7
8use super::FunctionKey;
9use crate::binding::Action;
10
11/// Detectable modifier state: what a keyboard trigger requires, and what the
12/// OS hook reports with each key event (`openlogi-hook` re-exports this type).
13/// `Fn` is absent: firmware-internal, never reported on non-function-row keys,
14/// and so unusable as a trigger (function-key-remapper spec, Appendix A).
15#[derive(
16    Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
17)]
18#[expect(
19    clippy::struct_excessive_bools,
20    reason = "four independent modifier flags, as the OS reports them"
21)]
22pub struct KeyModifiers {
23    /// Shift held.
24    pub shift: bool,
25    /// Control held.
26    pub control: bool,
27    /// Option/Alt held.
28    pub option: bool,
29    /// Command held.
30    pub command: bool,
31}
32
33impl KeyModifiers {
34    /// True when no modifiers are held.
35    #[must_use]
36    pub fn is_empty(&self) -> bool {
37        !self.shift && !self.control && !self.option && !self.command
38    }
39}
40
41/// A keyboard trigger: a keycode plus an optional modifier mask. The parse
42/// format is `[mod+]+key`, e.g. `"f1"`, `"shift+cmd+f5"`. Modifier names:
43/// `shift`, `control` (alias `ctrl`), `option` (alias `alt`), `command`
44/// (alias `cmd`). Key names are [`FunctionKey`]'s: `esc`, `f1`..`f19`.
45///
46/// Serializes as its string form (via `Display`) so it can be a TOML map key:
47/// `[keyboard.bindings]` keys are `"f1"`, `"shift+f2"`, etc.
48#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
49pub struct KeyTrigger {
50    /// Platform virtual keycode (macOS `kVK_*`).
51    pub keycode: u16,
52    /// Modifier mask that must also be held.
53    pub modifiers: KeyModifiers,
54}
55
56impl std::fmt::Display for KeyTrigger {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        let modifiers = &self.modifiers;
59        let mut separator = "";
60        for (enabled, name) in [
61            (modifiers.shift, "shift"),
62            (modifiers.control, "control"),
63            (modifiers.option, "option"),
64            (modifiers.command, "command"),
65        ] {
66            if enabled {
67                write!(f, "{separator}{name}")?;
68                separator = "+";
69            }
70        }
71        let key = FunctionKey::from_keycode(self.keycode).ok_or(std::fmt::Error)?;
72        write!(f, "{separator}{}", key.name())
73    }
74}
75
76// String-form serde so KeyTrigger can be a TOML map key.
77impl Serialize for KeyTrigger {
78    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
79        s.collect_str(self)
80    }
81}
82impl<'de> Deserialize<'de> for KeyTrigger {
83    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
84        let s = String::deserialize(d)?;
85        s.parse().map_err(serde::de::Error::custom)
86    }
87}
88
89/// Error returned by [`KeyTrigger`]'s `FromStr` impl.
90#[derive(Debug, Error)]
91#[error("invalid key trigger: {0}")]
92pub struct ParseTriggerError(pub String);
93
94impl std::str::FromStr for KeyTrigger {
95    type Err = ParseTriggerError;
96    fn from_str(s: &str) -> Result<Self, Self::Err> {
97        let mut mods = KeyModifiers::default();
98        let parts: Vec<&str> = s.split('+').map(str::trim).collect();
99        if parts.is_empty() || parts.iter().any(|p| p.is_empty()) {
100            return Err(ParseTriggerError("empty segment".into()));
101        }
102        // All but the last segment must be modifiers; the last is the key.
103        let (mod_parts, key_part) = parts.split_at(parts.len() - 1);
104        for part in mod_parts {
105            match part.to_ascii_lowercase().as_str() {
106                "shift" => mods.shift = true,
107                "control" | "ctrl" => mods.control = true,
108                "option" | "alt" => mods.option = true,
109                "command" | "cmd" => mods.command = true,
110                other => return Err(ParseTriggerError(format!("unknown modifier '{other}'"))),
111            }
112        }
113        let key = key_part[0].to_ascii_lowercase();
114        let keycode = FunctionKey::from_name(&key)
115            .ok_or_else(|| ParseTriggerError(format!("unknown key '{key}'")))?
116            .keycode();
117        Ok(KeyTrigger {
118            keycode,
119            modifiers: mods,
120        })
121    }
122}
123
124/// The top-level `[keyboard]` table. Bindings are keyed by [`KeyTrigger`].
125#[derive(Debug, Clone, Default, Serialize, Deserialize)]
126#[serde(deny_unknown_fields)]
127pub struct KeyboardConfig {
128    /// Function-key trigger → action map for the remapper.
129    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
130    pub bindings: BTreeMap<KeyTrigger, Action>,
131}