diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 7007d6bd8..d0f32c605 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -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" diff --git a/helix-term/build.rs b/helix-term/build.rs index 60a646590..cc092e8fc 100644 --- a/helix-term/build.rs +++ b/helix-term/build.rs @@ -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)] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 587a62454..9047baaa4 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6692,7 +6692,7 @@ fn jump_to_label(cx: &mut Context, labels: Vec, 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, 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 diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 31ab46744..cf893e7de 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -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) diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 150af9d66..124eae1d7 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -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"] } diff --git a/helix-view/build.rs b/helix-view/build.rs new file mode 100644 index 000000000..9909fbd04 --- /dev/null +++ b/helix-view/build.rs @@ -0,0 +1,5 @@ +fn main() { + // alias scancode feature flag + #[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))] + println!("cargo:rustc-cfg=scancode") +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7f473c4f4..078dd6612 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -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, 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 diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 6ba8944bb..d269aff3a 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -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; diff --git a/helix-view/src/scancode.rs b/helix-view/src/scancode.rs index 87a43b2e4..ae1d7951b 100644 --- a/helix-view/src/scancode.rs +++ b/helix-view/src/scancode.rs @@ -4,51 +4,146 @@ use anyhow; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; -use keyboard_query::{DeviceQuery, DeviceState}; - type ScanCodeKeyCodeMap = HashMap)>; -pub struct KeyboardState { - device_state: DeviceState, - previous_codes: Vec, -} - #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct ScanCodeMap { - // {: {: (char, shifted char)}} + // {: (char, shifted char)} map: ScanCodeKeyCodeMap, modifiers: Vec, shift_modifiers: Vec, } +pub use keyboard_state::KeyboardState; + impl Default for KeyboardState { fn default() -> Self { Self::new() } } -impl KeyboardState { - pub fn new() -> Self { - Self { - previous_codes: Vec::new(), - device_state: DeviceState::new(), - } +#[cfg(feature = "scancode-query")] +mod keyboard_state { + use keyboard_query::{DeviceQuery, DeviceState}; + + pub struct KeyboardState { + device_state: DeviceState, + previous_codes: Vec, } - pub fn get_keys(&mut self) -> (Vec, Vec) { - // detect new pressed keys to sync with crossterm sequential key parsing - let codes = self.device_state.get_keys(); - let new_codes = if codes.len() <= 1 { - codes.clone() - } else { - codes - .clone() - .into_iter() - .filter(|c| !self.previous_codes.contains(c)) - .collect() - }; - self.previous_codes = codes.clone(); - (codes, new_codes) + impl KeyboardState { + pub fn new() -> Self { + Self { + previous_codes: Vec::new(), + device_state: DeviceState::new(), + } + } + + pub fn get_scancodes(&mut self) -> Vec { + let codes = self.device_state.get_keys(); + let new_codes = if codes.len() <= 1 { + codes.clone() + } else { + codes + .clone() + .into_iter() + .filter(|c| !self.previous_codes.contains(c)) + .collect() + }; + self.previous_codes = codes.clone(); + 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; 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"),