* Feature: add monitor-switch buttons to remote toolbars
Add buttons to cycle through the remote displays from the toolbars:
- A main-toolbar button and a minimized-handle button, both using a shared SVG icon with the current monitor number overlaid.
- Two opt-in settings under Settings/Other. The minimized-toolbar option is nested under the main-toolbar option.
- The minimized button only appears once the toolbar is collapsed.
- Cycling does not move the remote cursor, matching the existing in-toolbar monitor buttons.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* fix: respect individual-window display mode when cycling
In "Show displays as individual windows" mode, route the cycle button through openMonitorInNewTabOrWindow like the monitor selector, so each display keeps its own window instead of repurposing the current one.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
---------
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Feature: add monitor-switch buttons to remote toolbars
Add a one-click "switch to next monitor" control to both desktop toolbars:
- Main toolbar: always shown when the remote has more than one monitor,
styled to match the existing blue icon buttons (white screen, black number).
- Minimized (draggable show/hide) toolbar: off by default, toggled via a new
"Show monitor switch on minimized toolbar" checkbox in the Display menu and
persisted as a local option.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Update remote_toolbar.dart
* refact: unify monitor-switch button icons, share tooltip
Addressing the review feedback on the monitor-switch toolbar buttons:
- Add assets/display_switcher.svg and use it for both the main and minimized buttons. This replaces the hand-drawn glyph (Containers + magic numbers) on the main toolbar. The icon scales with DPI/theme and the two toolbars stay visually consistent.
- Flip the minimized button's label to white for contrast, since the new icon has a solid screen.
- Move the tooltip string into a shared _MonitorCycle.tooltip getter so both buttons use one source of truth.
- Use const Offstage() for consistency with the surrounding returns.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
* Improve monitor-switch settings and toolbar behavior
- Nest the minimized-toolbar option under the main one in settings only show when the main option is enabled.
- Only show the minimized switch button on the collapsed toolbar handle, so it no longer duplicates the main switch while the toolbar is expanded.
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
---------
Signed-off-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
* Drag whole toolbar; snap to all four edges
Today the drag handle on the remote-session toolbar repositions only
the handle row -- the icons themselves stay centered at the top. This
change applies the position to the entire toolbar wrapper so dragging
the handle moves the whole thing, and extends snapping from top-only
to any of the four window edges.
When docked left/right the toolbar reflows vertically. A live ghost
preview shows where the toolbar will land while you drag, with a small
hysteresis bias to keep the preview from flickering near corners.
The legacy 'remote-menubar-drag-x' session option is read as a fallback
on first load so existing users keep their saved horizontal position;
new option keys are 'remote-menubar-edge' and 'remote-menubar-frac'.
Tested locally on Windows. macOS / Linux / web desktop use the same
shared widget with no platform-specific calls, but I did not verify
them.
* Load edge independently and clamp loaded fraction
Addresses CodeRabbit review on #15051: parse the saved edge regardless
of whether the new fraction option is present so a partial write of
frac doesn't reset the toolbar back to top, and clamp the loaded
fraction to the kOptionRemoteMenubarDragLeft/Right contract so a
corrupted or out-of-range saved value can't bypass the bounds until
the user drags again.
* Require edge activation zone to switch dock; preserve horizontal slide
Per review feedback on #15051: nearest-edge-wins made a low-intent
horizontal slide too easy to escalate into a high-impact orientation
change (vertical reflow on left/right dock). The default drag now
keeps the toolbar on its current dock edge and just updates the
fraction along that edge -- the prior horizontal-slide behavior.
An alternate edge is only previewed/committed when the cursor enters
its 32 px activation zone; once previewed, the cursor has to move
back 64 px before reverting (hysteresis at the zone boundary).
* Gate multi-edge docking behind a settings toggle; default = horizontal slide
Replaces the activation-zone approach with an explicit opt-in setting
in Settings -> Other ("Allow docking remote toolbar to any window
edge"). This addresses the concern that a low-intent horizontal drag
shouldn't be able to trigger a high-impact orientation change, while
still letting users who want multi-edge docking opt in cleanly.
Default (toggle off):
- The original horizontal slide is preserved.
- The bug fix from the first commit still applies: dragging the
handle moves the whole toolbar, and the position persists across
collapse/expand (no more re-center on re-open).
- Draggable is axis-locked to horizontal so the feedback widget
stays on the top line during drag.
Opt-in (toggle on):
- Full nearest-edge wins with the live preview ghost and corner
hysteresis; toolbar reflows vertically on left/right docks.
- Draggable is unlocked for 2D drag.
Reads the option via mainGetLocalBoolOptionSync so the toolbar's
default state matches what the settings checkbox shows; the option
key uses the allow- prefix so unset defaults to off.
Takes effect on next session (setting is read at session init).
The setting key (allow-multi-edge-toolbar-dock) is read by the
existing local-options machinery and persists per-install without
needing to be registered in libs/hbb_common's KEYS_LOCAL_SETTINGS.
Can add that registration in a parallel hbb_common PR if preferred.
* Fix remote toolbar drag positioning & persistence
Align drag fraction calculation with the toolbar's actual travel range,
keep preview sizing stable during drag, and preserve legacy horizontal
position storage when multi-edge docking is disabled.
Signed-off-by: fufesou <linlong1266@gmail.com>
* Remote toolbar snap edges
1. Translations
2. Apply option to remote windows on changed
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: avoid remote toolbar docking jumps on setting reload
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar docking updates and drag sync
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact: translation key
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(toolbar-snap-edges): test web
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar docking sync and vertical layout
Signed-off-by: fufesou <linlong1266@gmail.com>
* Fix remote toolbar monitor controls on side docks
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
* add option to hide stop-service when service is running
Signed-off-by: 21pages <sunboeasy@gmail.com>
* update hbb_common to upstream
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
* avatar
* refactor avatar display: unify rendering and resolve at use time
- Extract buildAvatarWidget() in common.dart to share avatar rendering
logic across desktop settings, desktop CM and mobile CM
- Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn),
to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs
- Store avatar as-is in local config, only resolve when displaying
(settings page) or sending (LoginRequest)
- Resolve avatar in LoginRequest before sending to remote peer
- Add error handling for network image load failures
- Guard against empty client.name[0] crash
- Show avatar in mobile settings page account tile
Signed-off-by: 21pages <sunboeasy@gmail.com>
* web: implement mainResolveAvatarUrl via js getByName
Signed-off-by: 21pages <sunboeasy@gmail.com>
* increase ipc Data enum size limit to 120 bytes
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
* feat(terminal): add reconnection buffer support for persistent sessions
Fix two related issues:
1. Reconnecting to persistent sessions shows blank screen - server now
automatically sends historical buffer on reconnection via SessionState
machine with pending_buffer, eliminating the need for client-initiated
buffer requests.
2. Terminal output before view ready causes NaN errors - buffer output
chunks on client side until terminal view has valid dimensions, then
flush in order on first valid resize.
Rust side:
- Introduce SessionState enum (Closed/Active) replacing bool is_opened
- Auto-attach pending buffer on reconnection in handle_open()
- Always drain output channel in read_outputs() to prevent overflow
- Increase channel buffer from 100 to 500
- Optimize get_recent() to collect whole chunks (avoids ANSI truncation)
- Extract create_terminal_data_response() helper (DRY)
- Add reconnected flag to TerminalOpened protobuf message
Flutter side:
- Buffer output chunks until terminal view has valid dimensions
- Flush buffered output on first valid resize via _markViewReady()
- Clear terminal on reconnection to avoid duplicate output from buffer replay
- Fix max_bytes type (u32) to match protobuf definition
- Pass reconnected field through FlutterHandler event
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix(terminal): add two-phase SIGWINCH for TUI app redraw and session remap on reconnection
Fix TUI apps (top, htop) not redrawing after reconnection. A single
resize-then-restore is too fast for ncurses to detect a size change,
so split across two read_outputs() polling cycles (~30ms apart) to
force a full redraw.
Also fix reconnection failure when client terminal_id doesn't match
any surviving server-side session ID by remapping the lowest surviving
session to the requested ID.
Rust side:
- Add two-phase SIGWINCH state machine (SigwinchPhase: TempResize →
Restore → Idle) with retry logic (max 3 attempts per phase)
- Add do_sigwinch_resize() for cross-platform PTY resize (direct PTY
and Windows helper mode)
- Add session remap logic for non-contiguous terminal_id reconnection
- Extract try_send_output() helper with rate-limited drop logging (DRY)
- Add 3-byte limit to UTF-8 continuation byte skipping in get_recent()
to prevent runaway on non-UTF-8 binary data
- Remove reconnected flag from flutter.rs (unused on client side)
Flutter side:
- Add reconnection screen clear and deferred flush logic
- Filter self from persistent_sessions restore list
- Add comments for web-related changes
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
* - UI display: display_name first
- Fallback: name
- Technical identity: still name
### What changed
- Added account display helpers and display_name state in user model:
- flutter/lib/models/user_model.dart:16
- Account/logout label now uses display_name (@name) when both exist:
- flutter/lib/mobile/pages/settings_page.dart:689
- flutter/lib/desktop/pages/desktop_setting_page.dart:2016
- flutter/lib/desktop/pages/desktop_setting_page.dart:2135
- Desktop Account info now shows both when applicable:
- Display Name: ...
- Username: ...
- flutter/lib/desktop/pages/desktop_setting_page.dart:2039
- Previously done group-list behavior remains:
- group user list displays display_name with name fallback
- flutter/lib/common/widgets/my_group.dart:187
- Persistence path for display_name remains enabled (including group cache/submodule field):
- libs/hbb_common/src/config.rs:2347
- src/client.rs:2630
- LoginRequest.my_name now resolves as:
1. OPTION_DISPLAY_NAME (manual override)
2. user_info.display_name
3. user_info.name
4. OS username fallback
* 1. GUID key (...Uninstall\{GUID}) is MSI-native metadata generated by Windows Installer.
2. Non-GUID key (...Uninstall\RustDesk) is explicitly written by RustDesk’s MSI compatibility component in res/msi/Package/Components/Regs.wxs:44, populated by preprocess.py --arp from .github/workflows/
flutter-build.yml:262.
So they were not using the same EstimatedSize logic:
- MSI GUID key: MSI-calculated size (KB).
- RustDesk key: custom script value from res/msi/preprocess.py:339 (previously bytes, now fixed to KB).
That mismatch is exactly why you saw different sizes.
* improve display name handling
- Append (@username) when multiple users share the same display name
- Trim whitespace from display_name before comparison and display
- Add missing translate() for Logout button on desktop
Signed-off-by: 21pages <sunboeasy@gmail.com>
* group peer filter match both user's display name and user's name
Signed-off-by: 21pages <sunboeasy@gmail.com>
* case-insensitive search in group peer filter
Signed-off-by: 21pages <sunboeasy@gmail.com>
---------
Signed-off-by: 21pages <sunboeasy@gmail.com>
Co-authored-by: 21pages <sunboeasy@gmail.com>
* fix(terminal): ensure tab close is resilient to session cleanup failures
- Wrap _closeTerminalSessionIfNeeded in isolated try/catch so that
tabController.closeBy always executes even if FFI calls throw
- Add clarifying comment in handleWindowCloseButton for single-tab
audit dialog flow
* fix(terminal): fix session reconnect ID mismatch and tab close race condition
Remap surviving persistent sessions to client-requested terminal IDs on
reconnect, preventing new shell creation when IDs are non-contiguous.
Snapshot peerTabCount before async operations in _closeTab to avoid race
with concurrent _closeAllTabs clearing the tab controller.
Remove debug log statements.
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
- Fix new tab not auto-focusing: add FocusNode to TerminalView and
request focus when tab is selected via tab state listener
- Fix NaN error when data arrives before terminal view layout: buffer
output data until terminal view has valid dimensions, flush on first
valid resize callback
Signed-off-by: fufesou <linlong1266@gmail.com>
Terminal tab keys use the format "peerId_terminalId". The previous code
used split('_')[0] or startsWith('$peerId_') to extract the peerId,
which breaks when the peerId itself contains underscores.
This can happen in two scenarios:
- Hostname-based ID: when OPTION_ALLOW_HOSTNAME_AS_ID is enabled, the
peerId is derived from the system hostname, which commonly contains
underscores (e.g. "my_dev_machine").
- Custom ID: the validation regex ^[a-zA-Z][\w-]{5,15}$ allows
underscores since \w matches [a-zA-Z0-9_], so IDs like "my_dev_01"
are valid.
Fix all three parsing sites in terminal_tab_page.dart to use
lastIndexOf('_'), which is safe because terminalId is always a plain
integer with no underscores.
* feat: Add relative mouse mode
- Add "Relative Mouse Mode" toggle in desktop toolbar and bind to InputModel
- Implement relative mouse movement path: Flutter pointer deltas -> `type: move_relative` -> new `MOUSE_TYPE_MOVE_RELATIVE` in Rust
- In server input service, simulate relative movement via Enigo and keep latest cursor position in sync
- Track pointer-lock center in Flutter (local widget + screen coordinates) and re-center OS cursor after each relative move
- Update pointer-lock center on window move/resize/restore/maximize and when remote display geometry changes
- Hide local cursor when relative mouse mode is active (both Flutter cursor and OS cursor), restore on leave/disable
- On Windows, clip OS cursor to the window rect while in relative mode and release clip when leaving/turning off
- Implement platform helpers: `get_cursor_pos`, `set_cursor_pos`, `show_cursor`, `clip_cursor` (no-op clip/hide on Linux for now)
- Add keyboard shortcut Ctrl+Alt+Shift+M to toggle relative mode (enabled by default, works on all platforms)
- Remove `enable-relative-mouse-shortcut` config option - shortcut is now always available when keyboard permission is granted
- Handle window blur/focus/minimize events to properly release/restore cursor constraints
- Add MOUSE_TYPE_MASK constant and unit tests for mouse event constants
Note: Relative mouse mode state is NOT persisted to config (session-only).
Note: On Linux, show_cursor and clip_cursor are no-ops; cursor hiding is handled by Flutter side.
Signed-off-by: fufesou <linlong1266@gmail.com>
* feat(mouse): relative mouse mode, exit hint
Signed-off-by: fufesou <linlong1266@gmail.com>
* refact(relative mouse): shortcut
Signed-off-by: fufesou <linlong1266@gmail.com>
---------
Signed-off-by: fufesou <linlong1266@gmail.com>
Each desktop isolate now independently tracks wakelock state.
WakelockPlus.disable() is only called when all tabs within the
same isolate are closed/minimized.
WakelockPlus ensures screen stays awake as long as any isolate
has wakelock enabled.
Signed-off-by: 21pages <sunboeasy@gmail.com>
- Route Windows server-to-client file reads through CM instead of the connection layer
- Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages
(ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult)
- Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up
on completion, cancellation, and errors
- Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path
validation on the CM side
- Improve Flutter file transfer UX and robustness:
- Use explicit percent/percentText progress fields
- Derive speed and cancel actions from the active job
- Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure
- Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories
Signed-off-by: fufesou <linlong1266@gmail.com>
* fix: prevent custom scale dialog from closing when interacting with slider
Wrapped MobileCustomScaleControls in GestureDetector with opaque behavior
to prevent touch events from propagating to parent dialog's clickMaskDismiss
handler. The slider now works correctly without closing the dialog.
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
* Update flutter/lib/mobile/widgets/custom_scale_widget.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update flutter/lib/mobile/widgets/custom_scale_widget.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update flutter/lib/mobile/widgets/custom_scale_widget.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update flutter/lib/mobile/widgets/custom_scale_widget.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Revert "fix: mobile remove "Scale custom" (#13323)"
This reverts commit 265d08fc3b.
* chore: keep remote_toolbar.dart cleanup (remove dead code)
The dead code removed in 265d08fc3 hasn't been used since Aug 2023.
Only reverting toolbar.dart is needed for the mobile Scale custom fix.
* Update flutter/lib/mobile/pages/remote_page.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* refactor: Implement CustomScaleControlsMixin for shared scaling logic across mobile and desktop widgets
- Introduced a new mixin `CustomScaleControlsMixin` to encapsulate custom scale control logic, allowing for code reuse in both mobile and desktop widgets.
- Refactored `_CustomScaleMenuControlsState` and `_MobileCustomScaleControlsState` to utilize the new mixin, simplifying the scaling logic and reducing code duplication.
- Updated slider handling and state management to leverage the mixin's methods for improved maintainability.
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
* Update flutter/lib/desktop/widgets/remote_toolbar.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update flutter/lib/mobile/widgets/custom_scale_widget.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update flutter/lib/mobile/pages/remote_page.dart
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* refactor: changed from mixin to abstract class
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
* Revert "Update flutter/lib/mobile/pages/remote_page.dart"
This reverts commit 7c35897408.
* refactor: remove unnecessary tap event handling in custom scale controls
- Removed the `onTap` handler from the
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
* refactor: simplify MobileCustomScaleControls usage in remote_page.dart
- Removed unnecessary GestureDetector wrapper around MobileCustomScaleControls for cleaner code.
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
---------
Signed-off-by: Alessandro De Blasis <alex@deblasis.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>