1use std::collections::HashMap;
2
3pub 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
25pub const DEFAULT_AUTO_MOUSE_LAYER_TIMEOUT_MS: u64 = 500;
27
28pub const DEFAULT_AUTO_MOUSE_LAYER_THRESHOLD: u16 = 1;
30
31pub 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#[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 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 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}