mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-05-06 22:28:13 +03:00
Add an opt-in keyboard-shortcut system that triggers session
actions (Send Ctrl+Alt+Del, Toggle Fullscreen, Switch Display,
Screenshot, Switch Tab, etc.) via three-modifier combinations
during a remote session.
Architecture
- Native: src/keyboard/shortcuts.rs intercepts at the encoder
layer (process_event and process_event_with_session), so the
feature is input-source-independent. Bindings persist as a
single JSON blob in LocalConfig.
- Web: matching + keydown intercept live in the separate hand-
written TS client at flutter/web/js/ (gitignored, not in this
repo). flutter/lib/web/bridge.dart::mainInit registers
window.onShortcutTriggered so the JS matcher can dispatch
back into the active session's ShortcutModel; the bridge's
mainReloadKeyboardShortcuts forwards to a JS reloadShortcuts
on settings writes.
- Three-modifier prefix (Ctrl+Alt+Shift; Cmd+Option+Shift on
macOS/iOS) sidesteps the need for a pass-through toggle.
- Flutter native path threads the explicit per-call SessionID
for tab-precise routing; rdev path uses globally-current
session.
UI
- Settings -> General -> Keyboard Shortcuts opens a dedicated
configuration page; desktop and mobile share a body widget.
- Recording dialog with live capture, prefix validation, and a
conflict-replace flow.
- Toolbar menu items display the bound shortcut inline.
- Default bindings (adapted from AnyDesk):
+Del Send Ctrl+Alt+Del
+Enter Toggle Fullscreen
+Left/Right Switch Display Prev/Next
+P Screenshot
+1..9 Switch Session Tab
Other
- AGENTS.md: documented (a) flutter_rust_bridge_codegen needs
a pinned version + Dart bridge wrappers should be hand-
written, and (b) the Web-target split where flutter/web/js/
is the runtime owner on Web rather than wasm-compiled Rust.
- 38 new i18n strings in src/lang/en.rs with Chinese
translations in src/lang/cn.rs.
Refs discussion #1933.
142 lines
4.9 KiB
Dart
142 lines
4.9 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../common.dart';
|
|
import '../consts.dart';
|
|
import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController;
|
|
import '../models/model.dart';
|
|
import '../models/platform_model.dart';
|
|
import '../models/state_model.dart';
|
|
|
|
/// Per-session shortcut dispatcher. Attached to FFI when a session is created.
|
|
///
|
|
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
|
|
/// session events containing the matched `action` id. The session event
|
|
/// listener in [FfiModel.startEventListener] forwards those to this model
|
|
/// via [onTriggered], which runs whatever callback the toolbar / menu
|
|
/// builders previously registered for that action id.
|
|
class ShortcutModel {
|
|
final WeakReference<FFI> parent;
|
|
final Map<String, VoidCallback> _callbacks = {};
|
|
|
|
ShortcutModel(this.parent);
|
|
|
|
/// Called by toolbar / menu builders to register what to do when the
|
|
/// matched shortcut fires.
|
|
void register(String actionId, VoidCallback callback) {
|
|
_callbacks[actionId] = callback;
|
|
}
|
|
|
|
void unregister(String actionId) {
|
|
_callbacks.remove(actionId);
|
|
}
|
|
|
|
/// Called by the session event listener when a `shortcut_triggered` event
|
|
/// arrives for this session.
|
|
void onTriggered(String actionId) {
|
|
final cb = _callbacks[actionId];
|
|
if (cb != null) {
|
|
cb();
|
|
} else {
|
|
debugPrint('shortcut_triggered: no handler for $actionId');
|
|
}
|
|
}
|
|
|
|
/// Read the bindings JSON from LocalConfig.
|
|
static List<Map<String, dynamic>> readBindings() {
|
|
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
|
if (raw.isEmpty) return [];
|
|
try {
|
|
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
|
final list = (parsed['bindings'] as List?) ?? [];
|
|
return list.cast<Map<String, dynamic>>();
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
static bool isEnabled() {
|
|
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
|
|
if (raw.isEmpty) return false;
|
|
try {
|
|
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
|
return parsed['enabled'] == true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Register the default-bound shortcut actions that aren't already wired by
|
|
/// `toolbarControls(...)` (which handles things like Ctrl+Alt+Shift+Del and the
|
|
/// screenshot action). Called once per session from the desktop / mobile
|
|
/// remote page, after the toolbar registrations have run.
|
|
///
|
|
/// [tabController] is the desktop window's tab controller; `null` on mobile /
|
|
/// web (where tab-switch shortcuts don't apply).
|
|
///
|
|
/// Each callback below is a no-op when the underlying state required to
|
|
/// service the action isn't available (e.g. only one display, only one tab).
|
|
void registerSessionShortcutActions(
|
|
FFI ffi, {
|
|
DesktopTabController? tabController,
|
|
}) {
|
|
final sessionId = ffi.sessionId;
|
|
|
|
// Toggle Fullscreen — desktop & web-desktop only. `stateGlobal.setFullscreen`
|
|
// handles native window vs. browser fullscreen; on mobile fullscreen is the
|
|
// permanent default, so we leave the action unregistered (becomes a logged
|
|
// no-op if a mobile user binds it).
|
|
if (isDesktop || isWebDesktop) {
|
|
ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () {
|
|
stateGlobal.setFullscreen(!stateGlobal.fullscreen.value);
|
|
});
|
|
}
|
|
|
|
// Switch Display Next / Prev — requires the peer to have at least 2
|
|
// displays. No-op when only one display is available or when the user has
|
|
// selected the "All displays" pseudo-display.
|
|
void switchDisplayBy(int delta) {
|
|
final pi = ffi.ffiModel.pi;
|
|
final count = pi.displays.length;
|
|
if (count <= 1) return;
|
|
final current = pi.currentDisplay;
|
|
if (current == kAllDisplayValue) return;
|
|
final next = ((current + delta) % count + count) % count;
|
|
bind.sessionSwitchDisplay(
|
|
isDesktop: isDesktop,
|
|
sessionId: sessionId,
|
|
value: Int32List.fromList([next]),
|
|
);
|
|
if (pi.isSupportMultiUiSession) {
|
|
// On multi-ui-session peers no switch-display message is sent back, so
|
|
// update the local state directly (mirrors `model.dart` handling).
|
|
ffi.ffiModel.switchToNewDisplay(next, sessionId, ffi.id);
|
|
}
|
|
}
|
|
|
|
ffi.shortcutModel.register(kShortcutActionSwitchDisplayNext, () {
|
|
switchDisplayBy(1);
|
|
});
|
|
ffi.shortcutModel.register(kShortcutActionSwitchDisplayPrev, () {
|
|
switchDisplayBy(-1);
|
|
});
|
|
|
|
// Switch Tab 1..9 — desktop only. The remote-screen tabs live in the
|
|
// window-scoped DesktopTabController, not on the FFI itself, so we need
|
|
// the controller from the page that owns this session. No-op on mobile /
|
|
// web (no controller passed) and when the requested tab index is out of
|
|
// range.
|
|
if (tabController != null) {
|
|
for (var n = 1; n <= 9; n++) {
|
|
final idx = n - 1;
|
|
ffi.shortcutModel.register(kShortcutActionSwitchTab(n), () {
|
|
if (tabController.state.value.tabs.length > idx) {
|
|
tabController.jumpTo(idx);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|