feat(wayland): keyboard mode, legacy translate (#14317)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-02-15 16:43:21 +08:00
committed by GitHub
parent b268aa1061
commit 779b7aaf02
5 changed files with 897 additions and 75 deletions

View File

@@ -1861,8 +1861,18 @@ class _KeyboardMenu extends StatelessWidget {
continue; continue;
} }
if (pi.isWayland && mode.key != kKeyMapMode) { if (pi.isWayland) {
continue; // Legacy mode is hidden on desktop control side because dead keys
// don't work properly on Wayland. When the control side is mobile,
// Legacy mode is used automatically (mobile always sends Legacy events).
if (mode.key == kKeyLegacyMode) {
continue;
}
// Translate mode requires server >= 1.4.6.
if (mode.key == kKeyTranslateMode &&
versionCmp(pi.version, '1.4.6') < 0) {
continue;
}
} }
var text = translate(mode.menu); var text = translate(mode.menu);

View File

@@ -261,6 +261,8 @@ impl KeyboardControllable for Enigo {
} else { } else {
if let Some(keyboard) = &mut self.custom_keyboard { if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_sequence(sequence) keyboard.key_sequence(sequence)
} else {
log::warn!("Enigo::key_sequence: no custom_keyboard set for Wayland!");
} }
} }
} }
@@ -277,6 +279,7 @@ impl KeyboardControllable for Enigo {
if let Some(keyboard) = &mut self.custom_keyboard { if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_down(key) keyboard.key_down(key)
} else { } else {
log::warn!("Enigo::key_down: no custom_keyboard set for Wayland!");
Ok(()) Ok(())
} }
} }
@@ -290,13 +293,24 @@ impl KeyboardControllable for Enigo {
} else { } else {
if let Some(keyboard) = &mut self.custom_keyboard { if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_up(key) keyboard.key_up(key)
} else {
log::warn!("Enigo::key_up: no custom_keyboard set for Wayland!");
} }
} }
} }
fn key_click(&mut self, key: Key) { fn key_click(&mut self, key: Key) {
if self.tfc_key_click(key).is_err() { if self.is_x11 {
self.key_down(key).ok(); // X11: try tfc first, then fallback to key_down/key_up
self.key_up(key); if self.tfc_key_click(key).is_err() {
self.key_down(key).ok();
self.key_up(key);
}
} else {
if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_click(key);
} else {
log::warn!("Enigo::key_click: no custom_keyboard set for Wayland!");
}
} }
} }
} }

View File

@@ -111,6 +111,10 @@ struct Input {
const KEY_CHAR_START: u64 = 9999; const KEY_CHAR_START: u64 = 9999;
// XKB keycode for Insert key (evdev KEY_INSERT code 110 + 8 for XKB offset)
#[cfg(target_os = "linux")]
const XKB_KEY_INSERT: u16 = evdev::Key::KEY_INSERT.code() + 8;
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct MouseCursorSub { pub struct MouseCursorSub {
inner: ConnInner, inner: ConnInner,
@@ -1105,8 +1109,12 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
// Clamp delta to prevent extreme/malicious values from reaching OS APIs. // Clamp delta to prevent extreme/malicious values from reaching OS APIs.
// This matches the Flutter client's kMaxRelativeMouseDelta constant. // This matches the Flutter client's kMaxRelativeMouseDelta constant.
const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000; const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000;
let dx = evt.x.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); let dx = evt
let dy = evt.y.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); .x
.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
let dy = evt
.y
.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
en.mouse_move_relative(dx, dy); en.mouse_move_relative(dx, dy);
// Get actual cursor position after relative movement for tracking // Get actual cursor position after relative movement for tracking
if let Some((x, y)) = crate::get_cursor_pos() { if let Some((x, y)) = crate::get_cursor_pos() {
@@ -1465,20 +1473,26 @@ fn map_keyboard_mode(evt: &KeyEvent) {
// Wayland // Wayland
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() { if !crate::platform::linux::is_x11() {
let mut en = ENIGO.lock().unwrap(); wayland_send_raw_key(evt.chr() as u16, evt.down);
let code = evt.chr() as u16;
if evt.down {
en.key_down(enigo::Key::Raw(code)).ok();
} else {
en.key_up(enigo::Key::Raw(code));
}
return; return;
} }
sim_rdev_rawkey_position(evt.chr() as _, evt.down); sim_rdev_rawkey_position(evt.chr() as _, evt.down);
} }
/// Send raw keycode on Wayland via the active backend (uinput or RemoteDesktop portal).
/// The keycode is expected to be a Linux keycode (evdev code + 8 for X11 compatibility).
#[cfg(target_os = "linux")]
#[inline]
fn wayland_send_raw_key(code: u16, down: bool) {
let mut en = ENIGO.lock().unwrap();
if down {
en.key_down(enigo::Key::Raw(code)).ok();
} else {
en.key_up(enigo::Key::Raw(code));
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) {
// When long-pressed the command key, then press and release // When long-pressed the command key, then press and release
@@ -1559,6 +1573,20 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
} }
fn process_chr(en: &mut Enigo, chr: u32, down: bool) { fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
// On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
// Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed)
if !is_hotkey_modifier_pressed(en) {
if down {
if let Ok(c) = char::try_from(chr) {
input_char_via_clipboard_server(en, c);
}
}
return;
}
}
let key = char_value_to_key(chr); let key = char_value_to_key(chr);
if down { if down {
@@ -1578,15 +1606,136 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
} }
fn process_unicode(en: &mut Enigo, chr: u32) { fn process_unicode(en: &mut Enigo, chr: u32) {
// On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
if let Ok(c) = char::try_from(chr) {
input_char_via_clipboard_server(en, c);
}
return;
}
if let Ok(chr) = char::try_from(chr) { if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string()); en.key_sequence(&chr.to_string());
} }
} }
fn process_seq(en: &mut Enigo, sequence: &str) { fn process_seq(en: &mut Enigo, sequence: &str) {
// On Wayland with uinput mode, use clipboard for text input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
input_text_via_clipboard_server(en, sequence);
return;
}
en.key_sequence(&sequence); en.key_sequence(&sequence);
} }
/// Delay in milliseconds to wait for clipboard to sync on Wayland.
/// This is an empirical value — Wayland provides no callback or event to confirm
/// clipboard content has been received by the compositor. Under heavy system load,
/// this delay may be insufficient, but there is no reliable alternative mechanism.
#[cfg(target_os = "linux")]
const CLIPBOARD_SYNC_DELAY_MS: u64 = 50;
/// Internal: Set clipboard content without delay.
/// Returns true if clipboard was set successfully.
#[cfg(target_os = "linux")]
fn set_clipboard_content(text: &str) -> bool {
use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux};
let mut clipboard = match Clipboard::new() {
Ok(cb) => cb,
Err(e) => {
log::error!("set_clipboard_content: failed to create clipboard: {:?}", e);
return false;
}
};
// Set both CLIPBOARD and PRIMARY selections
// Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD
if let Err(e) = clipboard
.set()
.clipboard(LinuxClipboardKind::Clipboard)
.text(text.to_owned())
{
log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e);
return false;
}
if let Err(e) = clipboard
.set()
.clipboard(LinuxClipboardKind::Primary)
.text(text.to_owned())
{
log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e);
// Continue anyway, CLIPBOARD might work
}
true
}
/// Set clipboard content for paste operation (sync version for use in blocking contexts).
///
/// Note: The original clipboard content is intentionally NOT restored after paste.
/// Restoring clipboard could cause race conditions where subsequent keystrokes
/// might accidentally paste the old clipboard content instead of the intended input.
/// This trade-off prioritizes input reliability over preserving clipboard state.
#[cfg(target_os = "linux")]
#[inline]
pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool {
if !set_clipboard_content(text) {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS));
true
}
/// Check if a character is ASCII printable (0x20-0x7E).
#[cfg(target_os = "linux")]
#[inline]
pub(super) fn is_ascii_printable(c: char) -> bool {
c as u32 >= 0x20 && c as u32 <= 0x7E
}
/// Input a single character via clipboard + Shift+Insert in server process.
#[cfg(target_os = "linux")]
#[inline]
fn input_char_via_clipboard_server(en: &mut Enigo, chr: char) {
input_text_via_clipboard_server(en, &chr.to_string());
}
/// Input text via clipboard + Shift+Insert in server process.
/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals.
///
/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale.
#[cfg(target_os = "linux")]
fn input_text_via_clipboard_server(en: &mut Enigo, text: &str) {
if text.is_empty() {
return;
}
if !set_clipboard_for_paste_sync(text) {
return;
}
// Use ENIGO's custom_keyboard directly to avoid creating new IPC connections
// which would cause excessive logging and keyboard device creation/destruction
if en.key_down(Key::Shift).is_err() {
log::error!("input_text_via_clipboard_server: failed to press Shift, skipping paste");
return;
}
if en.key_down(Key::Raw(XKB_KEY_INSERT)).is_err() {
log::error!("input_text_via_clipboard_server: failed to press Insert, releasing Shift");
en.key_up(Key::Shift);
return;
}
en.key_up(Key::Raw(XKB_KEY_INSERT));
en.key_up(Key::Shift);
// Brief delay to allow the target application to process the paste event.
// Empirical value — no reliable synchronization mechanism exists on Wayland.
std::thread::sleep(std::time::Duration::from_millis(20));
}
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
fn release_keys(en: &mut Enigo, to_release: &Vec<Key>) { fn release_keys(en: &mut Enigo, to_release: &Vec<Key>) {
for key in to_release { for key in to_release {
@@ -1621,6 +1770,64 @@ fn is_function_key(ck: &EnumOrUnknown<ControlKey>) -> bool {
return res; return res;
} }
/// Check if any hotkey modifier (Ctrl/Alt/Meta) is currently pressed.
/// Used to detect hotkey combinations like Ctrl+C, Alt+Tab, etc.
///
/// Note: Shift is intentionally NOT checked here. Shift+character produces a different
/// character (e.g., Shift+a → 'A'), which is normal text input, not a hotkey.
/// Shift is only relevant as a hotkey modifier when combined with Ctrl/Alt/Meta
/// (e.g., Ctrl+Shift+Z), in which case this function already returns true via Ctrl.
#[cfg(target_os = "linux")]
#[inline]
fn is_hotkey_modifier_pressed(en: &mut Enigo) -> bool {
get_modifier_state(Key::Control, en)
|| get_modifier_state(Key::RightControl, en)
|| get_modifier_state(Key::Alt, en)
|| get_modifier_state(Key::RightAlt, en)
|| get_modifier_state(Key::Meta, en)
|| get_modifier_state(Key::RWin, en)
}
/// Release Shift keys before character input in Legacy/Translate mode.
/// In these modes, the character has already been converted by the client,
/// so we should input it directly without Shift modifier affecting the result.
///
/// Note: Does NOT release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed,
/// to preserve combinations like Ctrl+Shift+Z.
#[cfg(target_os = "linux")]
fn release_shift_for_char_input(en: &mut Enigo) {
// Don't release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed.
// This preserves combinations like Ctrl+Shift+Z.
if is_hotkey_modifier_pressed(en) {
return;
}
// In translate mode, the client has already converted the keystroke to a character
// (e.g., Shift+a → 'A'). We release Shift here so the server inputs the character
// directly without Shift affecting the result.
//
// Shift is intentionally NOT restored after input — the client will send an explicit
// Shift key_up event when the user physically releases Shift. Restoring it here would
// cause a brief Shift re-press that could interfere with the next input event.
let is_x11 = crate::platform::linux::is_x11();
if get_modifier_state(Key::Shift, en) {
if !is_x11 {
en.key_up(Key::Shift);
} else {
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft));
}
}
if get_modifier_state(Key::RightShift, en) {
if !is_x11 {
en.key_up(Key::RightShift);
} else {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight));
}
}
}
fn legacy_keyboard_mode(evt: &KeyEvent) { fn legacy_keyboard_mode(evt: &KeyEvent) {
#[cfg(windows)] #[cfg(windows)]
crate::platform::windows::try_change_desktop(); crate::platform::windows::try_change_desktop();
@@ -1640,11 +1847,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
process_control_key(&mut en, &ck, down) process_control_key(&mut en, &ck, down)
} }
Some(key_event::Union::Chr(chr)) => { Some(key_event::Union::Chr(chr)) => {
// For character input in Legacy mode, we need to release Shift first.
// The character has already been converted by the client, so we should
// input it directly without Shift modifier affecting the result.
// Only Ctrl/Alt/Meta should be kept for hotkeys like Ctrl+C.
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
let record_key = chr as u64 + KEY_CHAR_START; let record_key = chr as u64 + KEY_CHAR_START;
record_pressed_key(KeysDown::EnigoKey(record_key), down); record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_chr(&mut en, chr, down) process_chr(&mut en, chr, down)
} }
Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), Some(key_event::Union::Unicode(chr)) => {
// Same as Chr: release Shift for Unicode input
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
process_unicode(&mut en, chr)
}
Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq),
_ => {} _ => {}
} }
@@ -1665,6 +1885,51 @@ fn translate_process_code(code: u32, down: bool) {
fn translate_keyboard_mode(evt: &KeyEvent) { fn translate_keyboard_mode(evt: &KeyEvent) {
match &evt.union { match &evt.union {
Some(key_event::Union::Seq(seq)) => { Some(key_event::Union::Seq(seq)) => {
// On Wayland, handle character input directly in this (--server) process using clipboard.
// This function runs in the --server process (logged-in user session), which has
// WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here.
//
// Why not let it go through uinput IPC:
// 1. For uinput mode: the uinput service thread runs in the --service (root) process,
// which typically lacks user session environment. Clipboard operations there are
// unreliable. Handling clipboard here avoids that issue.
// 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms
// based on its internal modifier state, which may not match our released state.
// Using clipboard bypasses this issue entirely.
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() {
let mut en = ENIGO.lock().unwrap();
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
// For hotkeys, we send character-based key events via Enigo instead of
// using the clipboard. This relies on the local keyboard layout for
// mapping characters to physical keys.
// This assumes client and server use the same keyboard layout (common case).
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
// characters which are mappable on most keyboard layouts.
if is_hotkey_modifier_pressed(&mut en) {
// For hotkeys, send character-based key events via Enigo.
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
for chr in seq.chars() {
if !is_ascii_printable(chr) {
log::warn!(
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
);
}
en.key_click(Key::Layout(chr));
}
return;
}
// Normal text input: release Shift and use clipboard
release_shift_for_char_input(&mut en);
input_text_via_clipboard_server(&mut en, seq);
return;
}
// Fr -> US // Fr -> US
// client: Shift + & => 1(send to remote) // client: Shift + & => 1(send to remote)
// remote: Shift + 1 => ! // remote: Shift + 1 => !
@@ -1682,11 +1947,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let simulate_win_hot_key = false; let simulate_win_hot_key = false;
if !simulate_win_hot_key { if !simulate_win_hot_key {
if get_modifier_state(Key::Shift, &mut en) { #[cfg(target_os = "linux")]
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); release_shift_for_char_input(&mut en);
} #[cfg(target_os = "windows")]
if get_modifier_state(Key::RightShift, &mut en) { {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); if get_modifier_state(Key::Shift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft));
}
if get_modifier_state(Key::RightShift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight));
}
} }
} }
for chr in seq.chars() { for chr in seq.chars() {
@@ -1706,7 +1976,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
Some(key_event::Union::Chr(..)) => { Some(key_event::Union::Chr(..)) => {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
translate_process_code(evt.chr(), evt.down); translate_process_code(evt.chr(), evt.down);
#[cfg(not(target_os = "windows"))] #[cfg(target_os = "linux")]
{
if !crate::platform::linux::is_x11() {
// Wayland: use uinput to send raw keycode
wayland_send_raw_key(evt.chr() as u16, evt.down);
} else {
sim_rdev_rawkey_position(evt.chr() as _, evt.down);
}
}
#[cfg(target_os = "macos")]
sim_rdev_rawkey_position(evt.chr() as _, evt.down); sim_rdev_rawkey_position(evt.chr() as _, evt.down);
} }
Some(key_event::Union::Unicode(..)) => { Some(key_event::Union::Unicode(..)) => {
@@ -1717,7 +1996,11 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
simulate_win2win_hotkey(*code, evt.down); simulate_win2win_hotkey(*code, evt.down);
} }
_ => { _ => {
log::debug!("Unreachable. Unexpected key event {:?}", &evt); log::debug!(
"Unreachable. Unexpected key event (mode={:?}, down={:?})",
&evt.mode,
&evt.down
);
} }
} }
} }

