Files
rustdesk/src/server/input_service.rs
fufesou 732b250815 fix(keyboard): legacy mode (#14435)
* fix(keyboard): legacy mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* Simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, chr to seq

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, early return if (!hotkey)&down

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(keyboard): legacy mode, pair down/up

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
2026-03-02 19:07:09 +08:00

2374 lines
78 KiB
Rust

#[cfg(target_os = "linux")]
use super::rdp_input::client::{RdpInputKeyboard, RdpInputMouse};
use super::*;
use crate::input::*;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use crate::whiteboard;
#[cfg(target_os = "macos")]
use dispatch::Queue;
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::{
get_time,
message_proto::{pointer_device_event::Union::TouchEvent, touch_event::Union::ScaleUpdate},
protobuf::EnumOrUnknown,
};
use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey};
#[cfg(target_os = "macos")]
use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput};
#[cfg(target_os = "linux")]
use scrap::wayland::pipewire::RDP_SESSION_INFO;
#[cfg(target_os = "linux")]
use std::sync::mpsc;
use std::{
convert::TryFrom,
ops::{Deref, DerefMut},
sync::atomic::{AtomicBool, Ordering},
thread,
time::{self, Duration, Instant},
};
#[cfg(windows)]
use winapi::um::winuser::WHEEL_DELTA;
const INVALID_CURSOR_POS: i32 = i32::MIN;
const INVALID_DISPLAY_IDX: i32 = -1;
#[derive(Default)]
struct StateCursor {
hcursor: u64,
cursor_data: Arc<Message>,
cached_cursor_data: HashMap<u64, Arc<Message>>,
}
impl super::service::Reset for StateCursor {
fn reset(&mut self) {
*self = Default::default();
crate::platform::reset_input_cache();
fix_key_down_timeout(true);
}
}
struct StatePos {
cursor_pos: (i32, i32),
}
impl Default for StatePos {
fn default() -> Self {
Self {
cursor_pos: (INVALID_CURSOR_POS, INVALID_CURSOR_POS),
}
}
}
impl super::service::Reset for StatePos {
fn reset(&mut self) {
self.cursor_pos = (INVALID_CURSOR_POS, INVALID_CURSOR_POS);
}
}
impl StatePos {
#[inline]
fn is_valid(&self) -> bool {
self.cursor_pos.0 != INVALID_CURSOR_POS
}
#[inline]
fn is_moved(&self, x: i32, y: i32) -> bool {
self.is_valid() && (self.cursor_pos.0 != x || self.cursor_pos.1 != y)
}
}
#[derive(Default)]
struct StateWindowFocus {
display_idx: i32,
}
impl super::service::Reset for StateWindowFocus {
fn reset(&mut self) {
self.display_idx = INVALID_DISPLAY_IDX;
}
}
impl StateWindowFocus {
#[inline]
fn is_valid(&self) -> bool {
self.display_idx != INVALID_DISPLAY_IDX
}
#[inline]
fn is_changed(&self, disp_idx: i32) -> bool {
self.is_valid() && self.display_idx != disp_idx
}
}
#[derive(Default, Clone, Copy)]
struct Input {
conn: i32,
time: i64,
x: i32,
y: i32,
}
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)]
pub struct MouseCursorSub {
inner: ConnInner,
cached: HashMap<u64, Arc<Message>>,
}
impl From<ConnInner> for MouseCursorSub {
fn from(inner: ConnInner) -> Self {
Self {
inner,
cached: HashMap::new(),
}
}
}
impl Subscriber for MouseCursorSub {
#[inline]
fn id(&self) -> i32 {
self.inner.id()
}
#[inline]
fn send(&mut self, msg: Arc<Message>) {
if let Some(message::Union::CursorData(cd)) = &msg.union {
if let Some(msg) = self.cached.get(&cd.id) {
self.inner.send(msg.clone());
} else {
self.inner.send(msg.clone());
let mut tmp = Message::new();
// only send id out, require client side cache also
tmp.set_cursor_id(cd.id);
self.cached.insert(cd.id, Arc::new(tmp));
}
} else {
self.inner.send(msg);
}
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
struct LockModesHandler {
caps_lock_changed: bool,
num_lock_changed: bool,
}
#[cfg(target_os = "macos")]
struct LockModesHandler;
impl LockModesHandler {
#[inline]
fn is_modifier_enabled(key_event: &KeyEvent, modifier: ControlKey) -> bool {
key_event.modifiers.contains(&modifier.into())
}
#[inline]
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
fn new_handler(key_event: &KeyEvent, _is_numpad_key: bool) -> Self {
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
Self::new(key_event, _is_numpad_key)
}
#[cfg(target_os = "macos")]
{
Self::new(key_event)
}
}
#[cfg(target_os = "linux")]
fn sleep_to_ensure_locked(v: bool, k: enigo::Key, en: &mut Enigo) {
if wayland_use_uinput() {
// Sleep at most 500ms to ensure the lock state is applied.
for _ in 0..50 {
std::thread::sleep(std::time::Duration::from_millis(10));
if en.get_key_state(k) == v {
break;
}
}
} else if wayland_use_rdp_input() {
// We can't call `en.get_key_state(k)` because there's no api for this.
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn new(key_event: &KeyEvent, is_numpad_key: bool) -> Self {
let mut en = ENIGO.lock().unwrap();
let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock);
let local_caps_enabled = en.get_key_state(enigo::Key::CapsLock);
let caps_lock_changed = event_caps_enabled != local_caps_enabled;
if caps_lock_changed {
en.key_click(enigo::Key::CapsLock);
#[cfg(target_os = "linux")]
Self::sleep_to_ensure_locked(event_caps_enabled, enigo::Key::CapsLock, &mut en);
}
let mut num_lock_changed = false;
#[allow(unused)]
let mut event_num_enabled = false;
if is_numpad_key {
let local_num_enabled = en.get_key_state(enigo::Key::NumLock);
event_num_enabled = Self::is_modifier_enabled(key_event, ControlKey::NumLock);
num_lock_changed = event_num_enabled != local_num_enabled;
} else if is_legacy_mode(key_event) {
#[cfg(target_os = "windows")]
{
num_lock_changed =
should_disable_numlock(key_event) && en.get_key_state(enigo::Key::NumLock);
}
}
if num_lock_changed {
en.key_click(enigo::Key::NumLock);
#[cfg(target_os = "linux")]
Self::sleep_to_ensure_locked(event_num_enabled, enigo::Key::NumLock, &mut en);
}
Self {
caps_lock_changed,
num_lock_changed,
}
}
#[cfg(target_os = "macos")]
fn new(key_event: &KeyEvent) -> Self {
let event_caps_enabled = Self::is_modifier_enabled(key_event, ControlKey::CapsLock);
// Do not use the following code to detect `local_caps_enabled`.
// Because the state of get_key_state will not affect simulation of `VIRTUAL_INPUT_STATE` in this file.
//
// let local_caps_enabled = VirtualInput::get_key_state(
// CGEventSourceStateID::CombinedSessionState,
// rdev::kVK_CapsLock,
// );
let local_caps_enabled = unsafe {
let _lock = VIRTUAL_INPUT_MTX.lock();
VIRTUAL_INPUT_STATE
.as_ref()
.map_or(false, |input| input.capslock_down)
};
if event_caps_enabled && !local_caps_enabled {
press_capslock();
} else if !event_caps_enabled && local_caps_enabled {
release_capslock();
}
Self {}
}
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
impl Drop for LockModesHandler {
fn drop(&mut self) {
// Do not change led state if is Wayland uinput.
// Because there must be a delay to ensure the lock state is applied on Wayland uinput,
// which may affect the user experience.
#[cfg(target_os = "linux")]
if wayland_use_uinput() {
return;
}
let mut en = ENIGO.lock().unwrap();
if self.caps_lock_changed {
en.key_click(enigo::Key::CapsLock);
}
if self.num_lock_changed {
en.key_click(enigo::Key::NumLock);
}
}
}
#[inline]
#[cfg(target_os = "windows")]
fn should_disable_numlock(evt: &KeyEvent) -> bool {
// disable numlock if press home etc when numlock is on,
// because we will get numpad value (7,8,9 etc) if not
match (&evt.union, evt.mode.enum_value_or(KeyboardMode::Legacy)) {
(Some(key_event::Union::ControlKey(ck)), KeyboardMode::Legacy) => {
return NUMPAD_KEY_MAP.contains_key(&ck.value());
}
_ => {}
}
false
}
pub const NAME_CURSOR: &'static str = "mouse_cursor";
pub const NAME_POS: &'static str = "mouse_pos";
pub const NAME_WINDOW_FOCUS: &'static str = "window_focus";
#[derive(Clone)]
pub struct MouseCursorService {
pub sp: ServiceTmpl<MouseCursorSub>,
}
impl Deref for MouseCursorService {
type Target = ServiceTmpl<MouseCursorSub>;
fn deref(&self) -> &Self::Target {
&self.sp
}
}
impl DerefMut for MouseCursorService {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sp
}
}
impl MouseCursorService {
pub fn new(name: String, need_snapshot: bool) -> Self {
Self {
sp: ServiceTmpl::<MouseCursorSub>::new(name, need_snapshot),
}
}
}
pub fn new_cursor() -> ServiceTmpl<MouseCursorSub> {
let svc = MouseCursorService::new(NAME_CURSOR.to_owned(), true);
ServiceTmpl::<MouseCursorSub>::repeat::<StateCursor, _, _>(&svc.clone(), 33, run_cursor);
svc.sp
}
pub fn new_pos() -> GenericService {
let svc = EmptyExtraFieldService::new(NAME_POS.to_owned(), false);
GenericService::repeat::<StatePos, _, _>(&svc.clone(), 33, run_pos);
svc.sp
}
pub fn new_window_focus() -> GenericService {
let svc = EmptyExtraFieldService::new(NAME_WINDOW_FOCUS.to_owned(), false);
GenericService::repeat::<StateWindowFocus, _, _>(&svc.clone(), 33, run_window_focus);
svc.sp
}
#[inline]
fn update_last_cursor_pos(x: i32, y: i32) {
let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap();
if lock.1 .0 != x || lock.1 .1 != y {
(lock.0, lock.1) = (Some(Instant::now()), (x, y))
}
}
fn run_pos(sp: EmptyExtraFieldService, state: &mut StatePos) -> ResultType<()> {
let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap();
if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS {
return Ok(());
}
if state.is_moved(x, y) {
let mut msg_out = Message::new();
msg_out.set_cursor_position(CursorPosition {
x,
y,
..Default::default()
});
let exclude = {
let now = get_time();
let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
if now - lock.time < 300 {
lock.conn
} else {
0
}
};
sp.send_without(msg_out, exclude);
}
state.cursor_pos = (x, y);
sp.snapshot(|sps| {
let mut msg_out = Message::new();
msg_out.set_cursor_position(CursorPosition {
x: state.cursor_pos.0,
y: state.cursor_pos.1,
..Default::default()
});
sps.send(msg_out);
Ok(())
})?;
Ok(())
}
fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> {
if let Some(hcursor) = crate::get_cursor()? {
if hcursor != state.hcursor {
let msg;
if let Some(cached) = state.cached_cursor_data.get(&hcursor) {
super::log::trace!("Cursor data cached, hcursor: {}", hcursor);
msg = cached.clone();
} else {
let mut data = crate::get_cursor_data(hcursor)?;
data.colors = hbb_common::compress::compress(&data.colors[..]).into();
let mut tmp = Message::new();
tmp.set_cursor_data(data);
msg = Arc::new(tmp);
state.cached_cursor_data.insert(hcursor, msg.clone());
super::log::trace!("Cursor data updated, hcursor: {}", hcursor);
}
state.hcursor = hcursor;
sp.send_shared(msg.clone());
state.cursor_data = msg;
}
}
sp.snapshot(|sps| {
sps.send_shared(state.cursor_data.clone());
Ok(())
})?;
Ok(())
}
fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> {
let displays = super::display_service::get_sync_displays();
if displays.len() <= 1 {
return Ok(());
}
let disp_idx = crate::get_focused_display(displays);
if let Some(disp_idx) = disp_idx.map(|id| id as i32) {
if state.is_changed(disp_idx) {
let mut misc = Misc::new();
misc.set_follow_current_display(disp_idx as i32);
let mut msg_out = Message::new();
msg_out.set_misc(misc);
sp.send(msg_out);
}
state.display_idx = disp_idx;
}
Ok(())
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
enum KeysDown {
RdevKey(RawKey),
EnigoKey(u64),
}
lazy_static::lazy_static! {
static ref ENIGO: Arc<Mutex<Enigo>> = {
Arc::new(Mutex::new(Enigo::new()))
};
static ref KEYS_DOWN: Arc<Mutex<HashMap<KeysDown, Instant>>> = Default::default();
static ref LATEST_PEER_INPUT_CURSOR: Arc<Mutex<Input>> = Default::default();
static ref LATEST_SYS_CURSOR_POS: Arc<Mutex<(Option<Instant>, (i32, i32))>> = Arc::new(Mutex::new((None, (INVALID_CURSOR_POS, INVALID_CURSOR_POS))));
// Track connections that are currently using relative mouse movement.
// Used to disable whiteboard/cursor display for all events while in relative mode.
static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default();
}
#[inline]
fn set_relative_mouse_active(conn: i32, active: bool) {
let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap();
if active {
lock.insert(conn);
} else {
lock.remove(&conn);
}
}
#[inline]
fn is_relative_mouse_active(conn: i32) -> bool {
RELATIVE_MOUSE_CONNS.lock().unwrap().contains(&conn)
}
/// Clears the relative mouse mode state for a connection.
///
/// This must be called when an authenticated connection is dropped (during connection teardown)
/// to avoid leaking the connection id in `RELATIVE_MOUSE_CONNS` (a `Mutex<HashSet<i32>>`).
/// Callers are responsible for invoking this on disconnect.
#[inline]
pub(crate) fn clear_relative_mouse_active(conn: i32) {
set_relative_mouse_active(conn, false);
}
static EXITING: AtomicBool = AtomicBool::new(false);
const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000);
// Actual diff of (x,y) is (1,1) here. But 5 may be tolerant.
const MOUSE_ACTIVE_DISTANCE: i32 = 5;
static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false);
// https://github.com/rustdesk/rustdesk/issues/9729
// We need to do some special handling for macOS when using the legacy mode.
#[cfg(target_os = "macos")]
static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(true);
// We use enigo to
// 1. Simulate mouse events
// 2. Simulate the legacy mode key events
// 3. Simulate the functioin key events, like LockScreen
#[inline]
#[cfg(target_os = "macos")]
fn enigo_ignore_flags() -> bool {
!LAST_KEY_LEGACY_MODE.load(Ordering::SeqCst)
}
#[inline]
#[cfg(target_os = "macos")]
fn set_last_legacy_mode(v: bool) {
LAST_KEY_LEGACY_MODE.store(v, Ordering::SeqCst);
ENIGO.lock().unwrap().set_ignore_flags(!v);
}
pub fn try_start_record_cursor_pos() -> Option<thread::JoinHandle<()>> {
if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) {
return None;
}
RECORD_CURSOR_POS_RUNNING.store(true, Ordering::SeqCst);
let handle = thread::spawn(|| {
let interval = time::Duration::from_millis(33);
loop {
if !RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) {
break;
}
let now = time::Instant::now();
if let Some((x, y)) = crate::get_cursor_pos() {
update_last_cursor_pos(x, y);
}
let elapsed = now.elapsed();
if elapsed < interval {
thread::sleep(interval - elapsed);
}
}
update_last_cursor_pos(INVALID_CURSOR_POS, INVALID_CURSOR_POS);
});
Some(handle)
}
pub fn try_stop_record_cursor_pos() {
let remote_count = AUTHED_CONNS
.lock()
.unwrap()
.iter()
.filter(|c| c.conn_type == AuthConnType::Remote)
.count();
if remote_count > 0 {
return;
}
RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst);
}
// mac key input must be run in main thread, otherwise crash on >= osx 10.15
#[cfg(target_os = "macos")]
lazy_static::lazy_static! {
static ref QUEUE: Queue = Queue::main();
}
#[cfg(target_os = "macos")]
struct VirtualInputState {
virtual_input: VirtualInput,
capslock_down: bool,
}
#[cfg(target_os = "macos")]
impl VirtualInputState {
fn new() -> Option<Self> {
VirtualInput::new(
CGEventSourceStateID::CombinedSessionState,
// Note: `CGEventTapLocation::Session` will be affected by the mouse events.
// When we're simulating key events, then move the physical mouse, the key events will be affected.
// It looks like https://github.com/rustdesk/rustdesk/issues/9729#issuecomment-2432306822
// 1. Press "Command" key in RustDesk
// 2. Move the physical mouse
// 3. Press "V" key in RustDesk
// Then the controlled side just prints "v" instead of pasting.
//
// Changing `CGEventTapLocation::Session` to `CGEventTapLocation::HID` fixes it.
// But we do not consider this as a bug, because it's not a common case,
// we consider only RustDesk operates the controlled side.
//
// https://developer.apple.com/documentation/coregraphics/cgeventtaplocation/
CGEventTapLocation::Session,
)
.map(|virtual_input| Self {
virtual_input,
capslock_down: false,
})
.ok()
}
#[inline]
fn simulate(&self, event_type: &EventType) -> ResultType<()> {
Ok(self.virtual_input.simulate(&event_type)?)
}
}
#[cfg(target_os = "macos")]
static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(());
#[cfg(target_os = "macos")]
static mut VIRTUAL_INPUT_STATE: Option<VirtualInputState> = None;
// First call set_uinput() will create keyboard and mouse clients.
// The clients are ipc connections that must live shorter than tokio runtime.
// Thus this function must not be called in a temporary runtime.
#[cfg(target_os = "linux")]
pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
// Keyboard and mouse both open /dev/uinput
// TODO: Make sure there's no race
set_uinput_resolution(minx, maxx, miny, maxy).await?;
let keyboard = super::uinput::client::UInputKeyboard::new().await?;
log::info!("UInput keyboard created");
let mouse = super::uinput::client::UInputMouse::new().await?;
log::info!("UInput mouse created");
ENIGO
.lock()
.unwrap()
.set_custom_keyboard(Box::new(keyboard));
ENIGO.lock().unwrap().set_custom_mouse(Box::new(mouse));
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn setup_rdp_input() -> ResultType<(), Box<dyn std::error::Error>> {
let mut en = ENIGO.lock()?;
let rdp_info_lock = RDP_SESSION_INFO.lock()?;
let rdp_info = rdp_info_lock.as_ref().ok_or("RDP session is None")?;
let keyboard = RdpInputKeyboard::new(rdp_info.conn.clone(), rdp_info.session.clone())?;
en.set_custom_keyboard(Box::new(keyboard));
log::info!("RdpInput keyboard created");
if let Some(stream) = rdp_info.streams.clone().into_iter().next() {
let resolution = rdp_info
.resolution
.lock()
.unwrap()
.unwrap_or(stream.get_size());
let mouse = RdpInputMouse::new(
rdp_info.conn.clone(),
rdp_info.session.clone(),
stream,
resolution,
)?;
en.set_custom_mouse(Box::new(mouse));
log::info!("RdpInput mouse created");
}
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
set_uinput_resolution(minx, maxx, miny, maxy).await?;
std::thread::spawn(|| {
if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() {
if let Some(mouse) = mouse
.as_mut_any()
.downcast_mut::<super::uinput::client::UInputMouse>()
{
allow_err!(mouse.send_refresh());
} else {
log::error!("failed downcast uinput mouse");
}
}
});
Ok(())
}
#[cfg(target_os = "linux")]
async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> {
super::uinput::client::set_resolution(minx, maxx, miny, maxy).await
}
pub fn is_left_up(evt: &MouseEvent) -> bool {
let buttons = evt.mask >> 3;
let evt_type = evt.mask & MOUSE_TYPE_MASK;
buttons == MOUSE_BUTTON_LEFT && evt_type == MOUSE_TYPE_UP
}
#[cfg(windows)]
pub fn mouse_move_relative(x: i32, y: i32) {
crate::platform::windows::try_change_desktop();
let mut en = ENIGO.lock().unwrap();
en.mouse_move_relative(x, y);
}
#[cfg(windows)]
fn modifier_sleep() {
// sleep for a while, this is only for keying in rdp in peer so far
std::thread::sleep(std::time::Duration::from_nanos(1));
}
#[inline]
#[cfg(not(target_os = "macos"))]
fn is_pressed(key: &Key, en: &mut Enigo) -> bool {
get_modifier_state(key.clone(), en)
}
// Sleep for 8ms is enough in my tests, but we sleep 12ms to be safe.
// sleep 12ms In my test, the characters are already output in real time.
#[inline]
#[cfg(target_os = "macos")]
fn key_sleep() {
// https://www.reddit.com/r/rustdesk/comments/1kn1w5x/typing_lags_when_connecting_to_macos_clients/
//
// There's a strange bug when running by `launchctl load -w /Library/LaunchAgents/abc.plist`
// `std::thread::sleep(Duration::from_millis(20));` may sleep 90ms or more.
// Though `/Applications/RustDesk.app/Contents/MacOS/rustdesk --server` in terminal is ok.
let now = Instant::now();
while now.elapsed() < Duration::from_millis(12) {
std::thread::sleep(Duration::from_millis(1));
}
}
#[inline]
fn get_modifier_state(key: Key, en: &mut Enigo) -> bool {
// https://github.com/rustdesk/rustdesk/issues/332
// on Linux, if RightAlt is down, RightAlt status is false, Alt status is true
// but on Windows, both are true
let x = en.get_key_state(key.clone());
match key {
Key::Shift => x || en.get_key_state(Key::RightShift),
Key::Control => x || en.get_key_state(Key::RightControl),
Key::Alt => x || en.get_key_state(Key::RightAlt),
Key::Meta => x || en.get_key_state(Key::RWin),
Key::RightShift => x || en.get_key_state(Key::Shift),
Key::RightControl => x || en.get_key_state(Key::Control),
Key::RightAlt => x || en.get_key_state(Key::Alt),
Key::RWin => x || en.get_key_state(Key::Meta),
_ => x,
}
}
#[allow(unreachable_code)]
pub fn handle_mouse(
evt: &MouseEvent,
conn: i32,
username: String,
argb: u32,
simulate: bool,
show_cursor: bool,
) {
#[cfg(target_os = "macos")]
{
// having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash
let evt = evt.clone();
QUEUE.exec_async(move || handle_mouse_(&evt, conn, username, argb, simulate, show_cursor));
return;
}
#[cfg(windows)]
crate::portable_service::client::handle_mouse(evt, conn, username, argb, simulate, show_cursor);
#[cfg(not(windows))]
handle_mouse_(evt, conn, username, argb, simulate, show_cursor);
}
// to-do: merge handle_mouse and handle_pointer
#[allow(unreachable_code)]
pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) {
#[cfg(target_os = "macos")]
{
// having GUI, run main GUI thread, otherwise crash
let evt = evt.clone();
QUEUE.exec_async(move || handle_pointer_(&evt, conn));
return;
}
#[cfg(windows)]
crate::portable_service::client::handle_pointer(evt, conn);
#[cfg(not(windows))]
handle_pointer_(evt, conn);
}
pub fn fix_key_down_timeout_loop() {
std::thread::spawn(move || loop {
std::thread::sleep(std::time::Duration::from_millis(10_000));
fix_key_down_timeout(false);
});
if let Err(err) = ctrlc::set_handler(move || {
fix_key_down_timeout_at_exit();
std::process::exit(0); // will call atexit on posix, but not on Windows
}) {
log::error!("Failed to set Ctrl-C handler: {}", err);
}
}
pub fn fix_key_down_timeout_at_exit() {
if EXITING.load(Ordering::SeqCst) {
return;
}
EXITING.store(true, Ordering::SeqCst);
fix_key_down_timeout(true);
log::info!("fix_key_down_timeout_at_exit");
}
#[inline]
#[cfg(target_os = "linux")]
pub fn clear_remapped_keycode() {
ENIGO.lock().unwrap().tfc_clear_remapped();
}
#[inline]
fn record_key_is_control_key(record_key: u64) -> bool {
record_key < KEY_CHAR_START
}
#[inline]
fn record_key_is_chr(record_key: u64) -> bool {
record_key >= KEY_CHAR_START
}
#[inline]
fn record_key_to_key(record_key: u64) -> Option<Key> {
if record_key_is_control_key(record_key) {
control_key_value_to_key(record_key as _)
} else if record_key_is_chr(record_key) {
let chr: u32 = (record_key - KEY_CHAR_START) as _;
Some(char_value_to_key(chr))
} else {
None
}
}
pub fn release_device_modifiers() {
let mut en = ENIGO.lock().unwrap();
for modifier in [
Key::Shift,
Key::Control,
Key::Alt,
Key::Meta,
Key::RightShift,
Key::RightControl,
Key::RightAlt,
Key::RWin,
] {
if get_modifier_state(modifier, &mut en) {
en.key_up(modifier);
}
}
}
#[inline]
fn release_record_key(record_key: KeysDown) {
let func = move || match record_key {
KeysDown::RdevKey(raw_key) => {
simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key)));
}
KeysDown::EnigoKey(key) => {
if let Some(key) = record_key_to_key(key) {
ENIGO.lock().unwrap().key_up(key);
log::debug!("Fixed {:?} timeout", key);
}
}
};
#[cfg(target_os = "macos")]
QUEUE.exec_async(func);
#[cfg(not(target_os = "macos"))]
func();
}
fn fix_key_down_timeout(force: bool) {
let key_down = KEYS_DOWN.lock().unwrap();
if key_down.is_empty() {
return;
}
let cloned = (*key_down).clone();
drop(key_down);
for (record_key, time) in cloned.into_iter() {
if force || time.elapsed().as_millis() >= 360_000 {
record_pressed_key(record_key, false);
release_record_key(record_key);
}
}
}
// e.g. current state of ctrl is down, but ctrl not in modifier, we should change ctrl to up, to make modifier state sync between remote and local
#[inline]
fn fix_modifier(
modifiers: &[EnumOrUnknown<ControlKey>],
key0: ControlKey,
key1: Key,
en: &mut Enigo,
) {
if get_modifier_state(key1, en) && !modifiers.contains(&EnumOrUnknown::new(key0)) {
#[cfg(windows)]
if key0 == ControlKey::Control && get_modifier_state(Key::Alt, en) {
// AltGr case
return;
}
en.key_up(key1);
log::debug!("Fixed {:?}", key1);
}
}
fn fix_modifiers(modifiers: &[EnumOrUnknown<ControlKey>], en: &mut Enigo, ck: i32) {
if ck != ControlKey::Shift.value() {
fix_modifier(modifiers, ControlKey::Shift, Key::Shift, en);
}
if ck != ControlKey::RShift.value() {
fix_modifier(modifiers, ControlKey::Shift, Key::RightShift, en);
}
if ck != ControlKey::Alt.value() {
fix_modifier(modifiers, ControlKey::Alt, Key::Alt, en);
}
if ck != ControlKey::RAlt.value() {
fix_modifier(modifiers, ControlKey::Alt, Key::RightAlt, en);
}
if ck != ControlKey::Control.value() {
fix_modifier(modifiers, ControlKey::Control, Key::Control, en);
}
if ck != ControlKey::RControl.value() {
fix_modifier(modifiers, ControlKey::Control, Key::RightControl, en);
}
if ck != ControlKey::Meta.value() {
fix_modifier(modifiers, ControlKey::Meta, Key::Meta, en);
}
if ck != ControlKey::RWin.value() {
fix_modifier(modifiers, ControlKey::Meta, Key::RWin, en);
}
}
// Update time to avoid send cursor position event to the peer.
// See `run_pos` --> `set_cursor_position` --> `exclude`
#[inline]
pub fn update_latest_input_cursor_time(conn: i32) {
let mut lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
lock.conn = conn;
lock.time = get_time();
}
#[inline]
fn get_last_input_cursor_pos() -> (i32, i32) {
let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
(lock.x, lock.y)
}
// check if mouse is moved by the controlled side user to make controlled side has higher mouse priority than remote.
fn active_mouse_(_conn: i32) -> bool {
true
/* this method is buggy (not working on macOS, making fast moving mouse event discarded here) and added latency (this is blocking way, must do in async way), so we disable it for now
// out of time protection
if LATEST_SYS_CURSOR_POS
.lock()
.unwrap()
.0
.map(|t| t.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT)
.unwrap_or(true)
{
return true;
}
// last conn input may be protected
if LATEST_PEER_INPUT_CURSOR.lock().unwrap().conn != conn {
return false;
}
let in_active_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE };
// Check if input is in valid range
match crate::get_cursor_pos() {
Some((x, y)) => {
let (last_in_x, last_in_y) = get_last_input_cursor_pos();
let mut can_active = in_active_dist(last_in_x, x) && in_active_dist(last_in_y, y);
// The cursor may not have been moved to last input position if system is busy now.
// While this is not a common case, we check it again after some time later.
if !can_active {
// 100 micros may be enough for system to move cursor.
// Mouse inputs on macOS are asynchronous. 1. Put in a queue to process in main thread. 2. Send event async.
// More reties are needed on macOS.
#[cfg(not(target_os = "macos"))]
let retries = 10;
#[cfg(target_os = "macos")]
let retries = 100;
#[cfg(not(target_os = "macos"))]
let sleep_interval: u64 = 10;
#[cfg(target_os = "macos")]
let sleep_interval: u64 = 30;
for _retry in 0..retries {
std::thread::sleep(std::time::Duration::from_micros(sleep_interval));
// Sleep here can also somehow suppress delay accumulation.
if let Some((x2, y2)) = crate::get_cursor_pos() {
let (last_in_x, last_in_y) = get_last_input_cursor_pos();
can_active = in_active_dist(last_in_x, x2) && in_active_dist(last_in_y, y2);
if can_active {
break;
}
}
}
}
if !can_active {
let mut lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap();
lock.x = INVALID_CURSOR_POS / 2;
lock.y = INVALID_CURSOR_POS / 2;
}
can_active
}
None => true,
}
*/
}
pub fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) {
if !active_mouse_(conn) {
return;
}
if EXITING.load(Ordering::SeqCst) {
return;
}
match &evt.union {
Some(TouchEvent(evt)) => match &evt.union {
Some(ScaleUpdate(_scale_evt)) => {
#[cfg(target_os = "windows")]
handle_scale(_scale_evt.scale);
}
_ => {}
},
_ => {}
}
}
pub fn handle_mouse_(
evt: &MouseEvent,
conn: i32,
_username: String,
_argb: u32,
simulate: bool,
_show_cursor: bool,
) {
if simulate {
handle_mouse_simulation_(evt, conn);
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let evt_type = evt.mask & MOUSE_TYPE_MASK;
// Relative (delta) mouse events do not include absolute coordinates, so
// whiteboard/cursor rendering must be disabled during relative mode to prevent
// incorrect cursor/whiteboard updates. We check both is_relative_mouse_active(conn)
// (connection already in relative mode from prior events) and evt_type (current
// event is relative) to guard against the first relative event before the flag is set.
if _show_cursor && !is_relative_mouse_active(conn) && evt_type != MOUSE_TYPE_MOVE_RELATIVE {
handle_mouse_show_cursor_(evt, conn, _username, _argb);
}
}
}
pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
if !active_mouse_(conn) {
return;
}
if EXITING.load(Ordering::SeqCst) {
return;
}
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
let buttons = evt.mask >> 3;
let evt_type = evt.mask & MOUSE_TYPE_MASK;
let mut en = ENIGO.lock().unwrap();
#[cfg(target_os = "macos")]
en.set_ignore_flags(enigo_ignore_flags());
#[cfg(not(target_os = "macos"))]
let mut to_release = Vec::new();
if evt_type == MOUSE_TYPE_DOWN {
fix_modifiers(&evt.modifiers[..], &mut en, 0);
#[cfg(target_os = "macos")]
en.reset_flag();
for ref ck in evt.modifiers.iter() {
if let Some(key) = KEY_MAP.get(&ck.value()) {
#[cfg(target_os = "macos")]
en.add_flag(key);
#[cfg(not(target_os = "macos"))]
if key != &Key::CapsLock && key != &Key::NumLock {
if !get_modifier_state(key.clone(), &mut en) {
en.key_down(key.clone()).ok();
#[cfg(windows)]
modifier_sleep();
to_release.push(key);
}
}
}
}
}
match evt_type {
MOUSE_TYPE_MOVE => {
// Switching back to absolute movement implicitly disables relative mouse mode.
set_relative_mouse_active(conn, false);
en.mouse_move_to(evt.x, evt.y);
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
conn,
time: get_time(),
x: evt.x,
y: evt.y,
};
}
// MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications.
// Each client independently decides whether to use relative mode.
// Multiple clients can mix absolute and relative movements without conflict,
// as the server simply applies the delta to the current cursor position.
MOUSE_TYPE_MOVE_RELATIVE => {
set_relative_mouse_active(conn, true);
// Clamp delta to prevent extreme/malicious values from reaching OS APIs.
// This matches the Flutter client's kMaxRelativeMouseDelta constant.
const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000;
let dx = evt
.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);
// Get actual cursor position after relative movement for tracking
if let Some((x, y)) = crate::get_cursor_pos() {
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
conn,
time: get_time(),
x,
y,
};
}
}
MOUSE_TYPE_DOWN => match buttons {
MOUSE_BUTTON_LEFT => {
allow_err!(en.mouse_down(MouseButton::Left));
}
MOUSE_BUTTON_RIGHT => {
allow_err!(en.mouse_down(MouseButton::Right));
}
MOUSE_BUTTON_WHEEL => {
allow_err!(en.mouse_down(MouseButton::Middle));
}
MOUSE_BUTTON_BACK => {
allow_err!(en.mouse_down(MouseButton::Back));
}
MOUSE_BUTTON_FORWARD => {
allow_err!(en.mouse_down(MouseButton::Forward));
}
_ => {}
},
MOUSE_TYPE_UP => match buttons {
MOUSE_BUTTON_LEFT => {
en.mouse_up(MouseButton::Left);
}
MOUSE_BUTTON_RIGHT => {
en.mouse_up(MouseButton::Right);
}
MOUSE_BUTTON_WHEEL => {
en.mouse_up(MouseButton::Middle);
}
MOUSE_BUTTON_BACK => {
en.mouse_up(MouseButton::Back);
}
MOUSE_BUTTON_FORWARD => {
en.mouse_up(MouseButton::Forward);
}
_ => {}
},
MOUSE_TYPE_WHEEL | MOUSE_TYPE_TRACKPAD => {
#[allow(unused_mut)]
let mut x = -evt.x;
#[allow(unused_mut)]
let mut y = evt.y;
#[cfg(not(windows))]
{
y = -y;
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
let is_track_pad = evt_type == MOUSE_TYPE_TRACKPAD;
#[cfg(target_os = "macos")]
{
// TODO: support track pad on win.
// fix shift + scroll(down/up)
if !is_track_pad
&& evt
.modifiers
.contains(&EnumOrUnknown::new(ControlKey::Shift))
{
x = y;
y = 0;
}
if x != 0 {
en.mouse_scroll_x(x, is_track_pad);
}
if y != 0 {
en.mouse_scroll_y(y, is_track_pad);
}
}
#[cfg(windows)]
if !is_track_pad {
x *= WHEEL_DELTA as i32;
y *= WHEEL_DELTA as i32;
}
#[cfg(not(target_os = "macos"))]
{
if y != 0 {
en.mouse_scroll_y(y);
}
if x != 0 {
en.mouse_scroll_x(x);
}
}
}
_ => {}
}
#[cfg(not(target_os = "macos"))]
for key in to_release {
en.key_up(key.clone());
}
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) {
let buttons = evt.mask >> 3;
let evt_type = evt.mask & MOUSE_TYPE_MASK;
match evt_type {
MOUSE_TYPE_MOVE => {
whiteboard::update_whiteboard(
whiteboard::get_key_cursor(conn),
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
x: evt.x as _,
y: evt.y as _,
argb,
btns: 0,
text: username,
}),
);
}
MOUSE_TYPE_UP => {
if buttons == MOUSE_BUTTON_LEFT {
// Some clients intentionally send button events without coordinates.
// Fall back to the last known cursor position to avoid jumping to (0, 0).
// TODO(protocol): (0, 0) is a valid screen coordinate. Consider using a dedicated
// sentinel value (e.g. INVALID_CURSOR_POS) or a protocol-level flag to distinguish
// "coordinates not provided" from "coordinates are (0, 0)". Impact is minor since
// this only affects whiteboard rendering and clicking exactly at (0, 0) is rare.
let (x, y) = if evt.x == 0 && evt.y == 0 {
get_last_input_cursor_pos()
} else {
(evt.x, evt.y)
};
whiteboard::update_whiteboard(
whiteboard::get_key_cursor(conn),
whiteboard::CustomEvent::Cursor(whiteboard::Cursor {
x: x as _,
y: y as _,
argb,
btns: buttons,
text: username,
}),
);
}
}
_ => {}
}
}
#[cfg(target_os = "windows")]
fn handle_scale(scale: i32) {
let mut en = ENIGO.lock().unwrap();
if scale == 0 {
en.key_up(Key::Control);
} else {
if en.key_down(Key::Control).is_ok() {
en.mouse_scroll_y(scale);
}
}
}
pub fn is_enter(evt: &KeyEvent) -> bool {
if let Some(key_event::Union::ControlKey(ck)) = evt.union {
if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value()
{
return true;
}
}
return false;
}
pub async fn lock_screen() {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
// xdg_screensaver lock not work on Linux from our service somehow
// loginctl lock-session also not work, they both work run rustdesk from cmd
std::thread::spawn(|| {
let mut key_event = KeyEvent::new();
key_event.set_chr('l' as _);
key_event.modifiers.push(ControlKey::Meta.into());
key_event.mode = KeyboardMode::Legacy.into();
key_event.down = true;
handle_key(&key_event);
key_event.down = false;
handle_key(&key_event);
});
} else if #[cfg(target_os = "macos")] {
// CGSession -suspend not real lock screen, it is user switch
std::thread::spawn(|| {
let mut key_event = KeyEvent::new();
key_event.set_chr('q' as _);
key_event.modifiers.push(ControlKey::Meta.into());
key_event.modifiers.push(ControlKey::Control.into());
key_event.mode = KeyboardMode::Legacy.into();
key_event.down = true;
handle_key(&key_event);
key_event.down = false;
handle_key(&key_event);
});
} else {
crate::platform::lock_screen();
}
}
}
#[inline]
#[cfg(target_os = "linux")]
pub fn handle_key(evt: &KeyEvent) {
handle_key_(evt);
}
#[inline]
#[cfg(target_os = "windows")]
pub fn handle_key(evt: &KeyEvent) {
crate::portable_service::client::handle_key(evt);
}
#[inline]
#[cfg(target_os = "macos")]
pub fn handle_key(evt: &KeyEvent) {
// having GUI, run main GUI thread, otherwise crash
let evt = evt.clone();
QUEUE.exec_async(move || handle_key_(&evt));
// Key sleep is required for macOS.
// If we don't sleep, the key press/release events may not take effect.
//
// For example, the controlled side osx `12.7.6` or `15.1.1`
// If we input characters quickly and continuously, and press or release "Shift" for a short period of time,
// it is possible that after releasing "Shift", the controlled side will still print uppercase characters.
// Though it is not very easy to reproduce.
key_sleep();
}
#[cfg(target_os = "macos")]
#[inline]
fn reset_input() {
unsafe {
let _lock = VIRTUAL_INPUT_MTX.lock();
VIRTUAL_INPUT_STATE = VirtualInputState::new();
}
}
#[cfg(target_os = "macos")]
pub fn reset_input_ondisconn() {
QUEUE.exec_async(reset_input);
}
fn sim_rdev_rawkey_position(code: KeyCode, keydown: bool) {
#[cfg(target_os = "windows")]
let rawkey = RawKey::ScanCode(code);
#[cfg(target_os = "linux")]
let rawkey = RawKey::LinuxXorgKeycode(code);
// // to-do: test android
// #[cfg(target_os = "android")]
// let rawkey = RawKey::LinuxConsoleKeycode(code);
#[cfg(target_os = "macos")]
let rawkey = RawKey::MacVirtualKeycode(code);
// map mode(1): Send keycode according to the peer platform.
record_pressed_key(KeysDown::RdevKey(rawkey), keydown);
let event_type = if keydown {
EventType::KeyPress(RdevKey::RawKey(rawkey))
} else {
EventType::KeyRelease(RdevKey::RawKey(rawkey))
};
simulate_(&event_type);
}
#[cfg(target_os = "windows")]
fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) {
let rawkey = RawKey::WinVirtualKeycode(code);
record_pressed_key(KeysDown::RdevKey(rawkey), keydown);
let event_type = if keydown {
EventType::KeyPress(RdevKey::RawKey(rawkey))
} else {
EventType::KeyRelease(RdevKey::RawKey(rawkey))
};
simulate_(&event_type);
}
#[inline]
#[cfg(target_os = "macos")]
fn simulate_(event_type: &EventType) {
unsafe {
let _lock = VIRTUAL_INPUT_MTX.lock();
if let Some(input) = VIRTUAL_INPUT_STATE.as_ref() {
let _ = input.simulate(&event_type);
}
}
}
#[inline]
#[cfg(target_os = "macos")]
fn press_capslock() {
let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock));
unsafe {
let _lock = VIRTUAL_INPUT_MTX.lock();
if let Some(input) = VIRTUAL_INPUT_STATE.as_mut() {
if input.simulate(&EventType::KeyPress(caps_key)).is_ok() {
input.capslock_down = true;
key_sleep();
}
}
}
}
#[cfg(target_os = "macos")]
#[inline]
fn release_capslock() {
let caps_key = RdevKey::RawKey(rdev::RawKey::MacVirtualKeycode(rdev::kVK_CapsLock));
unsafe {
let _lock = VIRTUAL_INPUT_MTX.lock();
if let Some(input) = VIRTUAL_INPUT_STATE.as_mut() {
if input.simulate(&EventType::KeyRelease(caps_key)).is_ok() {
input.capslock_down = false;
key_sleep();
}
}
}
}
#[cfg(not(target_os = "macos"))]
#[inline]
fn simulate_(event_type: &EventType) {
match rdev::simulate(&event_type) {
Ok(()) => (),
Err(_simulate_error) => {
log::error!("Could not send {:?}", &event_type);
}
}
}
#[inline]
fn control_key_value_to_key(value: i32) -> Option<Key> {
KEY_MAP.get(&value).and_then(|k| Some(*k))
}
#[inline]
fn char_value_to_key(value: u32) -> Key {
Key::Layout(std::char::from_u32(value).unwrap_or('\0'))
}
fn map_keyboard_mode(evt: &KeyEvent) {
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
// Wayland
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() {
wayland_send_raw_key(evt.chr() as u16, evt.down);
return;
}
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")]
fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) {
// When long-pressed the command key, then press and release
// the Tab key, there should be CGEventFlagCommand in the flag.
en.reset_flag();
for ck in key_event.modifiers.iter() {
if let Some(key) = KEY_MAP.get(&ck.value()) {
en.add_flag(key);
}
}
}
fn get_control_key_value(key_event: &KeyEvent) -> i32 {
if let Some(key_event::Union::ControlKey(ck)) = key_event.union {
ck.value()
} else {
-1
}
}
#[inline]
fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool {
key_event.modifiers.iter().any(|ck| {
let v = ck.value();
v == ControlKey::Control.value()
|| v == ControlKey::RControl.value()
|| v == ControlKey::Meta.value()
|| v == ControlKey::RWin.value()
|| {
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
v == ControlKey::Alt.value() || v == ControlKey::RAlt.value()
}
#[cfg(target_os = "macos")]
{
false
}
}
})
}
fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) {
let ck_value = get_control_key_value(key_event);
fix_modifiers(&key_event.modifiers[..], en, ck_value);
}
#[cfg(target_os = "linux")]
fn is_altgr_pressed() -> bool {
let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _);
KEYS_DOWN
.lock()
.unwrap()
.get(&KeysDown::RdevKey(altgr_rawkey))
.is_some()
}
#[cfg(not(target_os = "macos"))]
fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec<Key>) {
for ref ck in key_event.modifiers.iter() {
if let Some(key) = control_key_value_to_key(ck.value()) {
if !is_pressed(&key, en) {
#[cfg(target_os = "linux")]
if key == Key::Alt && is_altgr_pressed() {
continue;
}
en.key_down(key.clone()).ok();
to_release.push(key.clone());
#[cfg(windows)]
modifier_sleep();
}
}
}
}
fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, _to_release: &mut Vec<Key>) {
#[cfg(target_os = "macos")]
add_flags_to_enigo(en, key_event);
if key_event.down {
release_unpressed_modifiers(en, key_event);
#[cfg(not(target_os = "macos"))]
press_modifiers(en, key_event, _to_release);
}
}
fn process_control_key(en: &mut Enigo, ck: &EnumOrUnknown<ControlKey>, down: bool) {
if let Some(key) = control_key_value_to_key(ck.value()) {
if down {
en.key_down(key).ok();
} else {
en.key_up(key);
}
}
}
#[inline]
fn need_to_uppercase(en: &mut Enigo) -> bool {
get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en)
}
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: 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;
}
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
if !_hotkey {
if down {
if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string());
}
}
return;
}
let key = char_value_to_key(chr);
if down {
if en.key_down(key).is_ok() {
} else {
if let Ok(chr) = char::try_from(chr) {
let mut s = chr.to_string();
if need_to_uppercase(en) {
s = s.to_uppercase();
}
en.key_sequence(&s);
};
}
} else {
en.key_up(key);
}
}
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) {
en.key_sequence(&chr.to_string());
}
}
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);
}
/// 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"))]
fn release_keys(en: &mut Enigo, to_release: &Vec<Key>) {
for key in to_release {
en.key_up(key.clone());
}
}
fn record_pressed_key(record_key: KeysDown, down: bool) {
let mut key_down = KEYS_DOWN.lock().unwrap();
if down {
key_down.insert(record_key, Instant::now());
} else {
key_down.remove(&record_key);
}
}
fn is_function_key(ck: &EnumOrUnknown<ControlKey>) -> bool {
let mut res = false;
if ck.value() == ControlKey::CtrlAltDel.value() {
// have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main.
#[cfg(windows)]
std::thread::spawn(|| {
allow_err!(send_sas());
});
res = true;
} else if ck.value() == ControlKey::LockScreen.value() {
std::thread::spawn(|| {
lock_screen_2();
});
res = true;
}
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) {
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
let mut to_release: Vec<Key> = Vec::new();
let mut en = ENIGO.lock().unwrap();
sync_modifiers(&mut en, &evt, &mut to_release);
let down = evt.down;
match evt.union {
Some(key_event::Union::ControlKey(ck)) => {
if is_function_key(&ck) {
return;
}
let record_key = ck.value() as u64;
record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_control_key(&mut en, &ck, down)
}
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;
record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_chr(&mut en, chr, down, has_hotkey_modifiers(evt))
}
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),
_ => {}
}
#[cfg(not(target_os = "macos"))]
release_keys(&mut en, &to_release);
}
#[cfg(target_os = "windows")]
fn translate_process_code(code: u32, down: bool) {
crate::platform::windows::try_change_desktop();
match code >> 16 {
0 => sim_rdev_rawkey_position(code as _, down),
vk_code => sim_rdev_rawkey_virtual(vk_code, down),
};
}
fn translate_keyboard_mode(evt: &KeyEvent) {
match &evt.union {
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
// client: Shift + & => 1(send to remote)
// remote: Shift + 1 => !
//
// Try to release shift first.
// remote: Shift + 1 => 1
let mut en = ENIGO.lock().unwrap();
#[cfg(target_os = "macos")]
en.key_sequence(seq);
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
#[cfg(target_os = "windows")]
let simulate_win_hot_key = is_hot_key_modifiers_down(&mut en);
#[cfg(target_os = "linux")]
let simulate_win_hot_key = false;
if !simulate_win_hot_key {
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
#[cfg(target_os = "windows")]
{
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() {
// char in rust is 4 bytes.
// But for this case, char comes from keyboard. We only need 2 bytes.
#[cfg(target_os = "windows")]
if simulate_win_hot_key {
rdev::simulate_char(chr, true).ok();
} else {
rdev::simulate_unicode(chr as _).ok();
}
#[cfg(target_os = "linux")]
en.key_click(Key::Layout(chr));
}
}
}
Some(key_event::Union::Chr(..)) => {
#[cfg(target_os = "windows")]
translate_process_code(evt.chr(), evt.down);
#[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);
}
Some(key_event::Union::Unicode(..)) => {
// Do not handle unicode for now.
}
#[cfg(target_os = "windows")]
Some(key_event::Union::Win2winHotkey(code)) => {
simulate_win2win_hotkey(*code, evt.down);
}
_ => {
log::debug!(
"Unreachable. Unexpected key event (mode={:?}, down={:?})",
&evt.mode,
&evt.down
);
}
}
}
#[inline]
#[cfg(target_os = "windows")]
fn is_hot_key_modifiers_down(en: &mut Enigo) -> bool {
en.get_key_state(Key::Control)
|| en.get_key_state(Key::RightControl)
|| en.get_key_state(Key::Alt)
|| en.get_key_state(Key::RightAlt)
|| en.get_key_state(Key::Meta)
|| en.get_key_state(Key::RWin)
}
#[cfg(target_os = "windows")]
fn simulate_win2win_hotkey(code: u32, down: bool) {
let unicode: u16 = (code & 0x0000FFFF) as u16;
if down {
if rdev::simulate_key_unicode(unicode, false).is_ok() {
return;
}
}
let keycode: u16 = ((code >> 16) & 0x0000FFFF) as u16;
let scan = rdev::vk_to_scancode(keycode as _);
allow_err!(rdev::simulate_code(None, Some(scan), down));
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
fn skip_led_sync_control_key(_key: &ControlKey) -> bool {
false
}
// LockModesHandler should not be created when single meta is pressing and releasing.
// Because the drop function may insert "CapsLock Click" and "NumLock Click", which breaks single meta click.
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1496936687
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500415822
// https://github.com/rustdesk/rustdesk/issues/3928#issuecomment-1500773473
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn skip_led_sync_control_key(key: &ControlKey) -> bool {
matches!(
key,
ControlKey::Control
| ControlKey::RControl
| ControlKey::Meta
| ControlKey::Shift
| ControlKey::RShift
| ControlKey::Alt
| ControlKey::RAlt
| ControlKey::Tab
| ControlKey::Return
)
}
#[inline]
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn is_numpad_control_key(key: &ControlKey) -> bool {
matches!(
key,
ControlKey::Numpad0
| ControlKey::Numpad1
| ControlKey::Numpad2
| ControlKey::Numpad3
| ControlKey::Numpad4
| ControlKey::Numpad5
| ControlKey::Numpad6
| ControlKey::Numpad7
| ControlKey::Numpad8
| ControlKey::Numpad9
| ControlKey::NumpadEnter
)
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
fn skip_led_sync_rdev_key(_key: &RdevKey) -> bool {
false
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn skip_led_sync_rdev_key(key: &RdevKey) -> bool {
matches!(
key,
RdevKey::ControlLeft
| RdevKey::ControlRight
| RdevKey::MetaLeft
| RdevKey::MetaRight
| RdevKey::ShiftLeft
| RdevKey::ShiftRight
| RdevKey::Alt
| RdevKey::AltGr
| RdevKey::Tab
| RdevKey::Return
)
}
#[inline]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
fn is_legacy_mode(evt: &KeyEvent) -> bool {
evt.mode.enum_value_or(KeyboardMode::Legacy) == KeyboardMode::Legacy
}
pub fn handle_key_(evt: &KeyEvent) {
if EXITING.load(Ordering::SeqCst) {
return;
}
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let mut _lock_mode_handler = None;
#[cfg(not(any(target_os = "android", target_os = "ios")))]
match &evt.union {
Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)) => {
_lock_mode_handler = Some(LockModesHandler::new_handler(&evt, false));
}
Some(key_event::Union::ControlKey(ck)) => {
let key = ck.enum_value_or(ControlKey::Unknown);
if !skip_led_sync_control_key(&key) {
#[cfg(target_os = "macos")]
let is_numpad_key = false;
#[cfg(any(target_os = "windows", target_os = "linux"))]
let is_numpad_key = is_numpad_control_key(&key);
_lock_mode_handler = Some(LockModesHandler::new_handler(&evt, is_numpad_key));
}
}
Some(key_event::Union::Chr(code)) => {
if is_legacy_mode(&evt) {
_lock_mode_handler = Some(LockModesHandler::new_handler(evt, false));
} else {
let key = crate::keyboard::keycode_to_rdev_key(*code);
if !skip_led_sync_rdev_key(&key) {
#[cfg(target_os = "macos")]
let is_numpad_key = false;
#[cfg(any(target_os = "windows", target_os = "linux"))]
let is_numpad_key = crate::keyboard::is_numpad_rdev_key(&key);
_lock_mode_handler = Some(LockModesHandler::new_handler(evt, is_numpad_key));
}
}
}
_ => {}
}
match evt.mode.enum_value() {
Ok(KeyboardMode::Map) => {
#[cfg(target_os = "macos")]
set_last_legacy_mode(false);
map_keyboard_mode(evt);
}
Ok(KeyboardMode::Translate) => {
#[cfg(target_os = "macos")]
set_last_legacy_mode(false);
translate_keyboard_mode(evt);
}
_ => {
// All key down events are started from here,
// so we can reset the flag of last legacy mode here.
#[cfg(target_os = "macos")]
set_last_legacy_mode(true);
legacy_keyboard_mode(evt);
}
}
}
#[tokio::main(flavor = "current_thread")]
async fn lock_screen_2() {
lock_screen().await;
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
async fn send_sas() -> ResultType<()> {
if crate::platform::is_physical_console_session().unwrap_or(true) {
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??;
} else {
crate::platform::send_sas();
};
Ok(())
}
#[inline]
#[cfg(target_os = "linux")]
pub fn wayland_use_uinput() -> bool {
!crate::platform::is_x11() && crate::is_server()
}
#[inline]
#[cfg(target_os = "linux")]
pub fn wayland_use_rdp_input() -> bool {
!crate::platform::is_x11() && !crate::is_server()
}
#[cfg(target_os = "linux")]
pub struct TemporaryMouseMoveHandle {
thread_handle: Option<std::thread::JoinHandle<()>>,
tx: Option<mpsc::Sender<(i32, i32)>>,
}
#[cfg(target_os = "linux")]
impl TemporaryMouseMoveHandle {
pub fn new() -> Self {
let (tx, rx) = mpsc::channel::<(i32, i32)>();
let thread_handle = std::thread::spawn(move || {
log::debug!("TemporaryMouseMoveHandle thread started");
for (x, y) in rx {
ENIGO.lock().unwrap().mouse_move_to(x, y);
}
log::debug!("TemporaryMouseMoveHandle thread exiting");
});
TemporaryMouseMoveHandle {
thread_handle: Some(thread_handle),
tx: Some(tx),
}
}
pub fn move_mouse_to(&self, x: i32, y: i32) {
if let Some(tx) = &self.tx {
let _ = tx.send((x, y));
}
}
}
#[cfg(target_os = "linux")]
impl Drop for TemporaryMouseMoveHandle {
fn drop(&mut self) {
log::debug!("Dropping TemporaryMouseMoveHandle");
// Close the channel to signal the thread to exit.
self.tx.take();
// Wait for the thread to finish.
if let Some(thread_handle) = self.thread_handle.take() {
if let Err(e) = thread_handle.join() {
log::error!("Error joining TemporaryMouseMoveHandle thread: {:?}", e);
}
}
}
}
lazy_static::lazy_static! {
static ref MODIFIER_MAP: HashMap<i32, Key> = [
(ControlKey::Alt, Key::Alt),
(ControlKey::RAlt, Key::RightAlt),
(ControlKey::Control, Key::Control),
(ControlKey::RControl, Key::RightControl),
(ControlKey::Shift, Key::Shift),
(ControlKey::RShift, Key::RightShift),
(ControlKey::Meta, Key::Meta),
(ControlKey::RWin, Key::RWin),
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
static ref KEY_MAP: HashMap<i32, Key> =
[
(ControlKey::Alt, Key::Alt),
(ControlKey::Backspace, Key::Backspace),
(ControlKey::CapsLock, Key::CapsLock),
(ControlKey::Control, Key::Control),
(ControlKey::Delete, Key::Delete),
(ControlKey::DownArrow, Key::DownArrow),
(ControlKey::End, Key::End),
(ControlKey::Escape, Key::Escape),
(ControlKey::F1, Key::F1),
(ControlKey::F10, Key::F10),
(ControlKey::F11, Key::F11),
(ControlKey::F12, Key::F12),
(ControlKey::F2, Key::F2),
(ControlKey::F3, Key::F3),
(ControlKey::F4, Key::F4),
(ControlKey::F5, Key::F5),
(ControlKey::F6, Key::F6),
(ControlKey::F7, Key::F7),
(ControlKey::F8, Key::F8),
(ControlKey::F9, Key::F9),
(ControlKey::Home, Key::Home),
(ControlKey::LeftArrow, Key::LeftArrow),
(ControlKey::Meta, Key::Meta),
(ControlKey::Option, Key::Option),
(ControlKey::PageDown, Key::PageDown),
(ControlKey::PageUp, Key::PageUp),
(ControlKey::Return, Key::Return),
(ControlKey::RightArrow, Key::RightArrow),
(ControlKey::Shift, Key::Shift),
(ControlKey::Space, Key::Space),
(ControlKey::Tab, Key::Tab),
(ControlKey::UpArrow, Key::UpArrow),
(ControlKey::Numpad0, Key::Numpad0),
(ControlKey::Numpad1, Key::Numpad1),
(ControlKey::Numpad2, Key::Numpad2),
(ControlKey::Numpad3, Key::Numpad3),
(ControlKey::Numpad4, Key::Numpad4),
(ControlKey::Numpad5, Key::Numpad5),
(ControlKey::Numpad6, Key::Numpad6),
(ControlKey::Numpad7, Key::Numpad7),
(ControlKey::Numpad8, Key::Numpad8),
(ControlKey::Numpad9, Key::Numpad9),
(ControlKey::Cancel, Key::Cancel),
(ControlKey::Clear, Key::Clear),
(ControlKey::Menu, Key::Alt),
(ControlKey::Pause, Key::Pause),
(ControlKey::Kana, Key::Kana),
(ControlKey::Hangul, Key::Hangul),
(ControlKey::Junja, Key::Junja),
(ControlKey::Final, Key::Final),
(ControlKey::Hanja, Key::Hanja),
(ControlKey::Kanji, Key::Kanji),
(ControlKey::Convert, Key::Convert),
(ControlKey::Select, Key::Select),
(ControlKey::Print, Key::Print),
(ControlKey::Execute, Key::Execute),
(ControlKey::Snapshot, Key::Snapshot),
(ControlKey::Insert, Key::Insert),
(ControlKey::Help, Key::Help),
(ControlKey::Sleep, Key::Sleep),
(ControlKey::Separator, Key::Separator),
(ControlKey::Scroll, Key::Scroll),
(ControlKey::NumLock, Key::NumLock),
(ControlKey::RWin, Key::RWin),
(ControlKey::Apps, Key::Apps),
(ControlKey::Multiply, Key::Multiply),
(ControlKey::Add, Key::Add),
(ControlKey::Subtract, Key::Subtract),
(ControlKey::Decimal, Key::Decimal),
(ControlKey::Divide, Key::Divide),
(ControlKey::Equals, Key::Equals),
(ControlKey::NumpadEnter, Key::NumpadEnter),
(ControlKey::RAlt, Key::RightAlt),
(ControlKey::RControl, Key::RightControl),
(ControlKey::RShift, Key::RightShift),
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
static ref NUMPAD_KEY_MAP: HashMap<i32, bool> =
[
(ControlKey::Home, true),
(ControlKey::UpArrow, true),
(ControlKey::PageUp, true),
(ControlKey::LeftArrow, true),
(ControlKey::RightArrow, true),
(ControlKey::End, true),
(ControlKey::DownArrow, true),
(ControlKey::PageDown, true),
(ControlKey::Insert, true),
(ControlKey::Delete, true),
].iter().map(|(a, b)| (a.value(), b.clone())).collect();
}