mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
feat(wayland): keyboard mode, legacy translate (#14317)
Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user