mirror of https://github.com/helix-editor/helix
feat: hidapi to listen keyboard events
parent
a4a4ccdb58
commit
2a13d8303d
|
@ -1630,6 +1630,7 @@ dependencies = [
|
|||
"helix-stdx",
|
||||
"helix-tui",
|
||||
"helix-vcs",
|
||||
"hidapi",
|
||||
"keyboard_query",
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -1653,6 +1654,19 @@ version = "0.3.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hidapi"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.9"
|
||||
|
|
|
@ -37,6 +37,7 @@ integration = ["helix-event/integration_test"]
|
|||
git = ["helix-vcs/git"]
|
||||
scancode-query = ["helix-view/scancode-query"]
|
||||
scancode-evdev = ["helix-view/scancode-evdev"]
|
||||
scancode-hidapi = ["helix-view/scancode-hidapi"]
|
||||
|
||||
[[bin]]
|
||||
name = "hx"
|
||||
|
|
|
@ -11,7 +11,7 @@ fn main() {
|
|||
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
|
||||
|
||||
// alias scancode feature flag
|
||||
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))]
|
||||
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev", feature = "scancode-hidapi"))]
|
||||
println!("cargo:rustc-cfg=scancode")
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ term = ["crossterm"]
|
|||
unicode-lines = []
|
||||
scancode-query = ["keyboard_query"]
|
||||
scancode-evdev = ["evdev"]
|
||||
scancode-hidapi = ["hidapi"]
|
||||
|
||||
[dependencies]
|
||||
helix-stdx = { path = "../helix-stdx" }
|
||||
|
@ -54,8 +55,10 @@ log = "~0.4"
|
|||
parking_lot.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
keyboard_query = { version = "0.1.0", optional = true }
|
||||
keyboard_query = { version = "0.1", optional = true }
|
||||
evdev = { version = "0.13", features = ["tokio"], optional = true }
|
||||
hidapi = { version = "2.6", optional = true }
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { version = "5.4", features = ["std"] }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
fn main() {
|
||||
// alias scancode feature flag
|
||||
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev"))]
|
||||
#[cfg(any(feature = "scancode-query", feature = "scancode-evdev", feature = "scancode-hidapi"))]
|
||||
println!("cargo:rustc-cfg=scancode")
|
||||
}
|
||||
|
|
|
@ -160,6 +160,298 @@ mod keyboard_state {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "scancode-hidapi")]
|
||||
mod keyboard_state {
|
||||
use hidapi::HidApi;
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct KeyboardState {
|
||||
codes: [Arc<AtomicU16>; 2],
|
||||
_handles: Vec<std::thread::JoinHandle<()>>,
|
||||
}
|
||||
const HID_KEYBOARD_USAGE_PAGE: u16 = 0x01;
|
||||
const HID_KEYBOARD_USAGE_ID: u16 = 0x06;
|
||||
const HID_MODIFIERS_MASK: [(u8, u16); 4] = [
|
||||
(0x01, 29), // Left Control
|
||||
(0x02, 42), // Left Shift
|
||||
(0x04, 56), // Left Alt
|
||||
(0x20, 54), // Right Shift
|
||||
];
|
||||
|
||||
// https://usb.org/sites/default/files/hut1_22.pdf
|
||||
// 10 Keyboard/Keypad Page (0x07)
|
||||
fn hid_keycode_to_scancode(hid_keycode: &u8) -> Option<u16> {
|
||||
Some(match hid_keycode {
|
||||
4 => 30, // A
|
||||
5 => 48, // B
|
||||
6 => 46, // C
|
||||
7 => 32, // D
|
||||
8 => 18, // E
|
||||
9 => 33, // F
|
||||
10 => 34, // G
|
||||
11 => 35, // H
|
||||
12 => 23, // I
|
||||
13 => 36, // J
|
||||
14 => 37, // K
|
||||
15 => 38, // L
|
||||
16 => 50, // M
|
||||
17 => 49, // N
|
||||
18 => 24, // O
|
||||
19 => 25, // P
|
||||
20 => 16, // Q
|
||||
21 => 19, // R
|
||||
22 => 31, // S
|
||||
23 => 20, // T
|
||||
24 => 22, // U
|
||||
25 => 47, // V
|
||||
26 => 17, // W
|
||||
27 => 45, // X
|
||||
28 => 21, // Y
|
||||
29 => 44, // Z
|
||||
30 => 2, // 1
|
||||
31 => 3, // 2
|
||||
32 => 4, // 3
|
||||
33 => 5, // 4
|
||||
34 => 6, // 5
|
||||
35 => 7, // 6
|
||||
36 => 8, // 7
|
||||
37 => 9, // 8
|
||||
38 => 10, // 9
|
||||
39 => 11, // 0
|
||||
40 => 28, // Enter
|
||||
41 => 1, // Escape
|
||||
42 => 14, // Backspace
|
||||
43 => 15, // Tab
|
||||
44 => 57, // Space
|
||||
45 => 12, // Minus (-)
|
||||
46 => 13, // Equal (=)
|
||||
47 => 26, // Left Bracket ([)
|
||||
48 => 27, // Right Bracket (])
|
||||
49 => 43, // Backslash (\)
|
||||
50 => 43, // Non-US Hash (#)
|
||||
51 => 39, // Semicolon (;)
|
||||
52 => 40, // Apostrophe (')
|
||||
53 => 41, // Grave (`)
|
||||
54 => 51, // Comma (,)
|
||||
55 => 52, // Period (.)
|
||||
56 => 53, // Slash (/)
|
||||
57 => 58, // Caps Lock
|
||||
58 => 59, // F1
|
||||
59 => 60, // F2
|
||||
60 => 61, // F3
|
||||
61 => 62, // F4
|
||||
62 => 63, // F5
|
||||
63 => 64, // F6
|
||||
64 => 65, // F7
|
||||
65 => 66, // F8
|
||||
66 => 67, // F9
|
||||
67 => 68, // F10
|
||||
68 => 87, // F11
|
||||
69 => 88, // F12
|
||||
70 => 99, // Print Screen
|
||||
71 => 70, // Scroll Lock
|
||||
72 => 119, // Pause
|
||||
73 => 110, // Insert
|
||||
74 => 102, // Home
|
||||
75 => 104, // Page Up
|
||||
76 => 111, // Delete
|
||||
77 => 107, // End
|
||||
78 => 109, // Page Down
|
||||
79 => 106, // Right Arrow
|
||||
80 => 105, // Left Arrow
|
||||
81 => 108, // Down Arrow
|
||||
82 => 103, // Up Arrow
|
||||
83 => 69, // Num Lock
|
||||
84 => 98, // Keypad Slash (/)
|
||||
85 => 55, // Keypad Asterisk (*)
|
||||
86 => 74, // Keypad Minus (-)
|
||||
87 => 78, // Keypad Plus (+)
|
||||
88 => 96, // Keypad Enter
|
||||
89 => 79, // Keypad 1
|
||||
90 => 80, // Keypad 2
|
||||
91 => 81, // Keypad 3
|
||||
92 => 75, // Keypad 4
|
||||
93 => 76, // Keypad 5
|
||||
94 => 77, // Keypad 6
|
||||
95 => 71, // Keypad 7
|
||||
96 => 72, // Keypad 8
|
||||
97 => 73, // Keypad 9
|
||||
98 => 82, // Keypad 0
|
||||
99 => 83, // Keypad Period (.)
|
||||
100 => 127, // Non-US Backslash (|)
|
||||
101 => 115, // Application
|
||||
102 => 128, // Power
|
||||
103 => 129, // Keypad Equal (=)
|
||||
104 => 130, // F13
|
||||
105 => 131, // F14
|
||||
106 => 132, // F15
|
||||
107 => 133, // F16
|
||||
108 => 134, // F17
|
||||
109 => 135, // F18
|
||||
110 => 136, // F19
|
||||
111 => 137, // F20
|
||||
112 => 138, // F21
|
||||
113 => 139, // F22
|
||||
114 => 140, // F23
|
||||
115 => 141, // F24
|
||||
116 => 142, // Execute
|
||||
117 => 143, // Help
|
||||
118 => 144, // Menu
|
||||
119 => 145, // Select
|
||||
120 => 146, // Stop
|
||||
121 => 147, // Again
|
||||
122 => 148, // Undo
|
||||
123 => 149, // Cut
|
||||
124 => 150, // Copy
|
||||
125 => 151, // Paste
|
||||
126 => 152, // Find
|
||||
127 => 153, // Mute
|
||||
128 => 154, // Volume Up
|
||||
129 => 155, // Volume Down
|
||||
130 => 156, // Locking Caps Lock
|
||||
131 => 157, // Locking Num Lock
|
||||
132 => 158, // Locking Scroll Lock
|
||||
133 => 159, // Keypad Comma (,)
|
||||
134 => 160, // Keypad Equal Sign (=)
|
||||
135 => 161, // International1 (Ro)
|
||||
136 => 162, // International2 (Katakana/Hiragana)
|
||||
137 => 163, // International3 (Yen)
|
||||
138 => 164, // International4 (Henkan)
|
||||
139 => 165, // International5 (Muhenkan)
|
||||
140 => 166, // International6 (PC9800 Keypad ,)
|
||||
141 => 167, // International7
|
||||
142 => 168, // International8
|
||||
143 => 169, // International9
|
||||
144 => 170, // Lang1 (Hangul/English)
|
||||
145 => 171, // Lang2 (Hanja)
|
||||
146 => 172, // Lang3 (Katakana)
|
||||
147 => 173, // Lang4 (Hiragana)
|
||||
148 => 174, // Lang5 (Zenkaku/Hankaku)
|
||||
149 => 175, // Lang6
|
||||
150 => 176, // Lang7
|
||||
151 => 177, // Lang8
|
||||
152 => 178, // Lang9
|
||||
153 => 179, // Alternate Erase
|
||||
154 => 180, // SysReq/Attention
|
||||
155 => 181, // Cancel
|
||||
156 => 182, // Clear
|
||||
157 => 183, // Prior
|
||||
158 => 184, // Return
|
||||
159 => 185, // Separator
|
||||
160 => 186, // Out
|
||||
161 => 187, // Oper
|
||||
162 => 188, // Clear/Again
|
||||
163 => 189, // CrSel/Props
|
||||
164 => 190, // ExSel
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn hid_modifier_to_scancode(modifier_byte: &u8) -> Option<u16> {
|
||||
for (mask, scancode) in HID_MODIFIERS_MASK {
|
||||
if modifier_byte & mask != 0 {
|
||||
return Some(scancode);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
pub fn new() -> Self {
|
||||
let key1 = Arc::new(AtomicU16::new(0));
|
||||
let key2 = Arc::new(AtomicU16::new(0));
|
||||
|
||||
let mut handles = Vec::new();
|
||||
|
||||
match HidApi::new() {
|
||||
Ok(api) => {
|
||||
for device in api.device_list() {
|
||||
let device_name = format!(
|
||||
"{:?} ({:04x}:{:04x}) {} {}",
|
||||
device.path(),
|
||||
device.vendor_id(),
|
||||
device.product_id(),
|
||||
device.manufacturer_string().unwrap_or("-"),
|
||||
device.product_string().unwrap_or("-")
|
||||
);
|
||||
|
||||
if !(device.usage_page() == HID_KEYBOARD_USAGE_PAGE
|
||||
&& device.usage() == HID_KEYBOARD_USAGE_ID)
|
||||
{
|
||||
log::trace!("{device_name} isn't keyboard. skip");
|
||||
continue;
|
||||
}
|
||||
|
||||
let device = match device.open_device(&api) {
|
||||
Ok(device) => {
|
||||
log::info!("{device_name} start listen input reports");
|
||||
device
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{device_name} error on open device: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let k1 = Arc::clone(&key1);
|
||||
let k2 = Arc::clone(&key2);
|
||||
handles.push(std::thread::spawn(move || {
|
||||
let mut report = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
loop {
|
||||
match device.read(&mut report) {
|
||||
Ok(read) if read < 8 => {
|
||||
log::warn!("{device_name} partial read of input report");
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("{device_name} read event error: {e}");
|
||||
continue;
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
|
||||
for i in 2..8 {
|
||||
let hid_keycode = report[i];
|
||||
if hid_keycode == 0 {
|
||||
continue;
|
||||
};
|
||||
let Some(scancode) = hid_keycode_to_scancode(&hid_keycode)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
log::trace!(
|
||||
"{device_name} hid_keycode: {hid_keycode} scancode: {scancode}"
|
||||
);
|
||||
k1.store(scancode, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
|
||||
k2.store(hid_modifier_to_scancode(&report[0]).unwrap_or(0), Ordering::Relaxed);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error on initialize hidapi: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
_handles: handles,
|
||||
codes: [key1, key2],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_scancodes(&mut self) -> [u16; 2] {
|
||||
[
|
||||
self.codes[0].swap(0, Ordering::Relaxed), // key
|
||||
self.codes[1].swap(0, Ordering::Relaxed), // modifier
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScanCodeMap {
|
||||
pub fn new(map: HashMap<u16, (KeyCode, Option<KeyCode>)>) -> Self {
|
||||
let modifiers = map
|
||||
|
@ -196,12 +488,13 @@ impl ScanCodeMap {
|
|||
|
||||
pub fn apply(&self, event: KeyEvent, keyboard: &mut KeyboardState) -> KeyEvent {
|
||||
let codes = keyboard.get_scancodes();
|
||||
if codes.is_empty() {
|
||||
return event;
|
||||
}
|
||||
|
||||
// get fist non modifier key code
|
||||
let Some(scancode) = codes.iter().find(|c| !self.modifiers.contains(c)).cloned() else {
|
||||
// get first non modifier key code
|
||||
let Some(scancode) = codes
|
||||
.iter()
|
||||
.find(|c| **c != 0 || !self.modifiers.contains(c))
|
||||
.cloned()
|
||||
else {
|
||||
return event;
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue