Skip to content

Commit

Permalink
feat: allow using a list as linux dev value (#647)
Browse files Browse the repository at this point in the history
This allows a user to define devices using a sexpr list.

The old way (colon separated string) is not removed for compatibility.
  • Loading branch information
rszyma authored Dec 2, 2023
1 parent 3bd6810 commit 9e5c8d9
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 63 deletions.
17 changes: 13 additions & 4 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
34 changes: 27 additions & 7 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,6 @@ The default length limit is 128 keys.
[[linux-only-linux-dev]]
=== Linux only: linux-dev
<<table-of-contents,Back to ToC>>

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.
Expand Down Expand Up @@ -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
<<table-of-contents,Back to ToC>>
Expand All @@ -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"
)
)
----

Expand All @@ -526,15 +543,18 @@ 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.

.Example:
[source]
----
(defcfg
linux-dev-names-exclude "Device 1 name:Device \:2\: Name"
linux-dev-names-exclude (
"Device Name 1"
"Device Name 2"
)
)
----

Expand Down Expand Up @@ -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
Expand Down
153 changes: 102 additions & 51 deletions parser/src/cfg/defcfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -96,73 +96,83 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
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
),
}
}
}
"linux-x11-repeat-delay-rate" => {
#[cfg(any(target_os = "linux", target_os = "unknown"))]
{
let delay_rate = v.t.trim_matches('"').split(',').collect::<Vec<_>>();
let v = sexpr_to_str_or_err(val, label)?;
let delay_rate = v.split(',').collect::<Vec<_>>();
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)
Expand All @@ -179,31 +189,33 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
});
}
}
_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(',')
Expand All @@ -212,12 +224,12 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
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)
Expand All @@ -227,41 +239,38 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
}

"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");
}
}
"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");
}
}
}
Expand Down Expand Up @@ -338,6 +347,48 @@ pub fn parse_colon_separated_text(paths: &str) -> Vec<String> {
all_paths
}

#[cfg(any(target_os = "linux", target_os = "unknown"))]
pub fn parse_linux_dev(val: &SExpr) -> Result<Vec<String>> {
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<Vec<String>> =
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 {
Expand Down
Loading

0 comments on commit 9e5c8d9

Please sign in to comment.