Skip to content

Commit

Permalink
feat(linux): add linux-device-detect-mode to defcfg (#1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtroo authored Oct 19, 2024
1 parent 3644819 commit 4e53544
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 45 deletions.
7 changes: 7 additions & 0 deletions cfg_samples/kanata.kbd
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ If you need help, please feel welcome to ask in the GitHub discussions.
;;
;; linux-continue-if-no-devs-found yes

;; Kanata on Linux automatically detects and grabs input devices
;; when none of the explicit device configurations are in use.
;; In case kanata is undesirably grabbing mouse-like devices,
;; you can use a configuration item to change detection behaviour.
;;
;; linux-device-detect-mode keyboard-only

;; On Linux, you can ask kanata to run `xset r rate <delay> <rate>` on startup
;; and on live reload via the config below. The first number is the delay in ms
;; and the second number is the repeat rate in repeats/second.
Expand Down
35 changes: 35 additions & 0 deletions docs/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,41 @@ this behaviour by setting `linux-continue-if-no-devs-found`.
)
----

[[linux-only-linux-device-detect-mode]]
=== Linux only: linux-device-detect-mode
<<table-of-contents,Back to ToC>>

Kanata on Linux automatically detects and grabs input devices
when none of the explicit device configurations are in use.
In case kanata is undesirably grabbing mouse-like devices,
you can use a configuration item to change detection behaviour.

The configuration is `linux-device-detect-mode` and it has the options:

[cols="1,2"]
|===
| `keyboard-only`
| Grab devices that seem to be a keyboard only.

| `keyboard-mice`
| Grab devices that seem to be a keyboard only
and devices that declare **both** keyboard and mouse functionality.

| `any`
| Grab all keyboard-like and mouse-like devices.
|===

The default behaviour is:

[cols="1,2"]
|===
| When any mouse buttons or mouse scroll events are in `defsrc`:
| `any`

| Otherwise:
| `keyboard-mice`
|===

[[linux-only-linux-unicode-u-code]]
=== Linux only: linux-unicode-u-code
<<table-of-contents,Back to ToC>>
Expand Down
33 changes: 33 additions & 0 deletions parser/src/cfg/defcfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ use crate::custom_action::*;
#[allow(unused)]
use crate::{anyhow_expr, anyhow_span, bail, bail_expr, bail_span};

#[cfg(any(target_os = "linux", target_os = "unknown"))]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DeviceDetectMode {
KeyboardOnly,
KeyboardMice,
Any,
}
#[cfg(any(target_os = "linux", target_os = "unknown"))]
impl std::fmt::Display for DeviceDetectMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}

