mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-20 07:39:15 +08:00
Compare commits
39 Commits
hdie-tray
...
display_na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fc0367abd | ||
|
|
9111bfc1de | ||
|
|
9345fb754a | ||
|
|
779b7aaf02 | ||
|
|
b268aa1061 | ||
|
|
40f86fa639 | ||
|
|
980bc11e68 | ||
|
|
85db677982 | ||
|
|
2842315b1d | ||
|
|
6c541f7bfd | ||
|
|
067fab2b73 | ||
|
|
de6bf9dc7e | ||
|
|
54eae37038 | ||
|
|
0118e16132 | ||
|
|
626a091f55 | ||
|
|
4fa5e99e65 | ||
|
|
5ee9dcf42d | ||
|
|
6306f83316 | ||
|
|
96075fdf49 | ||
|
|
8c6dcf53a6 | ||
|
|
e1b1a927b8 | ||
|
|
1e6bfa7bb1 | ||
|
|
79ef4c4501 | ||
|
|
5f3ceef592 | ||
|
|
1a90e6b6c7 | ||
|
|
f112d097dc | ||
|
|
45cab7f808 | ||
|
|
216ec9d52b | ||
|
|
56a8f6b97b | ||
|
|
c76d10a438 | ||
|
|
f05f2178e5 | ||
|
|
226d7417b2 | ||
|
|
b0c8e65c6e | ||
|
|
4ae577c3c4 | ||
|
|
204e81a700 | ||
|
|
1f35830570 | ||
|
|
6b334f2977 | ||
|
|
0dc3c12aa5 | ||
|
|
ceffcce20e |
2
build.py
2
build.py
@@ -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.
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
],
|
||||
"finish-args": [
|
||||
"--share=ipc",
|
||||
"--socket=wayland",
|
||||
"--socket=x11",
|
||||
"--share=network",
|
||||
"--filesystem=home",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}"))),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
244
flutter/linux/wayland_shortcuts_inhibit.cc
Normal file
244
flutter/linux/wayland_shortcuts_inhibit.cc
Normal 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, ®istry_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)
|
||||
22
flutter/linux/wayland_shortcuts_inhibit.h
Normal file
22
flutter/linux/wayland_shortcuts_inhibit.h
Normal 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_
|
||||
@@ -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
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule libs/hbb_common updated: 900077a2c2...0b60b9ffa0
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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", "Kέρσορας μεγέθυνσης"),
|
||||
("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();
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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", "L’autorisation 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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
120
src/lang/ptbr.rs
120
src/lang/ptbr.rs
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
132
src/lang/tr.rs
132
src/lang/tr.rs
@@ -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ı Aç"),
|
||||
("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", "Ağ"),
|
||||
("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ı açı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ı açı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ı açık tutun"),
|
||||
("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"),
|
||||
("Continue with {}", "{} ile devam et"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
81
src/privacy_mode/macos.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))]
|
||||
{
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
src/tray.rs
11
src/tray.rs
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user