mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-11-10 19:00:53 +08:00
* test: remove tests that do not pin user-visible behavior Apply the AGENTS.md test rules to the existing Rust and Flutter suites. A test is kept when it is the only coverage of user- or API-visible behavior, the regression test of a reported or fixed bug, a security boundary (one test per rejection class), or a check against an external reference. Tests that assert private state or the order of internal steps, pin review-round corner cases, or repeat what a kept test already covers are removed. Rust: 679 -> 298 tests. Dart: 118 -> 45 tests. Test-only helpers left without callers go with them (applyLastOnlineStateForTesting, terminalKeyboardRowWidth, debugBufferedInputCount, tokio's test-util dev feature), and cfg gates and visibility that existed only for the deleted tests are narrowed. Runtime behavior is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep tests that pin security and user-visible behavior Restore six tests that review showed are the only coverage of their behavior: - update metadata rejects an artifact file name with path components; - ICE candidates from another session are dropped; - KCP delivers every multi-segment frame queued before a close; - set_files rejects an empty name in a multi-file transfer; - a lopsided elongated cursor keeps its centre hotspot (#16242); - toggling Show hidden during a pending same-path read issues a new read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep offline update and key-exchange downgrade rejections Restore three tests that are the only coverage of their rejection class: - offline update metadata rejects a signed artifact with an http or credential-bearing URL; - offline verification rejects a renamed local file and a version that does not match the release; - a key-exchange pick lowered in transit leaves both sides unable to decrypt, so a v0 fallback after a failed v1 decrypt cannot pass unnoticed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R * test: keep the decomposed-grapheme and reconnect-cancel tests Each catches a plausible wrong change that passes every kept test: mapping the first scalar under a locked Ctrl turns é into \x05, and a reconnect that does not fail pending reads leaves them hanging until the 30 s timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2839 lines
98 KiB
Rust
2839 lines
98 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;
|
|
use base::message_proto::{
|
|
pointer_device_event::Union::TouchEvent, touch_event::Union::ScaleUpdate,
|
|
};
|
|
#[cfg(target_os = "macos")]
|
|
use dispatch::Queue;
|
|
use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
|
|
use hbb_common::{get_time, 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();
|
|
CURSOR_SHAPES.lock().unwrap().clear();
|
|
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;
|
|
// On the DRM path get_cursor_data() may return a snapshot whose id has advanced past the
|
|
// requested `hcursor` (it returns the latest hardware cursor); file it in the cache AND
|
|
// record state.hcursor under the id ACTUALLY served, so a later reappearance of that exact
|
|
// shape dedupes correctly instead of being suppressed. Everything below is fully
|
|
// gated on the drm feature, so the drm-off build stays byte-identical to upstream.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
let mut drm_served_id = hcursor;
|
|
if let Some(cached) = state.cached_cursor_data.get(&hcursor) {
|
|
super::log::trace!("Cursor data cached, hcursor: {}", hcursor);
|
|
msg = cached.clone();
|
|
} else {
|
|
let data = crate::get_cursor_data(hcursor)?;
|
|
// File the shape under the id ACTUALLY served, not the one requested. Deliberately a
|
|
// NEW name rather than shadowing `hcursor`: the insert below reads as the requested
|
|
// id everywhere else in this function, and a cfg-gated shadow would make the two
|
|
// builds disagree about what that line means.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
let served_id = data.id;
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
{
|
|
drm_served_id = served_id;
|
|
}
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
let cache_key = served_id;
|
|
#[cfg(not(all(target_os = "linux", feature = "drm")))]
|
|
let cache_key = hcursor;
|
|
msg = cursor_shape_message(data, hbb_common::compress::compress);
|
|
// A DRM cursor id is derived from the shape's pixels plus geometry, so an animated
|
|
// pointer mints a new id on every shape change and this map would grow for the life
|
|
// of the service, each entry pinning a compressed cursor message. (Upstream's X11
|
|
// ids come from a small set of XFixes serials, so the map is effectively bounded
|
|
// there -- which is why the ceiling is gated and the stock build stays untouched.)
|
|
// Past the ceiling, drop the map and start over: the next request for any evicted
|
|
// shape just recompresses it, and the ceiling comfortably covers every static shape
|
|
// plus a generous animation window.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
{
|
|
const CURSOR_CACHE_MAX: usize = 64;
|
|
if state.cached_cursor_data.len() >= CURSOR_CACHE_MAX {
|
|
state.cached_cursor_data.clear();
|
|
CURSOR_SHAPES.lock().unwrap().clear();
|
|
// The shape being sent was kept above; keep it past the clear.
|
|
shared_cursor_shape(msg.clone());
|
|
}
|
|
}
|
|
limit_cursor_handles(&mut state.cached_cursor_data, &msg);
|
|
// A macOS seed marks a change, never the same one twice: filing it would only
|
|
// fill the map.
|
|
#[cfg(not(target_os = "macos"))]
|
|
state.cached_cursor_data.insert(cache_key, msg.clone());
|
|
super::log::trace!("Cursor data updated, hcursor: {}", cache_key);
|
|
}
|
|
#[cfg(not(all(target_os = "linux", feature = "drm")))]
|
|
{
|
|
state.hcursor = hcursor;
|
|
}
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
{
|
|
state.hcursor = drm_served_id;
|
|
}
|
|
// A new handle or seed for the shape already shown changes nothing a controller sees;
|
|
// a shape is one message however many handles name it.
|
|
if !Arc::ptr_eq(&msg, &state.cursor_data) {
|
|
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();
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
// Every shape the service has sent, by content id, for a controller that asks for one
|
|
// again. A shape under several handles is one message here, shared with `cached_cursor_data`,
|
|
// and cleared with it; see `limit_cursor_handles` for the ceilings. A shape dropped is
|
|
// rebuilt here the next time it is shown, in `run_cursor`, before any connection sends its
|
|
// `cursor_id`: whatever a controller has just been told to show, it can ask for.
|
|
static ref CURSOR_SHAPES: Mutex<CursorShapes> = Default::default();
|
|
}
|
|
|
|
/// The shapes sent, by content id, and the bytes they hold compressed.
|
|
#[derive(Default)]
|
|
struct CursorShapes {
|
|
shapes: HashMap<u64, Arc<Message>>,
|
|
bytes: usize,
|
|
}
|
|
|
|
impl CursorShapes {
|
|
fn get(&self, id: &u64) -> Option<&Arc<Message>> {
|
|
self.shapes.get(id)
|
|
}
|
|
|
|
fn clear(&mut self) {
|
|
self.shapes.clear();
|
|
self.bytes = 0;
|
|
}
|
|
|
|
/// The message kept for this shape, `msg` if there was none.
|
|
fn keep(&mut self, cd: &CursorData, msg: &Arc<Message>) -> Arc<Message> {
|
|
let bytes = &mut self.bytes;
|
|
self.shapes
|
|
.entry(cd.id)
|
|
.or_insert_with(|| {
|
|
*bytes += cd.colors.len();
|
|
msg.clone()
|
|
})
|
|
.clone()
|
|
}
|
|
}
|
|
|
|
/// The message for a shape the platform gave, named by content, so the per-connection send sends
|
|
/// a shape once however many handles the platform gives it; see
|
|
/// `is_peer_naming_cursors_by_content`. A shape sent before, under any handle, is reused without
|
|
/// being compressed again.
|
|
fn cursor_shape_message(
|
|
mut data: CursorData,
|
|
compress: impl FnOnce(&[u8]) -> Vec<u8>,
|
|
) -> Arc<Message> {
|
|
data.id = crate::cursor_content_id(data.width, data.height, data.hotx, data.hoty, &data.colors);
|
|
if let Some(msg) = cursor_data_message(data.id) {
|
|
return msg;
|
|
}
|
|
data.colors = compress(&data.colors[..]).into();
|
|
let mut msg = Message::new();
|
|
msg.set_cursor_data(data);
|
|
shared_cursor_shape(Arc::new(msg))
|
|
}
|
|
|
|
// A platform may mint a new handle each time it shows a shape (Chrome, Electron), so the handles
|
|
// filed would grow for the life of the service, each entry a u64 and an Arc even when every one
|
|
// names the same shape. Past this many handles, or this many bytes of compressed shapes, however
|
|
// few, the handles and the shapes start over together: a handle shown again is captured and named
|
|
// again. macOS files no handles, so there the bytes alone bound the shapes.
|
|
const CURSOR_HANDLES_MAX: usize = 4096;
|
|
const CURSOR_SHAPES_BYTES_MAX: usize = 32 << 20;
|
|
|
|
fn limit_cursor_handles(handles: &mut HashMap<u64, Arc<Message>>, sending: &Arc<Message>) {
|
|
if handles.len() >= CURSOR_HANDLES_MAX
|
|
|| CURSOR_SHAPES.lock().unwrap().bytes >= CURSOR_SHAPES_BYTES_MAX
|
|
{
|
|
handles.clear();
|
|
CURSOR_SHAPES.lock().unwrap().clear();
|
|
// The shape being sent was kept; keep it past the clear.
|
|
shared_cursor_shape(sending.clone());
|
|
}
|
|
}
|
|
|
|
/// The message already kept for this shape if there is one, so every handle for it shares it.
|
|
fn shared_cursor_shape(msg: Arc<Message>) -> Arc<Message> {
|
|
let Some(message::Union::CursorData(cd)) = &msg.union else {
|
|
return msg;
|
|
};
|
|
CURSOR_SHAPES.lock().unwrap().keep(cd, &msg)
|
|
}
|
|
|
|
/// The CursorData message of a shape the service has sent, for `Misc::request_cursor_data`.
|
|
pub fn cursor_data_message(id: u64) -> Option<Arc<Message>> {
|
|
CURSOR_SHAPES.lock().unwrap().get(&id).cloned()
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
lazy_static::lazy_static! {
|
|
static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc<Mutex<Vec<(Instant, String)>>> =
|
|
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 {
|
|
const KEY_UP_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(5);
|
|
|
|
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");
|
|
|
|
let mut en = ENIGO.lock().unwrap();
|
|
// enigo guessed x11 once at construction, which is what a Wayland greeter reads as, and
|
|
// then routes the devices installed below to a null xdo that drops everything silently.
|
|
// Reaching here means `wayland_use_uinput()` was true, so this states a fact.
|
|
en.set_is_x11(false);
|
|
// One lock for both, so there is no window where the keyboard is custom and the mouse is not.
|
|
en.set_custom_keyboard(Box::new(keyboard));
|
|
en.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()?;
|
|
// Same as `setup_uinput`: the caller is gated on `wayland_use_rdp_input()`.
|
|
en.set_is_x11(false);
|
|
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?;
|
|
|
|
// Confirm the device adopted the new range before the caller caches it.
|
|
// spawn_blocking because ENIGO is a std Mutex and send_refresh blocks on IPC.
|
|
tokio::task::spawn_blocking(move || {
|
|
if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() {
|
|
if let Some(mouse) = mouse
|
|
.as_mut_any()
|
|
.downcast_mut::<super::uinput::client::UInputMouse>()
|
|
{
|
|
return mouse.send_refresh();
|
|
}
|
|
bail!("failed to downcast custom mouse to UInputMouse");
|
|
}
|
|
// No custom mouse: nothing to refresh.
|
|
Ok(())
|
|
})
|
|
.await?
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
|
|
/// The last ABSOLUTE peer-injected pointer position, in the post-remap uinput layout space, with
|
|
/// when it was injected, a sequence number and the wayland snapshot generation it was mapped
|
|
/// against. Deliberately NOT `LATEST_PEER_INPUT_CURSOR`: its relative arm stores
|
|
/// `get_cursor_pos()`, an X-server coordinate that never went through the layout remap, and the
|
|
/// DRM cursor calibration subtracts a cursor-plane position from this, so the two spaces must
|
|
/// match. Only the uinput absolute path writes here.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
static LATEST_PEER_ABS_POS: std::sync::Mutex<
|
|
Option<((i32, i32), std::time::Instant, u64, u64, u64)>,
|
|
> = std::sync::Mutex::new(None);
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
static PEER_ABS_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
|
|
/// One absolute peer sample, as the calibration reads it.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub(crate) struct PeerAbsSample {
|
|
pub pos: (i32, i32),
|
|
pub age_ms: u64,
|
|
/// Distinct per injected move. Two measurements taken against the same sample are not
|
|
/// independent, whatever the cursor plane did in between.
|
|
pub seq: u64,
|
|
/// The layout generation the point was mapped against.
|
|
pub gen: u64,
|
|
/// The input-mapping epoch at injection: a sample from before a range adoption was mapped by
|
|
/// the old range. See `display_service::input_map_epoch`.
|
|
pub map_epoch: u64,
|
|
}
|
|
|
|
/// The peer moved the pointer to an absolute, post-remap position. Called under the ENIGO guard
|
|
/// of the move, which an adoption also needs for its refresh: a move stamped with the count of an
|
|
/// adoption was injected after that adoption's refresh.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
pub(crate) fn note_peer_absolute_move(x: i32, y: i32) {
|
|
let seq = PEER_ABS_SEQ.fetch_add(1, Ordering::Relaxed) + 1;
|
|
let gen = scrap::wayland::display::wayland_snapshot_generation();
|
|
let map_epoch = super::display_service::input_map_epoch();
|
|
*LATEST_PEER_ABS_POS.lock().unwrap() =
|
|
Some(((x, y), std::time::Instant::now(), seq, gen, map_epoch));
|
|
}
|
|
|
|
/// The pointer moved by a path that has no post-remap absolute position to offer: a relative
|
|
/// delta from the peer, or a move this process made on its own. The last sample no longer says
|
|
/// where the pointer is; forget it, the next absolute move restores it.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
pub(crate) fn note_pointer_moved_without_absolute_sample() {
|
|
*LATEST_PEER_ABS_POS.lock().unwrap() = None;
|
|
}
|
|
|
|
/// A move this process makes on its own. The forget runs once `lock` returned the guard the peer
|
|
/// move records its sample under, so no peer move lands between the forget and this move and
|
|
/// leaves its sample describing a pointer that has moved since.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
fn forget_sample_and_move<G>(lock: impl FnOnce() -> G, move_to: impl FnOnce(G)) {
|
|
let guard = lock();
|
|
note_pointer_moved_without_absolute_sample();
|
|
move_to(guard);
|
|
}
|
|
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
pub(crate) fn last_peer_abs_sample() -> Option<PeerAbsSample> {
|
|
let (pos, at, seq, gen, map_epoch) = (*LATEST_PEER_ABS_POS.lock().unwrap())?;
|
|
Some(PeerAbsSample {
|
|
pos,
|
|
age_ms: at.elapsed().as_millis() as u64,
|
|
seq,
|
|
gen,
|
|
map_epoch,
|
|
})
|
|
}
|
|
|
|
// 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);
|
|
// On Wayland with uinput, the client sends coordinates in the layout it was
|
|
// told at session init. If the compositor has since moved a monitor, correct
|
|
// them onto the current layout. https://github.com/rustdesk/rustdesk/issues/15601
|
|
#[cfg(target_os = "linux")]
|
|
let uinput = wayland_use_uinput();
|
|
#[cfg(target_os = "linux")]
|
|
let (mx, my) = if uinput {
|
|
super::display_service::remap_wayland_uinput_coord(evt.x, evt.y)
|
|
} else {
|
|
(evt.x, evt.y)
|
|
};
|
|
#[cfg(not(target_os = "linux"))]
|
|
let (mx, my) = (evt.x, evt.y);
|
|
en.mouse_move_to(mx, my);
|
|
// Only the uinput point is in the layout space the calibration measures in.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
if uinput {
|
|
note_peer_absolute_move(mx, my);
|
|
}
|
|
*LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input {
|
|
conn,
|
|
time: get_time(),
|
|
x: mx,
|
|
y: my,
|
|
};
|
|
}
|
|
// 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);
|
|
// The absolute sample stops describing the pointer the moment a delta lands. Drop it
|
|
// BEFORE the move, so no frame in between can measure the plane against it.
|
|
#[cfg(all(target_os = "linux", feature = "drm"))]
|
|
note_pointer_moved_without_absolute_sample();
|
|
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);
|
|
} else if matches!(event_type, EventType::KeyRelease(_)) {
|
|
hbb_common::throttled_log!(
|
|
VirtualInputState::KEY_UP_ERROR_LOG_INTERVAL,
|
|
error,
|
|
"Failed to inject macOS key-up: virtual input is not initialized"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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:
|
|
// - ASCII printable: input via key events (custom keyboard path, e.g. portal keysym)
|
|
// - Non-ASCII: input via clipboard paste
|
|
#[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 let Ok(c) = char::try_from(chr) {
|
|
if is_ascii_printable(c) {
|
|
if down {
|
|
en.key_down(Key::Layout(c)).ok();
|
|
} else {
|
|
en.key_up(Key::Layout(c));
|
|
}
|
|
} else if down {
|
|
input_char_via_clipboard_server(en, c);
|
|
}
|
|
} else {
|
|
log::warn!(
|
|
"Ignore invalid unicode scalar in Wayland+uinput path: {}",
|
|
chr
|
|
);
|
|
}
|
|
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:
|
|
// - ASCII printable: input via key sequence (custom keyboard path)
|
|
// - Non-ASCII: input via clipboard paste
|
|
#[cfg(target_os = "linux")]
|
|
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
|
if let Ok(c) = char::try_from(chr) {
|
|
if is_ascii_printable(c) {
|
|
en.key_sequence(&c.to_string());
|
|
} else {
|
|
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:
|
|
// - pure ASCII printable sequence: input via key sequence (custom keyboard path)
|
|
// - any non-ASCII present: input whole sequence via clipboard to preserve order
|
|
#[cfg(target_os = "linux")]
|
|
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
|
if sequence.chars().all(is_ascii_printable) {
|
|
en.key_sequence(sequence);
|
|
} else {
|
|
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;
|
|
#[cfg(target_os = "linux")]
|
|
const WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW: Duration = Duration::from_secs(1);
|
|
#[cfg(target_os = "linux")]
|
|
const WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS: usize = 256;
|
|
#[cfg(target_os = "linux")]
|
|
pub(super) const WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS: usize = 1024;
|
|
|
|
#[cfg(target_os = "linux")]
|
|
fn cleanup_wayland_clipboard_input_records(records: &mut Vec<(Instant, String)>, now: Instant) {
|
|
records.retain(|(created_at, _)| {
|
|
now.saturating_duration_since(*created_at) <= WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW
|
|
});
|
|
let len = records.len();
|
|
if len > WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS {
|
|
records.drain(0..(len - WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS));
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[inline]
|
|
fn normalize_wayland_clipboard_input_text(text: &str) -> String {
|
|
text.chars()
|
|
.take(WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS)
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[inline]
|
|
fn get_wayland_clipboard_input_normalized_text(text: &str) -> Option<String> {
|
|
let normalized = normalize_wayland_clipboard_input_text(text);
|
|
if normalized.is_empty() {
|
|
return None;
|
|
}
|
|
Some(normalized)
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[inline]
|
|
fn record_wayland_clipboard_input_for_sync_filter(text: &str) -> Option<(Instant, String)> {
|
|
if text.is_empty() || crate::platform::linux::is_x11() {
|
|
return None;
|
|
}
|
|
let normalized = get_wayland_clipboard_input_normalized_text(text)?;
|
|
let now = Instant::now();
|
|
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
|
cleanup_wayland_clipboard_input_records(&mut records, now);
|
|
records.push((now, normalized.clone()));
|
|
Some((now, normalized))
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
#[inline]
|
|
fn rollback_wayland_clipboard_input_record(record: (Instant, String)) {
|
|
let (created_at, normalized) = record;
|
|
let now = Instant::now();
|
|
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
|
cleanup_wayland_clipboard_input_records(&mut records, now);
|
|
if let Some(pos) = records
|
|
.iter()
|
|
.rposition(|(record_created_at, record_normalized)| {
|
|
*record_created_at == created_at && *record_normalized == normalized
|
|
})
|
|
{
|
|
records.remove(pos);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
pub(super) fn is_recent_wayland_clipboard_input(text: &str) -> bool {
|
|
if text.is_empty() || crate::platform::linux::is_x11() {
|
|
return false;
|
|
}
|
|
let Some(normalized) = get_wayland_clipboard_input_normalized_text(text) else {
|
|
return false;
|
|
};
|
|
let now = Instant::now();
|
|
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
|
cleanup_wayland_clipboard_input_records(&mut records, now);
|
|
records
|
|
.iter()
|
|
.any(|(_, record_normalized)| record_normalized == &normalized)
|
|
}
|
|
|
|
/// Internal: Set clipboard content without delay.
|
|
/// Returns true if clipboard was set successfully.
|
|
#[cfg(target_os = "linux")]
|
|
fn set_clipboard_content(text: &str) -> bool {
|
|
if let Err(e) = crate::clipboard::set_text_clipboard_with_owner_sync(
|
|
text,
|
|
crate::clipboard::ClipboardSide::Host,
|
|
) {
|
|
log::error!(
|
|
"set_clipboard_content: failed to set clipboard with owner marker: {:?}",
|
|
e
|
|
);
|
|
return false;
|
|
}
|
|
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 {
|
|
let record = record_wayland_clipboard_input_for_sync_filter(text);
|
|
if !set_clipboard_content(text) {
|
|
if let Some(record) = record {
|
|
rollback_wayland_clipboard_input_record(record);
|
|
}
|
|
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:
|
|
// - uinput mode (--service): keep clipboard handling in this process because
|
|
// clipboard is unreliable in root service context.
|
|
// - rdp_input mode (--server): forward sequence to custom keyboard handler so
|
|
// ASCII can use Portal keysym and non-ASCII can use clipboard.
|
|
#[cfg(target_os = "linux")]
|
|
if !crate::platform::linux::is_x11() {
|
|
let mut en = ENIGO.lock().unwrap();
|
|
if wayland_use_rdp_input() {
|
|
release_shift_for_char_input(&mut en);
|
|
en.key_sequence(seq);
|
|
return;
|
|
}
|
|
|
|
if wayland_use_uinput() {
|
|
// 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);
|
|
if seq.chars().all(is_ascii_printable) {
|
|
en.key_sequence(seq);
|
|
} else {
|
|
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(target_os = "linux")]
|
|
super::uinput::prepare_layout(evt);
|
|
|
|
#[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 {
|
|
// Not the peer's position: the calibration must not subtract from it.
|
|
#[cfg(feature = "drm")]
|
|
forget_sample_and_move(|| ENIGO.lock().unwrap(), |mut en| en.mouse_move_to(x, y));
|
|
#[cfg(not(feature = "drm"))]
|
|
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();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod cursor_shape_tests {
|
|
use super::*;
|
|
|
|
fn shape(id: u64) -> Arc<Message> {
|
|
let mut msg = Message::new();
|
|
msg.set_cursor_data(CursorData {
|
|
id,
|
|
..Default::default()
|
|
});
|
|
Arc::new(msg)
|
|
}
|
|
|
|
// One test, since the shapes are kept process-wide.
|
|
#[test]
|
|
fn a_shape_is_kept_once_and_can_be_asked_for_again() {
|
|
let first = shared_cursor_shape(shape(u64::MAX - 1));
|
|
let again = shared_cursor_shape(shape(u64::MAX - 1));
|
|
assert!(Arc::ptr_eq(&first, &again), "a second handle shares it");
|
|
let asked = cursor_data_message(u64::MAX - 1).expect("a sent shape is kept");
|
|
assert!(Arc::ptr_eq(&asked, &first));
|
|
assert!(cursor_data_message(u64::MAX - 2).is_none());
|
|
|
|
let raw = |handle| CursorData {
|
|
id: handle,
|
|
width: 4,
|
|
height: 4,
|
|
colors: vec![9u8; 4 * 4 * 4].into(),
|
|
..Default::default()
|
|
};
|
|
let mut compressed = 0;
|
|
let mut compress = |rgba: &[u8]| {
|
|
compressed += 1;
|
|
hbb_common::compress::compress(rgba)
|
|
};
|
|
let shown = cursor_shape_message(raw(1), &mut compress);
|
|
let again = cursor_shape_message(raw(2), &mut compress);
|
|
assert!(Arc::ptr_eq(&shown, &again), "a new handle, the same shape");
|
|
assert_eq!(compressed, 1, "a shape sent before is not compressed again");
|
|
|
|
let sending = shared_cursor_shape(shape(u64::MAX - 3));
|
|
let mut few = HashMap::from([(1, shown.clone())]);
|
|
limit_cursor_handles(&mut few, &sending);
|
|
assert_eq!(few.len(), 1, "below the ceiling nothing goes");
|
|
|
|
let mut handles: HashMap<u64, Arc<Message>> = (0..CURSOR_HANDLES_MAX as u64)
|
|
.map(|handle| (handle, shown.clone()))
|
|
.collect();
|
|
limit_cursor_handles(&mut handles, &sending);
|
|
assert!(
|
|
handles.is_empty(),
|
|
"past the ceiling the handles start over"
|
|
);
|
|
let Some(message::Union::CursorData(cd)) = &shown.union else {
|
|
panic!("a cursor shape");
|
|
};
|
|
assert!(
|
|
cursor_data_message(cd.id).is_none(),
|
|
"and the shapes with them"
|
|
);
|
|
assert!(
|
|
cursor_data_message(u64::MAX - 3).is_some(),
|
|
"but the shape being sent is kept"
|
|
);
|
|
|
|
let big = |id| {
|
|
let mut msg = Message::new();
|
|
msg.set_cursor_data(CursorData {
|
|
id,
|
|
colors: vec![0u8; CURSOR_SHAPES_BYTES_MAX / 2 + 1].into(),
|
|
..Default::default()
|
|
});
|
|
Arc::new(msg)
|
|
};
|
|
shared_cursor_shape(big(u64::MAX - 4));
|
|
let sending = shared_cursor_shape(big(u64::MAX - 5));
|
|
let mut few = HashMap::from([(1, shown.clone())]);
|
|
limit_cursor_handles(&mut few, &sending);
|
|
assert!(
|
|
few.is_empty(),
|
|
"past the byte ceiling the handles start over"
|
|
);
|
|
assert!(cursor_data_message(u64::MAX - 4).is_none());
|
|
assert!(cursor_data_message(u64::MAX - 5).is_some());
|
|
|
|
super::super::service::Reset::reset(&mut StateCursor::default());
|
|
assert!(
|
|
cursor_data_message(u64::MAX - 1).is_none(),
|
|
"a reset forgets them"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, target_os = "linux", feature = "drm"))]
|
|
mod peer_abs_sample_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn a_move_without_an_absolute_sample_forgets_the_last_one() {
|
|
note_peer_absolute_move(20, 30);
|
|
let s = last_peer_abs_sample().expect("an absolute move leaves a sample");
|
|
assert_eq!(s.pos, (20, 30));
|
|
assert!(s.age_ms < 1_000);
|
|
note_pointer_moved_without_absolute_sample();
|
|
assert_eq!(last_peer_abs_sample(), None);
|
|
}
|
|
}
|