Compare commits

...

15 Commits

Author SHA1 Message Date
Evgeniy Tatarkin f649177ffd
Merge cd2bb2530c into 4281228da3 2025-07-24 14:36:52 -03:00
Valtteri Koskivuori 4281228da3
fix(queries): Fix filesystem permissions for snakemake (#14061) 2025-07-24 13:09:40 -04:00
Evgeniy Tatarkin cd2bb2530c Update Cargo.lock 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 77656e27bd cargo fmt 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 6de385df51 fix lints 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 3295f2bd32 fix: rename var 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 88b1a0d3d8 fix: long pressed keys on hidapi 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 8ec463a0f5 style: format 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin e1e0dd00a6 feat: hidapi to listen keyboard events 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 0567bae0f3 fix: listen events from ungrabbed keyboard evdev devices 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 4c6b0ff447 evdev to fetch keyboard events 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin abb1c60afd fix: dont use sancode on macros replaying 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 94b5e0c210 clippy 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin bb62dad76f fix rebase 2025-07-21 08:33:12 +03:00
Evgeniy Tatarkin 77c1c0e0e1 map keyboard scancode to KeyCode 2025-07-21 08:33:12 +03:00
17 changed files with 1104 additions and 54 deletions

207
Cargo.lock generated
View File

@ -104,6 +104,18 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bitvec"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
dependencies = [
"funty",
"radium",
"tap",
"wyz",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -163,6 +175,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chardetng"
version = "0.1.17"
@ -275,7 +293,7 @@ dependencies = [
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
"winapi 0.3.9",
]
[[package]]
@ -284,7 +302,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
"winapi 0.3.9",
]
[[package]]
@ -401,6 +419,19 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "evdev"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c10865aeab1a7399b3c2d6046e8dcc7f5227b656f235ed63ef5ee45a47b8f8"
dependencies = [
"bitvec",
"cfg-if",
"libc",
"nix",
"tokio",
]
[[package]]
name = "faster-hex"
version = "0.10.0"
@ -434,7 +465,7 @@ checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e"
dependencies = [
"libc",
"thiserror 1.0.69",
"winapi",
"winapi 0.3.9",
]
[[package]]
@ -481,6 +512,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "funty"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures-core"
version = "0.3.31"
@ -1626,6 +1663,7 @@ dependencies = [
"chardetng",
"clipboard-win",
"crossterm",
"evdev",
"futures-util",
"helix-core",
"helix-dap",
@ -1635,6 +1673,8 @@ dependencies = [
"helix-stdx",
"helix-tui",
"helix-vcs",
"hidapi",
"keyboard_query",
"kstring",
"libc",
"log",
@ -1658,6 +1698,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"
@ -1966,6 +2019,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keyboard_query"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0107e577edabb13df9c65e4bbd07d4359ddfa80e4ef2c1471f75de783d2df71"
dependencies = [
"pkg-config",
"user32-sys",
"x11",
]
[[package]]
name = "kstring"
version = "2.0.2"
@ -2102,6 +2166,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nucleo"
version = "0.5.0"
@ -2215,6 +2291,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.0"
@ -2278,6 +2360,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "radium"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.8.5"
@ -2637,6 +2725,12 @@ dependencies = [
"syn",
]
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.20.0"
@ -2933,6 +3027,16 @@ dependencies = [
"serde",
]
[[package]]
name = "user32-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "utf16_iter"
version = "1.0.5"
@ -3042,6 +3146,12 @@ dependencies = [
"winsafe",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
@ -3052,6 +3162,12 @@ dependencies = [
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
@ -3088,6 +3204,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3115,6 +3240,21 @@ dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -3147,6 +3287,12 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -3159,6 +3305,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -3171,6 +3323,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3195,6 +3353,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -3207,6 +3371,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -3219,6 +3389,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -3231,6 +3407,12 @@ version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@ -3279,6 +3461,25 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
dependencies = [
"tap",
]
[[package]]
name = "x11"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "xtask"
version = "25.7.1"

View File

@ -35,6 +35,9 @@ default = ["git"]
unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"]
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"

View File

@ -9,6 +9,15 @@ fn main() {
#[cfg(windows)]
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
// alias scancode feature flag
println!("cargo::rustc-check-cfg=cfg(scancode)");
#[cfg(any(
feature = "scancode-query",
feature = "scancode-evdev",
feature = "scancode-hidapi"
))]
println!("cargo:rustc-cfg=scancode")
}
#[cfg(windows)]

View File

@ -6692,6 +6692,8 @@ 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(scancode)]
let event = cx.editor.scancode_apply(event);
let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(i) = event
.char()
@ -6709,6 +6711,8 @@ 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(scancode)]
let event = cx.editor.scancode_apply(event);
let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(inner) = event
.char()

