mirror of https://github.com/helix-editor/helix
evdev to fetch keyboard events
parent
abb1c60afd
commit
4c6b0ff447
|
@ -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"
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
// alias scancode feature flag
|
||||
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))]
|
||||
println!("cargo:rustc-cfg=scancode")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"),
|
||||
|
|
Loading…
Reference in New Issue