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