Adds a keyboard shortcut feature (Rust matcher + Dart UI + cross-language
parity tests) that lets users bind combinations like Ctrl+Alt+Shift+P to
session actions. Bindings are stored in LocalConfig under
`keyboard-shortcuts`; the matcher gates dispatch on `enabled` and
`pass_through` flags so flipping the master switch off is a hard stop.
Wire-up summary:
- src/keyboard/shortcuts.rs: matcher, default bindings, parity test against
flutter/test/fixtures/default_keyboard_shortcuts.json
- src/keyboard.rs: shortcut intercept in process_event{,_with_session},
feature-gated to `flutter`; runs before key swapping so users bind to
physical keys
- src/flutter_ffi.rs: main_reload_keyboard_shortcuts +
main_get_default_keyboard_shortcuts; reload_from_config seeded in main_init
- flutter/lib/common/widgets/keyboard_shortcuts/: shared config page body,
recording dialog, shortcut display formatter, action group registry
- flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart and
flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart: platform
shells around the shared body
- flutter/lib/models/shortcut_model.dart: per-session ShortcutModel +
registerSessionShortcutActions for actions with no toolbar TToggleMenu /
TRadioMenu (fullscreen, switch display/tab, close tab, voice call, etc.)
- flutter/lib/common/widgets/toolbar.dart: optional `actionId` field on
TToggleMenu / TRadioMenu, plus per-helper auto-register pass that wires
tagged entries' existing onChanged into the ShortcutModel
- flutter/test/keyboard_shortcuts_test.dart + fixtures: cross-language
parity (default bindings, supported key vocabulary)
Design principles applied during review:
1. Additions are fine; modifications to original logic must be deliberate.
Tagging an existing TToggleMenu entry with `actionId:` is an addition.
Rewriting its onChanged to satisfy a new contract is a modification —
and was reverted for every case where the original click behavior was
working. Four closures were touched and then reverted (mobile View
Mode, Privacy mode multi-impl, Relative mouse mode, Reverse mouse
wheel); their shortcuts are wired via standalone closures in
shortcut_model.dart instead.
2. Toolbar auto-register is reserved for entries whose onChanged is
inherently self-flipping — typically `sessionToggleOption(name)` where
the named option is flipped in place and the input bool is unused. The
register pass passes `!menu.value` from registration time, which is
harmless under self-flipping but wrong for closures that consume the
input bool directly. Tagging a non-self-flipping entry forces a closure
rewrite; choose non-toolbar registration in that case.
3. When shortcuts are disabled, toolbar behavior must be bit-for-bit
unchanged. The matcher's `enabled`-gate already guarantees no
dispatch; the auto-register pass is left unconditional (its only effect
is HashMap operations on a separate ShortcutModel) so mid-session
enable works without a reconnect. The trade-off is intentional and
documented at the top of toolbarControls.
4. Comments stay terse. Rationale lives in one place — the doc comment of
the helper or registration site, not duplicated at every call site.
5. Where an existing helper needs a new optional behavior (e.g.
`_OptionCheckBox` gaining a tooltip slot), the new branch must reduce
to byte-identical output for existing callers (`trailing == null`
case → original `Expanded(Text)` layout). Verified.
6. Action IDs and labels stay consistent. Renamed `reset_cursor` →
`reset_canvas` so the action ID matches its user-facing label
("Reset canvas") and capability flag.
Out-of-scope but included:
- AGENTS.md: documents flutter_rust_bridge no-codegen workflow and the
Web target's hand-written TS client, since both are load-bearing for
any new FFI work.
- remote_toolbar.dart: i18n fix for the per-monitor tooltip ("All
monitors" / "Monitor #N"), unrelated to shortcuts but kept here.
* fix(linux): prevent X11 BadWindow crash in get_focused_display
When the active window is destroyed between xdo_get_active_window and
xdo_get_window_location/xdo_get_window_size calls, the default X11
error handler terminates the process with a BadWindow error. This
causes the rustdesk --server process to crash and the remote session
to disconnect and reconnect every time the user closes a window.
Install a custom X error handler around the xdo calls that catches
BadWindow errors and returns gracefully instead of crashing.
Fixes: https://github.com/rustdesk/rustdesk/issues/9003
Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
Signed-off-by: easonysliu <easonysliu@tencent.com>
* fix(linux): prevent BadWindow crash in focus display lookup
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: easonysliu <easonysliu@tencent.com>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: easonysliu <easonysliu@tencent.com>
Co-authored-by: Claude (claude-opus-4-6) <noreply@anthropic.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(client): serialize X11 keyboard grab and debounce focus feedback
When two RustDesk sessions run fullscreen on separate monitors on
Linux/X11, keyboard input gets stuck on the wrong session or stops
working entirely. This happens because each Flutter isolate calls
change_grab_status concurrently, racing on KEYBOARD_HOOKED and the
rdev grab channel.
Additionally, XGrabKeyboard causes a focus-change feedback loop:
grab shifts focus away from the Flutter window, triggering PointerExit,
which releases the grab, restoring focus, triggering PointerEnter,
which re-grabs -- cycling at ~10 Hz and blocking keyboard input.
Fix by:
- Serializing grab transitions with a mutex and tracking the owning
session (by lc.session_id), so a stale Wait from session A cannot
clobber session B's freshly acquired grab.
- Debouncing Wait events (300 ms) from the same session that just
acquired the grab, breaking the X11 focus feedback loop.
- Refreshing the debounce timer on idempotent Run calls (enterView
while already owner), keeping the grab stable during normal use.
Signed-off-by: Sergiusz Michalik <github@latens.me>
* fix(client): add deferred release and dedup for debounced Wait
When a Wait is debounced (within 300ms of grab acquisition), schedule
a deferred release thread that re-checks after the debounce window.
If no new Run refreshed the grab, the deferred thread releases it,
ensuring a genuine leave within the debounce window is not lost.
Add a deferred_pending flag to GrabOwnerState to prevent spawning
redundant threads during the X11 focus feedback loop.
Signed-off-by: Sergiusz Michalik <github@latens.me>
* fix(client): use window-scoped ID and fix deferred-release re-arming
Address PR review feedback:
- Use per-window UUID instead of connection-scoped lc.session_id so two
windows viewing the same peer get distinct grab owners
- Reset deferred_pending on both idempotent Run refresh and owner
handoff, so a subsequent Wait can always spawn a fresh timer
- Replace manual Default impl with derive
* fix(client): recover from poisoned mutex instead of panicking
* docs: clarify cross-platform rationale for GrabOwnerState
* fix(client): only clear deferred_pending when timer snapshot matches
* fix(client): use full u128 window ID, downgrade grab logs to debug
- Widen GrabOwnerState.owner to u128 to avoid theoretical collision
from truncating a 128-bit UUID to 64 bits
- Downgrade all grab transition log::info! to log::debug! to reduce
log noise during routine window switches
- Clear deferred_pending on post-debounce release path to maintain
the "deferred_pending => timer in flight" invariant
* fix(client): gate GRAB_DEBOUNCE_MS with cfg(target_os = "linux")
* fix(grab): release grabbed keys without clobbering new owner state
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(keyboard): Simple refactor
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: Sergiusz Michalik <github@latens.me>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* fix(linux): enable mouse side buttons in remote sessions
Flutter's Linux embedder never delivers X11 button 8/9 (back/forward)
events to Dart, so mouse side buttons were silently dropped in remote
sessions.
Intercept these buttons at the GDK level via button-press/release-event
handlers on all windows (main + sub-windows) and forward them through
a dedicated platform channel to the active InputModel session.
Also add a defensive XSetPointerMapping call during enigo init to
extend the X11 core pointer button map to 9 buttons on servers where
it is smaller (e.g. minimal X server configurations).
* fix: address review feedback for side button support
- Use XOpenDisplay/XCloseDisplay instead of reading Display* from
xdo_t's private struct layout at offset 0 (fragile ABI assumption)
- Track side button down ownership per button via a Map instead of a
single slot, preventing cross-button mismatch on overlapping presses
* fix: gate side buttons on view-only and fix teardown
- Skip side button events in view-only sessions (consistent with
other mouse entry points)
- Release held side buttons on session close to avoid stuck buttons
on the remote
- Drop unpaired 'up' events instead of falling back to the active
model, which could send to the wrong session
* docs: add clarifying comments from review feedback
- Note global scope of XSetPointerMapping and that it runs once
via lazy_static singleton
- Clarify sub-window callback is safe on X11-only builds
- Document per-isolate design of initSideButtonChannel
* fix: replace broken XSetPointerMapping with diagnostic check
XSetPointerMapping requires the length to match XGetPointerMapping's
return value - it cannot extend the button count. The previous code
would trigger a BadValue X error on servers with fewer than 9 buttons.
Replace with a diagnostic-only check that logs whether the core
pointer has enough buttons for side button simulation. RustDesk's
uinput "Mouse passthrough" device already provides the needed buttons
in practice.
Also add .catchError to fire-and-forget side button releases during
session teardown to prevent unhandled async errors.
* fix: ensure side button releases bypass permission checks
If permissions change between button down and up (e.g. keyboardPerm
revoked, view-only toggled), sendMouse's early return would suppress
the release, leaving a stuck button on the remote.
Add _sendMouseUnchecked that bypasses permission checks, used for:
- Side button 'up' events (matching a recorded 'down')
- Forced releases during session teardown
Gate all permission checks (isViewOnly, keyboardPerm, isViewCamera)
at the 'down' entry point before recording in _sideButtonDownModels.
* fix: add NULL guards and avoid blocking platform channel handler
- Add NULL checks for FL_VIEW cast and channel creation in
on_subwindow_created (review feedback from fufesou)
- Use fire-and-forget (unawaited) for _sendMouseUnchecked calls
inside the platform channel handler to avoid blocking platform
messages when sessionSendMouse is slow (review feedback from Copilot)
* fix: remove circular import and skip X11 check on Wayland
- Move initSideButtonChannel() call from initEnv() in main.dart to
the InputModel constructor, removing the circular import between
main.dart and input_model.dart
- Skip check_x11_button_map() when DISPLAY is not set to avoid
noisy warnings on pure Wayland environments
* flutter: improve address book pull error handling
Summary:
- Show error messages when fetching the address book list fails.
- After the initial fetch, switching back to the AB tab no longer re-fetches it, even if an error occurred or the error banner was dismissed.
Tested:
- Self-hosted server:
- normal
- 403 responses
- legacy address book mode
- Public server
- Verified that switching tabs no longer re-fetches AB after the initial fetch, regardless of whether an error occurred or the error banner was cleared.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* use resp.statusCode in address book json decoding
Signed-off-by: 21pages <sunboeasy@gmail.com>
* flutter: clear address book list errors on reset
Signed-off-by: 21pages <sunboeasy@gmail.com>
* flutter: clear address book pull errors consistently
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Treat "Access to mobile devices is restricted in your country"
as a non-retriable connection error so the error dialog does not
trigger reconnect attempts.
Signed-off-by: 21pages <sunboeasy@gmail.com>
* docs: fix typos in documentation and code comments
- Fix 'seperated' -> 'separated' in remote_input.dart
- Fix 'seperators' -> 'separators' in fuse/cs.rs
- Update outdated 'OSX' -> 'macOS' in virtual display README
Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>
* impl(cm): implement change_theme and change_language callbacks
These callbacks were previously empty TODO stubs.
Now they properly invoke the Sciter UI handlers to notify
the UI when theme or language changes occur.
Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>
---------
Signed-off-by: pallab-js <sonowalpallabjyoti@gmail.com>
* tcp proxy
* fix per review
* fix per review
* Suppress secure_tcp info logs for TCP proxy requests
Signed-off-by: 21pages <sunboeasy@gmail.com>
* copilot review: redact tcp proxy logs, dedupe headers, and avoid body clone
Signed-off-by: 21pages <sunboeasy@gmail.com>
* format common.rs
Signed-off-by: 21pages <sunboeasy@gmail.com>
* copilot review: test function name
Signed-off-by: 21pages <sunboeasy@gmail.com>
* copilot review: format IPv6 tcp proxy log targets correctly
Signed-off-by: 21pages <sunboeasy@gmail.com>
* copilot review: normalize HTTP method before direct request dispatch
Signed-off-by: 21pages <sunboeasy@gmail.com>
* review: extract fallback helper, fix Content-Type override, add overall timeout
- Extract duplicated TCP proxy fallback logic into generic
`with_tcp_proxy_fallback` helper used by both `post_request` and
`http_request_sync`, eliminating code drift risk
- Allow caller-supplied Content-Type to override the default in
`parse_simple_header` instead of silently dropping it
- Take body by reference in `post_request_http` to avoid eager clone
when no fallback is needed
- Wrap entire `tcp_proxy_request` flow (connect + handshake + send +
receive) in an overall timeout to prevent indefinite stalls
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* review: make is_public case-insensitive and cover mixed-case rustdesk URLs
Signed-off-by: 21pages <sunboeasy@gmail.com>
* oidc: route auth requests through shared HTTP/tcp-proxy path while keeping TLS warmup
Signed-off-by: 21pages <sunboeasy@gmail.com>
* refactor: replace unused TryFrom<Response> with HbbHttpResponse::parse method
Remove TryFrom<Response> impl that was never called and replace the
private parse_hbb_http_response helper in account.rs with a public
parse() method on HbbHttpResponse, eliminating code duplication.
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>