mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-07 06:38:11 +03:00
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.
87 lines
4.8 KiB
Markdown
87 lines
4.8 KiB
Markdown
# 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.
|