View File

@@ -1,7 +1,8 @@
use crate::uinput::service::map_key; use super::input_service::set_clipboard_for_paste_sync;
use crate::uinput::service::{can_input_via_keysym, char_to_keysym, map_key};
use dbus::{blocking::SyncConnection, Path}; use dbus::{blocking::SyncConnection, Path};
use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable}; use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::ResultType; use hbb_common::{log, ResultType};
use scrap::wayland::pipewire::{get_portal, PwStreamInfo}; use scrap::wayland::pipewire::{get_portal, PwStreamInfo};
use scrap::wayland::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; use scrap::wayland::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal;
use std::collections::HashMap; use std::collections::HashMap;
@@ -19,14 +20,74 @@ pub mod client {
const PRESSED_DOWN_STATE: u32 = 1; const PRESSED_DOWN_STATE: u32 = 1;
const PRESSED_UP_STATE: u32 = 0; const PRESSED_UP_STATE: u32 = 0;
/// Modifier key state tracking for RDP input.
/// Portal API doesn't provide a way to query key state, so we track it ourselves.
#[derive(Default)]
struct ModifierState {
shift_left: bool,
shift_right: bool,
ctrl_left: bool,
ctrl_right: bool,
alt_left: bool,
alt_right: bool,
meta_left: bool,
meta_right: bool,
}
impl ModifierState {
fn update(&mut self, key: &Key, down: bool) {
match key {
Key::Shift => self.shift_left = down,
Key::RightShift => self.shift_right = down,
Key::Control => self.ctrl_left = down,
Key::RightControl => self.ctrl_right = down,
Key::Alt => self.alt_left = down,
Key::RightAlt => self.alt_right = down,
Key::Meta | Key::Super | Key::Windows | Key::Command => self.meta_left = down,
Key::RWin => self.meta_right = down,
// Handle raw keycodes for modifier keys (Linux evdev codes + 8)
// In translate mode, modifier keys may be sent as Chr events with raw keycodes.
// The +8 offset converts evdev codes to X11/XKB keycodes.
Key::Raw(code) => {
const EVDEV_OFFSET: u16 = 8;
const KEY_LEFTSHIFT: u16 = evdev::Key::KEY_LEFTSHIFT.code() + EVDEV_OFFSET;
const KEY_RIGHTSHIFT: u16 = evdev::Key::KEY_RIGHTSHIFT.code() + EVDEV_OFFSET;
const KEY_LEFTCTRL: u16 = evdev::Key::KEY_LEFTCTRL.code() + EVDEV_OFFSET;
const KEY_RIGHTCTRL: u16 = evdev::Key::KEY_RIGHTCTRL.code() + EVDEV_OFFSET;
const KEY_LEFTALT: u16 = evdev::Key::KEY_LEFTALT.code() + EVDEV_OFFSET;
const KEY_RIGHTALT: u16 = evdev::Key::KEY_RIGHTALT.code() + EVDEV_OFFSET;
const KEY_LEFTMETA: u16 = evdev::Key::KEY_LEFTMETA.code() + EVDEV_OFFSET;
const KEY_RIGHTMETA: u16 = evdev::Key::KEY_RIGHTMETA.code() + EVDEV_OFFSET;
match *code {
KEY_LEFTSHIFT => self.shift_left = down,
KEY_RIGHTSHIFT => self.shift_right = down,
KEY_LEFTCTRL => self.ctrl_left = down,
KEY_RIGHTCTRL => self.ctrl_right = down,
KEY_LEFTALT => self.alt_left = down,
KEY_RIGHTALT => self.alt_right = down,
KEY_LEFTMETA => self.meta_left = down,
KEY_RIGHTMETA => self.meta_right = down,
_ => {}
}
}
_ => {}
}
}
}
pub struct RdpInputKeyboard { pub struct RdpInputKeyboard {
conn: Arc<SyncConnection>, conn: Arc<SyncConnection>,
session: Path<'static>, session: Path<'static>,
modifier_state: ModifierState,
} }
impl RdpInputKeyboard { impl RdpInputKeyboard {
pub fn new(conn: Arc<SyncConnection>, session: Path<'static>) -> ResultType<Self> { pub fn new(conn: Arc<SyncConnection>, session: Path<'static>) -> ResultType<Self> {
Ok(Self { conn, session }) Ok(Self {
conn,
session,
modifier_state: ModifierState::default(),
})
} }
} }
@@ -39,29 +100,192 @@ pub mod client {
self self
} }
fn get_key_state(&mut self, _: Key) -> bool { fn get_key_state(&mut self, key: Key) -> bool {
// no api for this // Use tracked modifier state for supported keys
false match key {
Key::Shift => self.modifier_state.shift_left,
Key::RightShift => self.modifier_state.shift_right,
Key::Control => self.modifier_state.ctrl_left,
Key::RightControl => self.modifier_state.ctrl_right,
Key::Alt => self.modifier_state.alt_left,
Key::RightAlt => self.modifier_state.alt_right,
Key::Meta | Key::Super | Key::Windows | Key::Command => {
self.modifier_state.meta_left
}
Key::RWin => self.modifier_state.meta_right,
_ => false,
}
} }
fn key_sequence(&mut self, s: &str) { fn key_sequence(&mut self, s: &str) {
for c in s.chars() { for c in s.chars() {
let key = Key::Layout(c); let keysym = char_to_keysym(c);
let _ = handle_key(true, key, self.conn.clone(), &self.session); // ASCII characters: use keysym
let _ = handle_key(false, key, self.conn.clone(), &self.session); if can_input_via_keysym(c, keysym) {
if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym down: {:?}", e);
}
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e);
}
} else {
// Non-ASCII: use clipboard
input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session);
}
} }
} }
fn key_down(&mut self, key: Key) -> enigo::ResultType { fn key_down(&mut self, key: Key) -> enigo::ResultType {
handle_key(true, key, self.conn.clone(), &self.session)?; if let Key::Layout(chr) = key {
let keysym = char_to_keysym(chr);
// ASCII characters: use keysym
if can_input_via_keysym(chr, keysym) {
send_keysym(keysym, true, self.conn.clone(), &self.session)?;
} else {
// Non-ASCII: use clipboard (complete key press in key_down)
input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session);
}
} else {
handle_key(true, key.clone(), self.conn.clone(), &self.session)?;
// Update modifier state only after successful send —
// if handle_key fails, we don't want stale "pressed" state
// affecting subsequent key event decisions.
self.modifier_state.update(&key, true);
}
Ok(()) Ok(())
} }
fn key_up(&mut self, key: Key) { fn key_up(&mut self, key: Key) {
let _ = handle_key(false, key, self.conn.clone(), &self.session); // Intentionally asymmetric with key_down: update state BEFORE sending.
// On release, we always mark as released even if the send fails below,
// to avoid permanently stuck-modifier state in our tracker. The trade-off
// (tracker says "released" while OS may still have it pressed) is acceptable
// because such failures are rare and subsequent events will resynchronize.
self.modifier_state.update(&key, false);
if let Key::Layout(chr) = key {
// ASCII characters: send keysym up if we also sent it on key_down
let keysym = char_to_keysym(chr);
if can_input_via_keysym(chr, keysym) {
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session)
{
log::error!("Failed to send keysym up: {:?}", e);
}
}
// Non-ASCII: already handled completely in key_down via clipboard paste,
// no corresponding release needed (clipboard paste is an atomic operation)
} else {
if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) {
log::error!("Failed to handle key up: {:?}", e);
}
}
} }
fn key_click(&mut self, key: Key) { fn key_click(&mut self, key: Key) {
let _ = handle_key(true, key, self.conn.clone(), &self.session); if let Key::Layout(chr) = key {
let _ = handle_key(false, key, self.conn.clone(), &self.session); let keysym = char_to_keysym(chr);
// ASCII characters: use keysym
if can_input_via_keysym(chr, keysym) {
if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym down: {:?}", e);
}
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e);
}
} else {
// Non-ASCII: use clipboard
input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session);
}
} else {
if let Err(e) = handle_key(true, key.clone(), self.conn.clone(), &self.session) {
log::error!("Failed to handle key down: {:?}", e);
} else {
// Only mark modifier as pressed if key-down was actually delivered
self.modifier_state.update(&key, true);
}
// Always mark as released to avoid stuck-modifier state
self.modifier_state.update(&key, false);
if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) {
log::error!("Failed to handle key up: {:?}", e);
}
}
}
}
/// Input text via clipboard + Shift+Insert.
/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals.
///
/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale.
fn input_text_via_clipboard(text: &str, conn: Arc<SyncConnection>, session: &Path<'static>) {
if text.is_empty() {
return;
}
if !set_clipboard_for_paste_sync(text) {
return;
}
let portal = get_portal(&conn);
let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32;
let insert_keycode = evdev::Key::KEY_INSERT.code() as i32;
// Send Shift+Insert (universal paste shortcut)
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_DOWN_STATE,
) {
log::error!("input_text_via_clipboard: failed to press Shift: {:?}", e);
return;
}
// Press Insert
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
insert_keycode,
PRESSED_DOWN_STATE,
) {
log::error!("input_text_via_clipboard: failed to press Insert: {:?}", e);
// Still try to release Shift.
// Note: clipboard has already been set by set_clipboard_for_paste_sync but paste
// never happened. We don't attempt to restore the previous clipboard contents
// because reading the clipboard on Wayland requires focus/permission.
let _ = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
);
return;
}
// Release Insert
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
insert_keycode,
PRESSED_UP_STATE,
) {
log::error!(
"input_text_via_clipboard: failed to release Insert: {:?}",
e
);
}
// Release Shift
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::error!("input_text_via_clipboard: failed to release Shift: {:?}", e);
} }
} }
@@ -196,6 +420,39 @@ pub mod client {
} }
} }
/// Send a keysym via RemoteDesktop portal.
fn send_keysym(
keysym: i32,
down: bool,
conn: Arc<SyncConnection>,
session: &Path<'static>,
) -> ResultType<()> {
let state: u32 = if down {
PRESSED_DOWN_STATE
} else {
PRESSED_UP_STATE
};
let portal = get_portal(&conn);
log::trace!(
"send_keysym: calling notify_keyboard_keysym, keysym={:#x}, state={}",
keysym,
state
);
match remote_desktop_portal::notify_keyboard_keysym(
&portal,
session,
HashMap::new(),
keysym,
state,
) {
Ok(_) => {
log::trace!("send_keysym: notify_keyboard_keysym succeeded");
Ok(())
}
Err(e) => Err(e.into()),
}
}
fn get_raw_evdev_keycode(key: u16) -> i32 { fn get_raw_evdev_keycode(key: u16) -> i32 {
// 8 is the offset between xkb and evdev // 8 is the offset between xkb and evdev
let mut key = key as i32 - 8; let mut key = key as i32 - 8;
@@ -231,22 +488,86 @@ pub mod client {
} }
_ => { _ => {
if let Ok((key, is_shift)) = map_key(&key) { if let Ok((key, is_shift)) = map_key(&key) {
if is_shift { let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32;
remote_desktop_portal::notify_keyboard_keycode( if down {
// Press: Shift down first, then key down
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
state,
) {
log::error!("handle_key: failed to press Shift: {:?}", e);
return Err(e.into());
}
}
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal, &portal,
&session, &session,
HashMap::new(), HashMap::new(),
evdev::Key::KEY_LEFTSHIFT.code() as i32, key.code() as i32,
state, state,
)?; ) {
log::error!("handle_key: failed to press key: {:?}", e);
// Best-effort: release Shift if it was pressed
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::warn!(
"handle_key: best-effort Shift release also failed: {:?}",
e
);
}
}
return Err(e.into());
}
} else {
// Release: key up first, then Shift up
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
key.code() as i32,
PRESSED_UP_STATE,
) {
log::error!("handle_key: failed to release key: {:?}", e);
// Best-effort: still try to release Shift
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::warn!(
"handle_key: best-effort Shift release also failed: {:?}",
e
);
}
}
return Err(e.into());
}
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::error!("handle_key: failed to release Shift: {:?}", e);
return Err(e.into());
}
}
} }
remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
key.code() as i32,
state,
)?;
} }
} }
} }

