evdev to fetch keyboard events

pull/10977/head
Evgeniy Tatarkin 2025-01-23 22:55:31 +03:00
parent abb1c60afd
commit 4c6b0ff447
9 changed files with 159 additions and 52 deletions

View File

@ -35,7 +35,8 @@ default = ["git"]
unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"] unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"]
integration = ["helix-event/integration_test"] integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"] git = ["helix-vcs/git"]
scancode = ["helix-view/scancode"] scancode-query = ["helix-view/scancode-query"]
scancode-evdev = ["helix-view/scancode-evdev"]
[[bin]] [[bin]]
name = "hx" name = "hx"

View File

@ -9,6 +9,10 @@ fn main() {
#[cfg(windows)] #[cfg(windows)]
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico"); windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
// alias scancode feature flag
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))]
println!("cargo:rustc-cfg=scancode")
} }
#[cfg(windows)] #[cfg(windows)]

View File

@ -6692,7 +6692,7 @@ fn jump_to_label(cx: &mut Context, labels: Vec<Range>, behaviour: Movement) {
let view = view.id; let view = view.id;
let doc = doc.id(); let doc = doc.id();
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
#[cfg(feature = "scancode")] #[cfg(scancode)]
let event = cx.editor.scancode_apply(event); let event = cx.editor.scancode_apply(event);
let alphabet = &cx.editor.config().jump_label_alphabet; let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(i) = event let Some(i) = event
@ -6711,7 +6711,7 @@ fn jump_to_label(cx: &mut Context, labels: Vec<Range>, behaviour: Movement) {
} }
cx.on_next_key(move |cx, event| { cx.on_next_key(move |cx, event| {
doc_mut!(cx.editor, &doc).remove_jump_labels(view); doc_mut!(cx.editor, &doc).remove_jump_labels(view);
#[cfg(feature = "scancode")] #[cfg(scancode)]
let event = cx.editor.scancode_apply(event); let event = cx.editor.scancode_apply(event);
let alphabet = &cx.editor.config().jump_label_alphabet; let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(inner) = event let Some(inner) = event

View File

@ -840,7 +840,7 @@ impl EditorView {
let key_result = self.keymaps.get(mode, event); let key_result = self.keymaps.get(mode, event);
#[cfg(feature = "scancode")] #[cfg(scancode)]
let key_result = { let key_result = {
if !matches!( if !matches!(
key_result, key_result,
@ -937,7 +937,7 @@ impl EditorView {
} }
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) { fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
#[cfg(feature = "scancode")] #[cfg(scancode)]
// dont use scancode on macros replaying // dont use scancode on macros replaying
let event = if cxt.editor.macro_replaying.is_empty() { let event = if cxt.editor.macro_replaying.is_empty() {
cxt.editor.scancode_apply(event) cxt.editor.scancode_apply(event)

View File

@ -14,7 +14,8 @@ homepage.workspace = true
default = [] default = []
term = ["crossterm"] term = ["crossterm"]
unicode-lines = [] unicode-lines = []
scancode = ["keyboard_query"] scancode-query = ["keyboard_query"]
scancode-evdev = ["evdev"]
[dependencies] [dependencies]
helix-stdx = { path = "../helix-stdx" } helix-stdx = { path = "../helix-stdx" }
@ -56,6 +57,7 @@ thiserror.workspace = true
kstring = "2.0" kstring = "2.0"
keyboard_query = { version = "0.1.0", optional = true } keyboard_query = { version = "0.1.0", optional = true }
evdev = { version = "0.13", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
clipboard-win = { version = "5.4", features = ["std"] } clipboard-win = { version = "5.4", features = ["std"] }

View File

@ -0,0 +1,5 @@
fn main() {
// alias scancode feature flag
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))]
println!("cargo:rustc-cfg=scancode")
}

View File

@ -21,7 +21,7 @@ use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt}; use futures_util::{future, StreamExt};
use helix_lsp::{Call, LanguageServerId}; use helix_lsp::{Call, LanguageServerId};
#[cfg(feature = "scancode")] #[cfg(scancode)]
use crate::scancode::{deserialize_scancode, KeyboardState, ScanCodeMap}; use crate::scancode::{deserialize_scancode, KeyboardState, ScanCodeMap};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@ -383,7 +383,7 @@ pub struct Config {
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to /// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
/// `true`. /// `true`.
pub editor_config: bool, pub editor_config: bool,
#[cfg(feature = "scancode")] #[cfg(scancode)]
#[serde(skip_serializing, deserialize_with = "deserialize_scancode")] #[serde(skip_serializing, deserialize_with = "deserialize_scancode")]
pub scancode: ScanCodeMap, pub scancode: ScanCodeMap,
} }
@ -1063,7 +1063,7 @@ impl Default for Config {
end_of_line_diagnostics: DiagnosticFilter::Disable, end_of_line_diagnostics: DiagnosticFilter::Disable,
clipboard_provider: ClipboardProvider::default(), clipboard_provider: ClipboardProvider::default(),
editor_config: true, editor_config: true,
#[cfg(feature = "scancode")] #[cfg(scancode)]
scancode: ScanCodeMap::default(), scancode: ScanCodeMap::default(),
} }
} }
@ -1166,7 +1166,7 @@ pub struct Editor {
pub mouse_down_range: Option<Range>, pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache, pub cursor_cache: CursorCache,
#[cfg(feature = "scancode")] #[cfg(scancode)]
pub keyboard_state: KeyboardState, pub keyboard_state: KeyboardState,
} }
@ -1289,7 +1289,7 @@ impl Editor {
handlers, handlers,
mouse_down_range: None, mouse_down_range: None,
cursor_cache: CursorCache::default(), cursor_cache: CursorCache::default(),
#[cfg(feature = "scancode")] #[cfg(scancode)]
keyboard_state: KeyboardState::new(), keyboard_state: KeyboardState::new(),
} }
} }
@ -2309,7 +2309,7 @@ impl Editor {
self.last_cwd.as_deref() self.last_cwd.as_deref()
} }
#[cfg(feature = "scancode")] #[cfg(scancode)]
pub fn scancode_apply(&mut self, event: KeyEvent) -> KeyEvent { pub fn scancode_apply(&mut self, event: KeyEvent) -> KeyEvent {
self.config() self.config()
.scancode .scancode

View File

@ -15,7 +15,7 @@ pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;
pub mod register; pub mod register;
#[cfg(feature = "scancode")] #[cfg(scancode)]
pub mod scancode; pub mod scancode;
pub mod theme; pub mod theme;
pub mod tree; pub mod tree;

View File

@ -4,30 +4,34 @@ use anyhow;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use std::collections::HashMap; use std::collections::HashMap;
use keyboard_query::{DeviceQuery, DeviceState};
type ScanCodeKeyCodeMap = HashMap<u16, (KeyCode, Option<KeyCode>)>; type ScanCodeKeyCodeMap = HashMap<u16, (KeyCode, Option<KeyCode>)>;
pub struct KeyboardState {
device_state: DeviceState,
previous_codes: Vec<u16>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)] #[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct ScanCodeMap { pub struct ScanCodeMap {
// {<name>: {<code>: (char, shifted char)}} // {<code>: (char, shifted char)}
map: ScanCodeKeyCodeMap, map: ScanCodeKeyCodeMap,
modifiers: Vec<u16>, modifiers: Vec<u16>,
shift_modifiers: Vec<u16>, shift_modifiers: Vec<u16>,
} }
pub use keyboard_state::KeyboardState;
impl Default for KeyboardState { impl Default for KeyboardState {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
impl KeyboardState { #[cfg(feature = "scancode-query")]
mod keyboard_state {
use keyboard_query::{DeviceQuery, DeviceState};
pub struct KeyboardState {
device_state: DeviceState,
previous_codes: Vec<u16>,
}
impl KeyboardState {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
previous_codes: Vec::new(), previous_codes: Vec::new(),
@ -35,8 +39,7 @@ impl KeyboardState {
} }
} }
pub fn get_keys(&mut self) -> (Vec<u16>, Vec<u16>) { pub fn get_scancodes(&mut self) -> Vec<u16> {
// detect new pressed keys to sync with crossterm sequential key parsing
let codes = self.device_state.get_keys(); let codes = self.device_state.get_keys();
let new_codes = if codes.len() <= 1 { let new_codes = if codes.len() <= 1 {
codes.clone() codes.clone()
@ -48,7 +51,99 @@ impl KeyboardState {
.collect() .collect()
}; };
self.previous_codes = codes.clone(); self.previous_codes = codes.clone();
(codes, new_codes) new_codes
}
}
}
#[cfg(feature = "scancode-evdev")]
mod keyboard_state {
use evdev::{Device, KeyCode};
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::Arc;
pub struct KeyboardState {
codes: [Arc<AtomicU16>; 2],
_handle: std::thread::JoinHandle<()>,
}
fn is_keyboard(device: &Device) -> bool {
device
.supported_keys()
.map_or(false, |keys| keys.contains(KeyCode::KEY_ENTER))
}
impl KeyboardState {
pub fn new() -> Self {
let key1 = Arc::new(AtomicU16::new(0));
let key2 = Arc::new(AtomicU16::new(0));
let k1 = Arc::clone(&key1);
let k2 = Arc::clone(&key2);
let _handle = std::thread::spawn(move || {
// Try to find last keyboard input device
// TODO how to find actual system input device?
// TODO get input device path from config
let now = std::time::Instant::now();
let Some((device_path, mut device)) = evdev::enumerate()
.filter(|(_, dev)| is_keyboard(dev))
.last()
else {
log::warn!("No keyboard devices found");
return;
};
log::info!(
"Listen last keyboard input device '{}' (spend {}ms): {}",
device_path.display(),
now.elapsed().as_millis(),
device.name().unwrap_or_default()
);
// evdev constants
const KEY_STATE_RELEASE: i32 = 0;
let mut codes: [u16; 2] = [0, 0];
loop {
let Ok(stream) = device.fetch_events() else {
log::error!("Failed to fetch devices events");
continue;
};
for event in stream {
if evdev::EventType::KEY != event.event_type() {
continue;
};
let scancode: u16 = event.code();
if event.value() == KEY_STATE_RELEASE {
// reset state
codes = match (codes[0] == scancode, codes[1] == scancode) {
(true, false) => [0, codes[1]],
(false, true) => [0, codes[0]],
_ => [0, 0],
}
} else {
// don't repeat
if !codes.contains(&scancode) {
codes = [codes[1], scancode];
}
}
k1.store(codes[0], Ordering::Relaxed);
k2.store(codes[1], Ordering::Relaxed);
}
}
});
Self {
_handle,
codes: [key1, key2],
}
}
pub fn get_scancodes(&mut self) -> [u16; 2] {
[
self.codes[1].swap(0, Ordering::Relaxed),
self.codes[0].swap(0, Ordering::Relaxed),
]
}
} }
} }
@ -87,17 +182,13 @@ impl ScanCodeMap {
} }
pub fn apply(&self, event: KeyEvent, keyboard: &mut KeyboardState) -> KeyEvent { pub fn apply(&self, event: KeyEvent, keyboard: &mut KeyboardState) -> KeyEvent {
let (scancodes, new_codes) = keyboard.get_keys(); let codes = keyboard.get_scancodes();
if new_codes.is_empty() { if codes.is_empty() {
return event; return event;
} }
// get fist non modifier key code // get fist non modifier key code
let Some(scancode) = new_codes let Some(scancode) = codes.iter().find(|c| !self.modifiers.contains(c)).cloned() else {
.iter()
.find(|c| !self.modifiers.contains(c))
.cloned()
else {
return event; return event;
}; };
@ -109,7 +200,7 @@ impl ScanCodeMap {
let mut is_shifted = false; let mut is_shifted = false;
for c in &self.shift_modifiers { for c in &self.shift_modifiers {
if scancodes.contains(c) { if codes.contains(c) {
is_shifted = true; is_shifted = true;
break; break;
} }
@ -130,7 +221,9 @@ impl ScanCodeMap {
}; };
log::trace!( log::trace!(
"Scancodes: {scancodes:?} Scancode: {scancode:?} (key: {key:?}, shifted key: {shifted_key:?}) Is shifted: {is_shifted} Event source {event_before:?} New Event {event:?}" "{:?} map to {:?} by scancode {codes:?} (code: {scancode}, key: {key:?}, shifted key: {shifted_key:?})",
event_before.code,
event.code,
); );
event event
@ -249,6 +342,7 @@ mod defaults {
} }
fn qwerty() -> (&'static str, ScanCodeKeyCodeMap) { fn qwerty() -> (&'static str, ScanCodeKeyCodeMap) {
// https://github.com/emberian/evdev/blob/8feea0685b0acb8153e394ffc393cf560d30a16f/src/scancodes.rs#L30
( (
"qwerty", "qwerty",
HashMap::from_iter([ HashMap::from_iter([
@ -321,15 +415,16 @@ mod defaults {
entry!(66, "F8"), entry!(66, "F8"),
entry!(67, "F9"), entry!(67, "F9"),
entry!(68, "F10"), entry!(68, "F10"),
entry!(74, "-"),
entry!(78, "+"),
// Not processes by Helix
// entry!(69, "numlock"), // entry!(69, "numlock"),
// entry!(70, "scrolllock"), // entry!(70, "scrolllock"),
// entry!(71, "home"), // entry!(71, "home"),
// entry!(72, "up"), // entry!(72, "up"),
// entry!(73, "pageup"), // entry!(73, "pageup"),
entry!(74, "-"),
// entry!(75, "left"), // entry!(75, "left"),
// entry!(77, "right"), // entry!(77, "right"),
entry!(78, "+"),
// entry!(79, "end"), // entry!(79, "end"),
// entry!(80, "down"), // entry!(80, "down"),
// entry!(81, "pagedown"), // entry!(81, "pagedown"),