View File

@ -837,7 +837,34 @@ impl EditorView {
) -> Option<KeymapResult> {
let mut last_mode = mode;
self.pseudo_pending.extend(self.keymaps.pending());
let key_result = self.keymaps.get(mode, event);
#[cfg(scancode)]
let key_result = {
if !matches!(
key_result,
KeymapResult::NotFound | KeymapResult::Cancelled(_)
) {
key_result
} else {
match key_result {
KeymapResult::NotFound => {
self.keymaps.get(mode, cxt.editor.scancode_apply(event))
}
KeymapResult::Cancelled(mut keys) => {
// replay previous keys and try again by scancode
let _ = keys.pop();
for key in keys {
let _ = self.keymaps.get(mode, key);
}
self.keymaps.get(mode, cxt.editor.scancode_apply(event))
}
_ => unreachable!(),
}
}
};
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
let mut execute_command = |command: &commands::MappableCommand| {
@ -910,6 +937,13 @@ impl EditorView {
}
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
#[cfg(scancode)]
// dont use scancode on macros replaying
let event = if cxt.editor.macro_replaying.is_empty() {
cxt.editor.scancode_apply(event)
} else {
event
};
match (event, cxt.editor.count) {
// If the count is already started and the input is a number, always continue the count.
(key!(i @ '0'..='9'), Some(count)) => {

View File

@ -14,6 +14,9 @@ homepage.workspace = true
default = []
term = ["crossterm"]
unicode-lines = []
scancode-query = ["keyboard_query"]
scancode-evdev = ["evdev"]
scancode-hidapi = ["hidapi"]
[dependencies]
helix-stdx = { path = "../helix-stdx" }
@ -54,6 +57,11 @@ thiserror.workspace = true
kstring = "2.0"
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"] }

View File

@ -0,0 +1,10 @@
fn main() {
// alias scancode feature flag
println!("cargo::rustc-check-cfg=cfg(scancode)");
#[cfg(any(
feature = "scancode-query",
feature = "scancode-evdev",
feature = "scancode-hidapi"
))]
println!("cargo:rustc-cfg=scancode")
}

View File

@ -20,6 +20,10 @@ use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
use helix_lsp::{Call, LanguageServerId};
#[cfg(scancode)]
use crate::scancode::{deserialize_scancode, KeyboardState, ScanCodeMap};
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
@ -379,6 +383,9 @@ pub struct Config {
/// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
/// `true`.
pub editor_config: bool,
#[cfg(scancode)]
#[serde(skip_serializing, deserialize_with = "deserialize_scancode")]
pub scancode: ScanCodeMap,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -960,6 +967,7 @@ impl From<LineEndingConfig> for LineEnding {
LineEndingConfig::Crlf => LineEnding::Crlf,
#[cfg(feature = "unicode-lines")]
LineEndingConfig::FF => LineEnding::FF,
#[cfg(feature = "unicode-lines")]
LineEndingConfig::CR => LineEnding::CR,
#[cfg(feature = "unicode-lines")]
@ -1055,6 +1063,8 @@ impl Default for Config {
end_of_line_diagnostics: DiagnosticFilter::Disable,
clipboard_provider: ClipboardProvider::default(),
editor_config: true,
#[cfg(scancode)]
scancode: ScanCodeMap::default(),
}
}
}
@ -1156,6 +1166,8 @@ pub struct Editor {
pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache,
#[cfg(scancode)]
pub keyboard_state: KeyboardState,
}
pub type Motion = Box<dyn Fn(&mut Editor)>;
@ -1277,6 +1289,8 @@ impl Editor {
handlers,
mouse_down_range: None,
cursor_cache: CursorCache::default(),
#[cfg(scancode)]
keyboard_state: KeyboardState::new(),
}
}
@ -2294,6 +2308,13 @@ impl Editor {
pub fn get_last_cwd(&mut self) -> Option<&Path> {
self.last_cwd.as_deref()
}
#[cfg(scancode)]
pub fn scancode_apply(&mut self, event: KeyEvent) -> KeyEvent {
self.config()
.scancode
.apply(event, &mut self.keyboard_state)
}
}
fn try_restore_indent(doc: &mut Document, view: &mut View) {

View File

@ -1,11 +1,10 @@
//! Input event handling, currently backed by crossterm.
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
use anyhow::{anyhow, Error};
use helix_core::unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr};
use serde::de::{self, Deserialize, Deserializer};
use std::fmt;
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
pub enum Event {
FocusGained,
@ -334,7 +333,64 @@ impl std::str::FromStr for KeyEvent {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect();
let mut code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
let mut code = if let Ok(code) =
KeyCode::from_str(tokens.pop().ok_or_else(|| anyhow!("Missing key code"))?)
{
code
} else if s.ends_with('-') && tokens.last().is_some_and(|t| t.is_empty()) {
if s == "-" {
return Ok(KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::empty(),
});
} else {
let suggestion = format!("{}-{}", s.trim_end_matches('-'), keys::MINUS);
anyhow::bail!(
"Key '-' cannot be used with modifiers, use '{}' instead",
suggestion
)
}
} else {
anyhow::bail!("Invalid key code '{}'", s)
};
let mut modifiers = KeyModifiers::empty();
for token in tokens {
let flag = match token {
"S" => KeyModifiers::SHIFT,
"A" => KeyModifiers::ALT,
"C" => KeyModifiers::CONTROL,
"Meta" | "Cmd" | "Win" => KeyModifiers::SUPER,
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
};
if modifiers.contains(flag) {
return Err(anyhow!("Repeated key modifier '{}-'", token));
}
modifiers.insert(flag);
}
// Normalize character keys so that characters like C-S-r and C-R
// are represented by equal KeyEvents.
match code {
KeyCode::Char(ch)
if ch.is_ascii_lowercase() && modifiers.contains(KeyModifiers::SHIFT) =>
{
code = KeyCode::Char(ch.to_ascii_uppercase());
modifiers.remove(KeyModifiers::SHIFT);
}
_ => (),
}
Ok(KeyEvent { code, modifiers })
}
}
impl std::str::FromStr for KeyCode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
keys::BACKSPACE => KeyCode::Backspace,
keys::ENTER => KeyCode::Enter,
keys::LEFT => KeyCode::Left,
@ -396,55 +452,8 @@ impl std::str::FromStr for KeyEvent {
.then_some(KeyCode::F(function))
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
}
// Checking that the last token is empty ensures that this branch is only taken if
// `-` is used as a code. For example this branch will not be taken for `S-` (which is
// missing a code).
_ if s.ends_with('-') && tokens.last().is_some_and(|t| t.is_empty()) => {
if s == "-" {
return Ok(KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::empty(),
});
} else {
let suggestion = format!("{}-{}", s.trim_end_matches('-'), keys::MINUS);
return Err(anyhow!(
"Key '-' cannot be used with modifiers, use '{}' instead",
suggestion
));
}
}
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
};
let mut modifiers = KeyModifiers::empty();
for token in tokens {
let flag = match token {
"S" => KeyModifiers::SHIFT,
"A" => KeyModifiers::ALT,
"C" => KeyModifiers::CONTROL,
"Meta" | "Cmd" | "Win" => KeyModifiers::SUPER,
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
};
if modifiers.contains(flag) {
return Err(anyhow!("Repeated key modifier '{}-'", token));
}
modifiers.insert(flag);
}
// Normalize character keys so that characters like C-S-r and C-R
// are represented by equal KeyEvents.
match code {
KeyCode::Char(ch)
if ch.is_ascii_lowercase() && modifiers.contains(KeyModifiers::SHIFT) =>
{
code = KeyCode::Char(ch.to_ascii_uppercase());
modifiers.remove(KeyModifiers::SHIFT);
}
_ => (),
}
Ok(KeyEvent { code, modifiers })
})
}
}

