Files
rustdesk/AGENTS.md
rustdesk cd7686baa2 feat(shortcuts): user-configurable keyboard shortcuts for session actions
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.
2026-04-30 16:40:42 +08:00

4.8 KiB

RustDesk Guide

Project Layout

Directory Structure

  • src/ Rust app
  • src/server/ audio / clipboard / input / video / network
  • src/platform/ platform-specific code
  • src/ui/ legacy Sciter UI (deprecated)
  • flutter/ current UI
  • libs/hbb_common/ config / proto / shared utils
  • libs/scrap/ screen capture
  • libs/enigo/ input control
  • libs/clipboard/ clipboard
  • libs/hbb_common/src/config.rs all options

Key Components

  • Remote Desktop Protocol: Custom protocol implemented in src/rendezvous_mediator.rs for communicating with rustdesk-server
  • Screen Capture: Platform-specific screen capture in libs/scrap/
  • Input Handling: Cross-platform input simulation in libs/enigo/
  • Audio/Video Services: Real-time audio/video streaming in src/server/
  • File Transfer: Secure file transfer implementation in libs/hbb_common/

UI Architecture

  • Legacy UI: Sciter-based (deprecated) - files in src/ui/
  • Modern UI: Flutter-based - files in flutter/
    • Desktop: flutter/lib/desktop/
    • Mobile: flutter/lib/mobile/
    • Shared: flutter/lib/common/ and flutter/lib/models/

Rust Rules

  • Avoid unwrap() / expect() in production code.

  • Exceptions:

    • tests;
    • lock acquisition where failure means poisoning, not normal control flow.
  • Otherwise prefer Result + ? or explicit handling.

  • Do not ignore errors silently.

  • Avoid unnecessary .clone().

  • Prefer borrowing when practical.

  • Do not add dependencies unless needed.

  • Keep code simple and idiomatic.

Tokio Rules

  • Assume a Tokio runtime already exists.
  • Never create nested runtimes.
  • Never call Runtime::block_on() inside Tokio / async code.
  • Do not hide runtime creation inside helpers or libraries.
  • Do not hold locks across .await.
  • Prefer .await, tokio::spawn, channels.
  • Use spawn_blocking or dedicated threads for blocking work.
  • Do not use std::thread::sleep() in async code.

Flutter Rust Bridge

  • Do not run flutter_rust_bridge_codegen — it requires a specific pinned version that is not easy to set up locally.
  • When adding new FFI functions in src/flutter_ffi.rs, hand-write the corresponding Dart wrappers instead of regenerating.
  • Web bridge (committed): edit flutter/lib/web/bridge.dart directly. Follow the existing patterns there for SyncReturn<T> / Future<T> and the dart:js glue.
  • Native bridge (flutter/lib/generated_bridge.dart, src/bridge_generated.rs, src/bridge_generated.io.rs): these are gitignored and regenerated by the project's CI codegen. Manually editing them locally is fine for development testing, but those edits do not persist into commits.

Web (Flutter Web) Architecture

Flutter Web in this repo is not "Dart compiled to JS via Flutter alone". The runtime is split:

  • Native targets (Win/Mac/Linux/Android/iOS): Rust drives sessions via flutter_rust_bridge; Dart only renders UI.
  • Web target: Rust does not run. There is a separate hand-written TypeScript / JavaScript client at flutter/web/js/ (gitignored — not present in this repo, lives in the maintainer's local tree). It owns connection, codec, keyboard, clipboard, etc. — basically a JS port of the Rust client. The Dart UI talks to it through flutter/lib/web/bridge.dart, which uses dart:js to call JS-side functions and to register Dart-side callbacks on window.*.

Implications when adding any session-runtime feature (keyboard, clipboard, audio, …):

  • The Rust implementation in src/ is for native only. Don't try to compile it to wasm.
  • The matching Web-side logic must be written in TS/JS under flutter/web/js/src/. It's a translation of the Rust logic, usually simpler — Web is single-window, so any per-session-id plumbing in Rust collapses to a single global on Web.
  • flutter/lib/web/bridge.dart is the only place where Dart sees JS. Other Dart code stays platform-agnostic and goes through bind. Don't sprinkle if (isWeb) runtime branches in shared Dart files to call Web-specific logic — put the platform divergence in the bridge.
  • For JS → Dart events (e.g., a Web matcher firing), the convention is: Dart sets js.context['onFooBar'] = (...) {...} once at startup (typically in mainInit); the JS side calls window.onFooBar(...). See onLoadAbFinished, onLoadGroupFinished for reference.
  • The maintainer cannot easily run flutter_rust_bridge_codegen, so when a new FFI function lands in src/flutter_ffi.rs:
    1. add the Web counterpart to flutter/lib/web/bridge.dart by hand;
    2. note that on the Web target it may need to be a no-op or a JS bridge call rather than a real Rust invocation.

Editing Hygiene

  • Change only what is required.
  • Prefer the smallest valid diff.
  • Do not refactor unrelated code.
  • Do not make formatting-only changes.
  • Keep naming/style consistent with nearby code.