diff --git a/cfg_samples/kanata.kbd b/cfg_samples/kanata.kbd index b1fdbd509..246d4f844 100644 --- a/cfg_samples/kanata.kbd +++ b/cfg_samples/kanata.kbd @@ -55,6 +55,15 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; kanata does not parse it as multiple devices. ;; linux-dev /dev/input/path-to\:device + ;; Alternatively, you can use list syntax, where both backslashes and colons + ;; are parsed literally. List items are separated by spaces or newlines. + ;; Using quotation marks for each item is optional, and only required if an + ;; item contains spaces. + ;; linux-dev ( + ;; /dev/input/by-path/pci-0000:00:14.0-usb-0:1:1.0-event + ;; /dev/input/by-id/usb-Dell_Dell_USB_Keyboard-event-kbd + ;; ) + ;; The linux-dev-names-include entry is parsed identically to linux-dev. It ;; defines a list of device names that should be included. This is only ;; used if linux-dev is omitted. @@ -645,10 +654,10 @@ If you need help, please feel welcome to ask in the GitHub discussions. ;; This example is similar to the default caps-word behaviour but it moves the ;; 0-9 keys to capitalized key list from the extra non-terminating key list. cwc (caps-word-custom - 2000 - (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) - (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) - ) + 2000 + (a b c d e f g h i j k l m n o p q r s t u v w x y z 0 1 2 3 4 5 6 7 8 9) + (kp0 kp1 kp2 kp3 kp4 kp5 kp6 kp7 kp8 kp9 bspc del up down left rght) + ) ) ;; Can see a new action `rpt` in this layer. This repeats the most recently diff --git a/docs/config.adoc b/docs/config.adoc index bab770f19..6736ea9f2 100644 --- a/docs/config.adoc +++ b/docs/config.adoc @@ -461,7 +461,6 @@ The default length limit is 128 keys. [[linux-only-linux-dev]] === Linux only: linux-dev <> - By default, kanata will try to detect which input devices are keyboards and try to intercept them all. However, you may specify exact keyboard devices from the `/dev/input` directories using the `linux-dev` configuration. @@ -495,6 +494,21 @@ its file name, you must escape those colons with backslashes: ) ---- +Alternatively, you can use list syntax, where both backslashes and colons +are parsed literally. List items are separated by spaces or newlines. +Using quotation marks for each item is optional, and only required if an +item contains spaces. + +[source] +---- +(defcfg + linux-dev ( + /dev/input/path:to:device + "/dev/input/path to device" + ) +) +---- + [[linux-only-linux-dev-names-include]] === Linux only: linux-dev-names-include <> @@ -514,7 +528,10 @@ registering /dev/input/eventX: "Name goes here" [source] ---- (defcfg - linux-dev-names-include "Device 1 name:Device \:2\: Name" + linux-dev-names-include ( + "Device name 1" + "Device name 2" + ) ) ---- @@ -526,7 +543,7 @@ In the case that `linux-dev` is omitted, this option defines a list of device names that should be excluded. This option is parsed identically to `linux-dev`. -The `linux-dev-names-include and `linux-dev-names-exclude` options +The `linux-dev-names-include` and `linux-dev-names-exclude` options are not mutually exclusive but in practice it probably only makes sense to use one and not both. @@ -534,7 +551,10 @@ but in practice it probably only makes sense to use one and not both. [source] ---- (defcfg - linux-dev-names-exclude "Device 1 name:Device \:2\: Name" + linux-dev-names-exclude ( + "Device Name 1" + "Device Name 2" + ) ) ---- @@ -704,9 +724,9 @@ a non-applicable operating system. movemouse-inherit-accel-state yes movemouse-smooth-diagonals yes dynamic-macro-max-presses 1000 - linux-dev /dev/input/dev1:/dev/input/dev2 - linux-dev-names-include "Name 1:Name 2" - linux-dev-names-exclude "Name 3:Name 4" + linux-dev (/dev/input/dev1 /dev/input/dev2) + linux-dev-names-include ("Name 1" "Name 2") + linux-dev-names-exclude ("Name 3" "Name 4") linux-continue-if-no-devs-found yes linux-unicode-u-code v linux-unicode-termination space diff --git a/parser/src/cfg/defcfg.rs b/parser/src/cfg/defcfg.rs index 62b3f93cf..071d01246 100644 --- a/parser/src/cfg/defcfg.rs +++ b/parser/src/cfg/defcfg.rs @@ -4,7 +4,7 @@ use super::HashSet; use crate::cfg::check_first_expr; use crate::custom_action::*; #[allow(unused)] -use crate::{anyhow_expr, anyhow_span, bail, bail_expr}; +use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span}; #[derive(Debug)] pub struct CfgOptions { @@ -96,65 +96,74 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { Some(v) => v, None => bail_expr!(key, "Found a defcfg option missing a value"), }; - match (&key, &val) { - (SExpr::Atom(k), SExpr::Atom(v)) => { - if !seen_keys.insert(&k.t) { - bail_expr!(key, "Duplicate defcfg option {}", k.t); + match key { + SExpr::Atom(k) => { + let label = k.t.as_str(); + if !seen_keys.insert(label) { + bail_expr!(key, "Duplicate defcfg option {}", label); } - match k.t.as_str() { - k @ "sequence-timeout" => { - cfg.sequence_timeout = parse_cfg_val_u16(val, k, true)?; + match label { + "sequence-timeout" => { + cfg.sequence_timeout = parse_cfg_val_u16(val, label, true)?; } "sequence-input-mode" => { - cfg.sequence_input_mode = - SequenceInputMode::try_from_str(v.t.trim_matches('"')) - .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; + let v = sexpr_to_str_or_err(val, label)?; + cfg.sequence_input_mode = SequenceInputMode::try_from_str(v) + .map_err(|e| anyhow_expr!(val, "{}", e.to_string()))?; } - k @ "dynamic-macro-max-presses" => { - cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, k, false)?; + "dynamic-macro-max-presses" => { + cfg.dynamic_macro_max_presses = parse_cfg_val_u16(val, label, false)?; } "linux-dev" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev = parse_colon_separated_text(paths); + cfg.linux_dev = parse_linux_dev(val)?; + if cfg.linux_dev.is_empty() { + bail_expr!( + val, + "device list is empty, no devices will be intercepted" + ); + } } } "linux-dev-names-include" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev_names_include = Some(parse_colon_separated_text(paths)); + cfg.linux_dev_names_include = Some(parse_linux_dev(val)?); + if cfg.linux_dev.is_empty() { + log::warn!("linux-dev-names-include is empty"); + } } } "linux-dev-names-exclude" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let paths = v.t.trim_matches('"'); - cfg.linux_dev_names_exclude = Some(parse_colon_separated_text(paths)); + cfg.linux_dev_names_exclude = Some(parse_linux_dev(val)?); } } - _k @ "linux-unicode-u-code" => { + "linux-unicode-u-code" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_unicode_u_code = crate::keys::str_to_oscode( - v.t.trim_matches('"'), - ) - .ok_or_else(|| anyhow_expr!(val, "unknown code for {_k}: {}", v.t))?; + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_unicode_u_code = + crate::keys::str_to_oscode(v).ok_or_else(|| { + anyhow_expr!(val, "unknown code for {label}: {}", v) + })?; } } - _k @ "linux-unicode-termination" => { + "linux-unicode-termination" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_unicode_termination = match v.t.trim_matches('"') { + let v = sexpr_to_str_or_err(val, label)?; + cfg.linux_unicode_termination = match v { "enter" => UnicodeTermination::Enter, "space" => UnicodeTermination::Space, "enter-space" => UnicodeTermination::EnterSpace, "space-enter" => UnicodeTermination::SpaceEnter, _ => bail_expr!( val, - "{_k} got {}. It accepts: enter|space|enter-space|space-enter", - v.t + "{label} got {}. It accepts: enter|space|enter-space|space-enter", + v ), } } @@ -162,7 +171,8 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-x11-repeat-delay-rate" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - let delay_rate = v.t.trim_matches('"').split(',').collect::>(); + let v = sexpr_to_str_or_err(val, label)?; + let delay_rate = v.split(',').collect::>(); const ERRMSG: &str = "Invalid value for linux-x11-repeat-delay-rate.\nExpected two numbers 0-65535 separated by a comma, e.g. 200,25"; if delay_rate.len() != 2 { bail_expr!(val, "{}", ERRMSG) @@ -179,31 +189,33 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { }); } } - _k @ "windows-altgr" => { + "windows-altgr" => { #[cfg(any(target_os = "windows", target_os = "unknown"))] { const CANCEL: &str = "cancel-lctl-press"; const ADD: &str = "add-lctl-release"; - cfg.windows_altgr = match v.t.trim_matches('"') { + let v = sexpr_to_str_or_err(val, label)?; + cfg.windows_altgr = match v { CANCEL => AltGrBehaviour::CancelLctlPress, ADD => AltGrBehaviour::AddLctlRelease, _ => bail_expr!( val, - "Invalid value for {_k}: {}. Valid values are {},{}", - v.t, + "Invalid value for {label}: {}. Valid values are {},{}", + v, CANCEL, ADD ), } } } - _k @ "windows-interception-mouse-hwid" => { + "windows-interception-mouse-hwid" => { #[cfg(any( all(feature = "interception_driver", target_os = "windows"), target_os = "unknown" ))] { - let hwid = v.t.trim_matches('"'); + let v = sexpr_to_str_or_err(val, label)?; + let hwid = v; log::trace!("win hwid: {hwid}"); let hwid_vec = hwid .split(',') @@ -212,12 +224,12 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { hwid_bytes.push(b); hwid_bytes }) - }).map_err(|_| anyhow_expr!(val, "{_k} format is invalid. It should consist of integers separated by commas"))?; + }).map_err(|_| anyhow_expr!(val, "{label} format is invalid. It should consist of integers separated by commas"))?; let hwid_slice = hwid_vec.iter().copied().enumerate() .try_fold([0u8; HWID_ARR_SZ], |mut hwid, idx_byte| { let (i, b) = idx_byte; if i > HWID_ARR_SZ { - bail_expr!(val, "{_k} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") + bail_expr!(val, "{label} is too long; it should be up to {HWID_ARR_SZ} 8-bit unsigned integers") } hwid[i] = b; Ok(hwid) @@ -227,17 +239,17 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { } "process-unmapped-keys" => { - cfg.process_unmapped_keys = parse_defcfg_val_bool(val, &k.t)? + cfg.process_unmapped_keys = parse_defcfg_val_bool(val, label)? } - "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, &k.t)?, + "danger-enable-cmd" => cfg.enable_cmd = parse_defcfg_val_bool(val, label)?, "sequence-backtrack-modcancel" => { - cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, &k.t)? + cfg.sequence_backtrack_modcancel = parse_defcfg_val_bool(val, label)? } "log-layer-changes" => { - cfg.log_layer_changes = parse_defcfg_val_bool(val, &k.t)? + cfg.log_layer_changes = parse_defcfg_val_bool(val, label)? } "delegate-to-first-layer" => { - cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, &k.t)?; + cfg.delegate_to_first_layer = parse_defcfg_val_bool(val, label)?; if cfg.delegate_to_first_layer { log::info!("delegating transparent keys on other layers to first defined layer"); } @@ -245,23 +257,20 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result { "linux-continue-if-no-devs-found" => { #[cfg(any(target_os = "linux", target_os = "unknown"))] { - cfg.linux_continue_if_no_devs_found = parse_defcfg_val_bool(val, &k.t)? + cfg.linux_continue_if_no_devs_found = parse_defcfg_val_bool(val, label)? } } "movemouse-smooth-diagonals" => { - cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, &k.t)? + cfg.movemouse_smooth_diagonals = parse_defcfg_val_bool(val, label)? } "movemouse-inherit-accel-state" => { - cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, &k.t)? + cfg.movemouse_inherit_accel_state = parse_defcfg_val_bool(val, label)? } - _ => bail_expr!(key, "Unknown defcfg option {}", &k.t), + _ => bail_expr!(key, "Unknown defcfg option {}", label), }; } - (SExpr::List(_), _) => { - bail_expr!(key, "Lists are not allowed in defcfg"); - } - (_, SExpr::List(_)) => { - bail_expr!(val, "Lists are not allowed in defcfg"); + SExpr::List(_) => { + bail_expr!(key, "Lists are not allowed in as keys in defcfg"); } } } @@ -338,6 +347,48 @@ pub fn parse_colon_separated_text(paths: &str) -> Vec { all_paths } +#[cfg(any(target_os = "linux", target_os = "unknown"))] +pub fn parse_linux_dev(val: &SExpr) -> Result> { + Ok(match val { + SExpr::Atom(a) => { + let devs = parse_colon_separated_text(a.t.trim_matches('"')); + if devs.len() == 1 && devs[0].is_empty() { + bail_expr!(val, "an empty string is not a valid device name or path") + } + devs + } + SExpr::List(l) => { + let r: Result> = + l.t.iter() + .try_fold(Vec::with_capacity(l.t.len()), |mut acc, expr| match expr { + SExpr::Atom(path) => { + let trimmed_path = path.t.trim_matches('"').to_string(); + if trimmed_path.is_empty() { + bail_span!( + &path, + "an empty string is not a valid device name or path" + ) + } + acc.push(trimmed_path); + Ok(acc) + } + SExpr::List(inner_list) => { + bail_span!(&inner_list, "expected strings, found a list") + } + }); + + r? + } + }) +} + +fn sexpr_to_str_or_err<'a>(expr: &'a SExpr, label: &str) -> Result<&'a str> { + match expr { + SExpr::Atom(a) => Ok(a.t.trim_matches('"')), + SExpr::List(_) => bail_expr!(expr, "The value for {label} can't be a list"), + } +} + #[cfg(any(target_os = "linux", target_os = "unknown"))] #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct KeyRepeatSettings { diff --git a/parser/src/cfg/tests.rs b/parser/src/cfg/tests.rs index 627a1fd14..14a5787a5 100644 --- a/parser/src/cfg/tests.rs +++ b/parser/src/cfg/tests.rs @@ -1,5 +1,6 @@ use super::*; -use crate::cfg::sexpr::parse; +#[allow(unused_imports)] +use crate::cfg::sexpr::{parse, Span}; use kanata_keyberon::action::BooleanOperator::*; use std::sync::Mutex; @@ -1201,11 +1202,83 @@ fn list_action_not_in_list_error_message_is_good() { #[test] fn parse_device_paths() { + assert_eq!(parse_colon_separated_text(""), [""]); + assert_eq!(parse_colon_separated_text("device1"), ["device1"]); assert_eq!(parse_colon_separated_text("h:w"), ["h", "w"]); assert_eq!(parse_colon_separated_text("h\\:w"), ["h:w"]); assert_eq!(parse_colon_separated_text("h\\:w\\"), ["h:w\\"]); } +#[test] +#[cfg(any(target_os = "linux", target_os = "unknown"))] +fn test_parse_linux_dev() { + // The old colon separated devices format + assert_eq!( + parse_linux_dev(&SExpr::Atom(Spanned { + t: "\"Keyboard2:Input Device 1:pci-0000\\:00\\:14.0-usb-0\\:1\\:1.0-event\"" + .to_string(), + span: Span::default(), + })) + .expect("succeeds"), + [ + "Keyboard2", + "Input Device 1", + "pci-0000:00:14.0-usb-0:1:1.0-event" + ] + ); + parse_linux_dev(&SExpr::Atom(Spanned { + t: "\"\"".to_string(), + span: Span::default(), + })) + .expect_err("'' is not a valid device name/path, this should fail"); + + // The new device list format + assert_eq!( + parse_linux_dev(&SExpr::List(Spanned { + t: vec![ + SExpr::Atom(Spanned { + t: "Keyboard2".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: "\"Input Device 1\"".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: "pci-0000:00:14.0-usb-0:1:1.0-event".to_string(), + span: Span::default(), + }), + SExpr::Atom(Spanned { + t: r"backslashes\do\not\escape\:\anything".to_string(), + span: Span::default(), + }), + ], + span: Span::default(), + })) + .expect("succeeds"), + [ + "Keyboard2", + "Input Device 1", + "pci-0000:00:14.0-usb-0:1:1.0-event", + r"backslashes\do\not\escape\:\anything" + ] + ); + parse_linux_dev(&SExpr::List(Spanned { + t: vec![ + SExpr::Atom(Spanned { + t: "Device1".to_string(), + span: Span::default(), + }), + SExpr::List(Spanned { + t: vec![], + span: Span::default(), + }), + ], + span: Span::default(), + })) + .expect_err("nested lists in path list shouldn't be allowed"); +} + #[test] fn parse_all_defcfg() { let _lk = match CFG_PARSE_LOCK.lock() {