kanata_parser/cfg/
defcfg.rs

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            // 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 = "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 = "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
225/// Parse configuration entries from an expression starting with defcfg.
226pub 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    // Read k-v pairs from the configuration
232    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                            // Note: deflocalkeys should already be parsed when parsing defcfg,
736                            // so can use safely use str_to_oscode here; it will include user
737                            // configurations already.
738                            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    /// Always use a fixed number of ticks between presses and releases.
1058    /// This is the original kanata behaviour.
1059    /// This means that held action activations like in tap-hold do not behave as intended.
1060    Constant,
1061    /// Use the recorded number of ticks between presses and releases.
1062    /// This is newer behaviour.
1063    Recorded,
1064}