Skip to main content

rmk_config/resolved/
behavior.rs

1use std::collections::HashMap;
2
3/// Resolved behavioral configuration.
4pub struct Behavior {
5    pub tri_layer: Option<[u8; 3]>,
6    pub one_shot_timeout_ms: Option<u64>,
7    pub one_shot_modifiers: Option<OneShot>,
8    pub combos: Option<Combos>,
9    pub macros: Option<Macros>,
10    pub forks: Option<Forks>,
11    pub morse: Option<Morse>,
12    pub auto_mouse_layer: Vec<AutoMouseLayer>,
13}
14
15pub struct AutoMouseLayer {
16    pub device_id: Option<u8>,
17    pub target_layer: u8,
18    pub timeout_ms: u64,
19    pub threshold: u16,
20    pub deactivate_on_key: bool,
21    pub extra_mouse_keys: Vec<String>,
22    pub reset_timeout_on_key: bool,
23}
24
25/// Default idle timeout (in milliseconds) for [`AutoMouseLayer`] when not specified in `keyboard.toml`.
26pub const DEFAULT_AUTO_MOUSE_LAYER_TIMEOUT_MS: u64 = 500;
27
28/// Default motion threshold for [`AutoMouseLayer`] when not specified.
29pub const DEFAULT_AUTO_MOUSE_LAYER_THRESHOLD: u16 = 1;
30
31/// Fallback for `auto_mouse_layer_max_num` when no `keyboard.toml` is loaded.
32pub const DEFAULT_AUTO_MOUSE_LAYER_MAX_NUM: usize = 2;
33
34pub struct OneShot {
35    pub activate_on_keypress: Option<bool>,
36    pub quick_release: Option<bool>,
37}
38
39pub struct Combos {
40    pub combos: Vec<Combo>,
41    pub timeout_ms: Option<u64>,
42    pub prior_idle_time_ms: Option<u64>,
43}
44
45pub struct Combo {
46    pub actions: Vec<String>,
47    pub output: String,
48    pub layer: Option<u8>,
49}
50
51pub struct Macros {
52    pub macros: Vec<Macro>,
53}
54
55pub struct Macro {
56    pub operations: Vec<MacroOperation>,
57}
58
59/// Resolved macro operation — all durations are plain milliseconds.
60#[derive(Clone, Debug)]
61pub enum MacroOperation {
62    Tap { keycode: String },
63    Down { keycode: String },
64    Up { keycode: String },
65    Delay { duration_ms: u64 },
66    Text { text: String },
67}
68
69pub struct Forks {
70    pub forks: Vec<Fork>,
71}
72
73pub struct Fork {
74    pub trigger: String,
75    pub negative_output: String,
76    pub positive_output: String,
77    pub match_any: Option<String>,
78    pub match_none: Option<String>,
79    pub kept_modifiers: Option<String>,
80    pub bindable: bool,
81}
82
83pub struct Morse {
84    pub enable_flow_tap: bool,
85    pub prior_idle_time_ms: u64,
86    pub default_profile: MorseProfile,
87    pub profiles: HashMap<String, MorseProfile>,
88    pub morses: Vec<MorseKey>,
89}
90
91#[derive(Clone)]
92pub struct MorseProfile {
93    pub enable_flow_tap: Option<bool>,
94    pub unilateral_tap: Option<bool>,
95    pub permissive_hold: Option<bool>,
96    pub hold_on_other_press: Option<bool>,
97    pub normal_mode: Option<bool>,
98    pub hold_timeout_ms: Option<u64>,
99    pub gap_timeout_ms: Option<u64>,
100    pub quick_tap_timeout_ms: Option<u64>,
101}
102
103pub struct MorseKey {
104    pub profile: Option<String>,
105    pub tap: Option<String>,
106    pub hold: Option<String>,
107    pub hold_after_tap: Option<String>,
108    pub double_tap: Option<String>,
109    pub tap_actions: Option<Vec<String>>,
110    pub hold_actions: Option<Vec<String>>,
111    pub morse_actions: Option<Vec<MorseActionPair>>,
112}
113
114pub struct MorseActionPair {
115    pub pattern: String,
116    pub action: String,
117}
118
119impl crate::KeyboardTomlConfig {
120    /// Resolve behavioral configuration from TOML config.
121    pub fn behavior(&self) -> Result<Behavior, String> {
122        let toml_behavior = self.get_behavior_config()?;
123
124        let tri_layer = toml_behavior.tri_layer.map(|t| [t.upper, t.lower, t.adjust]);
125
126        let one_shot_timeout_ms = toml_behavior.one_shot.and_then(|o| o.timeout.map(|t| t.0));
127
128        let one_shot_modifiers = toml_behavior.one_shot_modifiers.map(|o| OneShot {
129            activate_on_keypress: o.activate_on_keypress,
130            quick_release: o.quick_release,
131        });
132
133        let combos = toml_behavior.combo.map(|c| Combos {
134            combos: c
135                .combos
136                .into_iter()
137                .map(|combo| Combo {
138                    actions: combo.actions,
139                    output: combo.output,
140                    layer: combo.layer,
141                })
142                .collect(),
143            timeout_ms: c.timeout.map(|t| t.0),
144            prior_idle_time_ms: c.prior_idle_time.map(|t| t.0),
145        });
146
147        let macros = toml_behavior.macros.map(|m| Macros {
148            macros: m
149                .macros
150                .into_iter()
151                .map(|mc| Macro {
152                    operations: mc.operations.into_iter().map(resolve_macro_operation).collect(),
153                })
154                .collect(),
155        });
156
157        let forks = toml_behavior.fork.map(|f| Forks {
158            forks: f
159                .forks
160                .into_iter()
161                .map(|fork| Fork {
162                    trigger: fork.trigger,
163                    negative_output: fork.negative_output,
164                    positive_output: fork.positive_output,
165                    match_any: fork.match_any,
166                    match_none: fork.match_none,
167                    kept_modifiers: fork.kept_modifiers,
168                    bindable: fork.bindable.unwrap_or(false),
169                })
170                .collect(),
171        });
172
173        let morse = toml_behavior.morse.map(|m| {
174            let profiles = m
175                .profiles
176                .as_ref()
177                .map(|p| {
178                    p.iter()
179                        .map(|(name, p)| (name.clone(), resolve_morse_profile(p)))
180                        .collect()
181                })
182                .unwrap_or_default();
183
184            let default_profile = MorseProfile {
185                enable_flow_tap: None,
186                unilateral_tap: m.unilateral_tap,
187                permissive_hold: m.permissive_hold,
188                hold_on_other_press: m.hold_on_other_press,
189                normal_mode: m.normal_mode,
190                hold_timeout_ms: Some(m.hold_timeout.as_ref().map(|t| t.0).unwrap_or(250)),
191                gap_timeout_ms: Some(m.gap_timeout.as_ref().map(|t| t.0).unwrap_or(250)),
192                quick_tap_timeout_ms: m.quick_tap_timeout.as_ref().map(|t| t.0),
193            };
194
195            let morses = m
196                .morses
197                .unwrap_or_default()
198                .into_iter()
199                .map(|mk| MorseKey {
200                    profile: mk.profile,
201                    tap: mk.tap,
202                    hold: mk.hold,
203                    hold_after_tap: mk.hold_after_tap,
204                    double_tap: mk.double_tap,
205                    tap_actions: mk.tap_actions,
206                    hold_actions: mk.hold_actions,
207                    morse_actions: mk.morse_actions.map(|pairs| {
208                        pairs
209                            .into_iter()
210                            .map(|p| MorseActionPair {
211                                pattern: p.pattern,
212                                action: p.action,
213                            })
214                            .collect()
215                    }),
216                })
217                .collect();
218
219            Morse {
220                enable_flow_tap: m.enable_flow_tap.unwrap_or(false),
221                prior_idle_time_ms: m.prior_idle_time.map(|t| t.0).unwrap_or(120),
222                default_profile,
223                profiles,
224                morses,
225            }
226        });
227
228        // Named profiles are interned into the fixed-capacity morse profile
229        // table; overflowing it would silently drop profiles at runtime.
230        if let Some(m) = &morse
231            && m.profiles.len() > self.rmk.morse_profile_max_num
232        {
233            return Err(format!(
234                "behavior.morse.profiles defines {} profiles, but `[rmk] morse_profile_max_num` is {}. Raise it in keyboard.toml",
235                m.profiles.len(),
236                self.rmk.morse_profile_max_num
237            ));
238        }
239
240        let auto_mouse_layer = toml_behavior
241            .auto_mouse_layer
242            .unwrap_or_default()
243            .into_iter()
244            .map(|a| AutoMouseLayer {
245                device_id: a.device_id,
246                target_layer: a.target_layer,
247                timeout_ms: a.timeout.map(|t| t.0).unwrap_or(DEFAULT_AUTO_MOUSE_LAYER_TIMEOUT_MS),
248                threshold: a.threshold.unwrap_or(DEFAULT_AUTO_MOUSE_LAYER_THRESHOLD),
249                deactivate_on_key: a.deactivate_on_key.unwrap_or(false),
250                extra_mouse_keys: a.extra_mouse_keys.unwrap_or_default(),
251                reset_timeout_on_key: a.reset_timeout_on_key.unwrap_or(false),
252            })
253            .collect();
254
255        Ok(Behavior {
256            tri_layer,
257            one_shot_timeout_ms,
258            one_shot_modifiers,
259            combos,
260            macros,
261            forks,
262            morse,
263            auto_mouse_layer,
264        })
265    }
266}
267
268fn resolve_macro_operation(op: crate::MacroOperation) -> MacroOperation {
269    match op {
270        crate::MacroOperation::Tap { keycode } => MacroOperation::Tap { keycode },
271        crate::MacroOperation::Down { keycode } => MacroOperation::Down { keycode },
272        crate::MacroOperation::Up { keycode } => MacroOperation::Up { keycode },
273        crate::MacroOperation::Delay { duration } => MacroOperation::Delay {
274            duration_ms: duration.0,
275        },
276        crate::MacroOperation::Text { text } => MacroOperation::Text { text },
277    }
278}
279
280fn resolve_morse_profile(p: &crate::MorseProfile) -> MorseProfile {
281    MorseProfile {
282        enable_flow_tap: p.enable_flow_tap,
283        unilateral_tap: p.unilateral_tap,
284        permissive_hold: p.permissive_hold,
285        hold_on_other_press: p.hold_on_other_press,
286        normal_mode: p.normal_mode,
287        hold_timeout_ms: p.hold_timeout.as_ref().map(|t| t.0),
288        gap_timeout_ms: p.gap_timeout.as_ref().map(|t| t.0),
289        quick_tap_timeout_ms: p.quick_tap_timeout.as_ref().map(|t| t.0),
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use std::fs;
296    use std::time::{SystemTime, UNIX_EPOCH};
297
298    use crate::KeyboardTomlConfig;
299
300    #[test]
301    fn morse_profile_enable_flow_tap_resolves_as_override() {
302        let toml = r#"
303[layout]
304rows = 1
305cols = 1
306map = "(0,0)"
307
308[keymap]
309layers = 1
310
311[[keymap.layer]]
312keys = "A"
313
314[behavior.morse]
315enable_flow_tap = true
316
317[behavior.morse.profiles.flow_on]
318enable_flow_tap = true
319
320[behavior.morse.profiles.flow_off]
321enable_flow_tap = false
322
323[behavior.morse.profiles.inherit]
324hold_timeout = "200ms"
325"#;
326
327        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
328        let path = std::env::temp_dir().join(format!("rmk-config-flow-tap-{}-{}.toml", std::process::id(), unique));
329
330        fs::write(&path, toml).unwrap();
331        let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path);
332        let _ = fs::remove_file(&path);
333
334        let behavior = config.behavior().unwrap();
335        let morse = behavior.morse.unwrap();
336        assert!(morse.enable_flow_tap);
337        assert_eq!(morse.default_profile.enable_flow_tap, None);
338        assert_eq!(morse.profiles["flow_on"].enable_flow_tap, Some(true));
339        assert_eq!(morse.profiles["flow_off"].enable_flow_tap, Some(false));
340        assert_eq!(morse.profiles["inherit"].enable_flow_tap, None);
341    }
342
343    #[test]
344    fn morse_profiles_overflowing_morse_profile_max_num_is_an_error() {
345        let toml = r#"
346[rmk]
347morse_profile_max_num = 1
348
349[layout]
350rows = 1
351cols = 1
352map = "(0,0)"
353
354[keymap]
355layers = 1
356
357[[keymap.layer]]
358keys = "A"
359
360[behavior.morse.profiles.p1]
361hold_timeout = "200ms"
362
363[behavior.morse.profiles.p2]
364hold_timeout = "300ms"
365"#;
366
367        let unique = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
368        let path = std::env::temp_dir().join(format!(
369            "rmk-config-profile-overflow-{}-{}.toml",
370            std::process::id(),
371            unique
372        ));
373
374        fs::write(&path, toml).unwrap();
375        let config = KeyboardTomlConfig::new_from_toml_path_with_event_defaults(&path);
376        let _ = fs::remove_file(&path);
377
378        let err = match config.behavior() {
379            Ok(_) => panic!("expected the profile-overflow error"),
380            Err(e) => e,
381        };
382        assert!(err.contains("morse_profile_max_num"), "unexpected error: {err}");
383    }
384}