Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linux): add linux-device-detect-mode to defcfg #1289

Merged
merged 5 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading