Skip to content

Commit

Permalink
feat: add mouse wheel events to defsrc (#592)
Browse files Browse the repository at this point in the history
This commit allows mapping mouse wheel events to defsrc.
This is only supported on Linux and Interception.

Co-authored-by: jtroo <[email protected]>
  • Loading branch information
rszyma and jtroo authored Nov 4, 2023
1 parent 20b06e9 commit 28bed11
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 23 deletions.
28 changes: 28 additions & 0 deletions parser/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,34 @@ fn parse_action_atom(ac_span: &Spanned<String>, s: &ParsedState) -> Result<&'sta
s.a.sref(s.a.sref_slice(CustomAction::MouseTap(Btn::Backward))),
)))
}
"mwu" | "mousewheelup" => {
return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice(
CustomAction::MWheelNotch {
direction: MWheelDirection::Up,
},
)))))
}
"mwd" | "mousewheeldown" => {
return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice(
CustomAction::MWheelNotch {
direction: MWheelDirection::Down,
},
)))))
}
"mwl" | "mousewheelleft" => {
return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice(
CustomAction::MWheelNotch {
direction: MWheelDirection::Left,
},
)))))
}
"mwr" | "mousewheelright" => {
return Ok(s.a.sref(Action::Custom(s.a.sref(s.a.sref_slice(
CustomAction::MWheelNotch {
direction: MWheelDirection::Right,
},
)))))
}
"rpt" | "repeat" | "rpt-key" => {
return Ok(s.a.sref(Action::Custom(
s.a.sref(s.a.sref_slice(CustomAction::Repeat)),
Expand Down
19 changes: 19 additions & 0 deletions parser/src/custom_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use anyhow::{anyhow, Result};
use kanata_keyberon::key_code::KeyCode;

use crate::keys::OsCode;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CustomAction {
Cmd(Vec<String>),
Expand All @@ -29,6 +31,9 @@ pub enum CustomAction {
interval: u16,
distance: u16,
},
MWheelNotch {
direction: MWheelDirection,
},
MoveMouse {
direction: MoveDirection,
interval: u16,
Expand Down Expand Up @@ -101,6 +106,20 @@ pub enum MWheelDirection {
Right,
}

impl TryFrom<OsCode> for MWheelDirection {
type Error = ();
fn try_from(value: OsCode) -> Result<Self, Self::Error> {
use OsCode::*;
Ok(match value {
MouseWheelUp => MWheelDirection::Up,
MouseWheelDown => MWheelDirection::Down,
MouseWheelLeft => MWheelDirection::Left,
MouseWheelRight => MWheelDirection::Right,
_ => return Err(()),
})
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MoveDirection {
Up,
Expand Down
20 changes: 20 additions & 0 deletions parser/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,16 @@ pub fn str_to_oscode(s: &str) -> Option<OsCode> {
"mfwd" | "mouseforward" => OsCode::BTN_EXTRA,
"mbck" | "mousebackward" => OsCode::BTN_SIDE,

// NOTE: these are linux and interception-only due to missing implementation for LLHOOK
#[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))]
"mwu" | "mousewheelup" => OsCode::MouseWheelUp,
#[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))]
"mwd" | "mousewheeldown" => OsCode::MouseWheelDown,
#[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))]
"mwl" | "mousewheelleft" => OsCode::MouseWheelLeft,
#[cfg(any(target_os = "linux", target_os = "unknown", feature = "interception_driver"))]
"mwr" | "mousewheelright" => OsCode::MouseWheelRight,

"hmpg" | "homepage" => OsCode::KEY_HOMEPAGE,
"mdia" | "media" => OsCode::KEY_MEDIA,
"mail" => OsCode::KEY_MAIL,
Expand Down Expand Up @@ -1018,6 +1028,16 @@ pub enum OsCode {
BTN_TRIGGER_HAPPY39 = 742,
BTN_TRIGGER_HAPPY40 = 743,
BTN_MAX = 744,

// Mouse wheel events are not a part of EV_KEY, so they technically
// shouldn't be there, but they're still there, because this way
// it's easier to implement allowing to add them to defsrc without
// making tons of changes all over the codebase.
MouseWheelUp = 745,
MouseWheelDown = 746,
MouseWheelLeft = 747,
MouseWheelRight = 748,

KEY_MAX = 767,
}

Expand Down
96 changes: 85 additions & 11 deletions src/kanata/linux.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{anyhow, bail, Result};
use evdev::{InputEvent, InputEventKind, RelativeAxisType};
use log::info;
use parking_lot::Mutex;
use std::convert::TryFrom;
Expand Down Expand Up @@ -35,11 +36,11 @@ impl Kanata {
let events = kbd_in.read().map_err(|e| anyhow!("failed read: {}", e))?;
log::trace!("{events:?}");

for in_event in events.into_iter() {
for in_event in events.iter().copied() {
let key_event = match KeyEvent::try_from(in_event) {
Ok(ev) => ev,
_ => {
// Pass-through non-key events
// Pass-through non-key and non-scroll events
let mut kanata = kanata.lock();
kanata
.kbd_out
Expand All @@ -51,15 +52,23 @@ impl Kanata {

check_for_exit(&key_event);

// Check if this keycode is mapped in the configuration. If it hasn't been mapped, send
// it immediately.
if !MAPPED_KEYS.lock().contains(&key_event.code) {
let mut kanata = kanata.lock();
kanata
.kbd_out
.write_key(key_event.code, key_event.value)
.map_err(|e| anyhow!("failed write key: {}", e))?;
continue;
if key_event.value == KeyValue::Tap {
// Scroll event for sure. Only scroll events produce Tap.
if !handle_scroll(&kanata, in_event, key_event.code, &events)? {
continue;
}
} else {
// Handle normal keypresses.
// Check if this keycode is mapped in the configuration.
// If it hasn't been mapped, send it immediately.
if !MAPPED_KEYS.lock().contains(&key_event.code) {
let mut kanata = kanata.lock();
kanata
.kbd_out
.write_raw(in_event)
.map_err(|e| anyhow!("failed write: {}", e))?;
continue;
};
}

// Send key events to the processing loop
Expand Down Expand Up @@ -113,3 +122,68 @@ impl Kanata {
Ok(())
}
}

/// Returns true if the scroll event should be sent to the processing loop, otherwise returns
/// false.
fn handle_scroll(
kanata: &Mutex<Kanata>,
in_event: InputEvent,
code: OsCode,
all_events: &[InputEvent],
) -> Result<bool> {
let direction: MWheelDirection = code.try_into().unwrap();
let scroll_distance = in_event.value().unsigned_abs() as u16;
match in_event.kind() {
InputEventKind::RelAxis(axis_type) => {
match axis_type {
RelativeAxisType::REL_WHEEL | RelativeAxisType::REL_HWHEEL => {
if MAPPED_KEYS.lock().contains(&code) {
return Ok(true);
}
// If we just used `write_raw` here, some of the scrolls issued by kanata would be
// REL_WHEEL_HI_RES + REL_WHEEL and some just REL_WHEEL and an issue like this one
// would happen: https://github.com/jtroo/kanata/issues/395
//
// So to fix this case, we need to use `scroll` which will also send hi-res scrolls
// along normal scrolls.
//
// However, if this is a normal scroll event, it may be sent alongside a hi-res
// scroll event. In this scenario, the hi-res event should be used to call
// scroll, and not the normal event. Otherwise, too much scrolling will happen.
let mut kanata = kanata.lock();
if !all_events.iter().any(|ev| {
matches!(
ev.kind(),
InputEventKind::RelAxis(
RelativeAxisType::REL_WHEEL_HI_RES
| RelativeAxisType::REL_HWHEEL_HI_RES
)
)
}) {
kanata
.kbd_out
.scroll(direction, scroll_distance * HI_RES_SCROLL_UNITS_IN_LO_RES)
.map_err(|e| anyhow!("failed write: {}", e))?;
}
Ok(false)
}
RelativeAxisType::REL_WHEEL_HI_RES | RelativeAxisType::REL_HWHEEL_HI_RES => {
if !MAPPED_KEYS.lock().contains(&code) {
// Passthrough if the scroll wheel event is not mapped
// in the configuration.
let mut kanata = kanata.lock();
kanata
.kbd_out
.scroll(direction, scroll_distance)
.map_err(|e| anyhow!("failed write: {}", e))?;
}
// Kanata will not handle high resolution scroll events for now.
// Full notch scrolling only.
Ok(false)
}
_ => unreachable!("expect to be handling a wheel event"),
}
}
_ => unreachable!("expect to be handling a wheel event"),
}
}
19 changes: 14 additions & 5 deletions src/kanata/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ impl Kanata {
}

/// Update keyberon layout state for press/release, handle repeat separately
fn handle_key_event(&mut self, event: &KeyEvent) -> Result<()> {
fn handle_input_event(&mut self, event: &KeyEvent) -> Result<()> {
log::debug!("process recv ev {event:?}");
let evc: u16 = event.code.into();
self.ticks_since_idle = 0;
Expand All @@ -466,6 +466,11 @@ impl Kanata {
let ret = self.handle_repeat(event);
return ret;
}
KeyValue::Tap => {
self.layout.bm().event(Event::Press(0, evc));
self.layout.bm().event(Event::Release(0, evc));
return Ok(());
}
};
self.layout.bm().event(kbrn_ev);
Ok(())
Expand Down Expand Up @@ -990,6 +995,10 @@ impl Kanata {
})
}
},
CustomAction::MWheelNotch { direction } => {
self.kbd_out
.scroll(*direction, HI_RES_SCROLL_UNITS_IN_LO_RES)?;
}
CustomAction::MoveMouse {
direction,
interval,
Expand Down Expand Up @@ -1600,8 +1609,8 @@ impl Kanata {
// are not cleared.
if (now - k.last_tick) > time::Duration::from_secs(60) {
log::debug!(
"clearing keyberon normal key states due to blocking for a while"
);
"clearing keyberon normal key states due to blocking for a while"
);
k.layout.bm().states.retain(|s| {
!matches!(
s,
Expand All @@ -1627,7 +1636,7 @@ impl Kanata {
#[cfg(feature = "perf_logging")]
let start = std::time::Instant::now();

if let Err(e) = k.handle_key_event(&kev) {
if let Err(e) = k.handle_input_event(&kev) {
break e;
}

Expand Down Expand Up @@ -1662,7 +1671,7 @@ impl Kanata {
#[cfg(feature = "perf_logging")]
let start = std::time::Instant::now();

if let Err(e) = k.handle_key_event(&kev) {
if let Err(e) = k.handle_input_event(&kev) {
break e;
}

Expand Down
39 changes: 32 additions & 7 deletions src/kanata/windows/interception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,7 @@ impl Kanata {
if mouse_to_intercept_hwid.is_some() {
intrcptn.set_filter(
ic::is_mouse,
ic::Filter::MouseFilter(
ic::MouseState::all()
& (!ic::MouseState::WHEEL)
& (!ic::MouseState::HWHEEL)
& (!ic::MouseState::MOVE),
),
ic::Filter::MouseFilter(ic::MouseState::all() & (!ic::MouseState::MOVE)),
);
}
let mut is_dev_interceptable: HashMap<ic::Device, bool> = HashMap::default();
Expand All @@ -70,13 +65,14 @@ impl Kanata {
};
KeyEvent { code, value }
}
ic::Stroke::Mouse { state, .. } => {
ic::Stroke::Mouse { state, rolling, .. } => {
if let Some(hwid) = mouse_to_intercept_hwid {
log::trace!("checking mouse stroke {:?}", strokes[i]);
if let Some(event) = mouse_state_to_event(
dev,
&hwid,
state,
rolling,
&intrcptn,
&mut is_dev_interceptable,
) {
Expand Down Expand Up @@ -123,6 +119,7 @@ fn mouse_state_to_event(
input_dev: ic::Device,
allowed_hwid: &[u8; HWID_ARR_SZ],
state: ic::MouseState,
rolling: i16,
intrcptn: &ic::Interception,
is_dev_interceptable: &mut HashMap<ic::Device, bool>,
) -> Option<KeyEvent> {
Expand Down Expand Up @@ -191,6 +188,34 @@ fn mouse_state_to_event(
code: OsCode::BTN_EXTRA,
value: KeyValue::Release,
})
} else if state.contains(ic::MouseState::WHEEL) {
let osc = if rolling >= 0 {
OsCode::MouseWheelUp
} else {
OsCode::MouseWheelDown
};
if MAPPED_KEYS.lock().contains(&osc) {
Some(KeyEvent {
code: osc,
value: KeyValue::Tap,
})
} else {
None
}
} else if state.contains(ic::MouseState::HWHEEL) {
let osc = if rolling >= 0 {
OsCode::MouseWheelRight
} else {
OsCode::MouseWheelLeft
};
if MAPPED_KEYS.lock().contains(&osc) {
Some(KeyEvent {
code: osc,
value: KeyValue::Tap,
})
} else {
None
}
} else {
None
}
Expand Down
Loading

0 comments on commit 28bed11

Please sign in to comment.