Compare commits

...

39 Commits

Author SHA1 Message Date
rustdesk
5fc0367abd 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.
2026-02-19 17:52:17 +08:00
rustdesk
9111bfc1de - 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
2026-02-19 09:43:55 +08:00
Nicola Spieser Buiss
9345fb754a fix: correct typos and improve code clarity (#14341)
- Fix 'clipbard' typos in clipboard.rs (function names, comments, strings)
- Fix 'seperate' typo in x11/server.rs comment
- Replace !is_ok() with idiomatic is_err() in updater.rs
- Fix double backtick typo in updater.rs comment

Co-authored-by: Ocean <ocean@Mac-mini-von-Ocean.local>
2026-02-17 14:29:50 +08:00
fufesou
779b7aaf02 feat(wayland): keyboard mode, legacy translate (#14317)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-15 16:43:21 +08:00
21pages
b268aa1061 Fix some single device multiple ids scenarios on MacOS (#14196)
* fix(macos): sync config to root when root config is empty

Signed-off-by: 21pages <sunboeasy@gmail.com>

* fix(server): gate startup on initial config sync; document CheckIfResendPk limitation

  - wait up to 3s for initial root->local config sync before starting server services
  - continue startup when timeout is hit, while keeping sync/watch running in background
  - avoid blocking non-server process startup
  - clarify that CheckIfResendPk only re-registers PK for current ID and does not solve multi-ID when root uses a non-default mac-generated ID

Signed-off-by: 21pages <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
2026-02-15 16:12:26 +08:00
Vance
40f86fa639 fix(mobile): account for safe area padding in canvas size calculation (#14285)
* fix(mobile): account for safe area padding in canvas size calculation

* fix(mobile): differentiate safe area handling for portrait vs landscape

* refact(ios): Simple refactor

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ios): canvas getSize, test -> Android

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix: comments

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-02-15 14:52:27 +08:00
rustdesk
980bc11e68 update common 2026-02-14 17:48:53 +08:00
Shaikh Naasir
85db677982 docs: fix typos in clipboard documentation (#13521)
Signed-off-by: Naasir <yoursdeveloper@protonmail.com>
2026-02-14 01:06:25 +08:00
fufesou
2842315b1d Fix/linux shortcuts inhibit (#14302)
* feat: Inhibit system shortcuts on Linux

Fixes #13013.

Signed-off-by: Max von Forell <max@vonforell.de>

* fix(linux): shortcuts inhibit

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: Max von Forell <max@vonforell.de>
Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Max von Forell <max@vonforell.de>
2026-02-11 16:11:47 +08:00
fufesou
6c541f7bfd fix(xdo): deb, libxdo3 | libxdo4 (#14314)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-11 16:11:15 +08:00
VenusGirl❤
067fab2b73 Update Korean (#14298)
Correct spacing and spelling
2026-02-10 18:48:30 +08:00
fufesou
de6bf9dc7e fix(ios): Add defensive timer cancellation for keyboard visibility (#14301)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-09 15:54:22 +08:00
fufesou
54eae37038 fix(ios): workaround physical keyboard after virtual keyboard hidden (#14207)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-09 00:36:25 +08:00
Hugo Breda
0118e16132 PT-BR language update (#14295)
* PT-BR language update

@rustdesk
Please merge. Thanks

* Update ptbr.rs

* Update ptbr.rs

Please submit, i will get back soon and finish all other stuff.

* PT-BR language update

Completed all missing PT-BR translations.
2026-02-09 00:31:47 +08:00
fufesou
626a091f55 fix(translation): OIDC, Continue with (#14271)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-02-06 14:18:48 +08:00
Daniel Marschall
4fa5e99e65 Remove unused option_env!(...) (#13959) 2026-02-03 20:55:34 +08:00
bilimiyorum
5ee9dcf42d Update tr.rs (#14160)
The previous PR was reverted due to an incorrect file path. This PR applies the same updates to src/lang/tr.rs.
2026-02-02 22:18:36 +08:00
Copilot
6306f83316 Fix non-link text color in dialogs with links for dark theme (#14220)
* Initial plan

* Fix dialog text color for dark theme with links

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Keep original link color (blue), only fix non-link text color

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* fix: dialog text color in dark theme

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
2026-02-01 12:18:07 +08:00
XLion
96075fdf49 Update tw.rs (#14138) 2026-01-31 16:38:09 +08:00
Copilot
8c6dcf53a6 iOS terminal: Add touch swipe and floating back button for exit (#14208)
* Initial plan

* Add iOS edge swipe gesture to exit terminal session

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Improve iOS edge swipe gesture with responsive thresholds and better gesture handling

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Fix: Reset _swipeCurrentX in onHorizontalDragStart to prevent stale state

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Add trackpad support documentation for iOS edge swipe gesture

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Add iOS-style circular back button to terminal page

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Remove trackpad support documentation - not needed with back button

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* Filter edge swipe gesture to touch-only input (exclude mouse/trackpad)

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* fix: missing import

Signed-off-by: fufesou <linlong1266@gmail.com>

* fix(ios): terminal swip exit gesture

Signed-off-by: fufesou <linlong1266@gmail.com>

* Update flutter/lib/mobile/pages/terminal_page.dart

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
Co-authored-by: fufesou <linlong1266@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-31 16:37:45 +08:00
fufesou
e1b1a927b8 fix(ios): capsLock, workaround #5871 (#14194)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-30 17:32:18 +08:00
fufesou
1e6bfa7bb1 fix(iPad): Magic Mouse, click (#14188)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-29 15:25:44 +08:00
fufesou
79ef4c4501 Copilot/fix action run error (#14186)
* Initial plan

* Fix macOS build: Remove @available check causing linker error

The @available check in GetDisplayName was causing the linker to look for
__isPlatformVersionAtLeast symbol which is not available when targeting
macOS 10.14. Since this function is only used for logging, we simplify it
to return "Unknown" for all displays, avoiding the runtime availability check.

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>

* fix(macOS): ___isPlatformVersionAtLeast is not available in macOS 10.14

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
2026-01-28 17:44:17 +08:00
twprh
5f3ceef592 Update de.rs (#14139)
zum Zeitpunkt der Anzeige ist der Datenschutz aktiviert bzw. schon beendet.

alternativ ginge auch:

Datenschutzmodus wurde aktiviert  
bzw.
 Datenschutzmodus wurde beendet
2026-01-28 15:16:27 +08:00
Lynilia
1a90e6b6c7 Update fr.rs (#14151) 2026-01-28 15:16:06 +08:00
John Fowler
f112d097dc Replacing incorrect quotation marks (#14144)
* Update Hungarian translations in hu.rs

Translation of new strings and some fixes.
John Fowler.

* Escape quotes in Hungarian language strings

Replacing Hungarian quotation marks

* Update Hungarian translations for various terms

Upload a new translation (hu.rs) file.
2026-01-28 15:15:29 +08:00
ThallesWS
45cab7f808 fix issue: #13911 'Double Click' bug on iPad with Magic Mouse (#14086)
* fix issue: #13911 'Double Click' bug on iPad with Magic Mouse

* remote_input.dart comments - gestures.dart organization and clean states of all interrupted gestures
2026-01-28 15:14:06 +08:00
fufesou
216ec9d52b fix(terminal): ios delete (#14147)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-28 15:12:42 +08:00
fufesou
56a8f6b97b fix(iOS): Unexpected mouse movement to (0,0) on idle (#14180)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-28 15:11:44 +08:00
Bin Li
c76d10a438 feat(macos): initial privacy mode support [a simple try] (#14102)
* feat(macos): add privacy mode support for macOS

## Summary
Add privacy mode functionality for macOS platform, allowing remote
desktop sessions to hide the screen content from local users.

## Changes

### Core Implementation (src/platform/macos.mm)
- Implement screen blackout using CGDisplayGammaTable API
- Implement input blocking using CGEventTap to intercept keyboard/mouse
- Store and restore original gamma values for proper cleanup

### Privacy Mode Integration (src/privacy_mode.rs, src/privacy_mode/macos.rs)
- Add macOS privacy mode implementation with PrivacyMode trait
- Register macOS privacy mode in PRIVACY_MODE_CREATOR
- Set DEFAULT_PRIVACY_MODE_IMPL for macOS platform
- Implement get_supported_privacy_mode_impl() for macOS

### Connection Handling (src/server/connection.rs)
- Add supported_privacy_mode_impl to platform_additions for macOS
- Enable privacy mode toggle in client UI when connecting via LAN IP

### Localization (src/lang/*.rs)
- Add "privacy_mode_impl_macos_tip" translation for en/cn/tw

## Safety & Security
- Implements Drop trait to ensure cleanup on normal exit
- macOS system automatically restores gamma table on process termination
- CGEventTap is automatically released when process terminates
- Tested with SIGKILL to verify crash recovery

## Testing
- Verified privacy mode toggle works via both ID and LAN IP connection
- Verified screen recovery after process crash (kill -9)
- Verified input restoration after process termination

* refactor: use existing 'Privacy mode' translation key

* refactor: rename gamma channel variables for better readability - rename r/g/b to red/green/blue to avoid variable shadowing confusion

* fix: add error handling for gamma table restoration with fallback to system reset

* fix: add error handling for CGEventTapCreate failure in privacy mode

* fix: only set display to black if original gamma was saved successfully

* fix: add error handling for CGSetDisplayTransferByTable when setting display to black

* fix: improve event tap callback to properly distinguish remote input from local input

* fix: missing macos.rs

* Fix: Add display validation before restoring gamma values

* Fix: Add mutex lock for thread safety in MacSetPrivacyMode

* Fix: Handle return values and add missing mouse events in macos privacy mode

* fix: only set conn_id after privacy mode is successfully turned on

* fix: reimplement privacy mode with stable display identification

Address code review concern: original gamma values stored with DisplayID
as key could become stale if display list changes between privacy mode
activations (e.g., display reconnected with different ID).

Solution:
- Use UUID instead of DisplayID as storage key (stable across reconnections)
- Clear g_originalGammas when privacy mode is turned off
- Register CGDisplayReconfigurationCallback to handle hot-plug events
- Validate display state via FindDisplayIdByUUID() before restoration

Key features:
- UUID-based display identification (stable across reconnections)
- Hot-plug support via CGDisplayReconfigurationCallback
- EventTap auto re-enable on system timeout
- Fallback to CGDisplayRestoreColorSyncSettings() for recovery
- Detailed error logging with display name/ID/UUID

* fix: ensure EventTap runs on main thread and improve gamma restore error handling

- Add SetupEventTapOnMainThread() to create EventTap on main thread using dispatch_sync, avoiding potential issues when called from background threads

- Add TeardownEventTapOnMainThread() for consistent cleanup on main thread

- Check [NSThread isMainThread] to avoid deadlock when already on main thread

- Add error tracking for gamma restoration during cleanup

- Use CGDisplayRestoreColorSyncSettings() as fallback when individual gamma restoration fails

* fix: remove invalid eventMask bits that caused undefined behavior in input blocking

* fix: address code review comments for macos privacy mode implementation

Changes to src/privacy_mode/macos.rs:
- Add check_on_conn_id() in turn_on_privacy() to prevent duplicate activation
- Add check_off_conn_id() in turn_off_privacy() to validate connection ID
- Add self.conn_id = 0 in clear() to reset connection state

Changes to src/platform/macos.mm:
- Add link comment for ENIGO_INPUT_EXTRA_VALUE referencing libs/enigo/src/macos/macos_impl.rs
- Fix NSLog format string mismatch (5 placeholders vs 4 values)
- Make ApplyBlackoutToDisplay() return bool for proper error handling
- Return false when UUID is empty since privacy mode requires ALL displays
- Add else branches with logging for:
  - CGGetDisplayTransferByTable failures
  - Zero gamma table capacity (not supported)
  - Zero blackout capacity
- Remove unused g_uuidToDisplayId variable (was only written, never read)

* fix(macos): add early return with privacy mode exit on display hotplug failures

Why large-scale changes are needed:

The code review suggested adding early return when errors occur in
DisplayReconfigurationCallback. However, simply returning early is not
enough - when a newly connected display cannot be blacked out, we must
exit privacy mode entirely to maintain security guarantees.

The challenge is that DisplayReconfigurationCallback already holds
g_privacyModeMutex, so calling MacSetPrivacyMode(false) directly would
cause a deadlock. This necessitated:

1. Extract TurnOffPrivacyModeInternal() - a lock-free internal function
   that can be safely called from within the callback
2. Refactor MacSetPrivacyMode(false) branch to use this internal function
3. Add early returns with TurnOffPrivacyModeInternal() calls at each
   failure point in DisplayReconfigurationCallback

Changes in DisplayReconfigurationCallback:
- UUID empty: log + exit privacy mode + early return
- Gamma table capacity zero: log + exit privacy mode + early return
- CGGetDisplayTransferByTable fails: log + exit privacy mode + early return
- ApplyBlackoutToDisplay fails: log + exit privacy mode + early return

* fix(macos): address code review feedback and improve privacy mode stability

Code Review Fixes:
- Add detailed comments for potential deadlock scenarios in dispatch_sync
  with g_privacyModeMutex (SetupEventTapOnMainThread/TeardownEventTapOnMainThread)
- Use async dispatch for privacy mode shutdown from DisplayReconfigurationCallback
  to avoid unregistering callback from within itself
- Extract RestoreAllGammas() helper function to reduce code duplication
- Fix Drop implementation in macos.rs to call self.clear() for consistency
- Add comment explaining why _state parameter is ignored on macOS
- Define DISPLAY_RECONFIG_MONITOR_DURATION_MS and GAMMA_CHECK_INTERVAL_MS constants
- Add gamma restoration when UUID retrieval fails during privacy mode activation

Privacy Mode Stability Improvements (Continuous Resolution Changes):
- Implement continuous gamma value monitoring with timer polling after display
  reconfiguration to handle rapid successive resolution changes
- Monitor gamma values every 200ms for 5 seconds after each resolution change
- Automatically reapply blackout if system (ColorSync) restores gamma
- Add IsDisplayBlackedOut() to detect if display gamma has been restored
- Use timestamp-based debouncing: monitoring period automatically extends
  when new reconfig events occur during active monitoring
- Ensure blackout remains effective even under continuous resolution changes
  where macOS may asynchronously restore gamma values multiple times

This ensures privacy mode remains stable and effective when users rapidly
change display resolution multiple times in succession.

---------

Co-authored-by: libin <libin.chat@outlook.com>
2026-01-27 16:38:37 +08:00
Alex Rijckaert
f05f2178e5 Update Dutch translations (#14136) 2026-01-26 14:16:21 +08:00
Hugo Breda
226d7417b2 PT-BR language update (#14135)
* PT-BR language update

@rustdesk
Please merge. Thanks

* Update ptbr.rs

* Update ptbr.rs

Please submit, i will get back soon and finish all other stuff.
2026-01-26 14:15:58 +08:00
bovirus
b0c8e65c6e Italian language update (#14129) 2026-01-26 14:15:45 +08:00
RustDesk
4ae577c3c4 Revert "Updated tr.rs (#14115)" (#14158)
This reverts commit 204e81a700.
2026-01-26 14:14:35 +08:00
bilimiyorum
204e81a700 Updated tr.rs (#14115)
Translation improvements have been made.
2026-01-26 14:11:58 +08:00
hatterp
1f35830570 Update pl.rs (#14112)
updated PL translation
2026-01-26 14:11:41 +08:00
VenusGirl❤
6b334f2977 Update ko.rs (#14110)
Update Korean
2026-01-25 16:37:34 +08:00
Mr-Update
0dc3c12aa5 Update de.rs (#14108) 2026-01-24 12:50:18 +08:00
RustDesk
ceffcce20e fix hide-tray=Y causing The application “RustDesk.app” is not open anymore. https://github.com/rustdesk/rustdesk/discussions/10210 (#14127) 2026-01-23 19:09:33 +08:00
91 changed files with 2960 additions and 400 deletions

View File

@@ -299,7 +299,7 @@ Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.

View File

@@ -55,6 +55,7 @@
],
"finish-args": [
"--share=ipc",
"--socket=wayland",
"--socket=x11",
"--share=network",
"--filesystem=home",

View File

@@ -1124,18 +1124,23 @@ class CustomAlertDialog extends StatelessWidget {
Widget createDialogContent(String text) {
final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)');
bool hasLink = linkRegExp.hasMatch(text);
// Early return: no link, use default theme color
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
final List<TextSpan> spans = [];
int start = 0;
bool hasLink = false;
linkRegExp.allMatches(text).forEach((match) {
hasLink = true;
if (match.start > start) {
spans.add(TextSpan(text: text.substring(start, match.start)));
}
spans.add(TextSpan(
text: match.group(0) ?? '',
style: TextStyle(
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
@@ -1153,13 +1158,9 @@ Widget createDialogContent(String text) {
spans.add(TextSpan(text: text.substring(start)));
}
if (!hasLink) {
return SelectableText(text, style: const TextStyle(fontSize: 15));
}
return SelectableText.rich(
TextSpan(
style: TextStyle(color: Colors.black, fontSize: 15),
style: const TextStyle(fontSize: 15),
children: spans,
),
);

View File

@@ -25,6 +25,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified }
// Is all the fields of the user needed?
class UserPayload {
String name = '';
String displayName = '';
String email = '';
String note = '';
String? verifier;
@@ -33,6 +34,7 @@ class UserPayload {
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
displayName = json['display_name'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
verifier = json['verifier'],
@@ -46,6 +48,7 @@ class UserPayload {
Map<String, dynamic> toJson() {
final Map<String, dynamic> map = {
'name': name,
'display_name': displayName,
'status': status == UserStatus.kDisabled
? 0
: status == UserStatus.kUnverified
@@ -58,9 +61,14 @@ class UserPayload {
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
'display_name': displayName,
};
return map;
}
String get displayNameOrName {
return displayName.trim().isEmpty ? name : displayName;
}
}
class PeerPayload {

View File

@@ -25,6 +25,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragStartCallback? onOneFingerPanStart;
GestureDragUpdateCallback? onOneFingerPanUpdate;
GestureDragEndCallback? onOneFingerPanEnd;
GestureDragCancelCallback? onOneFingerPanCancel;
// twoFingerScale : scale + pan event
GestureScaleStartCallback? onTwoFingerScaleStart;
@@ -169,6 +170,27 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
DragEndDetails(velocity: d.velocity);
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
switch (_currentState) {
case GestureState.oneFingerPan:
if (onOneFingerPanCancel != null) {
onOneFingerPanCancel!();
}
break;
case GestureState.twoFingerScale:
// Reset scale state if needed, currently self-contained
break;
case GestureState.threeFingerVerticalDrag:
// Reset drag state if needed, currently self-contained
break;
default:
break;
}
_currentState = GestureState.none;
}
}
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
@@ -717,6 +739,7 @@ RawGestureDetector getMixinGestureDetector({
GestureDragStartCallback? onOneFingerPanStart,
GestureDragUpdateCallback? onOneFingerPanUpdate,
GestureDragEndCallback? onOneFingerPanEnd,
GestureDragCancelCallback? onOneFingerPanCancel,
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
GestureScaleEndCallback? onTwoFingerScaleEnd,
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
@@ -765,6 +788,7 @@ RawGestureDetector getMixinGestureDetector({
..onOneFingerPanStart = onOneFingerPanStart
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;

View File

@@ -103,7 +103,7 @@ class ButtonOP extends StatelessWidget {
child: FittedBox(
fit: BoxFit.scaleDown,
child: Center(
child: Text('${translate("Continue with")} $opLabel')),
child: Text(translate("Continue with {$opLabel}"))),
),
),
],

View File

@@ -158,9 +158,9 @@ class _MyGroupState extends State<MyGroup> {
return Obx(() {
final userItems = gFFI.groupModel.users.where((p0) {
if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchAccessibleItemNameText.value.toLowerCase());
final search = searchAccessibleItemNameText.value.toLowerCase();
return p0.name.toLowerCase().contains(search) ||
p0.displayNameOrName.toLowerCase().contains(search);
}
return true;
}).toList();
@@ -187,6 +187,7 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserItem(UserPayload user) {
final username = user.name;
final displayName = user.displayNameOrName;
return InkWell(onTap: () {
isSelectedDeviceGroup.value = false;
if (selectedAccessibleItemName.value != username) {
@@ -229,7 +230,7 @@ class _MyGroupState extends State<MyGroup> {
),
),
).marginOnly(right: 4),
if (isMe) Flexible(child: Text(username)),
if (isMe) Flexible(child: Text(displayName)),
if (isMe)
Flexible(
child: Container(
@@ -246,7 +247,7 @@ class _MyGroupState extends State<MyGroup> {
),
),
),
if (!isMe) Expanded(child: Text(username)),
if (!isMe) Expanded(child: Text(displayName)),
],
).paddingSymmetric(vertical: 4),
),

View File

@@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState
// For mouse mode, we need to block the events when the cursor is in a blocked area.
// So we need to cache the last tap down position.
Offset? _lastTapDownPositionForMouseMode;
// Cache global position for onTap (which lacks position info).
Offset? _lastTapDownGlobalPosition;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
@@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState
onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
_lastTapDownGlobalPosition = d.globalPosition;
if (isNotTouchBasedDevice()) {
return;
}
@@ -154,11 +157,16 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
return;
}
if (handleTouch) {
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
if (isMoved) {
if (lastTapDownDetails != null) {
// If pan already handled 'down', don't send it again.
if (lastTapDownDetails != null && !_touchModePanStarted) {
await inputModel.tapDown(MouseButtons.left);
}
await inputModel.tapUp(MouseButtons.left);
@@ -170,6 +178,11 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
final lastPos = _lastTapDownGlobalPosition;
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
return;
}
if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.
@@ -424,6 +437,14 @@ class _RawTouchGestureDetectorRegionState
}
}
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
// or rejected by the gesture arena. Without this, the flag can remain
// stuck in the "started" state and cause issues such as the Magic Mouse
// double-click problem on iPad with magic mouse.
onOneFingerPanCancel() {
_touchModePanStarted = false;
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
@@ -557,6 +578,7 @@ class _RawTouchGestureDetectorRegionState
instance
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleStart = onTwoFingerScaleStart
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd

View File

@@ -2016,7 +2016,9 @@ class _AccountState extends State<_Account> {
Widget accountAction() {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
gFFI.userModel.userName.value.isEmpty
? 'Login'
: 'Logout (${gFFI.userModel.accountLabelWithHandle})',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
@@ -2037,6 +2039,9 @@ class _AccountState extends State<_Account> {
offstage: gFFI.userModel.userName.value.isEmpty,
child: Column(
children: [
if (gFFI.userModel.displayName.value.trim().isNotEmpty &&
gFFI.userModel.displayName.value != gFFI.userModel.userName.value)
text('Display Name', gFFI.userModel.displayName.value),
text('Username', gFFI.userModel.userName.value),
// text('Group', gFFI.groupModel.groupName.value),
],
@@ -2130,7 +2135,9 @@ class _PluginState extends State<_Plugin> {
Widget accountAction() {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
gFFI.userModel.userName.value.isEmpty
? 'Login'
: 'Logout (${gFFI.userModel.accountLabelWithHandle})',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
@@ -2538,6 +2545,49 @@ class WaylandCard extends StatefulWidget {
class _WaylandCardState extends State<WaylandCard> {
final restoreTokenKey = 'wayland-restore-token';
static const _kClearShortcutsInhibitorEventKey =
'clear-gnome-shortcuts-inhibitor-permission-res';
final _clearShortcutsInhibitorFailedMsg = ''.obs;
// Don't show the shortcuts permission reset button for now.
// Users can change it manually:
// "Settings" -> "Apps" -> "RustDesk" -> "Permissions" -> "Inhibit Shortcuts".
// For resetting(clearing) the permission from the portal permission store, you can
// use (replace <desktop-id> with the RustDesk desktop file ID):
// busctl --user call org.freedesktop.impl.portal.PermissionStore \
// /org/freedesktop/impl/portal/PermissionStore org.freedesktop.impl.portal.PermissionStore \
// DeletePermission sss "gnome" "shortcuts-inhibitor" "<desktop-id>"
// On a native install this is typically "rustdesk.desktop"; on Flatpak it is usually
// the exported desktop ID derived from the Flatpak app-id (e.g. "com.rustdesk.RustDesk.desktop").
//
// We may add it back in the future if needed.
final showResetInhibitorPermission = false;
@override
void initState() {
super.initState();
if (showResetInhibitorPermission) {
platformFFI.registerEventHandler(
_kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey,
(evt) async {
if (!mounted) return;
if (evt['success'] == true) {
setState(() {});
} else {
_clearShortcutsInhibitorFailedMsg.value =
evt['msg'] as String? ?? 'Unknown error';
}
});
}
}
@override
void dispose() {
if (showResetInhibitorPermission) {
platformFFI.unregisterEventHandler(
_kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey);
}
super.dispose();
}
@override
Widget build(BuildContext context) {
@@ -2545,9 +2595,16 @@ class _WaylandCardState extends State<WaylandCard> {
future: bind.mainHandleWaylandScreencastRestoreToken(
key: restoreTokenKey, value: "get"),
hasData: (restoreToken) {
final hasShortcutsPermission = showResetInhibitorPermission &&
bind.mainGetCommonSync(
key: "has-gnome-shortcuts-inhibitor-permission") ==
"true";
final children = [
if (restoreToken.isNotEmpty)
_buildClearScreenSelection(context, restoreToken),
if (hasShortcutsPermission)
_buildClearShortcutsInhibitorPermission(context),
];
return Offstage(
offstage: children.isEmpty,
@@ -2592,6 +2649,50 @@ class _WaylandCardState extends State<WaylandCard> {
),
);
}
Widget _buildClearShortcutsInhibitorPermission(BuildContext context) {
onConfirm() {
_clearShortcutsInhibitorFailedMsg.value = '';
bind.mainSetCommon(
key: "clear-gnome-shortcuts-inhibitor-permission", value: "");
gFFI.dialogManager.dismissAll();
}
showConfirmMsgBox() => msgBoxCommon(
gFFI.dialogManager,
'Confirmation',
Text(
translate('confirm-clear-shortcuts-inhibitor-permission-tip'),
),
[
dialogButton('OK', onPressed: onConfirm),
dialogButton('Cancel',
onPressed: () => gFFI.dialogManager.dismissAll())
]);
return Column(children: [
Obx(
() => _clearShortcutsInhibitorFailedMsg.value.isEmpty
? Offstage()
: Align(
alignment: Alignment.topLeft,
child: Text(_clearShortcutsInhibitorFailedMsg.value,
style: DefaultTextStyle.of(context)
.style
.copyWith(color: Colors.red))
.marginOnly(bottom: 10.0)),
),
_Button(
'Reset keyboard shortcuts permission',
showConfirmMsgBox,
tip: 'clear-shortcuts-inhibitor-permission-tip',
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Theme.of(context).colorScheme.error.withOpacity(0.75)),
),
),
]);
}
}
// ignore: non_constant_identifier_names

View File

@@ -1861,8 +1861,18 @@ class _KeyboardMenu extends StatelessWidget {
continue;
}
if (pi.isWayland && mode.key != kKeyMapMode) {
continue;
if (pi.isWayland) {
// Legacy mode is hidden on desktop control side because dead keys
// don't work properly on Wayland. When the control side is mobile,
// Legacy mode is used automatically (mobile always sends Legacy events).
if (mode.key == kKeyLegacyMode) {
continue;
}
// Translate mode requires server >= 1.4.6.
if (mode.key == kKeyTranslateMode &&
versionCmp(pi.version, '1.4.6') < 0) {
continue;
}
}
var text = translate(mode.menu);

View File

@@ -68,6 +68,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
double _viewInsetsBottom = 0;
final _uniqueKey = UniqueKey();
Timer? _timerDidChangeMetrics;
Timer? _iosKeyboardWorkaroundTimer;
final _blockableOverlayState = BlockableOverlayState();
@@ -140,6 +141,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
await gFFI.close();
_timer?.cancel();
_timerDidChangeMetrics?.cancel();
_iosKeyboardWorkaroundTimer?.cancel();
gFFI.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
@@ -206,7 +208,24 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.ffiModel.pi.version.isNotEmpty) {
gFFI.invokeMethod("enable_soft_keyboard", false);
}
// Workaround for iOS: physical keyboard input fails after virtual keyboard is hidden
// https://github.com/flutter/flutter/issues/39900
// https://github.com/rustdesk/rustdesk/discussions/11843#discussioncomment-13499698 - Virtual keyboard issue
if (isIOS) {
_iosKeyboardWorkaroundTimer?.cancel();
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 100), () {
if (!mounted) return;
_physicalFocusNode.unfocus();
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 50), () {
if (!mounted) return;
_physicalFocusNode.requestFocus();
});
});
}
} else {
_iosKeyboardWorkaroundTimer?.cancel();
_iosKeyboardWorkaroundTimer = null;
_timer?.cancel();
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,

View File

@@ -688,7 +688,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
SettingsTile(
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
? translate('Login')
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
: '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')),
leading: Icon(Icons.person),
onPressed: (context) {
if (gFFI.userModel.userName.value.isEmpty) {

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
@@ -41,6 +42,9 @@ class _TerminalPageState extends State<TerminalPage>
final GlobalKey _keyboardKey = GlobalKey();
double _keyboardHeight = 0;
late bool _showTerminalExtraKeys;
// For iOS edge swipe gesture
double _swipeStartX = 0;
double _swipeCurrentX = 0;
// For web only.
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
@@ -147,7 +151,7 @@ class _TerminalPageState extends State<TerminalPage>
}
Widget buildBody() {
return Scaffold(
final scaffold = Scaffold(
resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Stack(
@@ -164,6 +168,13 @@ class _TerminalPageState extends State<TerminalPage>
autofocus: true,
textStyle: _getTerminalStyle(),
backgroundOpacity: 0.7,
// The following comment is from xterm.dart source code:
// Workaround to detect delete key for platforms and IMEs that do not
// emit a hardware delete event. Preferred on mobile platforms. [false] by
// default.
//
// Android works fine without this workaround.
deleteDetection: isIOS,
padding: _calculatePadding(heightPx),
onSecondaryTapDown: (details, offset) async {
final selection = _terminalModel.terminalController.selection;
@@ -185,9 +196,108 @@ class _TerminalPageState extends State<TerminalPage>
),
),
if (_showTerminalExtraKeys) _buildFloatingKeyboard(),
// iOS-style circular close button in top-right corner
if (isIOS) _buildCloseButton(),
],
),
);
// Add iOS edge swipe gesture to exit (similar to Android back button)
if (isIOS) {
return LayoutBuilder(
builder: (context, constraints) {
final screenWidth = constraints.maxWidth;
// Base thresholds on screen width but clamp to reasonable logical pixel ranges
// Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels
final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0);
// Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels
final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0);
return RawGestureDetector(
behavior: HitTestBehavior.translucent,
gestures: <Type, GestureRecognizerFactory>{
HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(
debugOwner: this,
// Only respond to touch input, exclude mouse/trackpad
supportedDevices: kTouchBasedDeviceKinds,
),
(HorizontalDragGestureRecognizer instance) {
instance
// Capture initial touch-down position (before touch slop)
..onDown = (details) {
_swipeStartX = details.localPosition.dx;
_swipeCurrentX = details.localPosition.dx;
}
..onUpdate = (details) {
_swipeCurrentX = details.localPosition.dx;
}
..onEnd = (details) {
// Check if swipe started from left edge and moved right
if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) {
clientClose(sessionId, _ffi);
}
_swipeStartX = 0;
_swipeCurrentX = 0;
}
..onCancel = () {
_swipeStartX = 0;
_swipeCurrentX = 0;
};
},
),
},
child: scaffold,
);
},
);
}
return scaffold;
}
Widget _buildCloseButton() {
return Positioned(
top: 0,
right: 0,
child: SafeArea(
minimum: const EdgeInsets.only(
top: 16, // iOS standard margin
right: 16, // iOS standard margin
),
child: Semantics(
button: true,
label: translate('Close'),
child: Container(
width: 44, // iOS standard tap target size
height: 44,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5), // Half transparency
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
clientClose(sessionId, _ffi);
},
child: Tooltip(
message: translate('Close'),
child: const Icon(
Icons.chevron_left, // iOS-style back arrow
color: Colors.white,
size: 28,
),
),
),
),
),
),
),
);
}
Widget _buildFloatingKeyboard() {

View File

@@ -59,7 +59,8 @@ class CanvasCoords {
model.scale = json['scale'];
model.scrollX = json['scrollX'];
model.scrollY = json['scrollY'];
model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
model.scrollStyle =
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
model.size = Size(json['size']['w'], json['size']['h']);
return model;
}
@@ -418,6 +419,74 @@ class InputModel {
});
}
// https://github.com/flutter/flutter/issues/157241
// Infer CapsLock state from the character output.
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
// incorrect CapsLock state on iOS.
bool _getIosCapsFromCharacter(KeyEvent e) {
if (!isIOS) return false;
final ch = e.character;
return _getIosCapsFromCharacterImpl(
ch, HardwareKeyboard.instance.isShiftPressed);
}
// RawKeyEvent version of _getIosCapsFromCharacter.
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
if (!isIOS) return false;
final ch = e.character;
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
}
// Shared implementation for inferring CapsLock state from character.
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
//
// Limitations:
// 1. This inference assumes the client and server use the same keyboard layout.
// If layouts differ (e.g., client uses EN, server uses DE), the character output
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
// layout, making it impossible to correctly infer CapsLock state from the
// character alone.
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
// produces lowercase). This method cannot handle that case correctly.
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
if (ch == null || ch.length != 1) return false;
// Use Dart's built-in Unicode-aware case detection
final upper = ch.toUpperCase();
final lower = ch.toLowerCase();
final isUpper = upper == ch && lower != ch;
final isLower = lower == ch && upper != ch;
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
if (!isUpper && !isLower) return false;
return isUpper != shiftPressed;
}
int _buildLockModes(bool iosCapsLock) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (isIOS) {
if (iosCapsLock) {
lockModes |= (1 << capslock);
}
// Ignore "NumLock/ScrollLock" on iOS for now.
} else {
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
}
return lockModes;
}
// This function must be called after the peer info is received.
// Because `sessionGetKeyboardMode` relies on the peer version.
updateKeyboardMode() async {
@@ -550,6 +619,11 @@ class InputModel {
return KeyEventResult.handled;
}
bool iosCapsLock = false;
if (isIOS && e is RawKeyDownEvent) {
iosCapsLock = _getIosCapsFromRawCharacter(e);
}
final key = e.logicalKey;
if (e is RawKeyDownEvent) {
if (!e.repeat) {
@@ -586,7 +660,7 @@ class InputModel {
// * Currently mobile does not enable map mode
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
mapKeyboardModeRaw(e);
mapKeyboardModeRaw(e, iosCapsLock);
} else {
legacyKeyboardModeRaw(e);
}
@@ -622,6 +696,11 @@ class InputModel {
return KeyEventResult.handled;
}
bool iosCapsLock = false;
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
iosCapsLock = _getIosCapsFromCharacter(e);
}
if (e is KeyUpEvent) {
handleKeyUpEventModifiers(e);
} else if (e is KeyDownEvent) {
@@ -667,7 +746,8 @@ class InputModel {
e.character ?? '',
e.physicalKey.usbHidUsage & 0xFFFF,
// Show repeat event be converted to "release+press" events?
e is KeyDownEvent || e is KeyRepeatEvent);
e is KeyDownEvent || e is KeyRepeatEvent,
iosCapsLock);
} else {
legacyKeyboardMode(e);
}
@@ -676,23 +756,9 @@ class InputModel {
}
/// Send Key Event
void newKeyboardMode(String character, int usbHid, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
void newKeyboardMode(
String character, int usbHid, bool down, bool iosCapsLock) {
final lockModes = _buildLockModes(iosCapsLock);
bind.sessionHandleFlutterKeyEvent(
sessionId: sessionId,
character: character,
@@ -701,7 +767,7 @@ class InputModel {
downOrUp: down);
}
void mapKeyboardModeRaw(RawKeyEvent e) {
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
int positionCode = -1;
int platformCode = -1;
bool down;
@@ -732,27 +798,14 @@ class InputModel {
} else {
down = false;
}
inputRawKey(e.character ?? '', platformCode, positionCode, down);
inputRawKey(
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
}
/// Send raw Key Event
void inputRawKey(String name, int platformCode, int positionCode, bool down) {
const capslock = 1;
const numlock = 2;
const scrolllock = 3;
int lockModes = 0;
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.capsLock)) {
lockModes |= (1 << capslock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.numLock)) {
lockModes |= (1 << numlock);
}
if (HardwareKeyboard.instance.lockModesEnabled
.contains(KeyboardLockMode.scrollLock)) {
lockModes |= (1 << scrolllock);
}
void inputRawKey(String name, int platformCode, int positionCode, bool down,
bool iosCapsLock) {
final lockModes = _buildLockModes(iosCapsLock);
bind.sessionHandleFlutterRawKeyEvent(
sessionId: sessionId,
name: name,
@@ -826,6 +879,9 @@ class InputModel {
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
bool hasStaleButtonsOnMouseUp =
type == _kMouseEventUp && evt.buttons == _lastButtons;
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
if (type == _kMouseEventMove) {
@@ -850,7 +906,7 @@ class InputModel {
buttons = evt.buttons;
}
}
_lastButtons = evt.buttons;
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
out['buttons'] = buttons;
out['type'] = type;
@@ -1048,6 +1104,14 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
// May fix https://github.com/rustdesk/rustdesk/issues/13009
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
// Ignore this event to prevent cursor jumping.
debugPrint('Ignored synthesized hover at (0,0) on iOS');
return;
}
// Only update pointer region when relative mouse mode is enabled.
// This avoids unnecessary tracking when not in relative mode.
if (_relativeMouse.enabled.value) {
@@ -1210,6 +1274,28 @@ class InputModel {
_trackpadLastDelta = Offset.zero;
}
// iOS Magic Mouse duplicate event detection.
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
// for the same click in certain areas (like top-left corner).
int _lastMouseDownTimeMs = 0;
ui.Offset _lastMouseDownPos = ui.Offset.zero;
/// Check if a touch tap event should be ignored because it's a duplicate
/// of a recent mouse event (iOS Magic Mouse issue).
bool shouldIgnoreTouchTap(ui.Offset pos) {
if (!isIOS) return false;
final nowMs = DateTime.now().millisecondsSinceEpoch;
final dt = nowMs - _lastMouseDownTimeMs;
final distance = (_lastMouseDownPos - pos).distance;
// If touch tap is within 2000ms and 80px of the last mouse down,
// it's likely a duplicate event from the same Magic Mouse click.
if (dt >= 0 && dt < 2000 && distance < 80.0) {
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
return true;
}
return false;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1219,6 +1305,13 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
if (_relativeMouse.enabled.value) {
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
}
@@ -1760,9 +1853,9 @@ class InputModel {
// Simulate a key press event.
// `usbHidUsage` is the USB HID usage code of the key.
Future<void> tapHidKey(int usbHidUsage) async {
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
await Future.delayed(Duration(milliseconds: 100));
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false);
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
}
Future<void> onMobileVolumeUp() async =>

View File

@@ -2215,10 +2215,32 @@ class CanvasModel with ChangeNotifier {
double w = size.width - leftToEdge - rightToEdge;
double h = size.height - topToEdge - bottomToEdge;
if (isMobile) {
// Account for horizontal safe area insets on both orientations.
w = w - mediaData.padding.left - mediaData.padding.right;
// Vertically, subtract the bottom keyboard inset (viewInsets.bottom) and any
// bottom overlay (e.g. key-help tools) so the canvas is not covered.
h = h -
mediaData.viewInsets.bottom -
(parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ??
0);
// Orientation-specific handling:
// - Portrait: additionally subtract top padding (e.g. status bar / notch)
// - Landscape: does not subtract mediaData.padding.top/bottom (home indicator auto-hides)
final isPortrait = size.height > size.width;
if (isPortrait) {
// In portrait mode, subtract the top safe-area padding (e.g. status bar / notch)
// so the remote image is not truncated, while keeping the bottom inset to avoid
// introducing unnecessary blank space around the canvas.
//
// iOS -> Android, portrait, adjust mode:
// h = h (no padding subtracted): top and bottom are truncated
// https://github.com/user-attachments/assets/30ed4559-c27e-432b-847f-8fec23c9f998
// h = h - top - bottom: extra blank spaces appear
// https://github.com/user-attachments/assets/12a98817-3b4e-43aa-be0f-4b03cf364b7e
// h = h - top (current): works fine
// https://github.com/user-attachments/assets/95f047f2-7f47-4a36-8113-5023989a0c81
h = h - mediaData.padding.top;
}
}
return Size(w < 0 ? 0 : w, h < 0 ? 0 : h);
}

View File

@@ -16,9 +16,23 @@ bool refreshingUser = false;
class UserModel {
final RxString userName = ''.obs;
final RxString displayName = ''.obs;
final RxBool isAdmin = false.obs;
final RxString networkError = ''.obs;
bool get isLogin => userName.isNotEmpty;
String get displayNameOrUserName =>
displayName.value.trim().isEmpty ? userName.value : displayName.value;
String get accountLabelWithHandle {
final username = userName.value.trim();
if (username.isEmpty) {
return '';
}
final preferred = displayName.value.trim();
if (preferred.isEmpty || preferred == username) {
return username;
}
return '$preferred (@$username)';
}
WeakReference<FFI> parent;
UserModel(this.parent) {
@@ -98,7 +112,8 @@ class UserModel {
_updateLocalUserInfo() {
final userInfo = getLocalUserInfo();
if (userInfo != null) {
userName.value = userInfo['name'];
userName.value = (userInfo['name'] ?? '').toString();
displayName.value = (userInfo['display_name'] ?? '').toString();
}
}
@@ -110,10 +125,12 @@ class UserModel {
await gFFI.groupModel.reset();
}
userName.value = '';
displayName.value = '';
}
_parseAndUpdateUser(UserPayload user) {
userName.value = user.name;
displayName.value = user.displayName;
isAdmin.value = user.isAdmin;
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
if (isWeb) {

View File

@@ -1,6 +1,6 @@
# Project-level configuration.
cmake_minimum_required(VERSION 3.10)
project(runner LANGUAGES CXX)
project(runner LANGUAGES C CXX)
# The name of the executable created for the application. Change this to change
# the on-disk name of your application.
@@ -54,6 +54,55 @@ add_subdirectory(${FLUTTER_MANAGED_DIR})
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
# Wayland protocol for keyboard shortcuts inhibit
pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client)
pkg_check_modules(WAYLAND_PROTOCOLS_PKG QUIET wayland-protocols)
pkg_check_modules(WAYLAND_SCANNER_PKG QUIET wayland-scanner)
if(WAYLAND_PROTOCOLS_PKG_FOUND)
pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir)
endif()
if(WAYLAND_SCANNER_PKG_FOUND)
pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner)
endif()
if(WAYLAND_CLIENT_FOUND AND WAYLAND_PROTOCOLS_DIR AND WAYLAND_SCANNER)
set(KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL
"${WAYLAND_PROTOCOLS_DIR}/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml")
if(EXISTS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL})
set(WAYLAND_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/wayland-protocols")
file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR})
# Generate client header
add_custom_command(
OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
COMMAND ${WAYLAND_SCANNER} client-header
${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
VERBATIM
)
# Generate protocol code
add_custom_command(
OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
COMMAND ${WAYLAND_SCANNER} private-code
${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}
VERBATIM
)
set(WAYLAND_PROTOCOL_SOURCES
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
"${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c"
)
set(HAS_KEYBOARD_SHORTCUTS_INHIBIT TRUE)
endif()
endif()
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
# Define the application target. To change its name, change BINARY_NAME above,
@@ -63,9 +112,11 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
add_executable(${BINARY_NAME}
"main.cc"
"my_application.cc"
"wayland_shortcuts_inhibit.cc"
"bump_mouse.cc"
"bump_mouse_x11.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
${WAYLAND_PROTOCOL_SOURCES}
)
# Apply the standard set of build settings. This can be removed for applications
@@ -78,6 +129,13 @@ target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS})
# target_link_libraries(${BINARY_NAME} PRIVATE librustdesk)
# Wayland support for keyboard shortcuts inhibit
if(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
target_compile_definitions(${BINARY_NAME} PRIVATE HAS_KEYBOARD_SHORTCUTS_INHIBIT)
target_include_directories(${BINARY_NAME} PRIVATE ${WAYLAND_GENERATED_DIR})
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::WAYLAND_CLIENT)
endif()
# Run the Flutter tool portions of the build. This must not be removed.
add_dependencies(${BINARY_NAME} flutter_assemble)

View File

@@ -6,6 +6,11 @@
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
#include "wayland_shortcuts_inhibit.h"
#endif
#include <desktop_multi_window/desktop_multi_window_plugin.h>
#include "flutter/generated_plugin_registrant.h"
@@ -91,6 +96,13 @@ static void my_application_activate(GApplication* application) {
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_show(GTK_WIDGET(view));
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
// Register callback for sub-windows created by desktop_multi_window plugin
// Only sub-windows (remote windows) need keyboard shortcuts inhibition
desktop_multi_window_plugin_set_window_created_callback(
(WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow);
#endif
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();

View File

@@ -0,0 +1,244 @@
// Wayland keyboard shortcuts inhibit implementation
// Uses the zwp_keyboard_shortcuts_inhibit_manager_v1 protocol to request
// the compositor to disable system shortcuts for specific windows.
#include "wayland_shortcuts_inhibit.h"
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
#include <cstring>
#include <gdk/gdkwayland.h>
#include <wayland-client.h>
#include "keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h"
// Data structure to hold inhibitor state for each window
typedef struct {
struct zwp_keyboard_shortcuts_inhibit_manager_v1* manager;
struct zwp_keyboard_shortcuts_inhibitor_v1* inhibitor;
} ShortcutsInhibitData;
// Cleanup function for ShortcutsInhibitData
static void shortcuts_inhibit_data_free(gpointer data) {
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(data);
if (inhibit_data->inhibitor != NULL) {
zwp_keyboard_shortcuts_inhibitor_v1_destroy(inhibit_data->inhibitor);
}
if (inhibit_data->manager != NULL) {
zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(inhibit_data->manager);
}
g_free(inhibit_data);
}
// Wayland registry handler to find the shortcuts inhibit manager
static void registry_handle_global(void* data, struct wl_registry* registry,
uint32_t name, const char* interface,
uint32_t /*version*/) {
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(data);
if (strcmp(interface,
zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) {
inhibit_data->manager =
static_cast<zwp_keyboard_shortcuts_inhibit_manager_v1*>(wl_registry_bind(
registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface,
1));
}
}
static void registry_handle_global_remove(void* /*data*/, struct wl_registry* /*registry*/,
uint32_t /*name*/) {
// Not needed for this use case
}
static const struct wl_registry_listener registry_listener = {
registry_handle_global,
registry_handle_global_remove,
};
// Inhibitor event handlers
static void inhibitor_active(void* /*data*/,
struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) {
// Inhibitor is now active, shortcuts are being captured
}
static void inhibitor_inactive(void* /*data*/,
struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) {
// Inhibitor is now inactive, shortcuts restored to compositor
}
static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener inhibitor_listener = {
inhibitor_active,
inhibitor_inactive,
};
// Forward declaration
static void uninhibit_keyboard_shortcuts(GtkWindow* window);
// Inhibit keyboard shortcuts on Wayland for a specific window
static void inhibit_keyboard_shortcuts(GtkWindow* window) {
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window));
if (!GDK_IS_WAYLAND_DISPLAY(display)) {
return;
}
// Check if already inhibited for this window
if (g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data") != NULL) {
return;
}
ShortcutsInhibitData* inhibit_data = g_new0(ShortcutsInhibitData, 1);
struct wl_display* wl_display = gdk_wayland_display_get_wl_display(display);
if (wl_display == NULL) {
shortcuts_inhibit_data_free(inhibit_data);
return;
}
struct wl_registry* registry = wl_display_get_registry(wl_display);
if (registry == NULL) {
shortcuts_inhibit_data_free(inhibit_data);
return;
}
wl_registry_add_listener(registry, &registry_listener, inhibit_data);
wl_display_roundtrip(wl_display);
if (inhibit_data->manager == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
if (gdk_window == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window);
if (surface == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
GdkSeat* gdk_seat = gdk_display_get_default_seat(display);
if (gdk_seat == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
struct wl_seat* seat = gdk_wayland_seat_get_wl_seat(gdk_seat);
if (seat == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
inhibit_data->inhibitor =
zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts(
inhibit_data->manager, surface, seat);
if (inhibit_data->inhibitor == NULL) {
wl_registry_destroy(registry);
shortcuts_inhibit_data_free(inhibit_data);
return;
}
// Add listener to monitor active/inactive state
zwp_keyboard_shortcuts_inhibitor_v1_add_listener(
inhibit_data->inhibitor, &inhibitor_listener, window);
wl_display_roundtrip(wl_display);
wl_registry_destroy(registry);
// Associate the inhibit data with the window for cleanup on destroy
g_object_set_data_full(G_OBJECT(window), "shortcuts-inhibit-data",
inhibit_data, shortcuts_inhibit_data_free);
}
// Remove keyboard shortcuts inhibitor from a window
static void uninhibit_keyboard_shortcuts(GtkWindow* window) {
ShortcutsInhibitData* inhibit_data = static_cast<ShortcutsInhibitData*>(
g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data"));
if (inhibit_data == NULL) {
return;
}
// This will trigger shortcuts_inhibit_data_free via g_object_set_data
g_object_set_data(G_OBJECT(window), "shortcuts-inhibit-data", NULL);
}
// Focus event handlers for dynamic inhibitor management
static gboolean on_window_focus_in(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) {
if (GTK_IS_WINDOW(widget)) {
inhibit_keyboard_shortcuts(GTK_WINDOW(widget));
}
return FALSE; // Continue event propagation
}
static gboolean on_window_focus_out(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) {
if (GTK_IS_WINDOW(widget)) {
uninhibit_keyboard_shortcuts(GTK_WINDOW(widget));
}
return FALSE; // Continue event propagation
}
// Key for marking window as having focus handlers connected
static const char* const kFocusHandlersConnectedKey = "shortcuts-inhibit-focus-handlers-connected";
// Key for marking window as having a pending realize handler
static const char* const kRealizeHandlerConnectedKey = "shortcuts-inhibit-realize-handler-connected";
// Callback when window is realized (mapped to screen)
// Sets up focus-based inhibitor management
static void on_window_realize(GtkWidget* widget, gpointer /*user_data*/) {
if (GTK_IS_WINDOW(widget)) {
// Check if focus handlers are already connected to avoid duplicates
if (g_object_get_data(G_OBJECT(widget), kFocusHandlersConnectedKey) != NULL) {
return;
}
// Connect focus events for dynamic inhibitor management
g_signal_connect(widget, "focus-in-event",
G_CALLBACK(on_window_focus_in), NULL);
g_signal_connect(widget, "focus-out-event",
G_CALLBACK(on_window_focus_out), NULL);
// Mark as connected to prevent duplicate connections
g_object_set_data(G_OBJECT(widget), kFocusHandlersConnectedKey, GINT_TO_POINTER(1));
// If window already has focus, create inhibitor now
if (gtk_window_has_toplevel_focus(GTK_WINDOW(widget))) {
inhibit_keyboard_shortcuts(GTK_WINDOW(widget));
}
}
}
// Public API: Initialize shortcuts inhibit for a sub-window
void wayland_shortcuts_inhibit_init_for_subwindow(void* view) {
GtkWidget* widget = GTK_WIDGET(view);
GtkWidget* toplevel = gtk_widget_get_toplevel(widget);
if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) {
// Check if already initialized to avoid duplicate realize handlers
if (g_object_get_data(G_OBJECT(toplevel), kFocusHandlersConnectedKey) != NULL ||
g_object_get_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey) != NULL) {
return;
}
if (gtk_widget_get_realized(toplevel)) {
// Window is already realized, set up focus handlers now
on_window_realize(toplevel, NULL);
} else {
// Mark realize handler as connected to prevent duplicate connections
// if called again before window is realized
g_object_set_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey, GINT_TO_POINTER(1));
// Wait for window to be realized
g_signal_connect(toplevel, "realize",
G_CALLBACK(on_window_realize), NULL);
}
}
}
#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)

View File

@@ -0,0 +1,22 @@
// Wayland keyboard shortcuts inhibit support
// This module provides functionality to inhibit system keyboard shortcuts
// on Wayland compositors, allowing remote desktop windows to capture all
// key events including Super, Alt+Tab, etc.
#ifndef WAYLAND_SHORTCUTS_INHIBIT_H_
#define WAYLAND_SHORTCUTS_INHIBIT_H_
#include <gtk/gtk.h>
#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
// Initialize shortcuts inhibit for a sub-window created by desktop_multi_window plugin.
// This sets up focus-based inhibitor management: inhibitor is created when
// the window gains focus and destroyed when it loses focus.
//
// @param view The FlView of the sub-window
void wayland_shortcuts_inhibit_init_for_subwindow(void* view);
#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT)
#endif // WAYLAND_SHORTCUTS_INHIBIT_H_

View File

@@ -10,7 +10,7 @@ TODO: Move this lib to a separate project.
## How it works
Terminalogies:
Terminologies:
- cliprdr: this module
- local: the endpoint which initiates a file copy events
@@ -50,7 +50,7 @@ sequenceDiagram
r ->> l: Format List Response (notified)
r ->> l: Format Data Request (requests file list)
activate l
note left of l: Retrive file list from system clipboard
note left of l: Retrieve file list from system clipboard
l ->> r: Format Data Response (containing file list)
deactivate l
note over r: Update system clipboard with received file list
@@ -84,10 +84,10 @@ and copy files to remote.
The protocol was originally designed as an extension of the Windows RDP,
so the specific message packages fits windows well.
When starting cliprdr, a thread is spawn to create a invisible window
When starting cliprdr, a thread is spawned to create an invisible window
and to subscribe to OLE clipboard events.
The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was
set to handle a variaty of events.
set to handle a variety of events.
Detailed implementation is shown in pictures above.
@@ -108,18 +108,18 @@ after filtering out those pointing to our FUSE directory or duplicated,
send format list directly to remote.
The cliprdr server also uses clipboard client for setting clipboard,
or retrive paths from system.
or retrieve paths from system.
#### Local File List
The local file list is a temperary list of file metadata.
The local file list is a temporary list of file metadata.
When receiving file contents PDU from peer, the server picks
out the file requested and open it for reading if necessary.
Also when receiving Format Data Request PDU from remote asking for file list,
the local file list should be rebuilt from file list retrieved from Clipboard Client.
Some caching and preloading could done on it since applications are likely to read
Some caching and preloading could be done on it since applications are likely to read
on the list sequentially.
#### FUSE server

View File

@@ -261,6 +261,8 @@ impl KeyboardControllable for Enigo {
} else {
if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_sequence(sequence)
} else {
log::warn!("Enigo::key_sequence: no custom_keyboard set for Wayland!");
}
}
}
@@ -277,6 +279,7 @@ impl KeyboardControllable for Enigo {
if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_down(key)
} else {
log::warn!("Enigo::key_down: no custom_keyboard set for Wayland!");
Ok(())
}
}
@@ -290,13 +293,24 @@ impl KeyboardControllable for Enigo {
} else {
if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_up(key)
} else {
log::warn!("Enigo::key_up: no custom_keyboard set for Wayland!");
}
}
}
fn key_click(&mut self, key: Key) {
if self.tfc_key_click(key).is_err() {
self.key_down(key).ok();
self.key_up(key);
if self.is_x11 {
// X11: try tfc first, then fallback to key_down/key_up
if self.tfc_key_click(key).is_err() {
self.key_down(key).ok();
self.key_up(key);
}
} else {
if let Some(keyboard) = &mut self.custom_keyboard {
keyboard.key_click(key);
} else {
log::warn!("Enigo::key_click: no custom_keyboard set for Wayland!");
}
}
}
}

View File

@@ -98,7 +98,7 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error>
let mut e: *mut xcb_generic_error_t = std::ptr::null_mut();
let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _);
if reply.is_null() {
// TODO: Should seperate SHM disabled from SHM not supported?
// TODO: Should separate SHM disabled from SHM not supported?
return Err(Error::UnsupportedExtension);
} else {
// https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229

View File

@@ -336,7 +336,9 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
)
estimated_size = get_folder_size(dist_dir)
# EstimatedSize in uninstall registry must be in KB.
estimated_size_bytes = get_folder_size(dist_dir)
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
)

View File

@@ -2630,10 +2630,12 @@ impl LoginConfigHandler {
display_name =
serde_json::from_str::<serde_json::Value>(&LocalConfig::get_option("user_info"))
.map(|x| {
x.get("name")
.map(|x| x.as_str().unwrap_or_default())
x.get("display_name")
.and_then(|x| x.as_str())
.filter(|x| !x.is_empty())
.or_else(|| x.get("name").and_then(|x| x.as_str()))
.map(|x| x.to_owned())
.unwrap_or_default()
.to_owned()
})
.unwrap_or_default();
}

View File

@@ -197,7 +197,7 @@ pub fn check_clipboard_cm() -> ResultType<MultiClipboards> {
#[cfg(not(target_os = "android"))]
fn update_clipboard_(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
let to_update_data = proto::from_multi_clipbards(multi_clipboards);
let to_update_data = proto::from_multi_clipboards(multi_clipboards);
if to_update_data.is_empty() {
return;
}
@@ -432,7 +432,7 @@ impl ClipboardContext {
#[cfg(target_os = "macos")]
let is_kde_x11 = false;
let clear_holder_text = if is_kde_x11 {
"RustDesk placeholder to clear the file clipbard"
"RustDesk placeholder to clear the file clipboard"
} else {
""
}
@@ -672,7 +672,7 @@ mod proto {
}
#[cfg(not(target_os = "android"))]
pub fn from_multi_clipbards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
pub fn from_multi_clipboards(multi_clipboards: Vec<Clipboard>) -> Vec<ClipboardData> {
multi_clipboards
.into_iter()
.filter_map(from_clipboard)
@@ -814,7 +814,7 @@ pub mod clipboard_listener {
subscribers: listener_lock.subscribers.clone(),
};
let (tx_start_res, rx_start_res) = channel();
let h = start_clipbard_master_thread(handler, tx_start_res);
let h = start_clipboard_master_thread(handler, tx_start_res);
let shutdown = match rx_start_res.recv() {
Ok((Some(s), _)) => s,
Ok((None, err)) => {
@@ -854,7 +854,7 @@ pub mod clipboard_listener {
log::info!("Clipboard listener unsubscribed: {}", name);
}
fn start_clipbard_master_thread(
fn start_clipboard_master_thread(
handler: impl ClipboardHandler + Send + 'static,
tx_start_res: Sender<(Option<Shutdown>, String)>,
) -> JoinHandle<()> {

View File

@@ -1072,10 +1072,6 @@ fn get_api_server_(api: String, custom: String) -> String {
if !api.is_empty() {
return api.to_owned();
}
let api = option_env!("API_SERVER").unwrap_or_default();
if !api.is_empty() {
return api.into();
}
let s0 = get_custom_rendezvous_server(custom);
if !s0.is_empty() {
let s = crate::increase_port(&s0, -2);
@@ -1737,8 +1733,7 @@ pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> (Bytes, Bytes, secretbo
#[inline]
pub fn using_public_server() -> bool {
option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty()
&& crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty()
crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty()
}
pub struct ThrottledInterval {

View File

@@ -2759,6 +2759,11 @@ pub fn main_get_common(key: String) -> String {
None => "",
}
.to_string();
} else if key == "has-gnome-shortcuts-inhibitor-permission" {
#[cfg(target_os = "linux")]
return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string();
#[cfg(not(target_os = "linux"))]
return false.to_string();
} else {
if key.starts_with("download-data-") {
let id = key.replace("download-data-", "");
@@ -2920,6 +2925,29 @@ pub fn main_set_common(_key: String, _value: String) {
} else if _key == "cancel-downloader" {
crate::hbbs_http::downloader::cancel(&_value);
}
#[cfg(target_os = "linux")]
if _key == "clear-gnome-shortcuts-inhibitor-permission" {
std::thread::spawn(move || {
let (success, msg) =
match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() {
Ok(_) => (true, "".to_owned()),
Err(e) => (false, e.to_string()),
};
let data = HashMap::from([
(
"name",
serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"),
),
("success", serde_json::json!(success)),
("msg", serde_json::json!(msg)),
]);
let _res = flutter::push_global_event(
flutter::APP_TYPE_MAIN,
serde_json::ser::to_string(&data).unwrap_or("".to_owned()),
);
});
}
}
pub fn session_get_common_sync(

View File

@@ -80,6 +80,8 @@ pub enum UserStatus {
pub struct UserPayload {
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub note: Option<String>,
@@ -268,7 +270,12 @@ impl OidcSession {
);
LocalConfig::set_option(
"user_info".to_owned(),
serde_json::json!({ "name": auth_body.user.name, "status": auth_body.user.status }).to_string(),
serde_json::json!({
"name": auth_body.user.name,
"display_name": auth_body.user.display_name,
"status": auth_body.user.status
})
.to_string(),
);
}
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "عرض RustDesk"),
("This PC", "هذا الحاسب"),
("or", "او"),
("Continue with", "متابعة مع"),
("Elevate", "ارتقاء"),
("Zoom cursor", "تكبير المؤشر"),
("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "متابعة مع {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Паказаць RustDesk"),
("This PC", "Гэты кампутар"),
("or", "або"),
("Continue with", "Працягнуць з"),
("Elevate", "Павысіць"),
("Zoom cursor", "Павялічэнне курсора"),
("Accept sessions via password", "Прымаць сеансы па паролю"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Працягнуць з {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Покажи RustDesk"),
("This PC", "Този компютър"),
("or", "или"),
("Continue with", "Продължи с"),
("Elevate", "Повишаване"),
("Zoom cursor", "Уголемяване курсор"),
("Accept sessions via password", "Приемане сесии чрез парола"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Продължи с {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Mostra el RustDesk"),
("This PC", "Aquest equip"),
("or", "o"),
("Continue with", "Continua amb"),
("Elevate", "Permisos ampliats"),
("Zoom cursor", "Escala del ratolí"),
("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Continua amb {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "显示 RustDesk"),
("This PC", "此电脑"),
("or", ""),
("Continue with", "使用"),
("Elevate", "提权"),
("Zoom cursor", "缩放光标"),
("Accept sessions via password", "只允许密码访问"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", "更新日志"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "使用 {} 登录"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Zobrazit RustDesk"),
("This PC", "Tento počítač"),
("or", "nebo"),
("Continue with", "Pokračovat s"),
("Elevate", "Zvýšit"),
("Zoom cursor", "Kurzor přiblížení"),
("Accept sessions via password", "Přijímat relace pomocí hesla"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Pokračovat s {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Vis RustDesk"),
("This PC", "Denne PC"),
("or", "eller"),
("Continue with", "Fortsæt med"),
("Elevate", "Elevér"),
("Zoom cursor", "Zoom markør"),
("Accept sessions via password", "Acceptér sessioner via adgangskode"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Fortsæt med {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk anzeigen"),
("This PC", "Dieser PC"),
("or", "oder"),
("Continue with", "Fortfahren mit"),
("Elevate", "Zugriff gewähren"),
("Zoom cursor", "Cursor vergrößern"),
("Accept sessions via password", "Sitzung mit Passwort bestätigen"),
@@ -562,8 +561,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Modus 1"),
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
("Enter privacy mode", "Datenschutzmodus aktivieren"),
("Exit privacy mode", "Datenschutzmodus beenden"),
("Enter privacy mode", "Datenschutzmodus aktiviert"),
("Exit privacy mode", "Datenschutzmodus beendet"),
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
("input_source_1_tip", "Eingangsquelle 1"),
("input_source_2_tip", "Eingangsquelle 2"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
("Changelog", "Änderungsprotokoll"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
("Continue with {}", "Fortfahren mit {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Εμφάνιση RustDesk"),
("This PC", "Αυτός ο υπολογιστής"),
("or", "ή"),
("Continue with", "Συνέχεια με"),
("Elevate", "Ανύψωση"),
("Zoom cursor", "ρσορας μεγέθυνσης"),
("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Συνέχεια με {}"),
].iter().cloned().collect();
}

View File

@@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("default_proxy_tip", "Default protocol and port are Socks5 and 1080"),
("no_audio_input_device_tip", "No audio input device found."),
("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."),
("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"),
("confirm_clear_Wayland_screen_selection_tip", "Are you sure you want to clear the Wayland screen selection?"),
("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."),
("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."),
("floating_window_tip", "It helps to keep RustDesk background service"),

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", ""),
("This PC", ""),
("or", ""),
("Continue with", ""),
("Elevate", ""),
("Zoom cursor", ""),
("Accept sessions via password", ""),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", ""),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Mostrar RustDesk"),
("This PC", "Este PC"),
("or", "o"),
("Continue with", "Continuar con"),
("Elevate", "Elevar privilegios"),
("Zoom cursor", "Ampliar cursor"),
("Accept sessions via password", "Aceptar sesiones a través de contraseña"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Continuar con {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Kuva RustDesk"),
("This PC", "See arvuti"),
("or", "või"),
("Continue with", "Jätka koos"),
("Elevate", "Tõsta"),
("Zoom cursor", "Suumi kursorit"),
("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Jätka koos {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Erakutsi RustDesk"),
("This PC", "PC hau"),
("or", "edo"),
("Continue with", "Jarraitu honekin"),
("Elevate", "Igo maila"),
("Zoom cursor", "Handitu kurtsorea"),
("Accept sessions via password", "Onartu saioak pasahitzaren bidez"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "{} honekin jarraitu"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk نمایش"),
("This PC", "This PC"),
("or", "یا"),
("Continue with", "ادامه با"),
("Elevate", "ارتقاء"),
("Zoom cursor", " بزرگنمایی نشانگر ماوس"),
("Accept sessions via password", "قبول درخواست با رمز عبور"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "ادامه با {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Näytä RustDesk"),
("This PC", "Tämä tietokone"),
("or", "tai"),
("Continue with", "Jatka käyttäen"),
("Elevate", "Korota oikeudet"),
("Zoom cursor", "Suurennusosoitin"),
("Accept sessions via password", "Hyväksy istunnot salasanalla"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Jatka käyttäen {}"),
].iter().cloned().collect();
}

View File

@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
("Do this for all conflicts", "Appliquer à tous les conflits"),
("This is irreversible!", "Ceci est irréversible !"),
("This is irreversible!", "Cette action est irréversible !"),
("Deleting", "Suppression"),
("files", "fichiers"),
("Waiting", "En attente"),
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Afficher RustDesk"),
("This PC", "Ce PC"),
("or", "ou"),
("Continue with", "Continuer avec"),
("Elevate", "Élever les privilèges"),
("Zoom cursor", "Augmenter la taille du curseur"),
("Accept sessions via password", "Accepter les sessions via mot de passe"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
("rel-mouse-permission-lost-tip", "Lautorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
("Changelog", "Journal des modifications"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Maintenir lécran allumé lors des sessions sortantes"),
("keep-awake-during-incoming-sessions-label", "Maintenir lécran allumé lors des sessions entrantes"),
("Continue with {}", "Continuer avec {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk-ის ჩვენება"),
("This PC", "ეს კომპიუტერი"),
("or", "ან"),
("Continue with", "გაგრძელება"),
("Elevate", "უფლებების აწევა"),
("Zoom cursor", "კურსორის მასშტაბირება"),
("Accept sessions via password", "სესიების მიღება პაროლით"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "{}-ით გაგრძელება"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "הצג את RustDesk"),
("This PC", "מחשב זה"),
("or", "או"),
("Continue with", "המשך עם"),
("Elevate", "הפעל הרשאות מורחבות"),
("Zoom cursor", "הגדל סמן"),
("Accept sessions via password", "קבל הפעלות באמצעות סיסמה"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "המשך עם {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Prikaži RustDesk"),
("This PC", "Ovo računalo"),
("or", "ili"),
("Continue with", "Nastavi sa"),
("Elevate", "Izdigni"),
("Zoom cursor", "Zumiraj kursor"),
("Accept sessions via password", "Prihvati sesije preko lozinke"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Nastavi sa {}"),
].iter().cloned().collect();
}

View File

@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
("Configure", "Beállítás"),
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a Képernyőfelvétel jogosultságot."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
("Installing ...", "Telepítés…"),
("Install", "Telepítés"),
("Installation", "Telepítés"),
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Elfogadás?"),
("Open System Setting", "Rendszerbeállítások megnyitása"),
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a Hozzáférhetőség szolgáltatás használatát."),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a Kapcsolási szolgáltatás indítása gombra, vagy aktiválja a Képernyőfelvétel engedélyt."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
("Account", "Fiók"),
("Overwrite", "Felülírás"),
@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "A RustDesk megjelenítése"),
("This PC", "Ez a számítógép"),
("or", "vagy"),
("Continue with", "Folytatás a következővel"),
("Elevate", "Hozzáférés engedélyezése"),
("Zoom cursor", "Kurzor nagyítása"),
("Accept sessions via password", "Munkamenetek elfogadása jelszóval"),
@@ -408,15 +407,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a Bemenet figyelése jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a Hangfelvétel jogosultságot."),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
("Wait", "Várjon"),
("Elevation Error", "Emelt szintű hozzáférési hiba"),
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("Request Elevation", "Emelt szintű jogok igénylése"),
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
("Elevate successfully", "Emelt szintű jogok megadva"),
@@ -442,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Hanghívás"),
("Text chat", "Szöveges csevegés"),
("Stop voice call", "Hanghívás leállítása"),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a Mindig továbbító-kiszolgálón keresztül kapcsolódom opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("Reconnect", "Újrakapcsolódás"),
("Codec", "Kodek"),
("Resolution", "Felbontás"),
@@ -490,7 +489,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update", "Frissítés"),
("Enable", "Engedélyezés"),
("Disable", "Letiltás"),
("Options", "Beállítások"),
("Options", "Opciók"),
("resolution_original_tip", "Eredeti felbontás"),
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
("resolution_custom_tip", "Testre szabható felbontás"),
@@ -559,7 +558,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Kapcsolja ki az összeset"),
("True color (4:4:4)", "Valódi szín (4:4:4)"),
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például 9123456234/r."),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "1. mód"),
("privacy_mode_impl_virtual_display_tip", "2. mód"),
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
@@ -622,7 +621,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Power", "Főkapcsoló"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a /newbot parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
("About RustDesk", "A RustDesk névjegye"),
@@ -643,7 +642,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
("Authentication Required", "Hitelesítés szükséges"),
("Authenticate", "Hitelesítés"),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("Download", "Letöltés"),
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
@@ -682,9 +681,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Downloading {}", "{} letöltése"),
("{} Update", "{} frissítés"),
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a Letöltés gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("Auto update", "Automatikus frissítés"),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a Letöltés gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
("Use WebSocket", "WebSocket használata"),
("Trackpad speed", "Érintőpad sebessége"),
@@ -730,14 +729,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Megjegyzés beírása"),
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
("Show terminal extra keys", "További terminálgombok megjelenítése"),
("Relative mouse mode", "Relatív egérmód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."),
("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."),
("Relative mouse mode", "Relatív egér mód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
("Changelog", "Változáslista"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
("Continue with {}", "Folytatás a következővel: {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Tampilkan RustDesk"),
("This PC", "PC ini"),
("or", "atau"),
("Continue with", "Lanjutkan dengan"),
("Elevate", "Elevasi"),
("Zoom cursor", "Perbersar Kursor"),
("Accept sessions via password", "Izinkan sesi dengan kata sandi"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Lanjutkan dengan {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Visualizza RustDesk"),
("This PC", "Questo PC"),
("or", "O"),
("Continue with", "Continua con"),
("Elevate", "Eleva"),
("Zoom cursor", "Cursore zoom"),
("Accept sessions via password", "Accetta sessioni via password"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
("Changelog", "Novità programma"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
("Continue with {}", "Continua con {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk を表示"),
("This PC", "この PC"),
("or", "または"),
("Continue with", "で続行"),
("Elevate", "昇格"),
("Zoom cursor", "カーソルを拡大する"),
("Accept sessions via password", "パスワードでセッションを承認"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "{} で続行"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk 표시"),
("This PC", "이 PC"),
("or", "또는"),
("Continue with", "계속"),
("Elevate", "권한 상승"),
("Zoom cursor", "커서 확대/축소"),
("Accept sessions via password", "비밀번호를 통해 세션 수락"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."),
("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."),
("Changelog", "변경 기록"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
("Continue with {}", "{}(으)로 계속"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", ""),
("This PC", ""),
("or", ""),
("Continue with", ""),
("Elevate", ""),
("Zoom cursor", ""),
("Accept sessions via password", ""),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", ""),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Rodyti RustDesk"),
("This PC", "Šis kompiuteris"),
("or", "arba"),
("Continue with", "Tęsti su"),
("Elevate", "Pakelti"),
("Zoom cursor", "Mastelio keitimo žymeklis"),
("Accept sessions via password", "Priimti seansus naudojant slaptažodį"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Tęsti su {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Rādīt RustDesk"),
("This PC", "Šis dators"),
("or", "vai"),
("Continue with", "Turpināt ar"),
("Elevate", "Pacelt"),
("Zoom cursor", "Tālummaiņas kursors"),
("Accept sessions via password", "Pieņemt sesijas, izmantojot paroli"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Turpināt ar {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Vis RustDesk"),
("This PC", "Denne PC"),
("or", "eller"),
("Continue with", "Fortsett med"),
("Elevate", "Elever"),
("Zoom cursor", "Zoom markør"),
("Accept sessions via password", "Aksepter sesjoner via passord"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Fortsett med {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Toon RustDesk"),
("This PC", "Deze PC"),
("or", "of"),
("Continue with", "Ga verder met"),
("Elevate", "Verhoog"),
("Zoom cursor", "Zoom cursor"),
("Accept sessions via password", "Sessies accepteren via wachtwoord"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."),
("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."),
("Changelog", "Wijzigingenlogboek"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
("Continue with {}", "Ga verder met {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Pokaż RustDesk"),
("This PC", "Ten komputer"),
("or", "lub"),
("Continue with", "Kontynuuj z"),
("Elevate", "Uzyskaj uprawnienia"),
("Zoom cursor", "Powiększenie kursora"),
("Accept sessions via password", "Uwierzytelnij sesję używając hasła"),
@@ -737,7 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"),
("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"),
("Changelog", "Dziennik zmian"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
("Continue with {}", "Kontynuuj z {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", ""),
("This PC", ""),
("or", ""),
("Continue with", ""),
("Elevate", ""),
("Zoom cursor", ""),
("Accept sessions via password", ""),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", ""),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Exibir RustDesk"),
("This PC", "Este Computador"),
("or", "ou"),
("Continue with", "Continuar com"),
("Elevate", "Elevar"),
("Zoom cursor", "Aumentar cursor"),
("Accept sessions via password", "Aceitar sessões via senha"),
@@ -672,72 +671,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."),
("save-settings-tip", "Salvar configurações"),
("dont-show-again-tip", "Não mostrar novamente"),
("Take screenshot", ""),
("Taking screenshot", ""),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", ""),
("Default trackpad speed", ""),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("Take screenshot", "Capturar de tela"),
("Taking screenshot", "Capturando tela"),
("screenshot-merged-screen-not-supported-tip", "Mesclar a captura de tela de múltiplos monitores não é suportada no momento. Por favor, alterne para um único monitor e tente novamente."),
("screenshot-action-tip", "Por favor, selecione como seguir com a captura de tela."),
("Save as", "Salvar como"),
("Copy to clipboard", "Copiar para área de transferência"),
("Enable remote printer", "Habilitar impressora remota"),
("Downloading {}", "Baixando {}"),
("{} Update", "Atualização do {}"),
("{}-to-update-tip", "{} será fechado agora para instalar a nova versão."),
("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("Auto update", "Atualização automática"),
("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."),
("Use WebSocket", "Usar WebSocket"),
("Trackpad speed", "Velocidade do trackpad"),
("Default trackpad speed", "Velocidade padrão do trackpad"),
("Numeric one-time password", "Senha numérica de uso único"),
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
("Enable UDP hole punching", "Habilitar UDP hole punching"),
("View camera", "Visualizar câmera"),
("Enable camera", "Ativar câmera"),
("No cameras", "Sem câmeras"),
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
("Terminal", ""),
("Enable terminal", ""),
("New tab", ""),
("Keep terminal sessions on disconnect", ""),
("Terminal (Run as administrator)", ""),
("terminal-admin-login-tip", ""),
("Failed to get user token.", ""),
("Incorrect username or password.", ""),
("The user is not an administrator.", ""),
("Failed to check if the user is an administrator.", ""),
("Supported only in the installed version.", ""),
("elevation_username_tip", ""),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Terminal", "Terminal"),
("Enable terminal", "Habilitar Terminal"),
("New tab", "Nova aba"),
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
("Failed to get user token.", "Falha ao obter token do usuário."),
("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"),
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "Funciona somente na versão instalada"),
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Controle deslizante de escala personalizada"),
("Decrease", "Diminuir"),
("Increase", "Aumentar"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Show virtual mouse", "Mostrar mouse virtual"),
("Virtual mouse size", "Tamanho do mouse virtual"),
("Small", "Pequeno"),
("Large", "Grande"),
("Show virtual joystick", "Mostrar joystick virtual"),
("Edit note", "Editar nota"),
("Alias", "Apelido"),
("ScrollEdge", "Rolagem nas bordas"),
("Allow insecure TLS fallback", "Permitir fallback TLS inseguro"),
("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."),
("Disable UDP", "Desabilitar UDP"),
("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."),
("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."),
("input note here", "Insira uma nota aqui"),
("note-at-conn-end-tip", "Solicitar nota ao final da conexão"),
("Show terminal extra keys", "Mostrar teclas extras do terminal"),
("Relative mouse mode", "Modo de Mouse Relativo"),
("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."),
("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."),
("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."),
("rel-mouse-exit-{}-tip", "Pressione {} para sair."),
("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."),
("Changelog", "Registro de alterações"),
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
("Continue with {}", "Continuar com {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Afișează RustDesk"),
("This PC", "Acest PC"),
("or", "sau"),
("Continue with", "Continuă cu"),
("Elevate", "Sporește privilegii"),
("Zoom cursor", "Cursor lupă"),
("Accept sessions via password", "Acceptă începerea sesiunii folosind parola"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Continuă cu {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Показать RustDesk"),
("This PC", "Этот компьютер"),
("or", "или"),
("Continue with", "Продолжить с"),
("Elevate", "Повысить"),
("Zoom cursor", "Масштабировать курсор"),
("Accept sessions via password", "Принимать сеансы по паролю"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", "Журнал изменений"),
("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"),
("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"),
("Continue with {}", "Продолжить с {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Mustra RustDesk"),
("This PC", "Custu PC"),
("or", "O"),
("Continue with", "Sighi cun"),
("Elevate", "Cresche"),
("Zoom cursor", "Cursore de ismanniamentu"),
("Accept sessions via password", "Atzeta sessiones cun sa crae"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Sighi cun {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Zobraziť RustDesk"),
("This PC", "Tento počítač"),
("or", "alebo"),
("Continue with", "Pokračovať s"),
("Elevate", "Zvýšiť"),
("Zoom cursor", "Kurzor priblíženia"),
("Accept sessions via password", "Prijímanie relácií pomocou hesla"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Pokračovať s {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Prikaži RustDesk"),
("This PC", "Ta računalnik"),
("or", "ali"),
("Continue with", "Nadaljuj z"),
("Elevate", "Povzdig pravic"),
("Zoom cursor", "Prilagodi velikost miškinega kazalca"),
("Accept sessions via password", "Sprejmi seje z geslom"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Nadaljuj z {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Shfaq RustDesk"),
("This PC", "Ky PC"),
("or", "ose"),
("Continue with", "Vazhdo me"),
("Elevate", "Ngritja"),
("Zoom cursor", "Zmadho kursorin"),
("Accept sessions via password", "Prano sesionin nëpërmjet fjalëkalimit"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Vazhdo me {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Prikazi RustDesk"),
("This PC", "Ovaj PC"),
("or", "ili"),
("Continue with", "Nastavi sa"),
("Elevate", "Izdigni"),
("Zoom cursor", "Zumiraj kursor"),
("Accept sessions via password", "Prihvati sesije preko lozinke"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Nastavi sa {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Visa RustDesk"),
("This PC", "Denna dator"),
("or", "eller"),
("Continue with", "Fortsätt med"),
("Elevate", "Höj upp"),
("Zoom cursor", "Zoom"),
("Accept sessions via password", "Acceptera sessioner via lösenord"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Fortsätt med {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk ஐ காட்டு"),
("This PC", "இந்த PC"),
("or", "அல்லது"),
("Continue with", "உடன் தொடர்"),
("Elevate", "உயர்த்து"),
("Zoom cursor", "கர்சரை பெரிதாக்கு"),
("Accept sessions via password", "கடவுச்சொல் வழியாக அமர்வுகளை ஏற்று"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "{} உடன் தொடர்"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", ""),
("This PC", ""),
("or", ""),
("Continue with", ""),
("Elevate", ""),
("Zoom cursor", ""),
("Accept sessions via password", ""),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", ""),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "แสดง RustDesk"),
("This PC", "พีซีเครื่องนี้"),
("or", "หรือ"),
("Continue with", "ทำต่อด้วย"),
("Elevate", "ยกระดับ"),
("Zoom cursor", "ขยายเคอร์เซอร์"),
("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "ทำต่อด้วย {}"),
].iter().cloned().collect();
}

View File

@@ -3,8 +3,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
("Status", "Durum"),
("Your Desktop", "Sizin Masaüstünüz"),
("desk_tip", "Masaüstünüze bu ID ve şifre ile erişilebilir"),
("Password", "Şifre"),
("desk_tip", "Masaüstünüze bu ID ve parola ile erişilebilir"),
("Password", "Parola"),
("Ready", "Hazır"),
("Established", "Bağlantı sağlandı"),
("connecting_status", "Bağlanılıyor "),
@@ -13,16 +13,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Service is running", "Servis çalışıyor"),
("Service is not running", "Servis çalışmıyor"),
("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"),
("Control Remote Desktop", "Bağlanılacak Uzak Bağlantı ID"),
("Control Remote Desktop", "Uzak Masaüstünü Denetle"),
("Transfer file", "Dosya transferi"),
("Connect", "Bağlan"),
("Recent sessions", "Son Bağlanılanlar"),
("Recent sessions", "Son oturumlar"),
("Address book", "Adres Defteri"),
("Confirmation", "Onayla"),
("TCP tunneling", "TCP Tünelleri"),
("TCP tunneling", "TCP tünelleri"),
("Remove", "Kaldır"),
("Refresh random password", "Yeni rastgele şifre oluştur"),
("Set your own password", "Kendi şifreni oluştur"),
("Refresh random password", "Yeni rastgele parola oluştur"),
("Set your own password", "Kendi parolanı oluştur"),
("Enable keyboard/mouse", "Klavye ve Fareye izin ver"),
("Enable clipboard", "Kopyalanan geçici veriye izin ver"),
("Enable file transfer", "Dosya Transferine izin ver"),
@@ -47,9 +47,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"),
("Privacy Statement", "Gizlilik Beyanı"),
("Mute", "Sustur"),
("Build Date", "Yapım Tarihi"),
("Build Date", "Derleme Tarihi"),
("Version", "Sürüm"),
("Home", "Anasayfa"),
("Home", "Ana Sayfa"),
("Audio Input", "Ses Girişi"),
("Enhancements", "Geliştirmeler"),
("Hardware Codec", "Donanımsal Codec"),
@@ -64,18 +64,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Not available", "Erişilebilir değil"),
("Too frequent", "Çok sık"),
("Cancel", "İptal"),
("Skip", "Geç"),
("Skip", "Atla"),
("Close", "Kapat"),
("Retry", "Tekrar Dene"),
("OK", "Tamam"),
("Password Required", "Şifre Gerekli"),
("Please enter your password", "Lütfen şifrenizi giriniz"),
("Remember password", "Şifreyi hatırla"),
("Wrong Password", "Hatalı şifre"),
("Password Required", "Parola Gerekli"),
("Please enter your password", "Lütfen parolanızı giriniz"),
("Remember password", "Parolayı hatırla"),
("Wrong Password", "Hatalı parola"),
("Do you want to enter again?", "Tekrar giriş yapmak ister misiniz?"),
("Connection Error", "Bağlantı Hatası"),
("Error", "Hata"),
("Reset by the peer", "Eş tarafında sıfırla"),
("Reset by the peer", "Eş tarafından sıfırlandı"),
("Connecting...", "Bağlanılıyor..."),
("Connection in progress. Please wait.", "Bağlantı sağlanıyor. Lütfen bekleyiniz."),
("Please try 1 minute later", "Lütfen 1 dakika sonra tekrar deneyiniz"),
@@ -141,10 +141,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Timeout", "Zaman aşımı"),
("Failed to connect to relay server", "Relay sunucusuna bağlanılamadı"),
("Failed to connect via rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"),
("Failed to connect via relay server", "Relay oluşturma sunucusuna bağlanılamadı"),
("Failed to connect via relay server", "Aktarma sunucusuna bağlanılamadı"),
("Failed to make direct connection to remote desktop", "Uzak masaüstüne doğrudan bağlantı kurulamadı"),
("Set Password", "Şifre ayarla"),
("OS Password", "İşletim Sistemi Şifresi"),
("Set Password", "Parola ayarla"),
("OS Password", "İşletim Sistemi Parolası"),
("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."),
("Click to upgrade", "Yükseltmek için tıklayınız"),
("Configure", "Ayarla"),
@@ -184,7 +184,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Direct and unencrypted connection", "Doğrudan ve şifrelenmemiş bağlantı"),
("Relayed and unencrypted connection", "Aktarmalı ve şifrelenmemiş bağlantı"),
("Enter Remote ID", "Uzak ID'yi Girin"),
("Enter your password", "Şifrenizi girin"),
("Enter your password", "Parolanızı girin"),
("Logging in...", "Giriş yapılıyor..."),
("Enable RDP session sharing", "RDP oturum paylaşımını etkinleştir"),
("Auto Login", "Otomatik giriş"),
@@ -208,8 +208,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"),
("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"),
("Run without install", "Yüklemeden çalıştır"),
("Connect via relay", ""),
("Always connect via relay", "Always connect via relay"),
("Connect via relay", "Aktarmalı üzerinden bağlan"),
("Always connect via relay", "Her zaman aktarmalı üzerinden bağlan"),
("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"),
("Login", "Giriş yap"),
("Verify", "Doğrula"),
@@ -226,11 +226,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unselect all tags", "Tüm etiketlerin seçimini kaldır"),
("Network error", "Bağlantı hatası"),
("Username missed", "Kullanıcı adı boş"),
("Password missed", "Şifre boş"),
("Password missed", "Parola boş"),
("Wrong credentials", "Yanlış kimlik bilgileri"),
("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"),
("Edit Tag", "Etiketi düzenle"),
("Forget Password", "Şifreyi Unut"),
("Forget Password", "Parolayı Unut"),
("Favorites", "Favoriler"),
("Add to Favorites", "Favorilere ekle"),
("Remove from Favorites", "Favorilerden çıkar"),
@@ -268,9 +268,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Share screen", "Ekranı Paylaş"),
("Chat", "Mesajlaş"),
("Total", "Toplam"),
("items", "öğeler"),
("items", "ögeler"),
("Selected", "Seçildi"),
("Screen Capture", "Ekran görüntüsü"),
("Screen Capture", "Ekran Görüntüsü"),
("Input Control", "Giriş Kontrolü"),
("Audio Capture", "Ses Yakalama"),
("Do you accept?", "Kabul ediyor musun?"),
@@ -285,7 +285,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."),
("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."),
("Account", "Hesap"),
("Overwrite", "üzerine yaz"),
("Overwrite", "Üzerine yaz"),
("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"),
("Quit", "Çıkış"),
("Help", "Yardım"),
@@ -295,8 +295,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unsupported", "desteklenmiyor"),
("Peer denied", "eş reddedildi"),
("Please install plugins", "Lütfen eklentileri yükleyin"),
("Peer exit", "eş çıkışı"),
("Failed to turn off", "kapatılamadı"),
("Peer exit", "Eş çıkışı"),
("Failed to turn off", "Kapatılamadı"),
("Turned off", "Kapatıldı"),
("Language", "Dil"),
("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"),
@@ -308,32 +308,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Legacy mode", "Eski mod"),
("Map mode", "Haritalama modu"),
("Translate mode", "Çeviri modu"),
("Use permanent password", "Kalıcı şifre kullan"),
("Use both passwords", "İki şifreyi de kullan"),
("Set permanent password", "Kalıcı şifre oluştur"),
("Use permanent password", "Kalıcı parola kullan"),
("Use both passwords", "İki parolayı da kullan"),
("Set permanent password", "Kalıcı parola oluştur"),
("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"),
("Restart remote device", "Uzaktaki cihazı yeniden başlat"),
("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"),
("Are you sure you want to restart", "Yeniden başlatmak istediğine emin misin?"),
("Restarting remote device", "Uzaktan yeniden başlatılıyor"),
("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı şifre ile yeniden bağlanın"),
("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı parola ile yeniden bağlanın"),
("Copied", "Kopyalandı"),
("Exit Fullscreen", "Tam ekrandan çık"),
("Fullscreen", "Tam ekran"),
("Exit Fullscreen", "Tam Ekrandan Çık"),
("Fullscreen", "Tam Ekran"),
("Mobile Actions", "Mobil İşlemler"),
("Select Monitor", "Monitörü Seç"),
("Control Actions", "Kontrol Eylemleri"),
("Display Settings", "Görüntü ayarları"),
("Display Settings", "Görüntü Ayarları"),
("Ratio", "Oran"),
("Image Quality", "Görüntü kalitesi"),
("Image Quality", "Görüntü Kalitesi"),
("Scroll Style", "Kaydırma Stili"),
("Show Toolbar", "Araç Çubuğunu Göster"),
("Hide Toolbar", "Araç Çubuğunu Gizle"),
("Direct Connection", "Doğrudan Bağlantı"),
("Relay Connection", "Röle Bağlantısı"),
("Relay Connection", "Aktarmalı Bağlantı"),
("Secure Connection", "Güvenli Bağlantı"),
("Insecure Connection", "Güvenli Olmayan Bağlantı"),
("Scale original", "Orijinali ölçeklendir"),
("Scale adaptive", "Ölçek uyarlanabilir"),
("Scale original", "Orijinal ölçekte"),
("Scale adaptive", "Uyarlanabilir ölçekte"),
("General", "Genel"),
("Security", "Güvenlik"),
("Theme", "Tema"),
@@ -347,18 +347,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Enable audio", "Sesi Aktif Et"),
("Unlock Network Settings", "Ağ Ayarlarını"),
("Server", "Sunucu"),
("Direct IP Access", "Direk IP Erişimi"),
("Direct IP Access", "Doğrudan IP Erişimi"),
("Proxy", "Vekil"),
("Apply", "Uygula"),
("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"),
("Disconnect all devices?", "Tüm cihazların bağlantısı kesilsin mi?"),
("Clear", "Temizle"),
("Audio Input Device", "Ses Giriş Aygıtı"),
("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"),
("Network", ""),
("Pin Toolbar", "Araç Çubuğunu Sabitle"),
("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"),
("Recording", "Kayıt Ediliyor"),
("Directory", "Klasör"),
("Recording", "Kaydediliyor"),
("Directory", "Dizin"),
("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"),
("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"),
("Change", "Değiştir"),
@@ -384,16 +384,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "RustDesk'i Göster"),
("This PC", "Bu PC"),
("or", "veya"),
("Continue with", "bununla devam et"),
("Elevate", "Yükseltme"),
("Zoom cursor", "Yakınlaştırma imleci"),
("Accept sessions via password", "Oturumları parola ile kabul etme"),
("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"),
("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"),
("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."),
("One-time Password", "Tek Kullanımlık Şifre"),
("One-time Password", "Tek Kullanımlık Parola"),
("Use one-time password", "Tek seferlik parola kullanın"),
("One-time password length", "Tek seferlik şifre uzunluğu"),
("One-time password length", "Tek seferlik parola uzunluğu"),
("Request access to your device", "Cihazınıza erişim talep edin"),
("Hide connection management window", "Bağlantı yönetimi penceresini gizle"),
("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"),
@@ -442,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Sesli görüşme"),
("Text chat", "Metin sohbeti"),
("Stop voice call", "Sesli görüşmeyi durdur"),
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; röle aracılığıyla bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde bir röle kullanmak istiyorsanız, ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Röle Üzerinden Bağlan\" seçeneğini seçebilirsiniz."),
("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."),
("Reconnect", "Yeniden Bağlan"),
("Codec", "Kodlayıcı"),
("Resolution", "Çözünürlük"),
@@ -477,7 +476,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("no_desktop_title_tip", "Masaüstü mevcut değil"),
("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"),
("No need to elevate", "Yükseltmeye gerek yok"),
("System Sound", "Sistem Ses"),
("System Sound", "Sistem Sesi"),
("Default", "Varsayılan"),
("New RDP", "Yeni RDP"),
("Fingerprint", "Parmak İzi"),
@@ -495,7 +494,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"),
("resolution_custom_tip", "Özel çözünürlük"),
("Collapse toolbar", "Araç çubuğunu daralt"),
("Accept and Elevate", "Kabul et ve yükselt"),
("Accept and Elevate", "Kabul Et ve Yükselt"),
("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."),
("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."),
("Incoming connection", "Gelen bağlantı"),
@@ -534,7 +533,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."),
("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."),
("Don't show again", "Bir daha gösterme"),
("I Agree", "Kabul ediyorum"),
("I Agree", "Kabul Ediyorum"),
("Decline", "Reddet"),
("Timeout in minutes", "Zaman aşımı (dakika)"),
("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"),
@@ -559,7 +558,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Tümünü çıkar"),
("True color (4:4:4)", "Gerçek renk (4:4:4)"),
("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"),
("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (<domain>:<port>) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (<id>@<server_address>?key=<key_value>) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"<id>@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir röle bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."),
("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (<domain>:<port>) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (<id>@<server_address>?key=<key_value>) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"<id>@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir aktarma bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Mod 1"),
("privacy_mode_impl_virtual_display_tip", "Mod 2"),
("Enter privacy mode", "Gizlilik moduna gir"),
@@ -581,12 +580,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"),
("powered_by_me", "RustDesk tarafından desteklenmektedir"),
("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."),
("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir şifre ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."),
("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir parola ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."),
("Security Alert", "Güvenlik Uyarısı"),
("My address book", "Adres defterim"),
("Personal", "Kişisel"),
("Owner", "Sahip"),
("Set shared password", "Paylaşılan şifreyi ayarla"),
("Set shared password", "Paylaşılan parolayı ayarla"),
("Exist in", "İçinde varolan"),
("Read-only", "Salt okunur"),
("Read/Write", "Okuma/Yazma"),
@@ -599,7 +598,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Follow remote cursor", "Uzak imleci takip et"),
("Follow remote window focus", "Uzak pencere odağını takip et"),
("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."),
("no_audio_input_device_tip", "Varsayılan protokol ve port, Socks5 ve 1080'dir"),
("no_audio_input_device_tip", "Ses girişi aygıtı bulunamadı."),
("Incoming", "Gelen"),
("Outgoing", "Giden"),
("Clear Wayland screen selection", "Wayland ekran seçimini temizle"),
@@ -612,7 +611,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"),
("Keep screen on", "Ekranıık tut"),
("Never", "Asla"),
("During controlled", "Kontrol sırasınd"),
("During controlled", "Kontrol sırasında"),
("During service is on", "Servis açıkken"),
("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"),
("Back", "Geri"),
@@ -620,7 +619,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Volume up", "Sesi yükselt"),
("Volume down", "Sesi azalt"),
("Power", "Güç"),
("Telegram bot", "Telegram bot"),
("Telegram bot", "Telegram botu"),
("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."),
("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"),
("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"),
@@ -642,7 +641,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Invalid file name", "Geçersiz dosya adı"),
("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."),
("Authentication Required", "Kimlik Doğrulama Gerekli"),
("Authenticate", "Kimlik doğrulaması"),
("Authenticate", "Kimlik Doğrula"),
("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (<id>@<server_address>?key=<key_value>) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"<id>@public\" girin, genel sunucu için anahtara gerek yoktur."),
("Download", "İndir"),
("Upload folder", "Klasör yükle"),
@@ -661,9 +660,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."),
("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."),
("Install {} Printer", "{} Yazıcısını Yükle"),
("Outgoing Print Jobs", "Giden Baskı İşleri"),
("Incoming Print Jobs", "Gelen Baskı İşleri"),
("Incoming Print Job", "Gelen Baskı İşi"),
("Outgoing Print Jobs", "Giden Yazdırma İşleri"),
("Incoming Print Jobs", "Gelen Yazdırma İşleri"),
("Incoming Print Job", "Gelen Yazdırma İşi"),
("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"),
("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"),
("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."),
@@ -685,11 +684,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."),
("Auto update", "Otomatik güncelleme"),
("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."),
("websocket_tip", "WebSocket kullanıldığında yalnızca röle bağlantıları desteklenir."),
("websocket_tip", "WebSocket kullanıldığında yalnızca aktarma bağlantıları desteklenir."),
("Use WebSocket", "WebSocket'ı kullan"),
("Trackpad speed", "İzleme paneli hızı"),
("Default trackpad speed", "Varsayılan izleme paneli hızı"),
("Numeric one-time password", "Sayısal tek seferlik şifre"),
("Numeric one-time password", "Sayısal tek seferlik parola"),
("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"),
("Enable UDP hole punching", "UDP delik açmayı etkinleştir"),
("View camera", "Kamerayı görüntüle"),
@@ -701,16 +700,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("New tab", "Yeni sekme"),
("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarınıık tut"),
("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"),
("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve şifresini giriniz."),
("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve parolasını giriniz."),
("Failed to get user token.", "Kullanıcı belirteci alınamadı."),
("Incorrect username or password.", "Hatalı kullanıcı adı veya şifre."),
("Incorrect username or password.", "Hatalı kullanıcı adı veya parola."),
("The user is not an administrator.", "Kullanıcı bir yönetici değil."),
("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."),
("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."),
("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"),
("Preparing for installation ...", "Kuruluma hazırlanıyor..."),
("Show my cursor", "İmlecimi göster"),
("Scale custom", "Özel boyutlandır"),
("Scale custom", "Özel ölçekte"),
("Custom scale slider", "Özel ölçek kaydırıcısı"),
("Decrease", "Azalt"),
("Increase", "Arttır"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", "Değişiklik Günlüğü"),
("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranıık tutun"),
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranıık tutun"),
("Continue with {}", "{} ile devam et"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "顯示 RustDesk"),
("This PC", "此電腦"),
("or", ""),
("Continue with", "繼續"),
("Elevate", "提升權限"),
("Zoom cursor", "縮放游標"),
("Accept sessions via password", "只允許透過輸入密碼進行連線"),
@@ -729,15 +728,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server-oss-not-support-tip", "注意RustDesk 開源伺服器 (OSS server) 不包含此功能。"),
("input note here", "輸入備註"),
("note-at-conn-end-tip", "在連接結束時請求備註"),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Show terminal extra keys", "顯示終端機額外按鍵"),
("Relative mouse mode", "相對滑鼠模式"),
("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"),
("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"),
("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"),
("rel-mouse-exit-{}-tip", "按下 {} 退出"),
("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"),
("Changelog", "更新日誌"),
("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"),
("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"),
("Continue with {}", "使用 {} 登入"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Показати RustDesk"),
("This PC", "Цей ПК"),
("or", "чи"),
("Continue with", "Продовжити з"),
("Elevate", "Розширення прав"),
("Zoom cursor", "Збільшити вказівник"),
("Accept sessions via password", "Підтверджувати сеанси паролем"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Продовжити з {}"),
].iter().cloned().collect();
}

View File

@@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show RustDesk", "Hiện RustDesk"),
("This PC", "Máy tính này"),
("or", "hoặc"),
("Continue with", "Tiếp tục với"),
("Elevate", "Nâng quyền"),
("Zoom cursor", "Phóng to con trỏ"),
("Accept sessions via password", "Chấp nhận phiên qua mật khẩu"),
@@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Changelog", "Nhật ký thay đổi"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("Continue with {}", "Tiếp tục với {}"),
].iter().cloned().collect();
}

View File

@@ -2088,3 +2088,122 @@ pub fn is_selinux_enforcing() -> bool {
},
}
}
/// Get the app ID for shortcuts inhibitor permission.
/// Returns different ID based on whether running in Flatpak or native.
/// The ID must match the installed .desktop filename, as GNOME Shell's
/// inhibitShortcutsDialog uses `Shell.WindowTracker.get_window_app(window).get_id()`.
fn get_shortcuts_inhibitor_app_id() -> String {
if is_flatpak() {
// In Flatpak, FLATPAK_ID is set automatically by the runtime to the app ID
// (e.g., "com.rustdesk.RustDesk"). This is the most reliable source.
// Fall back to constructing from app name if not available.
match std::env::var("FLATPAK_ID") {
Ok(id) if !id.is_empty() => format!("{}.desktop", id),
_ => {
let app_name = crate::get_app_name();
format!("com.{}.{}.desktop", app_name.to_lowercase(), app_name)
}
}
} else {
format!("{}.desktop", crate::get_app_name().to_lowercase())
}
}
const PERMISSION_STORE_DEST: &str = "org.freedesktop.impl.portal.PermissionStore";
const PERMISSION_STORE_PATH: &str = "/org/freedesktop/impl/portal/PermissionStore";
const PERMISSION_STORE_IFACE: &str = "org.freedesktop.impl.portal.PermissionStore";
/// Clear GNOME shortcuts inhibitor permission via D-Bus.
/// This allows the permission dialog to be shown again.
pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> {
let app_id = get_shortcuts_inhibitor_app_id();
log::info!(
"Clearing shortcuts inhibitor permission for app_id: {}, is_flatpak: {}",
app_id,
is_flatpak()
);
let conn = dbus::blocking::Connection::new_session()?;
let proxy = conn.with_proxy(
PERMISSION_STORE_DEST,
PERMISSION_STORE_PATH,
std::time::Duration::from_secs(3),
);
// DeletePermission(s table, s id, s app) -> ()
let result: Result<(), dbus::Error> = proxy.method_call(
PERMISSION_STORE_IFACE,
"DeletePermission",
("gnome", "shortcuts-inhibitor", app_id.as_str()),
);
match result {
Ok(()) => {
log::info!("Successfully cleared GNOME shortcuts inhibitor permission");
Ok(())
}
Err(e) => {
let err_name = e.name().unwrap_or("");
// If the permission doesn't exist, that's also fine
if err_name == "org.freedesktop.portal.Error.NotFound"
|| err_name == "org.freedesktop.DBus.Error.UnknownObject"
|| err_name == "org.freedesktop.DBus.Error.ServiceUnknown"
{
log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name);
Ok(())
} else {
bail!("Failed to clear permission: {}", e)
}
}
}
}
/// Check if GNOME shortcuts inhibitor permission exists.
pub fn has_gnome_shortcuts_inhibitor_permission() -> bool {
let app_id = get_shortcuts_inhibitor_app_id();
let conn = match dbus::blocking::Connection::new_session() {
Ok(c) => c,
Err(e) => {
log::debug!("Failed to connect to session bus: {}", e);
return false;
}
};
let proxy = conn.with_proxy(
PERMISSION_STORE_DEST,
PERMISSION_STORE_PATH,
std::time::Duration::from_secs(3),
);
// Lookup(s table, s id) -> (a{sas} permissions, v data)
// We only need the permissions dict; check if app_id is a key.
let result: Result<
(
std::collections::HashMap<String, Vec<String>>,
dbus::arg::Variant<Box<dyn dbus::arg::RefArg>>,
),
dbus::Error,
> = proxy.method_call(
PERMISSION_STORE_IFACE,
"Lookup",
("gnome", "shortcuts-inhibitor"),
);
match result {
Ok((permissions, _)) => {
let found = permissions.contains_key(&app_id);
log::debug!(
"Shortcuts inhibitor permission lookup: app_id={}, found={}, keys={:?}",
app_id,
found,
permissions.keys().collect::<Vec<_>>()
);
found
}
Err(e) => {
log::debug!("Failed to query shortcuts inhibitor permission: {}", e);
false
}
}
}

View File

@@ -4,6 +4,13 @@
#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
#include <CoreGraphics/CoreGraphics.h>
#include <vector>
#include <map>
#include <set>
#include <mutex>
#include <string>
extern "C" bool CanUseNewApiForScreenCaptureCheck() {
#ifdef NO_InputMonitoringAuthStatus
return false;
@@ -292,3 +299,611 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
CFRelease(allModes);
return ret;
}
static CFMachPortRef g_eventTap = NULL;
static CFRunLoopSourceRef g_runLoopSource = NULL;
static std::mutex g_privacyModeMutex;
static bool g_privacyModeActive = false;
// Flag to request asynchronous shutdown of privacy mode.
// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling
// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential
// issues with unregistering a callback from within itself, which is not explicitly
// guaranteed to be safe by Apple documentation.
static bool g_privacyModeShutdownRequested = false;
// Timestamp of the last display reconfiguration event (in milliseconds).
// Used for debouncing rapid successive changes (e.g., multiple resolution changes).
static uint64_t g_lastReconfigTimestamp = 0;
// Flag indicating whether a delayed blackout reapplication is already scheduled.
// Prevents multiple concurrent delayed tasks from being created.
static bool g_blackoutReapplicationScheduled = false;
// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections
// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable
static std::map<std::string, std::vector<CGGammaValue>> g_originalGammas;
// The event source user data value used by enigo library for injected events.
// This allows us to distinguish remote input (which should be allowed) from local physical input.
// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE
static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100;
// Duration in milliseconds to monitor and enforce blackout after display reconfiguration.
// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes,
// so we need to actively monitor and reapply blackout during this period.
static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000;
// Interval in milliseconds between gamma checks during the monitoring period.
static const int64_t GAMMA_CHECK_INTERVAL_MS = 200;
// Helper function to get UUID string from DisplayID
static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
if (uuid == NULL) {
return "";
}
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
CFRelease(uuid);
if (uuidStr == NULL) {
return "";
}
char buffer[128];
if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(uuidStr);
return std::string(buffer);
}
CFRelease(uuidStr);
return "";
}
// Helper function to find DisplayID by UUID from current online displays
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
if (count == 0) return kCGNullDirectDisplay;
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
for (uint32_t i = 0; i < count; i++) {
std::string uuid = GetDisplayUUID(displays[i]);
if (uuid == targetUuid) {
return displays[i];
}
}
return kCGNullDirectDisplay;
}
// Helper function to restore gamma values for all displays in g_originalGammas.
// Returns true if all displays were restored successfully, false if any failed.
// Note: This function does NOT clear g_originalGammas - caller should do that if needed.
static bool RestoreAllGammas() {
bool allSuccess = true;
for (auto const& [uuid, gamma] : g_originalGammas) {
CGDirectDisplayID d = FindDisplayIdByUUID(uuid);
if (d == kCGNullDirectDisplay) {
NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str());
continue;
}
uint32_t sampleCount = gamma.size() / 3;
if (sampleCount > 0) {
const CGGammaValue* red = gamma.data();
const CGGammaValue* green = red + sampleCount;
const CGGammaValue* blue = green + sampleCount;
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
if (error != kCGErrorSuccess) {
NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
allSuccess = false;
}
}
}
return allSuccess;
}
// Helper function to apply blackout to a single display
static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error);
return false;
}
return true;
}
NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display);
return false;
}
// Forward declaration - defined later in the file
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal();
// Helper function to schedule asynchronous shutdown of privacy mode.
// This is called from DisplayReconfigurationCallback when an error occurs,
// instead of calling TurnOffPrivacyModeInternal() directly. This avoids
// potential issues with unregistering a callback from within itself.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleAsyncPrivacyModeShutdown(const char* reason) {
if (g_privacyModeShutdownRequested) {
// Already requested, no need to schedule again
return;
}
g_privacyModeShutdownRequested = true;
NSLog(@"Privacy mode shutdown requested: %s", reason);
// Schedule the actual shutdown on the main queue asynchronously
// This ensures we're outside the callback when we unregister it
dispatch_async(dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (g_privacyModeShutdownRequested && g_privacyModeActive) {
NSLog(@"Executing deferred privacy mode shutdown");
TurnOffPrivacyModeInternal();
}
g_privacyModeShutdownRequested = false;
});
}
// Helper function to apply blackout to all online displays.
// Must be called while holding g_privacyModeMutex.
static void ApplyBlackoutToAllDisplays() {
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
for (uint32_t i = 0; i < onlineCount; i++) {
ApplyBlackoutToDisplay(onlineDisplays[i]);
}
}
// Helper function to get current timestamp in milliseconds
static uint64_t GetCurrentTimestampMs() {
return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0);
}
// Helper function to check if a display's gamma is currently blacked out (all zeros).
// Returns true if gamma appears to be blacked out, false otherwise.
static bool IsDisplayBlackedOut(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity == 0) {
return true; // Can't check, assume it's fine
}
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) {
return true; // Can't read, assume it's fine
}
// Check if all values are zero (or very close to zero)
for (uint32_t i = 0; i < sampleCount; i++) {
if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) {
return false; // Not blacked out
}
}
return true;
}
// Internal function that monitors and enforces blackout for a period after display reconfiguration.
// This function checks gamma values periodically and reapplies blackout if needed.
// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally).
static void RunBlackoutMonitor() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
g_blackoutReapplicationScheduled = false;
return;
}
uint64_t now = GetCurrentTimestampMs();
// Calculate effective end time based on the last reconfig event
uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS;
// Check all displays and reapply blackout if any has been restored
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
bool needsReapply = false;
for (uint32_t i = 0; i < onlineCount; i++) {
if (!IsDisplayBlackedOut(onlineDisplays[i])) {
needsReapply = true;
break;
}
}
if (needsReapply) {
NSLog(@"Gamma was restored by system, reapplying blackout");
ApplyBlackoutToAllDisplays();
}
// Continue monitoring if we haven't reached the end time
if (now < effectiveEndTime) {
RunBlackoutMonitor();
} else {
NSLog(@"Blackout monitoring period ended");
g_blackoutReapplicationScheduled = false;
}
});
}
// Helper function to start monitoring and enforcing blackout after display reconfiguration.
// This is used after display reconfiguration events because macOS may restore
// default gamma (via ColorSync) at unpredictable times after display changes.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleDelayedBlackoutReapplication(const char* reason) {
// Update timestamp to current time
g_lastReconfigTimestamp = GetCurrentTimestampMs();
NSLog(@"Starting blackout monitor: %s", reason);
// Only schedule if not already scheduled
if (!g_blackoutReapplicationScheduled) {
g_blackoutReapplicationScheduled = true;
RunBlackoutMonitor();
}
// If already scheduled, the running monitor will see the updated timestamp
// and extend its monitoring period
}
// Display reconfiguration callback to handle display connect/disconnect events
//
// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown()
// instead of calling TurnOffPrivacyModeInternal() directly. This is because:
// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister
// this callback, and unregistering a callback from within itself is not explicitly
// guaranteed to be safe by Apple documentation.
// 2. Using async dispatch ensures we're completely outside the callback context when
// performing the cleanup, avoiding any potential undefined behavior.
static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) {
(void)userInfo;
// Note: We need to handle the callback carefully because:
// 1. macOS may call this callback multiple times during display reconfiguration
// 2. The system may restore ColorSync settings after our gamma change
// 3. We should not hold the lock for too long in the callback
// Skip begin configuration flag - wait for the actual change
if (flags & kCGDisplayBeginConfigurationFlag) {
return;
}
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
return;
}
if (flags & kCGDisplayAddFlag) {
// A display was added - apply blackout to it
NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
if (uuid.empty()) {
NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display);
ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display");
return;
}
// Save original gamma if not already saved for this UUID
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display");
return;
}
} else {
NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity");
return;
}
}
// Apply blackout to the new display immediately
if (!ApplyBlackoutToDisplay(display)) {
NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display");
return;
}
// Schedule a delayed re-application to handle ColorSync restoration
// macOS may restore default gamma for ALL displays after a new display is added,
// so we need to reapply blackout to all online displays, not just the new one
ScheduleDelayedBlackoutReapplication("after new display added");
} else if (flags & kCGDisplayRemoveFlag) {
// A display was removed - update our mapping and reapply blackout to remaining displays
NSLog(@"Display %u removed during privacy mode", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
(void)uuid; // UUID retrieved for potential future use or logging
// When a display is removed, macOS may reconfigure other displays and restore their gamma.
// Schedule a delayed re-application of blackout to all remaining online displays.
ScheduleDelayedBlackoutReapplication("after display removal");
} else if (flags & kCGDisplaySetModeFlag) {
// Display mode changed (resolution change, ColorSync/Night Shift interference, etc.)
// macOS resets gamma to default when display mode changes, so we need to reapply blackout.
// Schedule a delayed re-application because ColorSync restoration happens asynchronously.
NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display);
ScheduleDelayedBlackoutReapplication("after display mode change");
}
}
CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
(void)proxy;
(void)refcon;
// Handle EventTap being disabled by system timeout
if (type == kCGEventTapDisabledByTimeout) {
NSLog(@"EventTap was disabled by timeout, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Handle EventTap being disabled by user input
if (type == kCGEventTapDisabledByUserInput) {
NSLog(@"EventTap was disabled by user input, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Allow events explicitly injected by enigo (remote input), identified via custom user data.
int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData);
if (userData == ENIGO_INPUT_EXTRA_VALUE) {
return event;
}
// Block local physical HID input.
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) {
return NULL;
}
return event;
}
// Helper function to set up EventTap on the main thread
// Returns true if EventTap was successfully created and enabled
static bool SetupEventTapOnMainThread() {
__block bool success = false;
void (^setupBlock)(void) = ^{
if (g_eventTap) {
// Already set up
success = true;
return;
}
// Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special
// notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's
// type parameter, not through the event mask. They should NOT be included in eventMask
// as bit-shifting by these values causes undefined behavior.
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
(1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) |
(1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) |
(1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) |
(1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) |
(1 << kCGEventOtherMouseDragged) |
(1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel);
g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
eventMask, MyEventTapCallback, NULL);
if (g_eventTap) {
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CGEventTapEnable(g_eventTap, true);
success = true;
} else {
NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled.");
success = false;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
// Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock.
//
// IMPORTANT: Potential deadlock consideration:
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
setupBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), setupBlock);
}
return success;
}
// Helper function to tear down EventTap on the main thread
static void TeardownEventTapOnMainThread() {
void (^teardownBlock)(void) = ^{
if (g_eventTap) {
CGEventTapEnable(g_eventTap, false);
CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CFRelease(g_runLoopSource);
CFRelease(g_eventTap);
g_eventTap = NULL;
g_runLoopSource = NULL;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
//
// NOTE: We use dispatch_sync here instead of dispatch_async because:
// 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before
// proceeding with gamma restoration - using async would cause race conditions.
// 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order.
//
// IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread):
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
teardownBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), teardownBlock);
}
}
// Internal function to turn off privacy mode without acquiring the mutex
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal() {
if (!g_privacyModeActive) {
return true;
}
// 1. Unregister display reconfiguration callback
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 2. Input - restore (tear down EventTap on main thread)
TeardownEventTapOnMainThread();
// 3. Gamma - restore using UUID to find current DisplayID
bool restoreSuccess = RestoreAllGammas();
// 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net
// This ensures displays return to normal even if our restoration failed or
// if the system (ColorSync/Night Shift) modified gamma during privacy mode
CGDisplayRestoreColorSyncSettings();
// Clean up
g_originalGammas.clear();
g_privacyModeActive = false;
g_privacyModeShutdownRequested = false;
g_lastReconfigTimestamp = 0;
g_blackoutReapplicationScheduled = false;
return restoreSuccess;
}
extern "C" bool MacSetPrivacyMode(bool on) {
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (on) {
// Already in privacy mode
if (g_privacyModeActive) {
return true;
}
// 1. Input Blocking - set up EventTap on main thread
if (!SetupEventTapOnMainThread()) {
return false;
}
// 2. Register display reconfiguration callback to handle hot-plug events
CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 3. Gamma Blackout
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
uint32_t blackoutSuccessCount = 0;
uint32_t blackoutAttemptCount = 0;
for (uint32_t i = 0; i < count; i++) {
CGDirectDisplayID d = displays[i];
std::string uuid = GetDisplayUUID(d);
if (uuid.empty()) {
NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d);
// Privacy mode requires ALL connected displays to be successfully blacked out
// to ensure user privacy. If we can't identify a display (no UUID),
// we can't safely manage its state or restore it later.
// Therefore, we must abort the entire operation and clean up any resources
// already allocated (like event taps and reconfiguration callbacks).
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were already blacked out before this failure
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
// Save original gamma using UUID as key (stable across reconnections)
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str());
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str());
}
}
// Set to black only if we have saved original gamma for this display
if (g_originalGammas.find(uuid) != g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
blackoutAttemptCount++;
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error);
} else {
blackoutSuccessCount++;
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str());
}
}
}
// Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out
if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) {
NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount);
// Clean up: unregister callback and disable event tap since we're failing
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were successfully blacked out
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback");
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
g_privacyModeActive = true;
return true;
} else {
return TurnOffPrivacyModeInternal();
}
}

View File

@@ -23,6 +23,9 @@ pub mod win_mag;
#[cfg(windows)]
pub mod win_topmost_window;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(windows)]
mod win_virtual_display;
#[cfg(windows)]
@@ -105,7 +108,14 @@ lazy_static::lazy_static! {
}
#[cfg(not(windows))]
{
"".to_owned()
#[cfg(target_os = "macos")]
{
macos::PRIVACY_MODE_IMPL.to_owned()
}
#[cfg(not(target_os = "macos"))]
{
"".to_owned()
}
}
};
@@ -127,7 +137,13 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box<dyn PrivacyMode>;
lazy_static::lazy_static! {
static ref PRIVACY_MODE_CREATOR: Arc<Mutex<HashMap<&'static str, PrivacyModeCreator>>> = {
#[cfg(not(windows))]
let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(target_os = "macos")]
{
map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| {
Box::new(macos::PrivacyModeImpl::new(impl_key))
});
}
#[cfg(windows)]
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(windows)]
@@ -333,7 +349,14 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> {
vec_impls
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
// No translation is intended for privacy_mode_impl_macos_tip as it is a
// placeholder for macOS specific privacy mode implementation which currently
// doesn't provide multiple modes like Windows does.
vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")]
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
Vec::new()
}

81
src/privacy_mode/macos.rs Normal file
View File

@@ -0,0 +1,81 @@
use super::{PrivacyMode, PrivacyModeState};
use hbb_common::{anyhow::anyhow, ResultType};
extern "C" {
fn MacSetPrivacyMode(on: bool) -> bool;
}
pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos";
pub struct PrivacyModeImpl {
impl_key: String,
conn_id: i32,
}
impl PrivacyModeImpl {
pub fn new(impl_key: &str) -> Self {
Self {
impl_key: impl_key.to_owned(),
conn_id: 0,
}
}
}
impl PrivacyMode for PrivacyModeImpl {
fn is_async_privacy_mode(&self) -> bool {
false
}
fn init(&self) -> ResultType<()> {
Ok(())
}
fn clear(&mut self) {
unsafe {
MacSetPrivacyMode(false);
}
self.conn_id = 0;
}
fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType<bool> {
if self.check_on_conn_id(conn_id)? {
return Ok(true);
}
let success = unsafe { MacSetPrivacyMode(true) };
if !success {
return Err(anyhow!("Failed to turn on privacy mode"));
}
self.conn_id = conn_id;
Ok(true)
}
fn turn_off_privacy(&mut self, conn_id: i32, _state: Option<PrivacyModeState>) -> ResultType<()> {
// Note: The `_state` parameter is intentionally ignored on macOS.
// On Windows, it's used to notify the connection manager about privacy mode state changes
// (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation
// without the need for such cross-component state synchronization.
self.check_off_conn_id(conn_id)?;
let success = unsafe { MacSetPrivacyMode(false) };
if !success {
return Err(anyhow!("Failed to turn off privacy mode"));
}
self.conn_id = 0;
Ok(())
}
fn pre_conn_id(&self) -> i32 {
self.conn_id
}
fn get_impl_key(&self) -> &str {
&self.impl_key
}
}
impl Drop for PrivacyModeImpl {
fn drop(&mut self) {
// Use the same cleanup logic as other code paths to keep conn_id consistent
// and ensure all cleanup is centralized in one place.
self.clear();
}
}

View File

@@ -40,6 +40,7 @@ lazy_static::lazy_static! {
}
static SHOULD_EXIT: AtomicBool = AtomicBool::new(false);
static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false);
static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false);
#[derive(Clone)]
pub struct RendezvousMediator {
@@ -689,6 +690,7 @@ impl RendezvousMediator {
..Default::default()
});
socket.send(&msg_out).await?;
SENT_REGISTER_PK.store(true, Ordering::SeqCst);
Ok(())
}
@@ -904,3 +906,28 @@ async fn udp_nat_listen(
})?;
Ok(())
}
// When config is not yet synced from root, register_pk may have already been sent with a new generated pk.
// After config sync completes, the pk may change. This struct detects pk changes and triggers
// a re-registration by setting key_confirmed to false.
// NOTE:
// This only corrects PK registration for the current ID. If root uses a non-default mac-generated ID,
// this does not resolve the multi-ID issue by itself.
pub struct CheckIfResendPk {
pk: Option<Vec<u8>>,
}
impl CheckIfResendPk {
pub fn new() -> Self {
Self {
pk: Config::get_cached_pk(),
}
}
}
impl Drop for CheckIfResendPk {
fn drop(&mut self) {
if SENT_REGISTER_PK.load(Ordering::SeqCst) && Config::get_cached_pk() != self.pk {
Config::set_key_confirmed(false);
log::info!("Set key_confirmed to false due to pk changed, will resend register_pk");
}
}
}

View File

@@ -82,6 +82,10 @@ type ConnMap = HashMap<i32, ConnInner>;
#[cfg(any(target_os = "macos", target_os = "linux"))]
const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3;
#[cfg(any(target_os = "macos", target_os = "linux"))]
// 3s is enough for at least one initial sync attempt:
// 0.3s backoff + up to 1s connect timeout + up to 1s response timeout.
const CONFIG_SYNC_INITIAL_WAIT_SECS: u64 = 3;
lazy_static::lazy_static! {
pub static ref CHILD_PROCESS: Childs = Default::default();
@@ -600,7 +604,7 @@ pub async fn start_server(is_server: bool, no_server: bool) {
allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
tokio::spawn(async { sync_and_watch_config_dir().await });
wait_initial_config_sync().await;
#[cfg(target_os = "windows")]
crate::platform::try_kill_broker();
#[cfg(feature = "hwcodec")]
@@ -685,13 +689,43 @@ pub async fn start_ipc_url_server() {
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
async fn sync_and_watch_config_dir() {
async fn wait_initial_config_sync() {
if crate::platform::is_root() {
return;
}
// Non-server process should not block startup, but still keeps background sync/watch alive.
if !crate::is_server() {
tokio::spawn(async move {
sync_and_watch_config_dir(None).await;
});
return;
}
let (sync_done_tx, mut sync_done_rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
sync_and_watch_config_dir(Some(sync_done_tx)).await;
});
// Server process waits up to N seconds for initial root->local sync to reduce stale-start window.
tokio::select! {
_ = &mut sync_done_rx => {
}
_ = tokio::time::sleep(Duration::from_secs(CONFIG_SYNC_INITIAL_WAIT_SECS)) => {
log::warn!(
"timed out waiting {}s for initial config sync, continue startup and keep syncing in background",
CONFIG_SYNC_INITIAL_WAIT_SECS
);
}
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
async fn sync_and_watch_config_dir(sync_done_tx: Option<tokio::sync::oneshot::Sender<()>>) {
let mut cfg0 = (Config::get(), Config2::get());
let mut synced = false;
let mut is_root_config_empty = false;
let mut sync_done_tx = sync_done_tx;
let tries = if crate::is_server() { 30 } else { 3 };
log::debug!("#tries of ipc service connection: {}", tries);
use hbb_common::sleep;
@@ -706,6 +740,8 @@ async fn sync_and_watch_config_dir() {
Data::SyncConfig(Some(configs)) => {
let (config, config2) = *configs;
let _chk = crate::ipc::CheckIfRestart::new();
#[cfg(target_os = "macos")]
let _chk_pk = crate::CheckIfResendPk::new();
if !config.is_empty() {
if cfg0.0 != config {
cfg0.0 = config.clone();
@@ -717,8 +753,20 @@ async fn sync_and_watch_config_dir() {
Config2::set(config2);
log::info!("sync config2 from root");
}
} else {
// only on macos, because this issue was only reproduced on macos
#[cfg(target_os = "macos")]
{
// root config is empty, mark for sync in watch loop
// to prevent root from generating a new config on login screen
is_root_config_empty = true;
}
}
synced = true;
// Notify startup waiter once initial sync phase finishes successfully.
if let Some(tx) = sync_done_tx.take() {
let _ = tx.send(());
}
}
_ => {}
};
@@ -729,8 +777,14 @@ async fn sync_and_watch_config_dir() {
loop {
sleep(CONFIG_SYNC_INTERVAL_SECS).await;
let cfg = (Config::get(), Config2::get());
if cfg != cfg0 {
log::info!("config updated, sync to root");
let should_sync =
cfg != cfg0 || (is_root_config_empty && !cfg.0.is_empty());
if should_sync {
if is_root_config_empty {
log::info!("root config is empty, sync our config to root");
} else {
log::info!("config updated, sync to root");
}
match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await {
Err(e) => {
log::error!("sync config to root failed: {}", e);
@@ -745,6 +799,7 @@ async fn sync_and_watch_config_dir() {
_ => {
cfg0 = cfg;
conn.next_timeout(1000).await.ok();
is_root_config_empty = false;
}
}
}
@@ -755,6 +810,10 @@ async fn sync_and_watch_config_dir() {
}
}
}
// Notify startup waiter even when initial sync is skipped/failed, to avoid unnecessary waiting.
if let Some(tx) = sync_done_tx.take() {
let _ = tx.send(());
}
log::warn!("skipped config sync");
}

View File

@@ -1420,7 +1420,7 @@ impl Connection {
pi.platform = "Android".into();
}
#[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))]
let platform_additions = serde_json::Map::new();
let mut platform_additions = serde_json::Map::new();
#[cfg(any(
target_os = "windows",
target_os = "linux",
@@ -1453,6 +1453,13 @@ impl Connection {
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(target_os = "macos")]
{
platform_additions.insert(
"supported_privacy_mode_impl".into(),
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
{

View File

@@ -111,6 +111,10 @@ struct Input {
const KEY_CHAR_START: u64 = 9999;
// XKB keycode for Insert key (evdev KEY_INSERT code 110 + 8 for XKB offset)
#[cfg(target_os = "linux")]
const XKB_KEY_INSERT: u16 = evdev::Key::KEY_INSERT.code() + 8;
#[derive(Clone, Default)]
pub struct MouseCursorSub {
inner: ConnInner,
@@ -1105,8 +1109,12 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) {
// Clamp delta to prevent extreme/malicious values from reaching OS APIs.
// This matches the Flutter client's kMaxRelativeMouseDelta constant.
const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000;
let dx = evt.x.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
let dy = evt.y.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
let dx = evt
.x
.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
let dy = evt
.y
.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA);
en.mouse_move_relative(dx, dy);
// Get actual cursor position after relative movement for tracking
if let Some((x, y)) = crate::get_cursor_pos() {
@@ -1465,20 +1473,26 @@ fn map_keyboard_mode(evt: &KeyEvent) {
// Wayland
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() {
let mut en = ENIGO.lock().unwrap();
let code = evt.chr() as u16;
if evt.down {
en.key_down(enigo::Key::Raw(code)).ok();
} else {
en.key_up(enigo::Key::Raw(code));
}
wayland_send_raw_key(evt.chr() as u16, evt.down);
return;
}
sim_rdev_rawkey_position(evt.chr() as _, evt.down);
}
/// Send raw keycode on Wayland via the active backend (uinput or RemoteDesktop portal).
/// The keycode is expected to be a Linux keycode (evdev code + 8 for X11 compatibility).
#[cfg(target_os = "linux")]
#[inline]
fn wayland_send_raw_key(code: u16, down: bool) {
let mut en = ENIGO.lock().unwrap();
if down {
en.key_down(enigo::Key::Raw(code)).ok();
} else {
en.key_up(enigo::Key::Raw(code));
}
}
#[cfg(target_os = "macos")]
fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) {
// When long-pressed the command key, then press and release
@@ -1559,6 +1573,20 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
}
fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
// On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
// Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed)
if !is_hotkey_modifier_pressed(en) {
if down {
if let Ok(c) = char::try_from(chr) {
input_char_via_clipboard_server(en, c);
}
}
return;
}
}
let key = char_value_to_key(chr);
if down {
@@ -1578,15 +1606,136 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) {
}
fn process_unicode(en: &mut Enigo, chr: u32) {
// On Wayland with uinput mode, use clipboard for character input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
if let Ok(c) = char::try_from(chr) {
input_char_via_clipboard_server(en, c);
}
return;
}
if let Ok(chr) = char::try_from(chr) {
en.key_sequence(&chr.to_string());
}
}
fn process_seq(en: &mut Enigo, sequence: &str) {
// On Wayland with uinput mode, use clipboard for text input
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
input_text_via_clipboard_server(en, sequence);
return;
}
en.key_sequence(&sequence);
}
/// Delay in milliseconds to wait for clipboard to sync on Wayland.
/// This is an empirical value — Wayland provides no callback or event to confirm
/// clipboard content has been received by the compositor. Under heavy system load,
/// this delay may be insufficient, but there is no reliable alternative mechanism.
#[cfg(target_os = "linux")]
const CLIPBOARD_SYNC_DELAY_MS: u64 = 50;
/// Internal: Set clipboard content without delay.
/// Returns true if clipboard was set successfully.
#[cfg(target_os = "linux")]
fn set_clipboard_content(text: &str) -> bool {
use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux};
let mut clipboard = match Clipboard::new() {
Ok(cb) => cb,
Err(e) => {
log::error!("set_clipboard_content: failed to create clipboard: {:?}", e);
return false;
}
};
// Set both CLIPBOARD and PRIMARY selections
// Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD
if let Err(e) = clipboard
.set()
.clipboard(LinuxClipboardKind::Clipboard)
.text(text.to_owned())
{
log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e);
return false;
}
if let Err(e) = clipboard
.set()
.clipboard(LinuxClipboardKind::Primary)
.text(text.to_owned())
{
log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e);
// Continue anyway, CLIPBOARD might work
}
true
}
/// Set clipboard content for paste operation (sync version for use in blocking contexts).
///
/// Note: The original clipboard content is intentionally NOT restored after paste.
/// Restoring clipboard could cause race conditions where subsequent keystrokes
/// might accidentally paste the old clipboard content instead of the intended input.
/// This trade-off prioritizes input reliability over preserving clipboard state.
#[cfg(target_os = "linux")]
#[inline]
pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool {
if !set_clipboard_content(text) {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS));
true
}
/// Check if a character is ASCII printable (0x20-0x7E).
#[cfg(target_os = "linux")]
#[inline]
pub(super) fn is_ascii_printable(c: char) -> bool {
c as u32 >= 0x20 && c as u32 <= 0x7E
}
/// Input a single character via clipboard + Shift+Insert in server process.
#[cfg(target_os = "linux")]
#[inline]
fn input_char_via_clipboard_server(en: &mut Enigo, chr: char) {
input_text_via_clipboard_server(en, &chr.to_string());
}
/// Input text via clipboard + Shift+Insert in server process.
/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals.
///
/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale.
#[cfg(target_os = "linux")]
fn input_text_via_clipboard_server(en: &mut Enigo, text: &str) {
if text.is_empty() {
return;
}
if !set_clipboard_for_paste_sync(text) {
return;
}
// Use ENIGO's custom_keyboard directly to avoid creating new IPC connections
// which would cause excessive logging and keyboard device creation/destruction
if en.key_down(Key::Shift).is_err() {
log::error!("input_text_via_clipboard_server: failed to press Shift, skipping paste");
return;
}
if en.key_down(Key::Raw(XKB_KEY_INSERT)).is_err() {
log::error!("input_text_via_clipboard_server: failed to press Insert, releasing Shift");
en.key_up(Key::Shift);
return;
}
en.key_up(Key::Raw(XKB_KEY_INSERT));
en.key_up(Key::Shift);
// Brief delay to allow the target application to process the paste event.
// Empirical value — no reliable synchronization mechanism exists on Wayland.
std::thread::sleep(std::time::Duration::from_millis(20));
}
#[cfg(not(target_os = "macos"))]
fn release_keys(en: &mut Enigo, to_release: &Vec<Key>) {
for key in to_release {
@@ -1621,6 +1770,64 @@ fn is_function_key(ck: &EnumOrUnknown<ControlKey>) -> bool {
return res;
}
/// Check if any hotkey modifier (Ctrl/Alt/Meta) is currently pressed.
/// Used to detect hotkey combinations like Ctrl+C, Alt+Tab, etc.
///
/// Note: Shift is intentionally NOT checked here. Shift+character produces a different
/// character (e.g., Shift+a → 'A'), which is normal text input, not a hotkey.
/// Shift is only relevant as a hotkey modifier when combined with Ctrl/Alt/Meta
/// (e.g., Ctrl+Shift+Z), in which case this function already returns true via Ctrl.
#[cfg(target_os = "linux")]
#[inline]
fn is_hotkey_modifier_pressed(en: &mut Enigo) -> bool {
get_modifier_state(Key::Control, en)
|| get_modifier_state(Key::RightControl, en)
|| get_modifier_state(Key::Alt, en)
|| get_modifier_state(Key::RightAlt, en)
|| get_modifier_state(Key::Meta, en)
|| get_modifier_state(Key::RWin, en)
}
/// Release Shift keys before character input in Legacy/Translate mode.
/// In these modes, the character has already been converted by the client,
/// so we should input it directly without Shift modifier affecting the result.
///
/// Note: Does NOT release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed,
/// to preserve combinations like Ctrl+Shift+Z.
#[cfg(target_os = "linux")]
fn release_shift_for_char_input(en: &mut Enigo) {
// Don't release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed.
// This preserves combinations like Ctrl+Shift+Z.
if is_hotkey_modifier_pressed(en) {
return;
}
// In translate mode, the client has already converted the keystroke to a character
// (e.g., Shift+a → 'A'). We release Shift here so the server inputs the character
// directly without Shift affecting the result.
//
// Shift is intentionally NOT restored after input — the client will send an explicit
// Shift key_up event when the user physically releases Shift. Restoring it here would
// cause a brief Shift re-press that could interfere with the next input event.
let is_x11 = crate::platform::linux::is_x11();
if get_modifier_state(Key::Shift, en) {
if !is_x11 {
en.key_up(Key::Shift);
} else {
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft));
}
}
if get_modifier_state(Key::RightShift, en) {
if !is_x11 {
en.key_up(Key::RightShift);
} else {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight));
}
}
}
fn legacy_keyboard_mode(evt: &KeyEvent) {
#[cfg(windows)]
crate::platform::windows::try_change_desktop();
@@ -1640,11 +1847,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
process_control_key(&mut en, &ck, down)
}
Some(key_event::Union::Chr(chr)) => {
// For character input in Legacy mode, we need to release Shift first.
// The character has already been converted by the client, so we should
// input it directly without Shift modifier affecting the result.
// Only Ctrl/Alt/Meta should be kept for hotkeys like Ctrl+C.
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
let record_key = chr as u64 + KEY_CHAR_START;
record_pressed_key(KeysDown::EnigoKey(record_key), down);
process_chr(&mut en, chr, down)
}
Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr),
Some(key_event::Union::Unicode(chr)) => {
// Same as Chr: release Shift for Unicode input
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
process_unicode(&mut en, chr)
}
Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq),
_ => {}
}
@@ -1665,6 +1885,51 @@ fn translate_process_code(code: u32, down: bool) {
fn translate_keyboard_mode(evt: &KeyEvent) {
match &evt.union {
Some(key_event::Union::Seq(seq)) => {
// On Wayland, handle character input directly in this (--server) process using clipboard.
// This function runs in the --server process (logged-in user session), which has
// WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here.
//
// Why not let it go through uinput IPC:
// 1. For uinput mode: the uinput service thread runs in the --service (root) process,
// which typically lacks user session environment. Clipboard operations there are
// unreliable. Handling clipboard here avoids that issue.
// 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms
// based on its internal modifier state, which may not match our released state.
// Using clipboard bypasses this issue entirely.
#[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() {
let mut en = ENIGO.lock().unwrap();
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
// For hotkeys, we send character-based key events via Enigo instead of
// using the clipboard. This relies on the local keyboard layout for
// mapping characters to physical keys.
// This assumes client and server use the same keyboard layout (common case).
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
// characters which are mappable on most keyboard layouts.
if is_hotkey_modifier_pressed(&mut en) {
// For hotkeys, send character-based key events via Enigo.
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
for chr in seq.chars() {
if !is_ascii_printable(chr) {
log::warn!(
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
);
}
en.key_click(Key::Layout(chr));
}
return;
}
// Normal text input: release Shift and use clipboard
release_shift_for_char_input(&mut en);
input_text_via_clipboard_server(&mut en, seq);
return;
}
// Fr -> US
// client: Shift + & => 1(send to remote)
// remote: Shift + 1 => !
@@ -1682,11 +1947,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
#[cfg(target_os = "linux")]
let simulate_win_hot_key = false;
if !simulate_win_hot_key {
if get_modifier_state(Key::Shift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft));
}
if get_modifier_state(Key::RightShift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight));
#[cfg(target_os = "linux")]
release_shift_for_char_input(&mut en);
#[cfg(target_os = "windows")]
{
if get_modifier_state(Key::Shift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft));
}
if get_modifier_state(Key::RightShift, &mut en) {
simulate_(&EventType::KeyRelease(RdevKey::ShiftRight));
}
}
}
for chr in seq.chars() {
@@ -1706,7 +1976,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
Some(key_event::Union::Chr(..)) => {
#[cfg(target_os = "windows")]
translate_process_code(evt.chr(), evt.down);
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "linux")]
{
if !crate::platform::linux::is_x11() {
// Wayland: use uinput to send raw keycode
wayland_send_raw_key(evt.chr() as u16, evt.down);
} else {
sim_rdev_rawkey_position(evt.chr() as _, evt.down);
}
}
#[cfg(target_os = "macos")]
sim_rdev_rawkey_position(evt.chr() as _, evt.down);
}
Some(key_event::Union::Unicode(..)) => {
@@ -1717,7 +1996,11 @@ fn translate_keyboard_mode(evt: &KeyEvent) {
simulate_win2win_hotkey(*code, evt.down);
}
_ => {
log::debug!("Unreachable. Unexpected key event {:?}", &evt);
log::debug!(
"Unreachable. Unexpected key event (mode={:?}, down={:?})",
&evt.mode,
&evt.down
);
}
}
}

View File

@@ -1,7 +1,8 @@
use crate::uinput::service::map_key;
use super::input_service::set_clipboard_for_paste_sync;
use crate::uinput::service::{can_input_via_keysym, char_to_keysym, map_key};
use dbus::{blocking::SyncConnection, Path};
use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable};
use hbb_common::ResultType;
use hbb_common::{log, ResultType};
use scrap::wayland::pipewire::{get_portal, PwStreamInfo};
use scrap::wayland::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal;
use std::collections::HashMap;
@@ -19,14 +20,74 @@ pub mod client {
const PRESSED_DOWN_STATE: u32 = 1;
const PRESSED_UP_STATE: u32 = 0;
/// Modifier key state tracking for RDP input.
/// Portal API doesn't provide a way to query key state, so we track it ourselves.
#[derive(Default)]
struct ModifierState {
shift_left: bool,
shift_right: bool,
ctrl_left: bool,
ctrl_right: bool,
alt_left: bool,
alt_right: bool,
meta_left: bool,
meta_right: bool,
}
impl ModifierState {
fn update(&mut self, key: &Key, down: bool) {
match key {
Key::Shift => self.shift_left = down,
Key::RightShift => self.shift_right = down,
Key::Control => self.ctrl_left = down,
Key::RightControl => self.ctrl_right = down,
Key::Alt => self.alt_left = down,
Key::RightAlt => self.alt_right = down,
Key::Meta | Key::Super | Key::Windows | Key::Command => self.meta_left = down,
Key::RWin => self.meta_right = down,
// Handle raw keycodes for modifier keys (Linux evdev codes + 8)
// In translate mode, modifier keys may be sent as Chr events with raw keycodes.
// The +8 offset converts evdev codes to X11/XKB keycodes.
Key::Raw(code) => {
const EVDEV_OFFSET: u16 = 8;
const KEY_LEFTSHIFT: u16 = evdev::Key::KEY_LEFTSHIFT.code() + EVDEV_OFFSET;
const KEY_RIGHTSHIFT: u16 = evdev::Key::KEY_RIGHTSHIFT.code() + EVDEV_OFFSET;
const KEY_LEFTCTRL: u16 = evdev::Key::KEY_LEFTCTRL.code() + EVDEV_OFFSET;
const KEY_RIGHTCTRL: u16 = evdev::Key::KEY_RIGHTCTRL.code() + EVDEV_OFFSET;
const KEY_LEFTALT: u16 = evdev::Key::KEY_LEFTALT.code() + EVDEV_OFFSET;
const KEY_RIGHTALT: u16 = evdev::Key::KEY_RIGHTALT.code() + EVDEV_OFFSET;
const KEY_LEFTMETA: u16 = evdev::Key::KEY_LEFTMETA.code() + EVDEV_OFFSET;
const KEY_RIGHTMETA: u16 = evdev::Key::KEY_RIGHTMETA.code() + EVDEV_OFFSET;
match *code {
KEY_LEFTSHIFT => self.shift_left = down,
KEY_RIGHTSHIFT => self.shift_right = down,
KEY_LEFTCTRL => self.ctrl_left = down,
KEY_RIGHTCTRL => self.ctrl_right = down,
KEY_LEFTALT => self.alt_left = down,
KEY_RIGHTALT => self.alt_right = down,
KEY_LEFTMETA => self.meta_left = down,
KEY_RIGHTMETA => self.meta_right = down,
_ => {}
}
}
_ => {}
}
}
}
pub struct RdpInputKeyboard {
conn: Arc<SyncConnection>,
session: Path<'static>,
modifier_state: ModifierState,
}
impl RdpInputKeyboard {
pub fn new(conn: Arc<SyncConnection>, session: Path<'static>) -> ResultType<Self> {
Ok(Self { conn, session })
Ok(Self {
conn,
session,
modifier_state: ModifierState::default(),
})
}
}
@@ -39,29 +100,192 @@ pub mod client {
self
}
fn get_key_state(&mut self, _: Key) -> bool {
// no api for this
false
fn get_key_state(&mut self, key: Key) -> bool {
// Use tracked modifier state for supported keys
match key {
Key::Shift => self.modifier_state.shift_left,
Key::RightShift => self.modifier_state.shift_right,
Key::Control => self.modifier_state.ctrl_left,
Key::RightControl => self.modifier_state.ctrl_right,
Key::Alt => self.modifier_state.alt_left,
Key::RightAlt => self.modifier_state.alt_right,
Key::Meta | Key::Super | Key::Windows | Key::Command => {
self.modifier_state.meta_left
}
Key::RWin => self.modifier_state.meta_right,
_ => false,
}
}
fn key_sequence(&mut self, s: &str) {
for c in s.chars() {
let key = Key::Layout(c);
let _ = handle_key(true, key, self.conn.clone(), &self.session);
let _ = handle_key(false, key, self.conn.clone(), &self.session);
let keysym = char_to_keysym(c);
// ASCII characters: use keysym
if can_input_via_keysym(c, keysym) {
if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym down: {:?}", e);
}
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e);
}
} else {
// Non-ASCII: use clipboard
input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session);
}
}
}
fn key_down(&mut self, key: Key) -> enigo::ResultType {
handle_key(true, key, self.conn.clone(), &self.session)?;
if let Key::Layout(chr) = key {
let keysym = char_to_keysym(chr);
// ASCII characters: use keysym
if can_input_via_keysym(chr, keysym) {
send_keysym(keysym, true, self.conn.clone(), &self.session)?;
} else {
// Non-ASCII: use clipboard (complete key press in key_down)
input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session);
}
} else {
handle_key(true, key.clone(), self.conn.clone(), &self.session)?;
// Update modifier state only after successful send —
// if handle_key fails, we don't want stale "pressed" state
// affecting subsequent key event decisions.
self.modifier_state.update(&key, true);
}
Ok(())
}
fn key_up(&mut self, key: Key) {
let _ = handle_key(false, key, self.conn.clone(), &self.session);
// Intentionally asymmetric with key_down: update state BEFORE sending.
// On release, we always mark as released even if the send fails below,
// to avoid permanently stuck-modifier state in our tracker. The trade-off
// (tracker says "released" while OS may still have it pressed) is acceptable
// because such failures are rare and subsequent events will resynchronize.
self.modifier_state.update(&key, false);
if let Key::Layout(chr) = key {
// ASCII characters: send keysym up if we also sent it on key_down
let keysym = char_to_keysym(chr);
if can_input_via_keysym(chr, keysym) {
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session)
{
log::error!("Failed to send keysym up: {:?}", e);
}
}
// Non-ASCII: already handled completely in key_down via clipboard paste,
// no corresponding release needed (clipboard paste is an atomic operation)
} else {
if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) {
log::error!("Failed to handle key up: {:?}", e);
}
}
}
fn key_click(&mut self, key: Key) {
let _ = handle_key(true, key, self.conn.clone(), &self.session);
let _ = handle_key(false, key, self.conn.clone(), &self.session);
if let Key::Layout(chr) = key {
let keysym = char_to_keysym(chr);
// ASCII characters: use keysym
if can_input_via_keysym(chr, keysym) {
if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym down: {:?}", e);
}
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e);
}
} else {
// Non-ASCII: use clipboard
input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session);
}
} else {
if let Err(e) = handle_key(true, key.clone(), self.conn.clone(), &self.session) {
log::error!("Failed to handle key down: {:?}", e);
} else {
// Only mark modifier as pressed if key-down was actually delivered
self.modifier_state.update(&key, true);
}
// Always mark as released to avoid stuck-modifier state
self.modifier_state.update(&key, false);
if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) {
log::error!("Failed to handle key up: {:?}", e);
}
}
}
}
/// Input text via clipboard + Shift+Insert.
/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals.
///
/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale.
fn input_text_via_clipboard(text: &str, conn: Arc<SyncConnection>, session: &Path<'static>) {
if text.is_empty() {
return;
}
if !set_clipboard_for_paste_sync(text) {
return;
}
let portal = get_portal(&conn);
let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32;
let insert_keycode = evdev::Key::KEY_INSERT.code() as i32;
// Send Shift+Insert (universal paste shortcut)
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_DOWN_STATE,
) {
log::error!("input_text_via_clipboard: failed to press Shift: {:?}", e);
return;
}
// Press Insert
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
insert_keycode,
PRESSED_DOWN_STATE,
) {
log::error!("input_text_via_clipboard: failed to press Insert: {:?}", e);
// Still try to release Shift.
// Note: clipboard has already been set by set_clipboard_for_paste_sync but paste
// never happened. We don't attempt to restore the previous clipboard contents
// because reading the clipboard on Wayland requires focus/permission.
let _ = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
);
return;
}
// Release Insert
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
insert_keycode,
PRESSED_UP_STATE,
) {
log::error!(
"input_text_via_clipboard: failed to release Insert: {:?}",
e
);
}
// Release Shift
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::error!("input_text_via_clipboard: failed to release Shift: {:?}", e);
}
}
@@ -196,6 +420,39 @@ pub mod client {
}
}
/// Send a keysym via RemoteDesktop portal.
fn send_keysym(
keysym: i32,
down: bool,
conn: Arc<SyncConnection>,
session: &Path<'static>,
) -> ResultType<()> {
let state: u32 = if down {
PRESSED_DOWN_STATE
} else {
PRESSED_UP_STATE
};
let portal = get_portal(&conn);
log::trace!(
"send_keysym: calling notify_keyboard_keysym, keysym={:#x}, state={}",
keysym,
state
);
match remote_desktop_portal::notify_keyboard_keysym(
&portal,
session,
HashMap::new(),
keysym,
state,
) {
Ok(_) => {
log::trace!("send_keysym: notify_keyboard_keysym succeeded");
Ok(())
}
Err(e) => Err(e.into()),
}
}
fn get_raw_evdev_keycode(key: u16) -> i32 {
// 8 is the offset between xkb and evdev
let mut key = key as i32 - 8;
@@ -231,22 +488,86 @@ pub mod client {
}
_ => {
if let Ok((key, is_shift)) = map_key(&key) {
if is_shift {
remote_desktop_portal::notify_keyboard_keycode(
let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32;
if down {
// Press: Shift down first, then key down
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
state,
) {
log::error!("handle_key: failed to press Shift: {:?}", e);
return Err(e.into());
}
}
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
evdev::Key::KEY_LEFTSHIFT.code() as i32,
key.code() as i32,
state,
)?;
) {
log::error!("handle_key: failed to press key: {:?}", e);
// Best-effort: release Shift if it was pressed
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::warn!(
"handle_key: best-effort Shift release also failed: {:?}",
e
);
}
}
return Err(e.into());
}
} else {
// Release: key up first, then Shift up
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
key.code() as i32,
PRESSED_UP_STATE,
) {
log::error!("handle_key: failed to release key: {:?}", e);
// Best-effort: still try to release Shift
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::warn!(
"handle_key: best-effort Shift release also failed: {:?}",
e
);
}
}
return Err(e.into());
}
if is_shift {
if let Err(e) = remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
shift_keycode,
PRESSED_UP_STATE,
) {
log::error!("handle_key: failed to release Shift: {:?}", e);
return Err(e.into());
}
}
}
remote_desktop_portal::notify_keyboard_keycode(
&portal,
&session,
HashMap::new(),
key.code() as i32,
state,
)?;
}
}
}

View File

@@ -90,6 +90,13 @@ pub mod client {
}
fn key_sequence(&mut self, sequence: &str) {
// Sequence events are normally handled in the --server process before reaching here.
// Forward via IPC as a fallback — input_text_wayland can still handle ASCII chars
// via keysym/uinput, though non-ASCII will be skipped (no clipboard in --service).
log::debug!(
"UInputKeyboard::key_sequence called (len={})",
sequence.len()
);
allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string()))));
}
@@ -178,6 +185,9 @@ pub mod client {
pub mod service {
use super::*;
use hbb_common::lazy_static;
use scrap::wayland::{
pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop,
};
use std::{collections::HashMap, sync::Mutex};
lazy_static::lazy_static! {
@@ -309,6 +319,9 @@ pub mod service {
('/', (evdev::Key::KEY_SLASH, false)),
(';', (evdev::Key::KEY_SEMICOLON, false)),
('\'', (evdev::Key::KEY_APOSTROPHE, false)),
// Space is intentionally in both KEY_MAP_LAYOUT (char-to-evdev for text input)
// and KEY_MAP (Key::Space for key events). Both maps serve different lookup paths.
(' ', (evdev::Key::KEY_SPACE, false)),
// Shift + key
('A', (evdev::Key::KEY_A, true)),
@@ -364,6 +377,155 @@ pub mod service {
static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0)));
}
/// Input text on Wayland using layout-independent methods.
/// ASCII chars (0x20-0x7E): Portal keysym or uinput fallback
/// Non-ASCII chars: skipped — this runs in the --service (root) process where clipboard
/// operations are unreliable (typically no user session environment).
/// Non-ASCII input is normally handled by the --server process via input_text_via_clipboard_server.
fn input_text_wayland(text: &str, keyboard: &mut VirtualDevice) {
let portal_info = {
let session_info = RDP_SESSION_INFO.lock().unwrap();
session_info
.as_ref()
.map(|info| (info.conn.clone(), info.session.clone()))
};
for c in text.chars() {
let keysym = char_to_keysym(c);
if can_input_via_keysym(c, keysym) {
// Try Portal first — down+up on the same channel
if let Some((ref conn, ref session)) = portal_info {
let portal = scrap::wayland::pipewire::get_portal(conn);
if portal
.notify_keyboard_keysym(session, HashMap::new(), keysym, 1)
.is_ok()
{
if let Err(e) =
portal.notify_keyboard_keysym(session, HashMap::new(), keysym, 0)
{
log::warn!(
"input_text_wayland: portal key-up failed for keysym {:#x}: {:?}",
keysym,
e
);
}
continue;
}
}
// Portal unavailable or failed, fallback to uinput (down+up together)
let key = enigo::Key::Layout(c);
if let Ok((evdev_key, is_shift)) = map_key(&key) {
let mut shift_pressed = false;
if is_shift {
let shift_down =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
if keyboard.emit(&[shift_down]).is_ok() {
shift_pressed = true;
} else {
log::warn!("input_text_wayland: failed to press Shift for '{}'", c);
}
}
let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1);
let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0);
allow_err!(keyboard.emit(&[key_down, key_up]));
if shift_pressed {
let shift_up =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0);
allow_err!(keyboard.emit(&[shift_up]));
}
}
} else {
log::debug!("Skipping non-ASCII character in uinput service (no clipboard access)");
}
}
}
/// Send a single key down or up event for a Layout character.
/// Used by KeyDown/KeyUp to maintain correct press/release semantics.
/// `down`: true for key press, false for key release.
fn input_char_wayland_key_event(chr: char, down: bool, keyboard: &mut VirtualDevice) {
let keysym = char_to_keysym(chr);
let portal_state: u32 = if down { 1 } else { 0 };
if can_input_via_keysym(chr, keysym) {
let portal_info = {
let session_info = RDP_SESSION_INFO.lock().unwrap();
session_info
.as_ref()
.map(|info| (info.conn.clone(), info.session.clone()))
};
if let Some((ref conn, ref session)) = portal_info {
let portal = scrap::wayland::pipewire::get_portal(conn);
if portal
.notify_keyboard_keysym(session, HashMap::new(), keysym, portal_state)
.is_ok()
{
return;
}
}
// Portal unavailable or failed, fallback to uinput
let key = enigo::Key::Layout(chr);
if let Ok((evdev_key, is_shift)) = map_key(&key) {
if down {
// Press: Shift↓ (if needed) → Key↓
if is_shift {
let shift_down =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
if let Err(e) = keyboard.emit(&[shift_down]) {
log::warn!("input_char_wayland_key_event: failed to press Shift for '{}': {:?}", chr, e);
}
}
let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1);
allow_err!(keyboard.emit(&[key_down]));
} else {
// Release: Key↑ → Shift↑ (if needed)
let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0);
allow_err!(keyboard.emit(&[key_up]));
if is_shift {
let shift_up =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0);
if let Err(e) = keyboard.emit(&[shift_up]) {
log::warn!("input_char_wayland_key_event: failed to release Shift for '{}': {:?}", chr, e);
}
}
}
}
} else {
// Non-ASCII: no reliable down/up semantics available.
// Clipboard paste is atomic and handled elsewhere.
log::debug!(
"Skipping non-ASCII character key {} in uinput service",
if down { "down" } else { "up" }
);
}
}
/// Check if character can be input via keysym (ASCII printable with valid keysym).
#[inline]
pub(crate) fn can_input_via_keysym(c: char, keysym: i32) -> bool {
// ASCII printable: 0x20 (space) to 0x7E (tilde)
(c as u32 >= 0x20 && c as u32 <= 0x7E) && keysym != 0
}
/// Convert a Unicode character to X11 keysym.
pub(crate) fn char_to_keysym(c: char) -> i32 {
let codepoint = c as u32;
if codepoint == 0 {
// Null character has no keysym
0
} else if (0x20..=0x7E).contains(&codepoint) {
// ASCII printable (0x20-0x7E): keysym == Unicode codepoint
codepoint as i32
} else if (0xA0..=0xFF).contains(&codepoint) {
// Latin-1 supplement (0xA0-0xFF): keysym == Unicode codepoint (per X11 keysym spec)
codepoint as i32
} else {
// Everything else (control chars 0x01-0x1F, DEL 0x7F, and all other non-ASCII Unicode):
// keysym = 0x01000000 | codepoint (X11 Unicode keysym encoding)
(0x0100_0000 | codepoint) as i32
}
}
fn create_uinput_keyboard() -> ResultType<VirtualDevice> {
// TODO: ensure keys here
let mut keys = AttributeSet::<evdev::Key>::new();
@@ -390,13 +552,13 @@ pub mod service {
pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> {
if let Some(k) = KEY_MAP.get(&key) {
log::trace!("mapkey {:?}, get {:?}", &key, &k);
log::trace!("mapkey matched in KEY_MAP, evdev={:?}", &k);
return Ok((k.clone(), false));
} else {
match key {
enigo::Key::Layout(c) => {
if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) {
log::trace!("mapkey {:?}, get {:?}", &key, k);
log::trace!("mapkey Layout matched, evdev={:?}", k);
return Ok((k.clone(), is_shift.clone()));
}
}
@@ -421,41 +583,68 @@ pub mod service {
keyboard: &mut VirtualDevice,
data: &DataKeyboard,
) {
log::trace!("handle_keyboard {:?}", &data);
let data_desc = match data {
DataKeyboard::Sequence(seq) => format!("Sequence(len={})", seq.len()),
DataKeyboard::KeyDown(Key::Layout(_))
| DataKeyboard::KeyUp(Key::Layout(_))
| DataKeyboard::KeyClick(Key::Layout(_)) => "Layout(<redacted>)".to_string(),
_ => format!("{:?}", data),
};
log::trace!("handle_keyboard received: {}", data_desc);
match data {
DataKeyboard::Sequence(_seq) => {
// ignore
DataKeyboard::Sequence(seq) => {
// Normally handled by --server process (input_text_via_clipboard_server).
// Fallback: input_text_wayland handles ASCII via keysym/uinput;
// non-ASCII will be skipped (no clipboard access in --service process).
if !seq.is_empty() {
input_text_wayland(seq, keyboard);
}
}
DataKeyboard::KeyDown(enigo::Key::Raw(code)) => {
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
allow_err!(keyboard.emit(&[down_event]));
}
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event]));
}
DataKeyboard::KeyDown(key) => {
if let Ok((k, is_shift)) = map_key(key) {
if is_shift {
let down_event =
InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1);
allow_err!(keyboard.emit(&[down_event]));
}
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
if *code < 8 {
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let down_event = InputEvent::new(EventType::KEY, *code - 8, 1);
allow_err!(keyboard.emit(&[down_event]));
}
}
DataKeyboard::KeyUp(key) => {
if let Ok((k, _)) = map_key(key) {
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
DataKeyboard::KeyUp(enigo::Key::Raw(code)) => {
if *code < 8 {
log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code);
} else {
let up_event = InputEvent::new(EventType::KEY, *code - 8, 0);
allow_err!(keyboard.emit(&[up_event]));
}
}
DataKeyboard::KeyDown(key) => {
if let Key::Layout(chr) = key {
input_char_wayland_key_event(*chr, true, keyboard);
} else {
if let Ok((k, _is_shift)) = map_key(key) {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
allow_err!(keyboard.emit(&[down_event]));
}
}
}
DataKeyboard::KeyUp(key) => {
if let Key::Layout(chr) = key {
input_char_wayland_key_event(*chr, false, keyboard);
} else {
if let Ok((k, _)) = map_key(key) {
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
allow_err!(keyboard.emit(&[up_event]));
}
}
}
DataKeyboard::KeyClick(key) => {
if let Ok((k, _)) = map_key(key) {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
allow_err!(keyboard.emit(&[down_event, up_event]));
if let Key::Layout(chr) = key {
input_text_wayland(&chr.to_string(), keyboard);
} else {
if let Ok((k, _is_shift)) = map_key(key) {
let down_event = InputEvent::new(EventType::KEY, k.code(), 1);
let up_event = InputEvent::new(EventType::KEY, k.code(), 0);
allow_err!(keyboard.emit(&[down_event, up_event]));
}
}
}
DataKeyboard::GetKeyState(key) => {
@@ -580,9 +769,13 @@ pub mod service {
}
fn spawn_keyboard_handler(mut stream: Connection) {
log::debug!("spawn_keyboard_handler: new keyboard handler connection");
tokio::spawn(async move {
let mut keyboard = match create_uinput_keyboard() {
Ok(keyboard) => keyboard,
Ok(keyboard) => {
log::debug!("UInput keyboard device created successfully");
keyboard
}
Err(e) => {
log::error!("Failed to create keyboard {}", e);
return;
@@ -602,6 +795,7 @@ pub mod service {
handle_keyboard(&mut stream, &mut keyboard, &data).await;
}
_ => {
log::warn!("Unexpected data type in keyboard handler");
}
}
}

View File

@@ -10,12 +10,6 @@ use std::time::Duration;
pub fn start_tray() {
if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" {
#[cfg(target_os = "macos")]
{
loop {
std::thread::sleep(std::time::Duration::from_secs(1));
}
}
#[cfg(not(target_os = "macos"))]
{
return;
@@ -129,6 +123,11 @@ fn make_tray() -> hbb_common::ResultType<()> {
);
if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
// for fixing https://github.com/rustdesk/rustdesk/discussions/10210#discussioncomment-14600745
// so we start tray, but not to show it
if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" {
return;
}
// We create the icon once the event loop is actually running
// to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90
let tray = TrayIconBuilder::new()

View File

@@ -358,6 +358,22 @@ function getUserName() {
return '';
}
function getAccountLabelWithHandle() {
try {
var user = JSON.parse(handler.get_local_option("user_info"));
var username = (user.name || '').trim();
if (!username) {
return '';
}
var displayName = (user.display_name || '').trim();
if (!displayName || displayName == username) {
return username;
}
return displayName + " (@" + username + ")";
} catch(e) {}
return '';
}
// Shared dialog functions
function open_custom_server_dialog() {
var configOptions = handler.get_options();
@@ -493,7 +509,7 @@ class MyIdMenu: Reactor.Component {
}
function renderPop() {
var username = handler.get_local_option("access_token") ? getUserName() : '';
var accountLabel = handler.get_local_option("access_token") ? getAccountLabelWithHandle() : '';
return <popup>
<menu.context #config-options>
{!disable_settings && <li #enable-keyboard><span>{svg_checkmark}</span>{translate('Enable keyboard/mouse')}</li>}
@@ -521,8 +537,8 @@ class MyIdMenu: Reactor.Component {
{!disable_settings && <DirectServer />}
{!disable_settings && false && handler.using_public_server() && <li #allow-always-relay><span>{svg_checkmark}</span>{translate('Always connect via relay')}</li>}
{!disable_change_id && handler.is_ok_change_id() ? <div .separator /> : ""}
{!disable_account && (username ?
<li #logout>{translate('Logout')} ({username})</li> :
{!disable_account && (accountLabel ?
<li #logout>{translate('Logout')} ({accountLabel})</li> :
<li #login>{translate('Login')}</li>)}
{!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ? <li #change-id>{translate('Change ID')}</li> : ""}
<div .separator />
@@ -1430,6 +1446,9 @@ checkConnectStatus();
function set_local_user_info(user) {
var user_info = {name: user.name};
if (user.display_name) {
user_info.display_name = user.display_name;
}
if (user.status) {
user_info.status = user.status;
}

View File

@@ -123,7 +123,7 @@ fn check_update(manually: bool) -> ResultType<()> {
if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) {
return Ok(());
}
if !do_check_software_update().is_ok() {
if do_check_software_update().is_err() {
// ignore
return Ok(());
}
@@ -185,7 +185,7 @@ fn check_update(manually: bool) -> ResultType<()> {
let mut file = std::fs::File::create(&file_path)?;
file.write_all(&file_data)?;
}
// We have checked if the `conns`` is empty before, but we need to check again.
// We have checked if the `conns` is empty before, but we need to check again.
// No need to care about the downloaded file here, because it's rare case that the `conns` are empty
// before the download, but not empty after the download.
if has_no_active_conns() {