1use super::HashSet;
2use super::sexpr::SExpr;
3use super::{TrimAtomQuotes, error::*};
4use crate::cfg::check_first_expr;
5use crate::custom_action::*;
6use crate::keys::*;
7#[allow(unused)]
8use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span};
9
10#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12pub enum DeviceDetectMode {
13 KeyboardOnly,
14 KeyboardMice,
15 Any,
16}
17#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
18impl std::fmt::Display for DeviceDetectMode {
19 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20 write!(f, "{self:?}")
21 }
22}
23
24#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
25#[derive(Debug, Clone)]
26pub struct CfgLinuxOptions {
27 pub linux_dev: Vec<String>,
28 pub linux_dev_names_include: Option<Vec<String>>,
29 pub linux_dev_names_exclude: Option<Vec<String>>,
30 pub linux_continue_if_no_devs_found: bool,
31 pub linux_unicode_u_code: crate::keys::OsCode,
32 pub linux_unicode_termination: UnicodeTermination,
33 pub linux_x11_repeat_delay_rate: Option<KeyRepeatSettings>,
34 pub linux_use_trackpoint_property: bool,
35 pub linux_output_name: String,
36 pub linux_output_bus_type: LinuxCfgOutputBusType,
37 pub linux_device_detect_mode: Option<DeviceDetectMode>,
38}
39#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
40impl Default for CfgLinuxOptions {
41 fn default() -> Self {
42 Self {
43 linux_dev: vec![],
44 linux_dev_names_include: None,
45 linux_dev_names_exclude: None,
46 linux_continue_if_no_devs_found: false,
47 linux_unicode_u_code: crate::keys::OsCode::KEY_U,
49 linux_unicode_termination: UnicodeTermination::Enter,
51 linux_x11_repeat_delay_rate: None,
52 linux_use_trackpoint_property: false,
53 linux_output_name: "kanata".to_owned(),
54 linux_output_bus_type: LinuxCfgOutputBusType::BusI8042,
55 linux_device_detect_mode: None,
56 }
57 }
58}
59#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
60#[derive(Debug, Clone, Copy)]
61pub enum LinuxCfgOutputBusType {
62 BusUsb,
63 BusI8042,
64}
65
66#[cfg(any(target_os = "macos", target_os = "unknown"))]
67#[derive(Debug, Default, Clone)]
68pub struct CfgMacosOptions {
69 pub macos_dev_names_include: Option<Vec<String>>,
70 pub macos_dev_names_exclude: Option<Vec<String>>,
71}
72
73#[cfg(any(
74 all(feature = "interception_driver", target_os = "windows"),
75 target_os = "unknown"
76))]
77#[derive(Debug, Clone, Default)]
78pub struct CfgWinterceptOptions {
79 pub windows_interception_mouse_hwids: Option<Vec<[u8; HWID_ARR_SZ]>>,
80 pub windows_interception_mouse_hwids_exclude: Option<Vec<[u8; HWID_ARR_SZ]>>,
81 pub windows_interception_keyboard_hwids: Option<Vec<[u8; HWID_ARR_SZ]>>,
82 pub windows_interception_keyboard_hwids_exclude: Option<Vec<[u8; HWID_ARR_SZ]>>,
83}
84
85#[cfg(any(target_os = "windows", target_os = "unknown"))]
86#[derive(Debug, Clone, Default)]
87pub struct CfgWindowsOptions {
88 pub windows_altgr: AltGrBehaviour,
89 pub sync_keystates: bool,
90}
91
92#[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))]
93#[derive(Debug, Clone)]
94pub struct CfgOptionsGui {
95 pub tray_icon: Option<String>,
97 pub icon_match_layer_name: bool,
99 pub tooltip_layer_changes: bool,
101 pub tooltip_no_base: bool,
103 pub tooltip_show_blank: bool,
105 pub tooltip_duration: u16,
107 pub notify_cfg_reload: bool,
109 pub notify_cfg_reload_silent: bool,
111 pub notify_error: bool,
113 pub tooltip_size: (u16, u16),
115}
116#[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))]
117impl Default for CfgOptionsGui {
118 fn default() -> Self {
119 Self {
120 tray_icon: None,
121 icon_match_layer_name: true,
122 tooltip_layer_changes: false,
123 tooltip_show_blank: false,
124 tooltip_no_base: true,
125 tooltip_duration: 500,
126 notify_cfg_reload: true,
127 notify_cfg_reload_silent: false,
128 notify_error: true,
129 tooltip_size: (24, 24),
130 }
131 }
132}
133
134#[derive(Debug)]
135pub struct CfgOptions {
136 pub process_unmapped_keys: bool,
137 pub process_unmapped_keys_exceptions: Option<Vec<(OsCode, SExpr)>>,
138 pub block_unmapped_keys: bool,
139 pub allow_hardware_repeat: bool,
140 pub start_alias: Option<String>,
141 pub enable_cmd: bool,
142 pub sequence_timeout: u16,
143 pub sequence_input_mode: SequenceInputMode,
144 pub sequence_backtrack_modcancel: bool,
145 pub sequence_always_on: bool,
146 pub log_layer_changes: bool,
147 pub delegate_to_first_layer: bool,
148 pub movemouse_inherit_accel_state: bool,
149 pub movemouse_smooth_diagonals: bool,
150 pub override_release_on_activation: bool,
151 pub dynamic_macro_max_presses: u16,
152 pub dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour,
153 pub concurrent_tap_hold: bool,
154 pub rapid_event_delay: u16,
155 pub trans_resolution_behavior_v2: bool,
156 pub chords_v2_min_idle: u16,
157 #[cfg(any(
158 all(target_os = "windows", feature = "interception_driver"),
159 target_os = "linux",
160 target_os = "android",
161 target_os = "unknown"
162 ))]
163 pub mouse_movement_key: Option<OsCode>,
164 #[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
165 pub linux_opts: CfgLinuxOptions,
166 #[cfg(any(target_os = "macos", target_os = "unknown"))]
167 pub macos_opts: CfgMacosOptions,
168 #[cfg(any(target_os = "windows", target_os = "unknown"))]
169 pub windows_opts: CfgWindowsOptions,
170 #[cfg(any(
171 all(feature = "interception_driver", target_os = "windows"),
172 target_os = "unknown"
173 ))]
174 pub wintercept_opts: CfgWinterceptOptions,
175 #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))]
176 pub gui_opts: CfgOptionsGui,
177}
178
179impl Default for CfgOptions {
180 fn default() -> Self {
181 Self {
182 process_unmapped_keys: false,
183 process_unmapped_keys_exceptions: None,
184 block_unmapped_keys: false,
185 allow_hardware_repeat: true,
186 start_alias: None,
187 enable_cmd: false,
188 sequence_timeout: 1000,
189 sequence_input_mode: SequenceInputMode::HiddenSuppressed,
190 sequence_backtrack_modcancel: true,
191 sequence_always_on: false,
192 log_layer_changes: true,
193 delegate_to_first_layer: false,
194 movemouse_inherit_accel_state: false,
195 movemouse_smooth_diagonals: false,
196 override_release_on_activation: false,
197 dynamic_macro_max_presses: 128,
198 dynamic_macro_replay_delay_behaviour: ReplayDelayBehaviour::Recorded,
199 concurrent_tap_hold: false,
200 rapid_event_delay: 5,
201 trans_resolution_behavior_v2: true,
202 chords_v2_min_idle: 5,
203 #[cfg(any(
204 all(target_os = "windows", feature = "interception_driver"),
205 target_os = "linux",
206 target_os = "android",
207 target_os = "unknown"
208 ))]
209 mouse_movement_key: None,
210 #[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
211 linux_opts: Default::default(),
212 #[cfg(any(target_os = "windows", target_os = "unknown"))]
213 windows_opts: Default::default(),
214 #[cfg(any(
215 all(feature = "interception_driver", target_os = "windows"),
216 target_os = "unknown"
217 ))]
218 wintercept_opts: Default::default(),
219 #[cfg(any(target_os = "macos", target_os = "unknown"))]
220 macos_opts: Default::default(),
221 #[cfg(all(any(target_os = "windows", target_os = "unknown"), feature = "gui"))]
222 gui_opts: Default::default(),
223 }
224 }
225}
226
227pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
229 let mut seen_keys = HashSet::default();
230 let mut cfg = CfgOptions::default();
231 let mut exprs = check_first_expr(expr.iter(), "defcfg")?;
232 let mut is_process_unmapped_keys_defined = false;
233 loop {
235 let key = match exprs.next() {
236 Some(k) => k,
237 None => {
238 if !is_process_unmapped_keys_defined {
239 log::warn!(
240 "The item process-unmapped-keys is not defined in defcfg. Consider whether process-unmapped-keys should be yes vs. no."
241 );
242 }
243 return Ok(cfg);
244 }
245 };
246 let val = match exprs.next() {
247 Some(v) => v,
248 None => bail_expr!(key, "Found a defcfg option missing a value"),
249 };
250 match key {
251 SExpr::Atom(k) => {
252 let label = k.t.as_str();
253 if !seen_keys.insert(label) {
254 bail_expr!(key, "Duplicate defcfg option {}", label);
255 }
256 match label {
257 "sequence-timeout" => {
258 cfg.sequence_timeout = parse_cfg_val_u16(val, label, true)?;
259 }
260 "sequence-input-mode" => {
261 let v = sexpr_to_str_or_err(val, label)?;
262 cfg.sequence_input_mode = SequenceInputMode::try_from_str(v)
263 .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?;
264 }
265 "sequence-always-on" => {
266 cfg.sequence_always_on = parse_defcfg_val_bool(val, label)?
267 }
268 "dynamic-macro-max-presses" => {
269 cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, label, false)?;
270 }
271 "dynamic-macro-replay-delay-behaviour" => {
272 cfg.dynamic_macro_replay_delay_behaviour = val
273 .atom(None)
274 .map(|v| match v {
275 "constant" => Ok(ReplayDelayBehaviour::Constant),
276 "recorded" => Ok(ReplayDelayBehaviour::Recorded),
277 _ => bail_expr!(
278 val,
279 "this option must be one of: constant | recorded"
280 ),
281 })
282 .ok_or_else(|| {
283 anyhow_expr!(val, "this option must be one of: constant | recorded")
284 })??;
285 }
286 "linux-dev" => {
287 #[cfg(any(
288 target_os = "linux",
289 target_os = "android",
290 target_os = "unknown"
291 ))]
292 {
293 cfg.linux_opts.linux_dev = parse_dev(val)?;
294 if cfg.linux_opts.linux_dev.is_empty() {
295 bail_expr!(
296 val,
297 "device list is empty, no devices will be intercepted"
298 );
299 }
300 }
301 }
302 "linux-dev-names-include" => {
303 #[cfg(any(
304 target_os = "linux",
305 target_os = "android",
306 target_os = "unknown"
307 ))]
308 {
309 let dev_names = parse_dev(val)?;
310 if dev_names.is_empty() {
311 log::warn!("linux-dev-names-include is empty");
312 }
313 cfg.linux_opts.linux_dev_names_include = Some(dev_names);
314 }
315 }
316 "linux-dev-names-exclude" => {
317 #[cfg(any(
318 target_os = "linux",
319 target_os = "android",
320 target_os = "unknown"
321 ))]
322 {
323 cfg.linux_opts.linux_dev_names_exclude = Some(parse_dev(val)?);
324 }
325 }
326 "linux-unicode-u-code" => {
327 #[cfg(any(
328 target_os = "linux",
329 target_os = "android",
330 target_os = "unknown"
331 ))]
332 {
333 let v = sexpr_to_str_or_err(val, label)?;
334 cfg.linux_opts.linux_unicode_u_code = crate::keys::str_to_oscode(v)
335 .ok_or_else(|| {
336 anyhow_expr!(val, "unknown code for {label}: {}", v)
337 })?;
338 }
339 }
340 "linux-unicode-termination" => {
341 #[cfg(any(
342 target_os = "linux",
343 target_os = "android",
344 target_os = "unknown"
345 ))]
346 {
347 let v = sexpr_to_str_or_err(val, label)?;
348 cfg.linux_opts.linux_unicode_termination = match v {
349 "enter" => UnicodeTermination::Enter,
350 "space" => UnicodeTermination::Space,
351 "enter-space" => UnicodeTermination::EnterSpace,
352 "space-enter" => UnicodeTermination::SpaceEnter,
353 _ => bail_expr!(
354 val,
355 "{label} got {}. It accepts: enter|space|enter-space|space-enter",
356 v
357 ),
358 }
359 }
360 }
361 "linux-x11-repeat-delay-rate" => {
362 #[cfg(any(
363 target_os = "linux",
364 target_os = "android",
365 target_os = "unknown"
366 ))]
367 {
368 let v = sexpr_to_str_or_err(val, label)?;
369 let delay_rate = v.split(',').collect::<Vec<_>>();
370 const ERRMSG: &str = "Invalid value for linux-x11-repeat-delay-rate.\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25";
371 if delay_rate.len() != 2 {
372 bail_expr!(val, "{}", ERRMSG)
373 }
374 cfg.linux_opts.linux_x11_repeat_delay_rate = Some(KeyRepeatSettings {
375 delay: match str::parse::<u16>(delay_rate[0]) {
376 Ok(delay) => delay,
377 Err(_) => bail_expr!(val, "{}", ERRMSG),
378 },
379 rate: match str::parse::<u16>(delay_rate[1]) {
380 Ok(rate) => rate,
381 Err(_) => bail_expr!(val, "{}", ERRMSG),
382 },
383 });
384 }
385 }
386 "linux-use-trackpoint-property" => {
387 #[cfg(any(
388 target_os = "linux",
389 target_os = "android",
390 target_os = "unknown"
391 ))]
392 {
393 cfg.linux_opts.linux_use_trackpoint_property =
394 parse_defcfg_val_bool(val, label)?
395 }
396 }
397 "linux-output-device-name" => {
398 #[cfg(any(
399 target_os = "linux",
400 target_os = "android",
401 target_os = "unknown"
402 ))]
403 {
404 let device_name = sexpr_to_str_or_err(val, label)?;
405 if device_name.is_empty() {
406 log::warn!(
407 "linux-output-device-name is empty, using kanata as default value"
408 );
409 } else {
410 cfg.linux_opts.linux_output_name = device_name.to_owned();
411 }
412 }
413 }
414 "linux-output-device-bus-type" => {
415 let bus_type = sexpr_to_str_or_err(val, label)?;
416 match bus_type {
417 "USB" | "I8042" => {}
418 _ => bail_expr!(
419 val,
420 "Invalid value for linux-output-device-bus-type.\nExpected one of: USB or I8042"
421 ),
422 };
423 #[cfg(any(
424 target_os = "linux",
425 target_os = "android",
426 target_os = "unknown"
427 ))]
428 {
429 let bus_type = match bus_type {
430 "USB" => LinuxCfgOutputBusType::BusUsb,
431 "I8042" => LinuxCfgOutputBusType::BusI8042,
432 _ => unreachable!("validated earlier"),
433 };
434 cfg.linux_opts.linux_output_bus_type = bus_type;
435 }
436 }
437 "linux-device-detect-mode" => {
438 let detect_mode = sexpr_to_str_or_err(val, label)?;
439 match detect_mode {
440 "any" | "keyboard-only" | "keyboard-mice" => {}
441 _ => bail_expr!(
442 val,
443 "Invalid value for linux-device-detect-mode.\nExpected one of: any | keyboard-only | keyboard-mice"
444 ),
445 };
446 #[cfg(any(
447 target_os = "linux",
448 target_os = "android",
449 target_os = "unknown"
450 ))]
451 {
452 let detect_mode = Some(match detect_mode {
453 "any" => DeviceDetectMode::Any,
454 "keyboard-only" => DeviceDetectMode::KeyboardOnly,
455 "keyboard-mice" => DeviceDetectMode::KeyboardMice,
456 _ => unreachable!("validated earlier"),
457 });
458 cfg.linux_opts.linux_device_detect_mode = detect_mode;
459 }
460 }
461 "windows-altgr" => {
462 #[cfg(any(target_os = "windows", target_os = "unknown"))]
463 {
464 const CANCEL: &str = "cancel-lctl-press";
465 const ADD: &str = "add-lctl-release";
466 let v = sexpr_to_str_or_err(val, label)?;
467 cfg.windows_opts.windows_altgr = match v {
468 CANCEL => AltGrBehaviour::CancelLctlPress,
469 ADD => AltGrBehaviour::AddLctlRelease,
470 _ => bail_expr!(
471 val,
472 "Invalid value for {label}: {}. Valid values are {},{}",
473 v,
474 CANCEL,
475 ADD
476 ),
477 }
478 }
479 }
480 "windows-sync-keystates" => {
481 #[cfg(any(target_os = "windows", target_os = "unknown"))]
482 {
483 cfg.windows_opts.sync_keystates = parse_defcfg_val_bool(val, label)?;
484 }
485 }
486 "windows-interception-mouse-hwid" => {
487 #[cfg(any(
488 all(feature = "interception_driver", target_os = "windows"),
489 target_os = "unknown"
490 ))]
491 {
492 if cfg
493 .wintercept_opts
494 .windows_interception_mouse_hwids_exclude
495 .is_some()
496 {
497 bail_expr!(
498 val,
499 "{label} and windows-interception-mouse-hwid-exclude cannot both be included"
500 );
501 }
502 let v = sexpr_to_str_or_err(val, label)?;
503 let hwid = v;
504 log::trace!("win hwid: {hwid}");
505 let hwid_vec = hwid
506 .split(',')
507 .try_fold(vec![], |mut hwid_bytes, hwid_byte| {
508 hwid_byte.trim_matches(' ').parse::<u8>().map(|b| {
509 hwid_bytes.push(b);
510 hwid_bytes
511 })
512 }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of numbers [0,255] separated by commas"))?;
513 let hwid_slice = hwid_vec.iter().copied().enumerate()
514 .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| {
515 let (i, b) = idx_byte;
516 if i > HWID_ARR_SZ {
517 bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} numbers [0,255]")
518 }
519 hwid[i] = b;
520 Ok(hwid)
521 })?;
522 match cfg
523 .wintercept_opts
524 .windows_interception_mouse_hwids
525 .as_mut()
526 {
527 Some(v) => {
528 v.push(hwid_slice);
529 }
530 None => {
531 cfg.wintercept_opts.windows_interception_mouse_hwids =
532 Some(vec![hwid_slice]);
533 }
534 }
535 cfg.wintercept_opts
536 .windows_interception_mouse_hwids
537 .as_mut()
538 .unwrap()
539 .shrink_to_fit();
540 }
541 }
542 "windows-interception-mouse-hwids" => {
543 #[cfg(any(
544 all(feature = "interception_driver", target_os = "windows"),
545 target_os = "unknown"
546 ))]
547 {
548 if cfg
549 .wintercept_opts
550 .windows_interception_mouse_hwids_exclude
551 .is_some()
552 {
553 bail_expr!(
554 val,
555 "{label} and windows-interception-mouse-hwid-exclude cannot both be included"
556 );
557 }
558 let parsed_hwids = sexpr_to_hwids_vec(
559 val,
560 label,
561 "entry in windows-interception-mouse-hwids",
562 )?;
563 match cfg
564 .wintercept_opts
565 .windows_interception_mouse_hwids
566 .as_mut()
567 {
568 Some(v) => {
569 v.extend(parsed_hwids);
570 }
571 None => {
572 cfg.wintercept_opts.windows_interception_mouse_hwids =
573 Some(parsed_hwids);
574 }
575 }
576 cfg.wintercept_opts
577 .windows_interception_mouse_hwids
578 .as_mut()
579 .unwrap()
580 .shrink_to_fit();
581 }
582 }
583 "windows-interception-mouse-hwids-exclude" => {
584 #[cfg(any(
585 all(feature = "interception_driver", target_os = "windows"),
586 target_os = "unknown"
587 ))]
588 {
589 if cfg
590 .wintercept_opts
591 .windows_interception_mouse_hwids
592 .is_some()
593 {
594 bail_expr!(
595 val,
596 "{label} and windows-interception-mouse-hwid(s) cannot both be used"
597 );
598 }
599 let parsed_hwids = sexpr_to_hwids_vec(
600 val,
601 label,
602 "entry in windows-interception-mouse-hwids-exclude",
603 )?;
604 cfg.wintercept_opts.windows_interception_mouse_hwids_exclude =
605 Some(parsed_hwids);
606 }
607 }
608 "windows-interception-keyboard-hwids" => {
609 #[cfg(any(
610 all(feature = "interception_driver", target_os = "windows"),
611 target_os = "unknown"
612 ))]
613 {
614 if cfg
615 .wintercept_opts
616 .windows_interception_keyboard_hwids_exclude
617 .is_some()
618 {
619 bail_expr!(
620 val,
621 "{label} and windows-interception-keyboard-hwid-exclude cannot both be used"
622 );
623 }
624 let parsed_hwids = sexpr_to_hwids_vec(
625 val,
626 label,
627 "entry in windows-interception-keyboard-hwids",
628 )?;
629 cfg.wintercept_opts.windows_interception_keyboard_hwids =
630 Some(parsed_hwids);
631 }
632 }
633 "windows-interception-keyboard-hwids-exclude" => {
634 #[cfg(any(
635 all(feature = "interception_driver", target_os = "windows"),
636 target_os = "unknown"
637 ))]
638 {
639 if cfg
640 .wintercept_opts
641 .windows_interception_keyboard_hwids
642 .is_some()
643 {
644 bail_expr!(
645 val,
646 "{label} and windows-interception-keyboard-hwid cannot both be used"
647 );
648 }
649 let parsed_hwids = sexpr_to_hwids_vec(
650 val,
651 label,
652 "entry in windows-interception-keyboard-hwids-exclude",
653 )?;
654 cfg.wintercept_opts
655 .windows_interception_keyboard_hwids_exclude = Some(parsed_hwids);
656 }
657 }
658 "macos-dev-names-include" => {
659 #[cfg(any(target_os = "macos", target_os = "unknown"))]
660 {
661 let dev_names = parse_dev(val)?;
662 if dev_names.is_empty() {
663 log::warn!("macos-dev-names-include is empty");
664 }
665 cfg.macos_opts.macos_dev_names_include = Some(dev_names);
666 }
667 }
668 "macos-dev-names-exclude" => {
669 #[cfg(any(target_os = "macos", target_os = "unknown"))]
670 {
671 let dev_names = parse_dev(val)?;
672 if dev_names.is_empty() {
673 log::warn!("macos-dev-names-exclude is empty");
674 }
675 cfg.macos_opts.macos_dev_names_exclude = Some(dev_names);
676 }
677 }
678 "tray-icon" => {
679 #[cfg(all(
680 any(target_os = "windows", target_os = "unknown"),
681 feature = "gui"
682 ))]
683 {
684 let icon_path = sexpr_to_str_or_err(val, label)?;
685 if icon_path.is_empty() {
686 log::warn!("tray-icon is empty");
687 }
688 cfg.gui_opts.tray_icon = Some(icon_path.to_string());
689 }
690 }
691 "icon-match-layer-name" => {
692 #[cfg(all(
693 any(target_os = "windows", target_os = "unknown"),
694 feature = "gui"
695 ))]
696 {
697 cfg.gui_opts.icon_match_layer_name = parse_defcfg_val_bool(val, label)?
698 }
699 }
700 "tooltip-layer-changes" => {
701 #[cfg(all(
702 any(target_os = "windows", target_os = "unknown"),
703 feature = "gui"
704 ))]
705 {
706 cfg.gui_opts.tooltip_layer_changes = parse_defcfg_val_bool(val, label)?
707 }
708 }
709 "tooltip-show-blank" => {
710 #[cfg(all(
711 any(target_os = "windows", target_os = "unknown"),
712 feature = "gui"
713 ))]
714 {
715 cfg.gui_opts.tooltip_show_blank = parse_defcfg_val_bool(val, label)?
716 }
717 }
718 "tooltip-no-base" => {
719 #[cfg(all(
720 any(target_os = "windows", target_os = "unknown"),
721 feature = "gui"
722 ))]
723 {
724 cfg.gui_opts.tooltip_no_base = parse_defcfg_val_bool(val, label)?
725 }
726 }
727 "tooltip-duration" => {
728 #[cfg(all(
729 any(target_os = "windows", target_os = "unknown"),
730 feature = "gui"
731 ))]
732 {
733 cfg.gui_opts.tooltip_duration = parse_cfg_val_u16(val, label, false)?
734 }
735 }
736 "notify-cfg-reload" => {
737 #[cfg(all(
738 any(target_os = "windows", target_os = "unknown"),
739 feature = "gui"
740 ))]
741 {
742 cfg.gui_opts.notify_cfg_reload = parse_defcfg_val_bool(val, label)?
743 }
744 }
745 "notify-cfg-reload-silent" => {
746 #[cfg(all(
747 any(target_os = "windows", target_os = "unknown"),
748 feature = "gui"
749 ))]
750 {
751 cfg.gui_opts.notify_cfg_reload_silent =
752 parse_defcfg_val_bool(val, label)?
753 }
754 }
755 "notify-error" => {
756 #[cfg(all(
757 any(target_os = "windows", target_os = "unknown"),
758 feature = "gui"
759 ))]
760 {
761 cfg.gui_opts.notify_error = parse_defcfg_val_bool(val, label)?
762 }
763 }
764 "tooltip-size" => {
765 #[cfg(all(
766 any(target_os = "windows", target_os = "unknown"),
767 feature = "gui"
768 ))]
769 {
770 let v = sexpr_to_str_or_err(val, label)?;
771 let tooltip_size = v.split(',').collect::<Vec<_>>();
772 const ERRMSG: &str = "Invalid value for tooltip-size.\nExpected two numbers 0-65535 separated by a comma, e.g. 24,24";
773 if tooltip_size.len() != 2 {
774 bail_expr!(val, "{}", ERRMSG)
775 }
776 cfg.gui_opts.tooltip_size = (
777 match str::parse::<u16>(tooltip_size[0]) {
778 Ok(w) => w,
779 Err(_) => bail_expr!(val, "{}", ERRMSG),
780 },
781 match str::parse::<u16>(tooltip_size[1]) {
782 Ok(h) => h,
783 Err(_) => bail_expr!(val, "{}", ERRMSG),
784 },
785 );
786 }
787 }
788
789 "process-unmapped-keys" => {
790 is_process_unmapped_keys_defined = true;
791 if let Some(list) = val.list(None) {
792 let err = "Expected (all-except key1 ... keyN).";
793 if list.len() < 2 {
794 bail_expr!(val, "{err}");
795 }
796 match list[0].atom(None) {
797 Some("all-except") => {}
798 _ => {
799 bail_expr!(val, "{err}");
800 }
801 };
802 let mut key_exceptions: Vec<(OsCode, SExpr)> = vec![];
806 for key_expr in list[1..].iter() {
807 let key = key_expr.atom(None).and_then(str_to_oscode).ok_or_else(
808 || anyhow_expr!(key_expr, "Expected a known key name."),
809 )?;
810 if key_exceptions.iter().any(|k_exc| k_exc.0 == key) {
811 bail_expr!(key_expr, "Duplicate key name is not allowed.");
812 }
813 key_exceptions.push((key, key_expr.clone()));
814 }
815 cfg.process_unmapped_keys = true;
816 cfg.process_unmapped_keys_exceptions = Some(key_exceptions);
817 } else {
818 cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)?
819 }
820 }
821
822 "block-unmapped-keys" => {
823 cfg.block_unmapped_keys = parse_defcfg_val_bool(val, label)?
824 }
825 "allow-hardware-repeat" => {
826 cfg.allow_hardware_repeat = parse_defcfg_val_bool(val, label)?
827 }
828 "alias-to-trigger-on-load" => {
829 cfg.start_alias = parse_defcfg_val_string(val, label)?
830 }
831 "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, label)?,
832 "sequence-backtrack-modcancel" => {
833 cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, label)?
834 }
835 "log-layer-changes" => {
836 cfg.log_layer_changes = parse_defcfg_val_bool(val, label)?
837 }
838 "delegate-to-first-layer" => {
839 cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, label)?;
840 if cfg.delegate_to_first_layer {
841 log::info!(
842 "delegating transparent keys on other layers to first defined layer"
843 );
844 }
845 }
846 "linux-continue-if-no-devs-found" => {
847 #[cfg(any(
848 target_os = "linux",
849 target_os = "android",
850 target_os = "unknown"
851 ))]
852 {
853 cfg.linux_opts.linux_continue_if_no_devs_found =
854 parse_defcfg_val_bool(val, label)?
855 }
856 }
857 "movemouse-smooth-diagonals" => {
858 cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, label)?
859 }
860 "movemouse-inherit-accel-state" => {
861 cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)?
862 }
863 "override-release-on-activation" => {
864 cfg.override_release_on_activation = parse_defcfg_val_bool(val, label)?
865 }
866 "concurrent-tap-hold" => {
867 cfg.concurrent_tap_hold = parse_defcfg_val_bool(val, label)?
868 }
869 "rapid-event-delay" => {
870 cfg.rapid_event_delay = parse_cfg_val_u16(val, label, false)?
871 }
872 "transparent-key-resolution" => {
873 let v = sexpr_to_str_or_err(val, label)?;
874 cfg.trans_resolution_behavior_v2 = match v {
875 "to-base-layer" => false,
876 "layer-stack" => true,
877 _ => bail_expr!(
878 val,
879 "{label} got {}. It accepts: 'to-base-layer' or 'layer-stack'",
880 v
881 ),
882 };
883 }
884 "chords-v2-min-idle" | "chords-v2-min-idle-experimental" => {
885 if label == "chords-v2-min-idle-experimental" {
886 log::warn!(
887 "You should replace chords-v2-min-idle-experimental with chords-v2-min-idle\n\
888 Using -experimental will be invalid in the future."
889 )
890 }
891 let min_idle = parse_cfg_val_u16(val, label, true)?;
892 if min_idle < 5 {
893 bail_expr!(val, "{label} must be 5-65535");
894 }
895 cfg.chords_v2_min_idle = min_idle;
896 }
897 "mouse-movement-key" => {
898 #[cfg(any(
899 all(target_os = "windows", feature = "interception_driver"),
900 target_os = "linux",
901 target_os = "android",
902 target_os = "unknown"
903 ))]
904 {
905 if let Some(keystr) = parse_defcfg_val_string(val, label)? {
906 if let Some(key) = str_to_oscode(&keystr) {
907 cfg.mouse_movement_key = Some(key);
908 } else {
909 bail_expr!(val, "{label} not a recognised key code");
910 }
911 } else {
912 bail_expr!(val, "{label} not a string for a key code");
913 }
914 }
915 }
916 _ => bail_expr!(key, "Unknown defcfg option {}", label),
917 };
918 }
919 SExpr::List(_) => {
920 bail_expr!(key, "Lists are not allowed in as keys in defcfg");
921 }
922 }
923 }
924}
925
926fn parse_defcfg_val_string(expr: &SExpr, _label: &str) -> Result<Option<String>> {
927 match expr {
928 SExpr::Atom(v) => Ok(Some(v.t.clone())),
929 _ => Ok(None),
930 }
931}
932
933pub const FALSE_VALUES: [&str; 3] = ["no", "false", "0"];
934pub const TRUE_VALUES: [&str; 3] = ["yes", "true", "1"];
935pub const BOOLEAN_VALUES: [&str; 6] = ["yes", "true", "1", "no", "false", "0"];
936
937fn parse_defcfg_val_bool(expr: &SExpr, label: &str) -> Result<bool> {
938 match &expr {
939 SExpr::Atom(v) => {
940 let val = v.t.trim_atom_quotes().to_ascii_lowercase();
941 if TRUE_VALUES.contains(&val.as_str()) {
942 Ok(true)
943 } else if FALSE_VALUES.contains(&val.as_str()) {
944 Ok(false)
945 } else {
946 bail_expr!(
947 expr,
948 "The value for {label} must be one of: {}",
949 BOOLEAN_VALUES.join(", ")
950 );
951 }
952 }
953 SExpr::List(_) => {
954 bail_expr!(
955 expr,
956 "The value for {label} cannot be a list, it must be one of: {}",
957 BOOLEAN_VALUES.join(", "),
958 )
959 }
960 }
961}
962
963fn parse_cfg_val_u16(expr: &SExpr, label: &str, exclude_zero: bool) -> Result<u16> {
964 let start = if exclude_zero { 1 } else { 0 };
965 match &expr {
966 SExpr::Atom(v) => Ok(str::parse::<u16>(v.t.trim_atom_quotes())
967 .ok()
968 .and_then(|u| {
969 if exclude_zero && u == 0 {
970 None
971 } else {
972 Some(u)
973 }
974 })
975 .ok_or_else(|| anyhow_expr!(expr, "{label} must be {start}-65535"))?),
976 SExpr::List(_) => {
977 bail_expr!(
978 expr,
979 "The value for {label} cannot be a list, it must be a number {start}-65535",
980 )
981 }
982 }
983}
984
985pub fn parse_colon_separated_text(paths: &str) -> Vec<String> {
986 let mut all_paths = vec![];
987 let mut full_dev_path = String::new();
988 let mut dev_path_iter = paths.split(':').peekable();
989 while let Some(dev_path) = dev_path_iter.next() {
990 if dev_path.ends_with('\\') && dev_path_iter.peek().is_some() {
991 full_dev_path.push_str(dev_path.trim_end_matches('\\'));
992 full_dev_path.push(':');
993 continue;
994 } else {
995 full_dev_path.push_str(dev_path);
996 }
997 all_paths.push(full_dev_path.clone());
998 full_dev_path.clear();
999 }
1000 all_paths.shrink_to_fit();
1001 all_paths
1002}
1003
1004#[cfg(any(
1005 target_os = "linux",
1006 target_os = "android",
1007 target_os = "macos",
1008 target_os = "unknown"
1009))]
1010pub fn parse_dev(val: &SExpr) -> Result<Vec<String>> {
1011 Ok(match val {
1012 SExpr::Atom(a) => {
1013 let devs = parse_colon_separated_text(a.t.trim_atom_quotes());
1014 if devs.len() == 1 && devs[0].is_empty() {
1015 bail_expr!(val, "an empty string is not a valid device name or path")
1016 }
1017 devs
1018 }
1019 SExpr::List(l) => {
1020 let r: Result<Vec<String>> =
1021 l.t.iter()
1022 .try_fold(Vec::with_capacity(l.t.len()), |mut acc, expr| match expr {
1023 SExpr::Atom(path) => {
1024 let trimmed_path = path.t.trim_atom_quotes().to_string();
1025 if trimmed_path.is_empty() {
1026 bail_span!(
1027 path,
1028 "an empty string is not a valid device name or path"
1029 )
1030 }
1031 acc.push(trimmed_path);
1032 Ok(acc)
1033 }
1034 SExpr::List(inner_list) => {
1035 bail_span!(inner_list, "expected strings, found a list")
1036 }
1037 });
1038
1039 r?
1040 }
1041 })
1042}
1043
1044fn sexpr_to_str_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a str> {
1045 match expr {
1046 SExpr::Atom(a) => Ok(a.t.trim_atom_quotes()),
1047 SExpr::List(_) => bail_expr!(expr, "The value for {label} can't be a list"),
1048 }
1049}
1050
1051#[cfg(any(
1052 all(feature = "interception_driver", target_os = "windows"),
1053 target_os = "unknown"
1054))]
1055fn sexpr_to_list_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a [SExpr]> {
1056 match expr {
1057 SExpr::Atom(_) => bail_expr!(expr, "The value for {label} must be a list"),
1058 SExpr::List(l) => Ok(&l.t),
1059 }
1060}
1061
1062#[cfg(any(
1063 all(feature = "interception_driver", target_os = "windows"),
1064 target_os = "unknown"
1065))]
1066fn sexpr_to_hwids_vec(
1067 val: &SExpr,
1068 label: &str,
1069 entry_label: &str,
1070) -> Result<Vec<[u8; HWID_ARR_SZ]>> {
1071 let hwids = sexpr_to_list_or_err(val, label)?;
1072 let mut parsed_hwids = vec![];
1073 for hwid_expr in hwids.iter() {
1074 let hwid = sexpr_to_str_or_err(hwid_expr, entry_label)?;
1075 log::trace!("win hwid: {hwid}");
1076 let hwid_vec = hwid
1077 .split(',')
1078 .try_fold(vec![], |mut hwid_bytes, hwid_byte| {
1079 hwid_byte.trim_matches(' ').parse::<u8>().map(|b| {
1080 hwid_bytes.push(b);
1081 hwid_bytes
1082 })
1083 }).map_err(|_| anyhow_expr!(hwid_expr, "Entry in {label} is invalid. Entries should be numbers [0,255] separated by commas"))?;
1084 let hwid_slice = hwid_vec.iter().copied().enumerate()
1085 .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| {
1086 let (i, b) = idx_byte;
1087 if i > HWID_ARR_SZ {
1088 bail_expr!(hwid_expr, "entry in {label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers")
1089 }
1090 hwid[i] = b;
1091 Ok(hwid)
1092 });
1093 parsed_hwids.push(hwid_slice?);
1094 }
1095 parsed_hwids.shrink_to_fit();
1096 Ok(parsed_hwids)
1097}
1098
1099#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
1100#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1101pub struct KeyRepeatSettings {
1102 pub delay: u16,
1103 pub rate: u16,
1104}
1105
1106#[cfg(any(target_os = "linux", target_os = "android", target_os = "unknown"))]
1107#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1108pub enum UnicodeTermination {
1109 Enter,
1110 Space,
1111 SpaceEnter,
1112 EnterSpace,
1113}
1114
1115#[cfg(any(target_os = "windows", target_os = "unknown"))]
1116#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1117pub enum AltGrBehaviour {
1118 #[default]
1119 DoNothing,
1120 CancelLctlPress,
1121 AddLctlRelease,
1122}
1123
1124#[cfg(any(target_os = "windows", target_os = "unknown"))]
1125#[cfg(any(
1126 all(feature = "interception_driver", target_os = "windows"),
1127 target_os = "unknown"
1128))]
1129pub const HWID_ARR_SZ: usize = 1024;
1130
1131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1132pub enum ReplayDelayBehaviour {
1133 Constant,
1137 Recorded,
1140}