View File

@@ -90,6 +90,13 @@ pub mod client {
} }
fn key_sequence(&mut self, sequence: &str) { fn key_sequence(&mut self, sequence: &str) {
// Sequence events are normally handled in the --server process before reaching here.
// Forward via IPC as a fallback — input_text_wayland can still handle ASCII chars
// via keysym/uinput, though non-ASCII will be skipped (no clipboard in --service).
log::debug!(
"UInputKeyboard::key_sequence called (len={})",
sequence.len()
);
allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string())))); allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string()))));
} }
@@ -178,6 +185,9 @@ pub mod client {
pub mod service { pub mod service {
use super::*; use super::*;
use hbb_common::lazy_static; use hbb_common::lazy_static;
use scrap::wayland::{
pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop,
};
use std::{collections::HashMap, sync::Mutex}; use std::{collections::HashMap, sync::Mutex};
lazy_static::lazy_static! { lazy_static::lazy_static! {
@@ -309,6 +319,9 @@ pub mod service {
('/', (evdev::Key::KEY_SLASH, false)), ('/', (evdev::Key::KEY_SLASH, false)),
(';', (evdev::Key::KEY_SEMICOLON, false)), (';', (evdev::Key::KEY_SEMICOLON, false)),
('\'', (evdev::Key::KEY_APOSTROPHE, false)), ('\'', (evdev::Key::KEY_APOSTROPHE, false)),
// Space is intentionally in both KEY_MAP_LAYOUT (char-to-evdev for text input)
// and KEY_MAP (Key::Space for key events). Both maps serve different lookup paths.
(' ', (evdev::Key::KEY_SPACE, false)),
// Shift + key // Shift + key
('A', (evdev::Key::KEY_A, true)), ('A', (evdev::Key::KEY_A, true)),
@@ -364,6 +377,155 @@ pub mod service {
static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0))); static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0)));
} }
/// Input text on Wayland using layout-independent methods.
/// ASCII chars (0x20-0x7E): Portal keysym or uinput fallback
/// Non-ASCII chars: skipped — this runs in the --service (root) process where clipboard
/// operations are unreliable (typically no user session environment).
/// Non-ASCII input is normally handled by the --server process via input_text_via_clipboard_server.
fn input_text_wayland(text: &str, keyboard: &mut VirtualDevice) {
let portal_info = {
let session_info = RDP_SESSION_INFO.lock().unwrap();
session_info
.as_ref()
.map(|info| (info.conn.clone(), info.session.clone()))
};
for c in text.chars() {
let keysym = char_to_keysym(c);
if can_input_via_keysym(c, keysym) {
// Try Portal first — down+up on the same channel
if let Some((ref conn, ref session)) = portal_info {
let portal = scrap::wayland::pipewire::get_portal(conn);
if portal
.notify_keyboard_keysym(session, HashMap::new(), keysym, 1)
.is_ok()
{
if let Err(e) =
portal.notify_keyboard_keysym(session, HashMap::new(), keysym, 0)
{
log::warn!(
"input_text_wayland: portal key-up failed for keysym {:#x}: {:?}",
keysym,
e
);
}
continue;
}
}
// Portal unavailable or failed, fallback to uinput (down+up together)
let key = enigo::Key::Layout(c);
if let Ok((evdev_key, is_shift)) = map_key(&key) {
let mut shift_pressed = false;
if is_shift {
let shift_down =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
if keyboard.emit(&[shift_down]).is_ok() {
shift_pressed = true;
} else {
log::warn!("input_text_wayland: failed to press Shift for '{}'", c);
}
}
let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1);
let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0);
allow_err!(keyboard.emit(&[key_down, key_up]));
if shift_pressed {
let shift_up =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0);
allow_err!(keyboard.emit(&[shift_up]));
}
}
} else {
log::debug!("Skipping non-ASCII character in uinput service (no clipboard access)");
}
}
}
/// Send a single key down or up event for a Layout character.
/// Used by KeyDown/KeyUp to maintain correct press/release semantics.
/// `down`: true for key press, false for key release.
fn input_char_wayland_key_event(chr: char, down: bool, keyboard: &mut VirtualDevice) {
let keysym = char_to_keysym(chr);
let portal_state: u32 = if down { 1 } else { 0 };
if can_input_via_keysym(chr, keysym) {
let portal_info = {
let session_info = RDP_SESSION_INFO.lock().unwrap();
session_info
.as_ref()
.map(|info| (info.conn.clone(), info.session.clone()))
};
if let Some((ref conn, ref session)) = portal_info {
let portal = scrap::wayland::pipewire::get_portal(conn);
if portal
.notify_keyboard_keysym(session, HashMap::new(), keysym, portal_state)
.is_ok()
{
return;
}
}
// Portal unavailable or failed, fallback to uinput
let key = enigo::Key::Layout(chr);
if let Ok((evdev_key, is_shift)) = map_key(&key) {
if down {
// Press: Shift↓ (if needed) → Key↓
if is_shift {
let shift_down =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
if let Err(e) = keyboard.emit(&[shift_down]) {
log::warn!("input_char_wayland_key_event: failed to press Shift for '{}': {:?}", chr, e);
}
}
let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1);
allow_err!(keyboard.emit(&[key_down]));
} else {
// Release: Key↑ → Shift↑ (if needed)
let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0);
allow_err!(keyboard.emit(&[key_up]));
if is_shift {
let shift_up =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0);
if let Err(e) = keyboard.emit(&[shift_up]) {
log::warn!("input_char_wayland_key_event: failed to release Shift for '{}': {:?}", chr, e);
}
}
}
}
} else {
// Non-ASCII: no reliable down/up semantics available.
// Clipboard paste is atomic and handled elsewhere.
log::debug!(
"Skipping non-ASCII character key {} in uinput service",
if down { "down" } else { "up" }
);
}
}
/// Check if character can be input via keysym (ASCII printable with valid keysym).
#[inline]
pub(crate) fn can_input_via_keysym(c: char, keysym: i32) -> bool {
// ASCII printable: 0x20 (space) to 0x7E (tilde)
(c as u32 >= 0x20 && c as u32 <= 0x7E) && keysym != 0
}
/// Convert a Unicode character to X11 keysym.
pub(crate) fn char_to_keysym(c: char) -> i32 {
let codepoint = c as u32;
if codepoint == 0 {
// Null character has no keysym
0
} else if (0x20..=0x7E).contains(&codepoint) {
// ASCII printable (0x20-0x7E): keysym == Unicode codepoint
codepoint as i32
} else if (0xA0..=0xFF).contains(&codepoint) {
// Latin-1 supplement (0xA0-0xFF): keysym == Unicode codepoint (per X11 keysym spec)
codepoint as i32
} else {
// Everything else (control chars 0x01-0x1F, DEL 0x7F, and all other non-ASCII Unicode):
// keysym = 0x01000000 | codepoint (X11 Unicode keysym encoding)
(0x0100_0000 | codepoint) as i32
}
}
fn create_uinput_keyboard() -> ResultType<VirtualDevice> { fn create_uinput_keyboard() -> ResultType<VirtualDevice> {
// TODO: ensure keys here // TODO: ensure keys here
let mut keys = AttributeSet::<evdev::Key>::new(); let mut keys = AttributeSet::<evdev::Key>::new();
@@ -390,13 +552,13 @@ pub mod service {
pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> { pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> {
if let Some(k) = KEY_MAP.get(&key) { if let Some(k) = KEY_MAP.get(&key) {
log::trace!("mapkey {:?}, get {:?}", &key, &k); log::trace!("mapkey matched in KEY_MAP, evdev={:?}", &k);
return Ok((k.clone(), false)); return Ok((k.clone(), false));
} else { } else {
match key { match key {
enigo::Key::Layout(c) => { enigo::Key::Layout(c) => {
if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) { if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) {
log::trace!("mapkey {:?}, get {:?}", &key, k); log::trace!("mapkey Layout matched, evdev={:?}", k);
return Ok((k.clone(), is_shift.clone())); return Ok((k.clone(), is_shift.clone()));
} }
} }
@@ -421,41 +583,68 @@ pub mod service {
keyboard: &mut VirtualDevice, keyboard: &mut VirtualDevice,
data: &DataKeyboard, data: &DataKeyboard,
) { ) {
log::trace!("handle_keyboard {:?}", &data); let data_desc = match data {
DataKeyboard::Sequence(seq) => format!("Sequence(len={})", seq.len()),
DataKeyboard::KeyDown(Key::Layout(_))
| DataKeyboard::KeyUp(Key::Layout(_))
| DataKeyboard::KeyClick(Key::Layout(_)) => "Layout(<redacted>)".to_string(),
_ => format!("{:?}", data),
};
log::trace!("handle_keyboard received: {}", data_desc);
match data { match data {
DataKeyboard::Sequence(_seq) => { DataKeyboard::Sequence(seq) => {
// ignore // Normally handled by --server process (input_text_via_clipboard_server).
// Fallback: input_text_wayland handles ASCII via keysym/uinput;
// non-ASCII will be skipped (no clipboard access in --service process).
if !seq.is_empty() {
input_text_wayland(seq, keyboard);
}
} }
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); if *code < 8 {
allow_err!(keyboard.emit(&[down_event])); log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} } else {
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event]));
}
DataKeyboard::KeyDown(key) => {
if let Ok((k, is_shift)) = map_key(key) {
if is_shift {
let down_event =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
allow_err!(keyboard.emit(&[down_event]));
}
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
allow_err!(keyboard.emit(&[down_event])); allow_err!(keyboard.emit(&[down_event]));
} }
} }
DataKeyboard::KeyUp(key) => { DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
if let Ok((k, _)) = map_key(key) { if *code < 8 {
let up_event = InputEvent::new(EventType::KEY, k.code(), 0); log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event])); allow_err!(keyboard.emit(&[up_event]));
} }
} }
DataKeyboard::KeyDown(key) => {
if let Key::Layout(chr) = key {
input_char_wayland_key_event(*chr, true, keyboard);
} else {
if let Ok((k, _is_shift)) = map_key(key) {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
allow_err!(keyboard.emit(&[down_event]));
}
}
}
DataKeyboard::KeyUp(key) => {
if let Key::Layout(chr) = key {
input_char_wayland_key_event(*chr, false, keyboard);
} else {
if let Ok((k, _)) = map_key(key) {
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
allow_err!(keyboard.emit(&[up_event]));
}
}
}
DataKeyboard::KeyClick(key) => { DataKeyboard::KeyClick(key) => {
if let Ok((k, _)) = map_key(key) { if let Key::Layout(chr) = key {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1); input_text_wayland(&chr.to_string(), keyboard);
let up_event = InputEvent::new(EventType::KEY, k.code(), 0); } else {
allow_err!(keyboard.emit(&[down_event, up_event])); if let Ok((k, _is_shift)) = map_key(key) {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
allow_err!(keyboard.emit(&[down_event, up_event]));
}
} }
} }
DataKeyboard::GetKeyState(key) => { DataKeyboard::GetKeyState(key) => {
@@ -580,9 +769,13 @@ pub mod service {
} }
fn spawn_keyboard_handler(mut stream: Connection) { fn spawn_keyboard_handler(mut stream: Connection) {
log::debug!("spawn_keyboard_handler: new keyboard handler connection");
tokio::spawn(async move { tokio::spawn(async move {
let mut keyboard = match create_uinput_keyboard() { let mut keyboard = match create_uinput_keyboard() {
Ok(keyboard) => keyboard, Ok(keyboard) => {
log::debug!("UInput keyboard device created successfully");
keyboard
}
Err(e) => { Err(e) => {
log::error!("Failed to create keyboard {}", e); log::error!("Failed to create keyboard {}", e);
return; return;
@@ -602,6 +795,7 @@ pub mod service {
handle_keyboard(&mut stream, &mut keyboard, &data).await; handle_keyboard(&mut stream, &mut keyboard, &data).await;
} }
_ => { _ => {
log::warn!("Unexpected data type in keyboard handler");
} }
} }
} }