From 3def8b3933357bdb3f80a1d56d13a56b3d678aa2 Mon Sep 17 00:00:00 2001 From: Ori Supino Date: Thu, 12 Mar 2026 18:54:29 +0200 Subject: [PATCH] working state for linux hyperland --- Cargo.lock | 164 ++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 361 +++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 481 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 258af4c..f716820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,52 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[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" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "cocoa" version = "0.22.0" @@ -85,6 +119,25 @@ dependencies = [ "libc", ] +[[package]] +name = "evdev" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c574a78f2156dbd8d6c1bab4bf43f251bffdd84c3b3682ebe82e413fdfec76fe" +dependencies = [ + "bitvec", + "cfg-if", + "libc", + "nix", + "thiserror", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "foreign-types" version = "0.3.2" @@ -100,6 +153,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "lazy_static" version = "1.5.0" @@ -121,6 +180,28 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + [[package]] name = "objc" version = "0.2.7" @@ -136,6 +217,30 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rdev" version = "0.5.3" @@ -152,15 +257,65 @@ dependencies = [ "x11", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typeLan" version = "0.1.0" dependencies = [ "core-foundation 0.9.4", "core-foundation-sys 0.8.7", + "evdev", "rdev", ] +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + [[package]] name = "winapi" version = "0.3.9" @@ -183,6 +338,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[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" diff --git a/Cargo.toml b/Cargo.toml index 1018589..46bf45d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" rdev = "0.5.3" core-foundation = "0.9" core-foundation-sys = "0.8" +evdev = "0.11" diff --git a/src/main.rs b/src/main.rs index f9bbfa0..4fb91e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,32 @@ use rdev::{listen, Event, EventType, Key}; +#[cfg(target_os = "linux")] +use evdev::{Device, InputEventKind, Key as EvKey}; use std::collections::HashSet; use std::fs::File; use std::io::{self, BufRead}; use std::sync::{Arc, Mutex}; +#[cfg(target_os = "linux")] +use std::process::Command; + +#[cfg(target_os = "macos")] use core_foundation::base::TCFType; +#[cfg(target_os = "macos")] use core_foundation::string::CFString; +#[cfg(target_os = "macos")] use core_foundation_sys::base::CFTypeRef; +#[cfg(target_os = "macos")] use core_foundation_sys::string::CFStringRef; // FFI bindings to macOS Text Input Source (TIS) APIs from the Carbon framework. +#[cfg(target_os = "macos")] #[repr(C)] struct __TISInputSource; +#[cfg(target_os = "macos")] type TISInputSourceRef = *mut __TISInputSource; -#[link(name = "Carbon", kind = "framework")] +#[cfg(target_os = "macos")] +#[link(name = "Carbon")] extern "C" { fn TISCopyInputSourceForLanguage(language: CFStringRef) -> TISInputSourceRef; fn TISSelectInputSource(source: TISInputSourceRef) -> i32; // OSStatus @@ -56,55 +68,71 @@ fn main() { let word_en = Arc::new(Mutex::new(String::new())); let word_he = Arc::new(Mutex::new(String::new())); - // Clone for the callback closure - let en_dict_cb = en_dict.clone(); - let he_dict_cb = he_dict.clone(); - let word_en_cb = Arc::clone(&word_en); - let word_he_cb = Arc::clone(&word_he); - - let callback = move |event: Event| { - let mut word_en = word_en_cb.lock().unwrap(); - let mut word_he = word_he_cb.lock().unwrap(); - - match event.event_type { - EventType::KeyPress(key) => match key { - Key::Space | Key::Return => { - if !word_en.is_empty() || !word_he.is_empty() { - println!( - "Word finished. EN candidate: '{}', HE candidate: '{}'", - *word_en, *word_he - ); - check_and_switch_candidates( - &word_en, - &word_he, - &en_dict_cb, - &he_dict_cb, - ); - word_en.clear(); - word_he.clear(); + #[cfg(target_os = "macos")] + { + // Clone for the callback closure + let en_dict_cb = en_dict.clone(); + let he_dict_cb = he_dict.clone(); + let word_en_cb = Arc::clone(&word_en); + let word_he_cb = Arc::clone(&word_he); + + let callback = move |event: Event| { + let mut word_en = word_en_cb.lock().unwrap(); + let mut word_he = word_he_cb.lock().unwrap(); + + match event.event_type { + EventType::KeyPress(key) => match key { + Key::Space | Key::Return => { + if !word_en.is_empty() || !word_he.is_empty() { + println!( + "Word finished. EN candidate: '{}', HE candidate: '{}'", + *word_en, *word_he + ); + check_and_switch_candidates( + &word_en, + &word_he, + &en_dict_cb, + &he_dict_cb, + ); + word_en.clear(); + word_he.clear(); + } } - } - Key::Backspace => { - word_en.pop(); - word_he.pop(); - } - _ => { - if let Some(ch) = key_to_english_char(key) { - word_en.push(ch); + Key::Backspace => { + word_en.pop(); + word_he.pop(); } - if let Some(ch) = key_to_hebrew_char(key) { - word_he.push(ch); + _ => { + if let Some(ch) = key_to_english_char(key) { + word_en.push(ch); + } + if let Some(ch) = key_to_hebrew_char(key) { + word_he.push(ch); + } } - } - }, - EventType::KeyRelease(_) => {} - _ => {} + }, + EventType::KeyRelease(_) => {} + _ => {} + } + }; + + println!("Listening for keyboard events. Press Space or Enter to check a word."); + if let Err(err) = listen(callback) { + eprintln!("Error while listening for keyboard events: {:?}", err); } - }; + } - println!("Listening for keyboard events. Press Space or Enter to check a word."); - if let Err(err) = listen(callback) { - eprintln!("Error while listening for keyboard events: {:?}", err); + #[cfg(target_os = "linux")] + { + // Capture Hyprland instance signature (if available) for use when running under sudo. + if let Err(err) = capture_hypr_signature() { + eprintln!("Warning: could not capture Hyprland instance signature: {:?}", err); + } + + println!("Listening for keyboard events via evdev. Press Space or Enter to check a word."); + if let Err(err) = run_linux_evdev_listener(word_en, word_he, en_dict, he_dict) { + eprintln!("Error while listening for keyboard events via evdev: {:?}", err); + } } } @@ -148,6 +176,7 @@ fn check_and_switch_candidates( } } +#[cfg(target_os = "macos")] fn switch_layout_to(lang: Language) { let code = match lang { Language::English => "en", @@ -177,6 +206,248 @@ fn switch_layout_to(lang: Language) { } } +#[cfg(target_os = "linux")] +fn switch_layout_to(lang: Language) { + // This assumes your Hyprland config sets: + // kb_layout = us,il + // so index 0 = English (us), index 1 = Hebrew (il). + let index = match lang { + Language::English => 0_i32, + Language::Hebrew => 1_i32, + }; + + println!( + "Attempting to switch Hyprland keyboard layout via hyprctl switchxkblayout current {}", + index + ); + + let mut cmd = Command::new("hyprctl"); + cmd.arg("switchxkblayout") + .arg("current") + .arg(index.to_string()); + + // When running under sudo, Hyprland env vars may not be set. + // If we have previously captured them, inject them into the environment. + if let Ok(env) = std::fs::read_to_string(".hypr_env") { + for line in env.lines() { + if let Some((k, v)) = line.split_once('=') { + let key = k.trim(); + let val = v.trim(); + if !key.is_empty() && !val.is_empty() { + cmd.env(key, val); + } + } + } + } + + match cmd.status() { + Ok(status) if status.success() => { + println!( + "Switched Hyprland keyboard layout to index {} ({}).", + index, + match lang { + Language::English => "English/us", + Language::Hebrew => "Hebrew/il", + } + ); + } + Ok(status) => { + eprintln!( + "hyprctl exited with status code {:?} while switching xkb layout to index {}", + status.code(), + index + ); + } + Err(err) => { + eprintln!( + "Failed to execute hyprctl switchxkblayout current {}: {}. \ +Ensure hyprctl is installed and kb_layout is configured as 'us,il'.", + index, + err + ); + } + } +} + +#[cfg(target_os = "linux")] +fn capture_hypr_signature() -> io::Result<()> { + let mut lines = Vec::new(); + + if let Ok(sig) = std::env::var("HYPRLAND_INSTANCE_SIGNATURE") { + lines.push(format!("HYPRLAND_INSTANCE_SIGNATURE={}", sig)); + } + + if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") { + lines.push(format!("XDG_RUNTIME_DIR={}", runtime)); + } + + if !lines.is_empty() { + std::fs::write(".hypr_env", lines.join("\n"))?; + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +fn run_linux_evdev_listener( + word_en: Arc>, + word_he: Arc>, + en_dict: HashSet, + he_dict: HashSet, +) -> io::Result<()> { + use std::fs; + + let mut devices = Vec::new(); + for entry in fs::read_dir("/dev/input")? { + let entry = entry?; + let path = entry.path(); + if !path.to_string_lossy().contains("event") { + continue; + } + + if let Ok(dev) = Device::open(&path) { + if dev.supported_keys().map_or(false, |keys| { + keys.contains(EvKey::KEY_A) || keys.contains(EvKey::KEY_SPACE) + }) { + println!("Using input device: {}", path.display()); + devices.push(dev); + } + } + } + + if devices.is_empty() { + return Err(io::Error::new( + io::ErrorKind::NotFound, + "No suitable keyboard input devices found under /dev/input", + )); + } + + loop { + for dev in devices.iter_mut() { + for ev in dev.fetch_events()? { + if let InputEventKind::Key(key) = ev.kind() { + // value: 1 = key press, 0 = release, 2 = repeat + if ev.value() != 1 { + continue; + } + + let mut word_en_lock = word_en.lock().unwrap(); + let mut word_he_lock = word_he.lock().unwrap(); + + match key { + EvKey::KEY_SPACE | EvKey::KEY_ENTER => { + if !word_en_lock.is_empty() || !word_he_lock.is_empty() { + println!( + "Word finished. EN candidate: '{}', HE candidate: '{}'", + *word_en_lock, *word_he_lock + ); + check_and_switch_candidates( + &word_en_lock, + &word_he_lock, + &en_dict, + &he_dict, + ); + word_en_lock.clear(); + word_he_lock.clear(); + } + } + EvKey::KEY_BACKSPACE => { + word_en_lock.pop(); + word_he_lock.pop(); + } + _ => { + if let Some(ch) = evkey_to_english_char(key) { + word_en_lock.push(ch); + } + if let Some(ch) = evkey_to_hebrew_char(key) { + word_he_lock.push(ch); + } + } + } + } + } + } + } +} + +#[cfg(target_os = "linux")] +fn evkey_to_english_char(key: EvKey) -> Option { + match key { + EvKey::KEY_A => Some('a'), + EvKey::KEY_B => Some('b'), + EvKey::KEY_C => Some('c'), + EvKey::KEY_D => Some('d'), + EvKey::KEY_E => Some('e'), + EvKey::KEY_F => Some('f'), + EvKey::KEY_G => Some('g'), + EvKey::KEY_H => Some('h'), + EvKey::KEY_I => Some('i'), + EvKey::KEY_J => Some('j'), + EvKey::KEY_K => Some('k'), + EvKey::KEY_L => Some('l'), + EvKey::KEY_M => Some('m'), + EvKey::KEY_N => Some('n'), + EvKey::KEY_O => Some('o'), + EvKey::KEY_P => Some('p'), + EvKey::KEY_Q => Some('q'), + EvKey::KEY_R => Some('r'), + EvKey::KEY_S => Some('s'), + EvKey::KEY_T => Some('t'), + EvKey::KEY_U => Some('u'), + EvKey::KEY_V => Some('v'), + EvKey::KEY_W => Some('w'), + EvKey::KEY_X => Some('x'), + EvKey::KEY_Y => Some('y'), + EvKey::KEY_Z => Some('z'), + EvKey::KEY_1 => Some('1'), + EvKey::KEY_2 => Some('2'), + EvKey::KEY_3 => Some('3'), + EvKey::KEY_4 => Some('4'), + EvKey::KEY_5 => Some('5'), + EvKey::KEY_6 => Some('6'), + EvKey::KEY_7 => Some('7'), + EvKey::KEY_8 => Some('8'), + EvKey::KEY_9 => Some('9'), + EvKey::KEY_0 => Some('0'), + _ => None, + } +} + +#[cfg(target_os = "linux")] +fn evkey_to_hebrew_char(key: EvKey) -> Option { + match key { + EvKey::KEY_Q => Some('ק'), + EvKey::KEY_W => Some('ו'), + EvKey::KEY_E => Some('ק'), // placeholder; adjust as needed + EvKey::KEY_R => Some('ר'), + EvKey::KEY_T => Some('ת'), + EvKey::KEY_Y => Some('י'), + // Adjusted mapping so that the physical key sequence "akuo" + // corresponds to the Hebrew word "שלום". + EvKey::KEY_U => Some('ו'), + EvKey::KEY_I => Some('ט'), + EvKey::KEY_O => Some('ם'), + EvKey::KEY_P => Some('פ'), + EvKey::KEY_A => Some('ש'), + EvKey::KEY_S => Some('ד'), + EvKey::KEY_D => Some('ג'), + EvKey::KEY_F => Some('כ'), + EvKey::KEY_G => Some('ע'), + EvKey::KEY_H => Some('י'), + EvKey::KEY_J => Some('ח'), + EvKey::KEY_K => Some('ל'), + EvKey::KEY_L => Some('ך'), + EvKey::KEY_Z => Some('ז'), + EvKey::KEY_X => Some('ס'), + EvKey::KEY_C => Some('ב'), + EvKey::KEY_V => Some('ה'), + EvKey::KEY_B => Some('נ'), + EvKey::KEY_N => Some('מ'), + EvKey::KEY_M => Some('צ'), + _ => None, + } +} + fn key_to_english_char(key: Key) -> Option { match key { Key::KeyA => Some('a'),