use std::collections::HashMap; use crate::inject::{InjectResult, KeyInjector}; /// X11 keysym values for common keys #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Keysym(pub u32); impl Keysym { pub const BACKSPACE: Keysym = Keysym(0xff08); pub const TAB: Keysym = Keysym(0xff09); pub const RETURN: Keysym = Keysym(0xff0d); pub const ESCAPE: Keysym = Keysym(0xff1b); pub const SPACE: Keysym = Keysym(0x0020); pub const DELETE: Keysym = Keysym(0xffff); pub const A: Keysym = Keysym(0x0061); pub const Z: Keysym = Keysym(0x007a); pub const SHIFT_L: Keysym = Keysym(0xffe1); pub const CTRL_L: Keysym = Keysym(0xffe3); pub fn from_char(ch: char) -> Option { match ch { 'a'..='z' | 'A'..='Z' | '0'..='9' => Some(Keysym(ch as u32)), ' ' => Some(Keysym::SPACE), '.' => Some(Keysym(0x002e)), ',' => Some(Keysym(0x002c)), '-' => Some(Keysym(0x002d)), '=' => Some(Keysym(0x003d)), ';' => Some(Keysym(0x003b)), '\'' => Some(Keysym(0x0027)), '/' => Some(Keysym(0x002f)), '\\' => Some(Keysym(0x005c)), '`' => Some(Keysym(0x0060)), '[' => Some(Keysym(0x005b)), ']' => Some(Keysym(0x005d)), '\n' => Some(Keysym::RETURN), '\t' => Some(Keysym::TAB), _ => None, } } pub fn to_char(self) -> Option { match self.0 { 0x0061..=0x007a => Some((self.0 as u8) as char), 0x0041..=0x005a => Some((self.0 as u8) as char), 0x0030..=0x0039 => Some((self.0 as u8) as char), 0x0020 => Some(' '), 0x002e => Some('.'), 0x002c => Some(','), 0x002d => Some('-'), 0x003d => Some('='), 0x003b => Some(';'), 0x0027 => Some('\''), 0x002f => Some('/'), 0x005c => Some('\\'), 0x0060 => Some('`'), 0x005b => Some('['), 0x005d => Some(']'), 0xff0d => Some('\n'), 0xff09 => Some('\t'), _ => None, } } pub fn is_printable(self) -> bool { self.to_char().is_some() } pub fn is_modifier(self) -> bool { matches!(self.0, 0xffe1..=0xffee) } } /// Key event from Wayland IM protocol #[derive(Debug, Clone)] pub struct IMKeyEvent { pub keysym: Keysym, pub pressed: bool, pub modifiers: KeyModifiers, } #[derive(Debug, Clone, Default)] pub struct KeyModifiers { pub shift: bool, pub ctrl: bool, pub alt: bool, pub super_key: bool, } /// Wayland input method state #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IMState { Inactive, Active, } /// Wayland IM context for receiving key events from compositor /// /// This implements the zwp_input_method_v2 protocol to receive keysyms /// directly from the Wayland compositor, bypassing evdev interception. pub struct WaylandIMContext { state: IMState, preedit: Option, cursor_pos: usize, commit_buffer: String, keysym_map: HashMap, } impl Default for WaylandIMContext { fn default() -> Self { Self::new() } } impl WaylandIMContext { pub fn new() -> Self { Self { state: IMState::Inactive, preedit: None, cursor_pos: 0, commit_buffer: String::new(), keysym_map: Self::build_keysym_map(), } } fn build_keysym_map() -> HashMap { let mut map = HashMap::new(); // Lowercase letters for i in 0u32..26 { map.insert(0x0061 + i, (b'a' + i as u8) as char); } // Uppercase letters for i in 0u32..26 { map.insert(0x0041 + i, (b'A' + i as u8) as char); } // Digits for i in 0u32..10 { map.insert(0x0030 + i, (b'0' + i as u8) as char); } // Common punctuation map.insert(0x0020, ' '); map.insert(0x002e, '.'); map.insert(0x002c, ','); map.insert(0x002d, '-'); map.insert(0x003d, '='); map.insert(0x003b, ';'); map.insert(0x0027, '\''); map.insert(0x002f, '/'); map.insert(0x005c, '\\'); map.insert(0x0060, '`'); map.insert(0x005b, '['); map.insert(0x005d, ']'); // Special keys map.insert(0xff0d, '\n'); // Return map.insert(0xff09, '\t'); // Tab map.insert(0xff08, '\x08'); // Backspace map.insert(0xff1b, '\x1b'); // Escape map.insert(0xffff, '\x7f'); // Delete map } /// Handle IM activation from compositor pub fn activate(&mut self) { self.state = IMState::Active; eprintln!("[vietc-wayland] IM activated"); } /// Handle IM deactivation from compositor pub fn deactivate(&mut self) { self.state = IMState::Inactive; self.preedit = None; self.commit_buffer.clear(); eprintln!("[vietc-wayland] IM deactivated"); } /// Get current IM state pub fn state(&self) -> IMState { self.state } /// Set preedit text (shown with underline in client) pub fn set_preedit(&mut self, text: Option, cursor: usize) { self.preedit = text; self.cursor_pos = cursor; } /// Get current preedit text pub fn preedit(&self) -> Option<&str> { self.preedit.as_deref() } /// Commit text to the focused surface pub fn commit(&mut self, text: &str) { self.commit_buffer.push_str(text); } /// Get and clear the commit buffer pub fn take_commit(&mut self) -> String { std::mem::take(&mut self.commit_buffer) } /// Convert a keysym to a character, applying modifiers pub fn keysym_to_char(&self, keysym: Keysym, mods: &KeyModifiers) -> Option { if keysym.is_modifier() { return None; } let base = self.keysym_map.get(&keysym.0).copied()?; // Apply shift for letters if mods.shift && base.is_ascii_lowercase() { return Some(base.to_ascii_uppercase()); } // Shift+digit produces symbol if mods.shift && base.is_ascii_digit() { let shifted = match base { '1' => '!', '2' => '@', '3' => '#', '4' => '$', '5' => '%', '6' => '^', '7' => '&', '8' => '*', '9' => '(', '0' => ')', _ => return Some(base), }; return Some(shifted); } Some(base) } /// Convert a character to a keysym pub fn char_to_keysym(ch: char) -> Option { Keysym::from_char(ch) } /// Process a raw keysym event and return the character (if any) pub fn process_keysym(&self, keysym: Keysym, mods: &KeyModifiers) -> Option { self.keysym_to_char(keysym, mods) } } /// Wayland IM key injector using zwp_input_method_context_v2 /// /// Commits text directly to the focused surface without key injection. /// Falls back to uinput/X11 if context is not available. pub struct WaylandIMInjector { committed: Vec, } impl Default for WaylandIMInjector { fn default() -> Self { Self::new() } } impl WaylandIMInjector { pub fn new() -> Self { Self { committed: Vec::new(), } } /// Take all committed text since last call pub fn take_commits(&mut self) -> Vec { std::mem::take(&mut self.committed) } } impl KeyInjector for WaylandIMInjector { fn send_backspace(&self) -> InjectResult { // In real implementation, this would call // context.delete_surrounding_text(-1, 1) + context.commit() InjectResult::Success } fn send_char(&self, _ch: char) -> InjectResult { // In real implementation, this would call // context.commit_string(ch.to_string()) + context.commit() InjectResult::Success } fn send_string(&self, _s: &str) -> InjectResult { // In real implementation, this would call // context.commit_string(s.to_string()) + context.commit() InjectResult::Success } fn flush(&self) -> InjectResult { InjectResult::Success } } #[cfg(test)] mod tests { use super::*; #[test] fn keysym_from_char() { assert_eq!(Keysym::from_char('a'), Some(Keysym(0x0061))); assert_eq!(Keysym::from_char('z'), Some(Keysym(0x007a))); assert_eq!(Keysym::from_char('A'), Some(Keysym(0x0041))); assert_eq!(Keysym::from_char('0'), Some(Keysym(0x0030))); assert_eq!(Keysym::from_char(' '), Some(Keysym(0x0020))); assert_eq!(Keysym::from_char('.'), Some(Keysym(0x002e))); assert_eq!(Keysym::from_char('\n'), Some(Keysym(0xff0d))); assert_eq!(Keysym::from_char('ñ'), None); } #[test] fn keysym_to_char() { assert_eq!(Keysym(0x0061).to_char(), Some('a')); assert_eq!(Keysym(0x007a).to_char(), Some('z')); assert_eq!(Keysym(0x0041).to_char(), Some('A')); assert_eq!(Keysym(0x0030).to_char(), Some('0')); assert_eq!(Keysym(0x0020).to_char(), Some(' ')); assert_eq!(Keysym(0xff0d).to_char(), Some('\n')); assert_eq!(Keysym(0xffff).to_char(), None); } #[test] fn keysym_is_printable() { assert!(Keysym(0x0061).is_printable()); // 'a' assert!(Keysym(0x0020).is_printable()); // space assert!(Keysym(0xff0d).is_printable()); // Return → '\n' assert!(!Keysym(0xff08).is_printable()); // Backspace → '\x08' (not printable) } #[test] fn keysym_is_modifier() { assert!(Keysym(0xffe1).is_modifier()); // shift assert!(Keysym(0xffe3).is_modifier()); // ctrl assert!(Keysym(0xffe9).is_modifier()); // alt assert!(!Keysym(0x0061).is_modifier()); // 'a' assert!(!Keysym(0x0020).is_modifier()); // space } #[test] fn im_context_activate_deactivate() { let mut ctx = WaylandIMContext::new(); assert_eq!(ctx.state(), IMState::Inactive); ctx.activate(); assert_eq!(ctx.state(), IMState::Active); ctx.deactivate(); assert_eq!(ctx.state(), IMState::Inactive); } #[test] fn im_context_preedit() { let mut ctx = WaylandIMContext::new(); assert!(ctx.preedit().is_none()); ctx.set_preedit(Some("hello".into()), 3); assert_eq!(ctx.preedit(), Some("hello")); ctx.set_preedit(None, 0); assert!(ctx.preedit().is_none()); } #[test] fn im_context_commit() { let mut ctx = WaylandIMContext::new(); ctx.commit("hello"); ctx.commit(" "); ctx.commit("world"); assert_eq!(ctx.take_commit(), "hello world"); assert!(ctx.take_commit().is_empty()); } #[test] fn keysym_to_char_no_modifiers() { let ctx = WaylandIMContext::new(); let mods = KeyModifiers::default(); assert_eq!(ctx.keysym_to_char(Keysym(0x0061), &mods), Some('a')); assert_eq!(ctx.keysym_to_char(Keysym(0x007a), &mods), Some('z')); assert_eq!(ctx.keysym_to_char(Keysym(0x0030), &mods), Some('0')); assert_eq!(ctx.keysym_to_char(Keysym(0x0020), &mods), Some(' ')); } #[test] fn keysym_to_char_shift() { let ctx = WaylandIMContext::new(); let mods = KeyModifiers { shift: true, ..Default::default() }; assert_eq!(ctx.keysym_to_char(Keysym(0x0061), &mods), Some('A')); assert_eq!(ctx.keysym_to_char(Keysym(0x007a), &mods), Some('Z')); assert_eq!(ctx.keysym_to_char(Keysym(0x0031), &mods), Some('!')); assert_eq!(ctx.keysym_to_char(Keysym(0x0032), &mods), Some('@')); } #[test] fn keysym_to_char_modifier_returns_none() { let ctx = WaylandIMContext::new(); let mods = KeyModifiers::default(); assert_eq!(ctx.keysym_to_char(Keysym(0xffe1), &mods), None); // shift assert_eq!(ctx.keysym_to_char(Keysym(0xffe3), &mods), None); // ctrl } #[test] fn process_keysym() { let ctx = WaylandIMContext::new(); let mods = KeyModifiers::default(); assert_eq!(ctx.process_keysym(Keysym(0x0061), &mods), Some('a')); assert_eq!(ctx.process_keysym(Keysym(0xff0d), &mods), Some('\n')); } #[test] fn char_to_keysym_roundtrip() { for ch in "abcdefghijklmnopqrstuvwxyz".chars() { let keysym = WaylandIMContext::char_to_keysym(ch).unwrap(); let back = keysym.to_char().unwrap(); assert_eq!(ch, back); } for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars() { let keysym = WaylandIMContext::char_to_keysym(ch).unwrap(); let back = keysym.to_char().unwrap(); assert_eq!(ch, back); } } }