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"]
|
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"
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -4,29 +4,33 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "scancode-query")]
|
||||||
|
mod keyboard_state {
|
||||||
|
use keyboard_query::{DeviceQuery, DeviceState};
|
||||||
|
|
||||||
|
pub struct KeyboardState {
|
||||||
|
device_state: DeviceState,
|
||||||
|
previous_codes: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
impl KeyboardState {
|
impl KeyboardState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -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"),
|
||||||
|
|
Loading…
Reference in New Issue