kanata_parser/cfg/
defcfg.rs

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            // historically was the only option, so make KEY_U the default
48            linux_unicode_u_code: crate::keys::OsCode::KEY_U,
49            // historically was the only option, so make Enter the default
50            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    /// File name / path to the tray icon file.
96    pub tray_icon: Option<String>,
97    /// Whether to match layer names to icon files without an explicit 'icon' field
98    pub icon_match_layer_name: bool,
99    /// Show tooltip on layer changes showing layer icons
100    pub tooltip_layer_changes: bool,
101    /// Show tooltip on layer changes for the default/base layer
102    pub tooltip_no_base: bool,
103    /// Show tooltip on layer changes even for layers without an icon
104    pub tooltip_show_blank: bool,
105    /// Show tooltip on layer changes for this duration (ms)
106    pub tooltip_duration: u16,
107    /// Show system notification message on config reload
108    pub notify_cfg_reload: bool,
109    /// Disable sound for the system notification message on config reload
110    pub notify_cfg_reload_silent: bool,
111    /// Show system notification message on errors
112    pub notify_error: bool,
113    /// Set tooltip size (width, height)
114    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
227/// Parse configuration entries from an expression starting with defcfg.
228pub 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    // Read k-v pairs from the configuration
234    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                            // Note: deflocalkeys should already be parsed when parsing defcfg,
803                            // so can use safely use str_to_oscode here; it will include user
804                            // configurations already.
805                            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    /// Always use a fixed number of ticks between presses and releases.
1134    /// This is the original kanata behaviour.
1135    /// This means that held action activations like in tap-hold do not behave as intended.
1136    Constant,
1137    /// Use the recorded number of ticks between presses and releases.
1138    /// This is newer behaviour.
1139    Recorded,
1140}