View File

@ -15,6 +15,8 @@ pub mod info;
pub mod input;
pub mod keyboard;
pub mod register;
#[cfg(scancode)]
pub mod scancode;
pub mod theme;
pub mod tree;
pub mod view;

View File

@ -0,0 +1,749 @@
use crate::input::KeyEvent;
use crate::keyboard::{KeyCode, ModifierKeyCode};
use anyhow;
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
type ScanCodeKeyCodeMap = HashMap<u16, (KeyCode, Option<KeyCode>)>;
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct ScanCodeMap {
// {<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()
}
}
#[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(),
device_state: DeviceState::new(),
}
}
pub fn get_scancodes(&mut self) -> Vec<u16> {
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;
struct DeviceHandle {
_path: std::path::PathBuf,
_handle: tokio::task::JoinHandle<()>,
}
pub struct KeyboardState {
codes: [Arc<AtomicU16>; 2],
_handle: Vec<DeviceHandle>,
}
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));
// find keyboards
let keyboards = evdev::enumerate()
.filter(|(_, dev)| is_keyboard(dev))
.collect::<Vec<_>>();
let mut handles = Vec::new();
// evdev constant
const KEY_STATE_RELEASE: i32 = 0;
for (path, mut item) in keyboards {
// skip already grabbed keyboards
let is_grabbed = item.grab().is_err();
if !is_grabbed {
if let Err(e) = item.ungrab() {
log::error!("Failed to ungrab input: {e}");
}
}
if is_grabbed {
continue;
}
let k1 = Arc::clone(&key1);
let k2 = Arc::clone(&key2);
let mut codes = [0, 0];
let device_path = path.to_str().unwrap_or_default().to_owned();
let handle = tokio::task::spawn(async move {
let device_name = item.name().unwrap_or_default().to_owned();
log::info!("Start listen events from: {device_name} ({device_path})");
let Ok(mut events) = item.into_event_stream() else {
log::error!("Failed to stream events from: {device_name} ({device_path})");
return;
};
while let Ok(event) = events.next_event().await {
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);
}
});
handles.push(DeviceHandle {
_path: path,
_handle: handle,
})
}
Self {
_handle: handles,
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),
]
}
}
}
#[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;
}
_ => (),
};
let mut pressed= 0;
// use last pressed key
for i in 0..6 {
let hid_keycode = report[7 - 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}"
);
pressed = scancode;
break;
}
k1.store(pressed, Ordering::Relaxed);
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].load(Ordering::Relaxed), // key
self.codes[1].load(Ordering::Relaxed), // modifier
]
}
}
}
impl ScanCodeMap {
pub fn new(map: HashMap<u16, (KeyCode, Option<KeyCode>)>) -> Self {
let modifiers = map
.iter()
.filter_map(|(code, (key, _))| {
if matches!(key, KeyCode::Modifier(_)) {
Some(*code)
} else {
None
}
})
.collect();
let shift_modifiers = map
.iter()
.filter_map(|(code, (key, _))| {
if matches!(
key,
KeyCode::Modifier(ModifierKeyCode::LeftShift)
| KeyCode::Modifier(ModifierKeyCode::RightShift)
) {
Some(*code)
} else {
None
}
})
.collect();
Self {
map,
modifiers,
shift_modifiers,
}
}
pub fn apply(&self, event: KeyEvent, keyboard: &mut KeyboardState) -> KeyEvent {
let codes = keyboard.get_scancodes();
// get first non modifier key code
let Some(scancode) = codes
.iter()
.find(|c| **c != 0 || !self.modifiers.contains(c))
.cloned()
else {
return event;
};
let Some((key, shifted_key)) = self.map.get(&scancode) else {
return event;
};
let event_before = event;
let mut is_shifted = false;
for c in &self.shift_modifiers {
if codes.contains(c) {
is_shifted = true;
break;
}
}
let event = KeyEvent {
code: match key {
KeyCode::Char(c) => {
if is_shifted | c.is_ascii_uppercase() {
(*shifted_key).unwrap_or(*key)
} else {
*key
}
}
_ => *key,
},
..event
};
log::trace!(
"{:?} map to {:?} by scancode {codes:?} (code: {scancode}, key: {key:?}, shifted key: {shifted_key:?})",
event_before.code,
event.code,
);
event
}
}
pub fn deserialize_scancode<'de, D>(deserializer: D) -> Result<ScanCodeMap, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
struct ScanCodeRawConfig {
layout: String,
map: Option<HashMap<String, Vec<(u16, Vec<String>)>>>,
}
let value = ScanCodeRawConfig::deserialize(deserializer)?;
// load only specified in user settings layout
let map = if let Some(map) = value
.map
.and_then(|m| m.into_iter().find(|(k, _)| k == &value.layout))
{
HashMap::from_iter(
map.1
.into_iter()
.map(|(scancode, chars)| {
if chars.is_empty() {
anyhow::bail!(
"Invalid scancode. Empty map for scancode: {scancode} on layout: {}",
value.layout
);
}
if chars.len() > 2 {
anyhow::bail!(
"Invalid scancode. To many variants for scancode: {scancode} on layout: {}",
value.layout
);
}
let keycode = str::parse::<KeyCode>(&chars[0]).map_err(|e| {
anyhow::anyhow!(
"On parse scancode: {scancode} on layout: {} - {e}",
value.layout
)
})?;
let shifted_keycode = if let Some(c) = chars.get(1) {
Some(str::parse::<KeyCode>(c).map_err(|e| {
anyhow::anyhow!(
"On parse scancode: {scancode} on layout: {} - {e}",
value.layout
)
})?)
} else {
None
};
Ok((scancode, (keycode, shifted_keycode)))
})
.collect::<anyhow::Result<Vec<_>>>()
.map_err(|e| <D::Error as Error>::custom(e))?,
)
} else {
// lookup in hardcoded defaults
let Some(map) = defaults::LAYOUTS.get(value.layout.as_str()) else {
return Err(<D::Error as Error>::custom(format!(
"Scancode layout not found for: {}",
value.layout
)));
};
log::debug!(
"User defined scancode layout not found: {}. Use default",
value.layout
);
map.to_owned()
};
Ok(ScanCodeMap::new(map))
}
mod defaults {
use super::ScanCodeKeyCodeMap;
use crate::keyboard::KeyCode;
use std::collections::HashMap;
use std::str::FromStr;
macro_rules! entry {
($scancode:expr, $keycode:literal) => {
(
$scancode,
(
KeyCode::from_str($keycode).expect("Failed to parse {$keycode} as KeyCode"),
None,
),
)
};
($scancode:expr, $keycode:literal, $shifted_keycode:literal) => {
(
$scancode,
(
KeyCode::from_str($keycode).expect("Failed to parse {$keycode} as KeyCode"),
Some(
KeyCode::from_str($shifted_keycode)
.expect("Failed to parse {$shifted_keycode} as KeyCode"),
),
),
)
};
}
pub static LAYOUTS: once_cell::sync::Lazy<HashMap<&'static str, ScanCodeKeyCodeMap>> =
once_cell::sync::Lazy::new(init);
fn init() -> HashMap<&'static str, ScanCodeKeyCodeMap> {
HashMap::from_iter([qwerty()])
}
fn qwerty() -> (&'static str, ScanCodeKeyCodeMap) {
// https://github.com/emberian/evdev/blob/8feea0685b0acb8153e394ffc393cf560d30a16f/src/scancodes.rs#L30
(
"qwerty",
HashMap::from_iter([
entry!(1, "esc"),
entry!(2, "1", "!"),
entry!(3, "2", "@"),
entry!(4, "3", "#"),
entry!(5, "4", "$"),
entry!(5, "4", "$"),
entry!(6, "5", "%"),
entry!(7, "6", "^"),
entry!(8, "7", "&"),
entry!(9, "8", "*"),
entry!(10, "9", "("),
entry!(11, "0", ")"),
entry!(12, "-", "_"),
entry!(13, "=", "+"),
entry!(14, "backspace"),
entry!(15, "tab"),
entry!(16, "q", "Q"),
entry!(17, "w", "W"),
entry!(18, "e", "E"),
entry!(19, "r", "R"),
entry!(20, "t", "T"),
entry!(21, "y", "Y"),
entry!(22, "u", "U"),
entry!(23, "i", "I"),
entry!(24, "o", "O"),
entry!(25, "p", "P"),
entry!(26, "[", "{"),
entry!(27, "]", "}"),
entry!(28, "ret"),
entry!(29, "leftcontrol"),
entry!(30, "a", "A"),
entry!(31, "s", "S"),
entry!(32, "d", "D"),
entry!(33, "f", "F"),
entry!(34, "g", "G"),
entry!(35, "h", "H"),
entry!(36, "j", "J"),
entry!(37, "k", "K"),
entry!(38, "l", "L"),
entry!(39, ";", ":"),
entry!(40, "'", "\""),
entry!(41, "`", "~"),
entry!(42, "leftshift"),
entry!(43, "\\", "|"),
entry!(44, "z", "Z"),
entry!(45, "x", "X"),
entry!(46, "c", "C"),
entry!(47, "v", "V"),
entry!(48, "b", "B"),
entry!(49, "n", "N"),
entry!(50, "m", "M"),
entry!(51, ",", "<"),
entry!(52, ".", ">"),
entry!(53, "/", "|"),
entry!(54, "rightshift"),
entry!(55, "printscreen"),
entry!(56, "leftalt"),
entry!(57, "space"),
entry!(58, "capslock"),
entry!(59, "F1"),
entry!(60, "F2"),
entry!(61, "F3"),
entry!(62, "F4"),
entry!(63, "F5"),
entry!(64, "F6"),
entry!(65, "F7"),
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!(75, "left"),
// entry!(77, "right"),
// entry!(79, "end"),
// entry!(80, "down"),
// entry!(81, "pagedown"),
// entry!(82, "ins"),
// entry!(83, "del"),
]),
)
}
}

0
runtime/queries/snakemake/LICENSE 100755 → 100644
View File

View File

View File

View File

View File

View File