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"]
integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"]
scancode = ["helix-view/scancode"]
scancode-query = ["helix-view/scancode-query"]
scancode-evdev = ["helix-view/scancode-evdev"]
[[bin]]
name = "hx"

View File

@ -9,6 +9,10 @@ fn main() {
#[cfg(windows)]
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)]

View File

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

View File

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

View File

@ -14,7 +14,8 @@ homepage.workspace = true
default = []
term = ["crossterm"]
unicode-lines = []
scancode = ["keyboard_query"]
scancode-query = ["keyboard_query"]
scancode-evdev = ["evdev"]
[dependencies]
helix-stdx = { path = "../helix-stdx" }
@ -56,6 +57,7 @@ thiserror.workspace = true
kstring = "2.0"
keyboard_query = { version = "0.1.0", optional = true }
evdev = { version = "0.13", optional = true }
[target.'cfg(windows)'.dependencies]
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 helix_lsp::{Call, LanguageServerId};
#[cfg(feature = "scancode")]
#[cfg(scancode)]
use crate::scancode::{deserialize_scancode, KeyboardState, ScanCodeMap};
use tokio_stream::wrappers::UnboundedReceiverStream;
@ -383,7 +383,7 @@ pub struct Config {
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
/// `true`.
pub editor_config: bool,
#[cfg(feature = "scancode")]
#[cfg(scancode)]
#[serde(skip_serializing, deserialize_with = "deserialize_scancode")]
pub scancode: ScanCodeMap,
}
@ -1063,7 +1063,7 @@ impl Default for Config {
end_of_line_diagnostics: DiagnosticFilter::Disable,
clipboard_provider: ClipboardProvider::default(),
editor_config: true,
#[cfg(feature = "scancode")]
#[cfg(scancode)]
scancode: ScanCodeMap::default(),
}
}
@ -1166,7 +1166,7 @@ pub struct Editor {
pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache,
#[cfg(feature = "scancode")]
#[cfg(scancode)]
pub keyboard_state: KeyboardState,
}
@ -1289,7 +1289,7 @@ impl Editor {
handlers,
mouse_down_range: None,
cursor_cache: CursorCache::default(),
#[cfg(feature = "scancode")]
#[cfg(scancode)]
keyboard_state: KeyboardState::new(),
}
}
@ -2309,7 +2309,7 @@ impl Editor {
self.last_cwd.as_deref()
}
#[cfg(feature = "scancode")]
#[cfg(scancode)]
pub fn scancode_apply(&mut self, event: KeyEvent) -> KeyEvent {
self.config()
.scancode

View File

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

View File

@ -4,30 +4,34 @@ use anyhow;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use keyboard_query::{DeviceQuery, DeviceState};
type ScanCodeKeyCodeMap = HashMap<u16, (KeyCode, Option<KeyCode>)>;
pub struct KeyboardState {
device_state: DeviceState,
previous_codes: Vec<u16>,
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct ScanCodeMap {
// {<name>: {<code>: (char, shifted char)}}
// {<code>: (char, shifted char)}
map: ScanCodeKeyCodeMap,
modifiers: Vec<u16>,
shift_modifiers: Vec<u16>,
}
pub use keyboard_state::KeyboardState;
impl Default for KeyboardState {
fn default() -> Self {
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 {
Self {
previous_codes: Vec::new(),
@ -35,8 +39,7 @@ impl KeyboardState {
}
}
pub fn get_keys(&mut self) -> (Vec<u16>, Vec<u16>) {
// detect new pressed keys to sync with crossterm sequential key parsing
pub fn get_scancodes(&mut self) -> Vec<u16> {
let codes = self.device_state.get_keys();
let new_codes = if codes.len() <= 1 {
codes.clone()
@ -48,7 +51,99 @@ impl KeyboardState {
.collect()
};
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 {
let (scancodes, new_codes) = keyboard.get_keys();
if new_codes.is_empty() {
let codes = keyboard.get_scancodes();
if codes.is_empty() {
return event;
}
// get fist non modifier key code
let Some(scancode) = new_codes
.iter()
.find(|c| !self.modifiers.contains(c))
.cloned()
else {
let Some(scancode) = codes.iter().find(|c| !self.modifiers.contains(c)).cloned() else {
return event;
};
@ -109,7 +200,7 @@ impl ScanCodeMap {
let mut is_shifted = false;
for c in &self.shift_modifiers {
if scancodes.contains(c) {
if codes.contains(c) {
is_shifted = true;
break;
}
@ -130,7 +221,9 @@ impl ScanCodeMap {
};
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
@ -249,6 +342,7 @@ mod defaults {
}
fn qwerty() -> (&'static str, ScanCodeKeyCodeMap) {
// https://github.com/emberian/evdev/blob/8feea0685b0acb8153e394ffc393cf560d30a16f/src/scancodes.rs#L30
(
"qwerty",
HashMap::from_iter([
@ -321,15 +415,16 @@ mod defaults {
entry!(66, "F8"),
entry!(67, "F9"),
entry!(68, "F10"),
entry!(74, "-"),
entry!(78, "+"),
// Not processes by Helix
// entry!(69, "numlock"),
// entry!(70, "scrolllock"),
// entry!(71, "home"),
// entry!(72, "up"),
// entry!(73, "pageup"),
entry!(74, "-"),
// entry!(75, "left"),
// entry!(77, "right"),
entry!(78, "+"),
// entry!(79, "end"),
// entry!(80, "down"),
// entry!(81, "pagedown"),