#[cfg(any(target_os = "linux", target_os = "unknown"))]
#[derive(Debug, Clone)]
pub struct CfgLinuxOptions {
Expand All @@ -18,6 +32,7 @@ pub struct CfgLinuxOptions {
pub linux_x11_repeat_delay_rate: Option<KeyRepeatSettings>,
pub linux_use_trackpoint_property: bool,
pub linux_output_bus_type: LinuxCfgOutputBusType,
pub linux_device_detect_mode: Option<DeviceDetectMode>,
}
#[cfg(any(target_os = "linux", target_os = "unknown"))]
impl Default for CfgLinuxOptions {
Expand All @@ -34,6 +49,7 @@ impl Default for CfgLinuxOptions {
linux_x11_repeat_delay_rate: None,
linux_use_trackpoint_property: false,
linux_output_bus_type: LinuxCfgOutputBusType::BusI8042,
linux_device_detect_mode: None,
}
}
}
Expand Down Expand Up @@ -331,6 +347,23 @@ pub fn parse_defcfg(expr: &[SExpr]) -> Result<CfgOptions> {
cfg.linux_opts.linux_output_bus_type = bus_type;
}
}
"linux-device-detect-mode" => {
let detect_mode = sexpr_to_str_or_err(val, label)?;
match detect_mode {
"any" | "keyboard-only" | "keyboard-mice" => {},
_ => bail_expr!(val, "Invalid value for linux-device-detect-mode.\nExpected one of: any | keyboard-only | keyboard-mice"),
};
#[cfg(any(target_os = "linux", target_os = "unknown"))]
{
let detect_mode = Some(match detect_mode {
"any" => DeviceDetectMode::Any,
"keyboard-only" => DeviceDetectMode::KeyboardOnly,
"keyboard-mice" => DeviceDetectMode::KeyboardMice,
_ => unreachable!("validated earlier"),
});
cfg.linux_opts.linux_device_detect_mode = detect_mode;
}
}
"windows-altgr" => {
#[cfg(any(target_os = "windows", target_os = "unknown"))]
{
Expand Down
42 changes: 38 additions & 4 deletions parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,8 @@ pub fn parse_cfg_raw_string(
}
replace_custom_str_oscode_mapping(&local_keys.unwrap_or_default());

let cfg = root_exprs
#[allow(unused_mut)]
let mut cfg = root_exprs
.iter()
.find(gen_first_atom_filter("defcfg"))
.map(|cfg| parse_defcfg(cfg))
Expand Down Expand Up @@ -644,7 +645,14 @@ pub fn parse_cfg_raw_string(
"Exactly one defsrc is allowed, found more. Delete the extras."
)
}
let (mut mapped_keys, mapping_order) = parse_defsrc(src_expr, &cfg)?;
let (mut mapped_keys, mapping_order, _mouse_in_defsrc) = parse_defsrc(src_expr, &cfg)?;
#[cfg(any(target_os = "linux", target_os = "unknown"))]
if cfg.linux_opts.linux_device_detect_mode.is_none() {
cfg.linux_opts.linux_device_detect_mode = Some(match _mouse_in_defsrc {
MouseInDefsrc::MouseUsed => DeviceDetectMode::Any,
MouseInDefsrc::NoMouse => DeviceDetectMode::KeyboardMice,
});
}

let var_exprs = root_exprs
.iter()
Expand Down Expand Up @@ -1041,20 +1049,46 @@ fn parse_deflocalkeys(
Ok(localkeys)
}

#[derive(Debug, Copy, Clone)]
enum MouseInDefsrc {
MouseUsed,
NoMouse,
}

/// Parse mapped keys from an expression starting with defsrc. Returns the key mapping as well as
/// a vec of the indexes in order. The length of the returned vec should be matched by the length
/// of all layer declarations.
fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec<usize>)> {
fn parse_defsrc(
expr: &[SExpr],
defcfg: &CfgOptions,
) -> Result<(MappedKeys, Vec<usize>, MouseInDefsrc)> {
let exprs = check_first_expr(expr.iter(), "defsrc")?;
let mut mkeys = MappedKeys::default();
let mut ordered_codes = Vec::new();
let mut is_mouse_used = MouseInDefsrc::NoMouse;
for expr in exprs {
let s = match expr {
SExpr::Atom(a) => &a.t,
_ => bail_expr!(expr, "No lists allowed in defsrc"),
};
let oscode = str_to_oscode(s)
.ok_or_else(|| anyhow_expr!(expr, "Unknown key in defsrc: \"{}\"", s))?;
is_mouse_used = match (is_mouse_used, oscode) {
(
MouseInDefsrc::NoMouse,
OsCode::BTN_LEFT
| OsCode::BTN_RIGHT
| OsCode::BTN_MIDDLE
| OsCode::BTN_SIDE
| OsCode::BTN_EXTRA
| OsCode::MouseWheelUp
| OsCode::MouseWheelDown
| OsCode::MouseWheelLeft
| OsCode::MouseWheelRight,
) => MouseInDefsrc::MouseUsed,
_ => is_mouse_used,
};

if mkeys.contains(&oscode) {
bail_expr!(expr, "Repeat declaration of key in defsrc: \"{}\"", s)
}
Expand All @@ -1077,7 +1111,7 @@ fn parse_defsrc(expr: &[SExpr], defcfg: &CfgOptions) -> Result<(MappedKeys, Vec<
}

mkeys.shrink_to_fit();
Ok((mkeys, ordered_codes))
Ok((mkeys, ordered_codes, is_mouse_used))
}

type LayerIndexes = HashMap<String, usize>;
Expand Down
3 changes: 3 additions & 0 deletions parser/src/cfg/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use kanata_keyberon::action::BooleanOperator::*;
use std::sync::{Mutex, MutexGuard};

mod ambiguous;
mod device_detect;
mod environment;
mod macros;

Expand Down Expand Up @@ -62,6 +63,8 @@ fn parse_cfg(cfg: &str) -> Result<IntermediateCfg> {
.all(|layer| layer[usize::from(NORMAL_KEY_ROW)]
.iter()
.all(|action| *action != DEFAULT_ACTION)));
#[cfg(any(target_os = "linux", target_os = "unknown"))]
assert!(icfg.options.linux_opts.linux_device_detect_mode.is_some());
}
icfg
}
Expand Down
66 changes: 66 additions & 0 deletions parser/src/cfg/tests/device_detect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#[cfg(target_os = "linux")]
mod linux {
use super::super::*;

#[test]
fn linux_device_parses_properly() {
let source = r#"
(defcfg linux-device-detect-mode any)
(defsrc) (deflayer base)"#;
let icfg = parse_cfg(source)
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect("no error");
assert_eq!(
icfg.options.linux_opts.linux_device_detect_mode,
Some(DeviceDetectMode::Any)
);

let source = r#"
(defcfg linux-device-detect-mode keyboard-only)
(defsrc) (deflayer base)"#;
let icfg = parse_cfg(source)
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect("no error");
assert_eq!(
icfg.options.linux_opts.linux_device_detect_mode,
Some(DeviceDetectMode::KeyboardOnly)
);

let source = r#"
(defcfg linux-device-detect-mode keyboard-mice)
(defsrc) (deflayer base)"#;
let icfg = parse_cfg(source)
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect("no error");
assert_eq!(
icfg.options.linux_opts.linux_device_detect_mode,
Some(DeviceDetectMode::KeyboardMice)
);

let source = r#"(defsrc mmid) (deflayer base 1)"#;
let icfg = parse_cfg(source)
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect("no error");
assert_eq!(
icfg.options.linux_opts.linux_device_detect_mode,
Some(DeviceDetectMode::Any)
);

let source = r#"(defsrc a) (deflayer base b)"#;
let icfg = parse_cfg(source)
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect("no error");
assert_eq!(
icfg.options.linux_opts.linux_device_detect_mode,
Some(DeviceDetectMode::KeyboardMice)
);

let source = r#"
(defcfg linux-device-detect-mode not an opt)
(defsrc) (deflayer base)"#;
parse_cfg(source)
.map(|_| ())
.map_err(|e| log::info!("{:?}", miette::Error::from(e)))
.expect_err("error should happen");
}
}
4 changes: 2 additions & 2 deletions parser/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,13 @@ pub fn str_to_oscode(s: &str) -> Option<OsCode> {
#[cfg(target_os = "windows")]
"PrintScreen" | "prtsc" | "prnt" => OsCode::KEY_PRINT,

// NOTE: these are linux and interception-only due to missing implementation for LLHOOK.
// Unknown: is macOS supported? I haven't reviewed.
"mlft" | "mouseleft" | "🖰1" | "‹🖰" => OsCode::BTN_LEFT,
"mrgt" | "mouseright" | "🖰2" | "🖰›" => OsCode::BTN_RIGHT,
"mmid" | "mousemid" | "🖰3" => OsCode::BTN_MIDDLE,
"mbck" | "mousebackward" | "🖰4" => OsCode::BTN_SIDE,
"mfwd" | "mouseforward" | "🖰5" => OsCode::BTN_EXTRA,

// NOTE: these are linux and interception-only due to missing implementation for LLHOOK
"mwu" | "mousewheelup" => OsCode::MouseWheelUp,
"mwd" | "mousewheeldown" => OsCode::MouseWheelDown,
"mwl" | "mousewheelleft" => OsCode::MouseWheelLeft,
Expand Down
1 change: 1 addition & 0 deletions src/kanata/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl Kanata {
k.continue_if_no_devices,
k.include_names.clone(),
k.exclude_names.clone(),
k.device_detect_mode,
) {
Ok(kbd_in) => kbd_in,
Err(e) => {
Expand Down
15 changes: 15 additions & 0 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ pub struct Kanata {
/// Config items from `defcfg`.
#[cfg(target_os = "linux")]
pub x11_repeat_rate: Option<KeyRepeatSettings>,
/// Determines what types of devices to grab based on autodetection mode.
#[cfg(target_os = "linux")]
pub device_detect_mode: DeviceDetectMode,
/// Fake key actions that are waiting for a certain duration of keyboard idling.
pub waiting_for_idle: HashSet<FakeKeyOnIdle>,
/// Number of ticks since kanata was idle.
Expand Down Expand Up @@ -392,6 +395,12 @@ impl Kanata {
},
#[cfg(target_os = "linux")]
x11_repeat_rate: cfg.options.linux_opts.linux_x11_repeat_delay_rate,
#[cfg(target_os = "linux")]
device_detect_mode: cfg
.options
.linux_opts
.linux_device_detect_mode
.expect("parser should default to some"),
waiting_for_idle: HashSet::default(),
ticks_since_idle: 0,
movemouse_buffer: None,
Expand Down Expand Up @@ -511,6 +520,12 @@ impl Kanata {
},
#[cfg(target_os = "linux")]
x11_repeat_rate: cfg.options.linux_opts.linux_x11_repeat_delay_rate,
#[cfg(target_os = "linux")]
device_detect_mode: cfg
.options
.linux_opts
.linux_device_detect_mode
.expect("parser should default to some"),
waiting_for_idle: HashSet::default(),
ticks_since_idle: 0,
movemouse_buffer: None,
Expand Down
Loading

0 comments on commit 4e53544

Please sign in to comment.