fix(keyboard): wayland clipboard input prompt (#14700)

* fix(keyboard): wayland clipboard input prompt

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

* fix(wayland): Simple refactor

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

* fix(wayland): clipboard input, remove unused code

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

* fix(wayland): Simple refactor

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

* fix(wayland): dialog, better enableAndContinue

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

* fix(wayland): input dialog consent

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

* fix(wayland): prompt text

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

* fix(wayland): text input

1. Use `keysym` for the installed version if possible.
2. Use the clipboard if the string cannot be fully handled by `keysym`.

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

* fix(wayland): input prompt dialog

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

* fix(wayland): translations

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

* fix(wayland): dialog, title type

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

* fix(wayland): better decode_utf8_prefix()

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

* fix(wayland): better process_chr()

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

* fix(wayland): unit tests

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

* fix(wayland): input prompt dialog, no icon

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

* fix(wayland): input dialog, Toast show the result

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

* fix(wayland): input dialog, showToast() on persist failed

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

* fix(wayland): input prompt, better dialog

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

* fix(wayland): input prompt dialog, translations

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

* fix(input): better wayland clipboard input prompt

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

* fix(input): wayland clipboard, link external app

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

* fix(input): trivial changes

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

* fix(input): wayland clipboard input, dialog content

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

* fix(input): tranlsations

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

* fix(input): translations

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

* fix(input): translations

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

* fix(input): translations

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-06-02 16:06:35 +08:00
committed by GitHub
parent 00032854eb
commit 3217125dd3
60 changed files with 1144 additions and 103 deletions

View File

@@ -13,8 +13,64 @@ import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
bool isEditOsPassword = false; bool isEditOsPassword = false;
const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard';
const String kWaylandKeyboardIssueUrl =
'https://github.com/rustdesk/rustdesk/issues/14586';
final Set<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
Future<bool> openWaylandKeyboardIssueUrl() {
return launchUrl(
Uri.parse(kWaylandKeyboardIssueUrl),
mode: LaunchMode.externalApplication,
);
}
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
}
void setWaylandKeyboardPromptSuppressedForConnection(
String connectionId, bool suppressed) {
if (suppressed) {
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
} else {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
}
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
bool shouldShowWaylandKeyboardPrompt({
required String connectionId,
required bool isWaylandPeer,
required bool allowWaylandKeyboardRemembered,
}) {
return isWaylandPeer &&
!allowWaylandKeyboardRemembered &&
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
}
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
),
child: Text(
text,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
);
}
// macOS privacy mode blacks out all online displays, so switching the remote // macOS privacy mode blacks out all online displays, so switching the remote
// display does not weaken the local privacy protection. // display does not weaken the local privacy protection.
@@ -93,12 +149,179 @@ handleOsPasswordAction(
} }
} }
void showWaylandKeyboardInputWarningDialog(
{required String id,
required String connectionId,
required FFI ffi,
required Future<void> Function() onEnable}) {
bool remember = false;
bool consentInProgress = false;
bool dialogClosed = false;
final dialogFuture = ffi.dialogManager.show((setState, close, context) {
void safeSetState(VoidCallback fn) {
if (dialogClosed) {
return;
}
try {
setState(fn);
} catch (e) {
debugPrint('Ignore setState after dialog disposal: $e');
}
}
void closeDialog() {
if (dialogClosed) {
return;
}
dialogClosed = true;
close();
}
Future<void> enableAndContinue() async {
if (consentInProgress || dialogClosed) {
return;
}
consentInProgress = true;
safeSetState(() {});
try {
await onEnable();
} catch (e, st) {
debugPrint('Failed to enable Wayland keyboard input consent: $e');
debugPrintStack(stackTrace: st);
consentInProgress = false;
safeSetState(() {});
return;
}
ffi.inputModel.keyboardInputAllowed = true;
var rememberPersisted = true;
if (remember) {
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, true));
} catch (e) {
rememberPersisted = false;
debugPrint('Failed to persist Wayland keyboard input consent: $e');
}
}
// Always suppress prompt for current connection after explicit consent.
setWaylandKeyboardPromptSuppressedForConnection(connectionId, true);
closeDialog();
if (remember && !rememberPersisted) {
// It's a rare edge case that persisting the user's choice fails.
// Failed to persist the user's choice, but still allow keyboard input for current session.
showToast(translate('Failed'));
}
}
void cancel() {
if (consentInProgress) {
return;
}
closeDialog();
}
return CustomAlertDialog(
title: null,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
msgboxContent(
'',
'wayland-keyboard-input-disabled-tip',
'wayland-keyboard-input-consent-tip',
),
SizedBox(height: isMobile ? 2 : 6),
if (isMobile) ...[
Text(
translate('wayland-keyboard-input-applies-to-tip'),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
).marginOnly(bottom: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
waylandKeyboardScopeChip(
context, translate('Send clipboard keystrokes')),
waylandKeyboardScopeChip(
context, translate('wayland-soft-keyboard-input-label')),
],
).marginOnly(bottom: 10),
],
TextButton(
onPressed: consentInProgress
? null
: () async {
try {
final opened = await openWaylandKeyboardIssueUrl();
if (!opened) {
// Opening this optional help link almost never fails in
// normal desktop environments. Keep the result handled
// for review hygiene, but avoid a low-value user toast.
debugPrint('Failed to open Wayland keyboard issue URL');
}
} catch (e) {
debugPrint(
'Failed to open Wayland keyboard issue URL: $e');
}
},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
translate('Why this happens'),
style: const TextStyle(decoration: TextDecoration.underline),
),
).marginOnly(bottom: 6),
CheckboxListTile(
value: remember,
dense: true,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate('remember-wayland-keyboard-choice-tip')),
onChanged: consentInProgress
? null
: (v) {
safeSetState(() => remember = v == true);
},
),
],
),
actions: [
dialogButton(
'Cancel',
onPressed: consentInProgress ? null : cancel,
isOutline: true,
),
dialogButton(
'OK',
onPressed:
consentInProgress ? null : () => unawaited(enableAndContinue()),
),
],
onCancel: consentInProgress ? null : cancel,
onSubmit: consentInProgress ? null : () => unawaited(enableAndContinue()),
);
}, clickMaskDismiss: false, backDismiss: false);
unawaited(dialogFuture.whenComplete(() => dialogClosed = true));
}
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) { List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi; final pi = ffiModel.pi;
final perms = ffiModel.permissions; final perms = ffiModel.permissions;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn; final isDefaultConn = ffi.connType == ConnType.defaultConn;
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
List<TTextMenu> v = []; List<TTextMenu> v = [];
// elevation // elevation
@@ -148,11 +371,60 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(TTextMenu( v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')), child: Text(translate('Send clipboard keystrokes')),
onPressed: () async { onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); Future<void> sendClipboardKeystrokes() async {
if (data != null && data.text != null) { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
bind.sessionInputString( if (data != null && data.text != null) {
sessionId: sessionId, value: data.text ?? ""); bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
}
} }
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: isWaylandPeer,
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
ffi.inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: id,
connectionId: sessionId.toString(),
ffi: ffi,
onEnable: sendClipboardKeystrokes,
);
return;
}
await sendClipboardKeystrokes();
}));
}
if (isDefaultConn &&
isWaylandPeer &&
(mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard) ||
isWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString()))) {
v.add(TTextMenu(
child: Text(translate('wayland-keyboard-input-reset-choice-tip')),
onPressed: () async {
var persistedCleared = false;
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, false));
persistedCleared = true;
} catch (e) {
debugPrint(
'Failed to clear persisted Wayland keyboard permission: $e');
} finally {
clearWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString());
ffi.inputModel.keyboardInputAllowed = false;
if (isMobile) {
await ffi.invokeMethod("enable_soft_keyboard", false);
}
}
showToast(translate(persistedCleared ? 'Successful' : 'Failed'));
})); }));
} }
// reset canvas // reset canvas
@@ -766,7 +1038,8 @@ List<TToggleMenu> toolbarPrivacyMode(
final ffiModel = ffi.ffiModel; final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi; final pi = ffiModel.pi;
final sessionId = ffi.sessionId; final sessionId = ffi.sessionId;
final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false; final hasPrivacyModePermission =
ffiModel.permissions['privacy_mode'] != false;
// Backend revocation already attempts to turn privacy mode off. // Backend revocation already attempts to turn privacy mode off.
// Still keep this menu when privacy mode is active, so users can turn it off // Still keep this menu when privacy mode is active, so users can turn it off
@@ -776,8 +1049,8 @@ List<TToggleMenu> toolbarPrivacyMode(
} }
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) { getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
final enabled = final enabled = !ffiModel.viewOnly &&
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty); (hasPrivacyModePermission || privacyModeState.isNotEmpty);
return TToggleMenu( return TToggleMenu(
value: privacyModeState.isNotEmpty, value: privacyModeState.isNotEmpty,
onChanged: enabled onChanged: enabled

View File

@@ -101,6 +101,9 @@ class _RemotePageState extends State<RemotePage>
Function(bool)? _onEnterOrLeaveImage4Toolbar; Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi; late FFI _ffi;
Worker? _waylandKeyboardModeWorker;
bool _waylandKeyboardModeNormalized = false;
bool _waylandKeyboardModeNormalizing = false;
SessionID get sessionId => _ffi.sessionId; SessionID get sessionId => _ffi.sessionId;
@@ -178,6 +181,48 @@ class _RemotePageState extends State<RemotePage>
// Register callback to cancel debounce timer when relative mouse mode is disabled // Register callback to cancel debounce timer when relative mouse mode is disabled
_ffi.inputModel.onRelativeMouseModeDisabled = _ffi.inputModel.onRelativeMouseModeDisabled =
_cancelPointerLockCenterDebounceTimer; _cancelPointerLockCenterDebounceTimer;
_waylandKeyboardModeWorker = ever(_ffi.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
});
if (_ffi.ffiModel.pi.isSet.value) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
}
Future<void> _normalizeWaylandKeyboardModeIfNeeded() async {
if (!mounted ||
_waylandKeyboardModeNormalized ||
_waylandKeyboardModeNormalizing) {
return;
}
_waylandKeyboardModeNormalizing = true;
try {
final pi = _ffi.ffiModel.pi;
if (pi.platform != kPeerPlatformLinux || !pi.isWayland) return;
final mapSupported = bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: kKeyMapMode);
if (!mapSupported) return;
final current = await bind.sessionGetKeyboardMode(sessionId: sessionId);
if (!mounted) return;
if (current == kKeyMapMode) {
_waylandKeyboardModeNormalized = true;
return;
}
await bind.sessionSetKeyboardMode(
sessionId: sessionId, value: kKeyMapMode);
if (!mounted) return;
await _ffi.inputModel.updateKeyboardMode();
if (!mounted) return;
_waylandKeyboardModeNormalized = true;
} catch (e, st) {
debugPrint('Failed to normalize Wayland keyboard mode: $e');
debugPrintStack(stackTrace: st);
} finally {
_waylandKeyboardModeNormalizing = false;
}
} }
/// Cancel the pointer lock center debounce timer /// Cancel the pointer lock center debounce timer
@@ -318,6 +363,7 @@ class _RemotePageState extends State<RemotePage>
_pointerLockCenterDebounceTimer?.cancel(); _pointerLockCenterDebounceTimer?.cancel();
_pointerLockCenterDebounceTimer = null; _pointerLockCenterDebounceTimer = null;
_waylandKeyboardModeWorker?.dispose();
// Clear callback reference to prevent memory leaks and stale references // Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null; _ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...). // Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
@@ -331,6 +377,9 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.disposeImage(); _ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages(); _ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose(); _rawKeyFocusNode.dispose();
if (closeSession) {
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
}
await _ffi.close(closeSession: closeSession); await _ffi.close(closeSession: closeSession);
_timer?.cancel(); _timer?.cancel();
_ffi.dialogManager.dismissAll(); _ffi.dialogManager.dismissAll();

View File

@@ -2324,18 +2324,8 @@ class _KeyboardMenu extends StatelessWidget {
continue; continue;
} }
if (pi.isWayland) { if (pi.isWayland && mode.key != kKeyMapMode) {
// Legacy mode is hidden on desktop control side because dead keys continue;
// 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); var text = translate(mode.menu);

View File

@@ -75,6 +75,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
final FocusNode _physicalFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard var _showEdit = false; // use soft keyboard
Worker? _waylandKeyboardGateWorker;
bool _waylandKeyboardGateInitialized = false;
InputModel get inputModel => gFFI.inputModel; InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId; SessionID get sessionId => gFFI.sessionId;
@@ -121,6 +124,20 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
isKeyboardVisible: keyboardVisibilityController.isVisible); isKeyboardVisible: keyboardVisibilityController.isVisible);
}); });
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
inputModel.keyboardInputAllowed = true;
// Wayland sessions may use clipboard-based text input on the controlled side.
// Require explicit user confirmation before allowing soft-keyboard and
// clipboard-assisted text input. Physical keyboard events are not gated here.
_waylandKeyboardGateWorker = ever(gFFI.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
_initWaylandKeyboardGateIfNeeded();
}
});
if (gFFI.ffiModel.pi.isSet.value) {
_initWaylandKeyboardGateIfNeeded();
}
} }
@override @override
@@ -143,6 +160,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
await gFFI.invokeMethod("enable_soft_keyboard", true); await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose(); _mobileFocusNode.dispose();
_physicalFocusNode.dispose(); _physicalFocusNode.dispose();
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
_waylandKeyboardGateWorker?.dispose();
inputModel.keyboardInputAllowed = true;
await gFFI.close(); await gFFI.close();
_timer?.cancel(); _timer?.cancel();
_iosKeyboardWorkaroundTimer?.cancel(); _iosKeyboardWorkaroundTimer?.cancel();
@@ -171,6 +191,40 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.invokeMethod("try_sync_clipboard"); gFFI.invokeMethod("try_sync_clipboard");
} }
bool _shouldGateKeyboardForWayland() {
if (!(isAndroid || isIOS)) return false;
final pi = gFFI.ffiModel.pi;
return pi.platform == kPeerPlatformLinux && pi.isWayland;
}
void _initWaylandKeyboardGateIfNeeded() {
if (!mounted) return;
if (_waylandKeyboardGateInitialized) return;
if (!_shouldGateKeyboardForWayland()) return;
_waylandKeyboardGateInitialized = true;
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (!shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = true;
return;
}
inputModel.keyboardInputAllowed = false;
// Ensure soft keyboard is not active before user confirms.
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
setState(() {});
}
// to-do: It should be better to use transparent color instead of the bgColor. // to-do: It should be better to use transparent color instead of the bgColor.
// But for now, the transparent color will cause the canvas to be white. // But for now, the transparent color will cause the canvas to be white.
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay. // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
@@ -302,7 +356,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
content == '【】')) { content == '【】')) {
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
bind.sessionInputString(sessionId: sessionId, value: content); bind.sessionInputString(sessionId: sessionId, value: content);
openKeyboard(); _openKeyboardUnlocked();
return; return;
} }
bind.sessionInputString(sessionId: sessionId, value: content); bind.sessionInputString(sessionId: sessionId, value: content);
@@ -314,6 +368,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// handle mobile virtual keyboard // handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) { void handleSoftKeyboardInput(String newValue) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (isIOS) { if (isIOS) {
_handleIOSSoftKeyboardInput(newValue); _handleIOSSoftKeyboardInput(newValue);
} else { } else {
@@ -322,6 +379,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
} }
void inputChar(String char) { void inputChar(String char) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (char == '\n') { if (char == '\n') {
char = 'VK_RETURN'; char = 'VK_RETURN';
} else if (char == ' ') { } else if (char == ' ') {
@@ -331,6 +391,29 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
} }
void openKeyboard() { void openKeyboard() {
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: widget.id,
connectionId: sessionId.toString(),
ffi: gFFI,
onEnable: () async {
_openKeyboardUnlocked();
},
);
return;
}
_openKeyboardUnlocked();
}
void _openKeyboardUnlocked() {
inputModel.keyboardInputAllowed = true;
gFFI.invokeMethod("enable_soft_keyboard", true); gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work // destroy first, so that our _value trick can work
_value = initText; _value = initText;

View File

@@ -474,6 +474,10 @@ class InputModel {
late final SessionID sessionId; late final SessionID sessionId;
// Local gate for clipboard-assisted input flows on mobile Wayland dialogs.
// It should not block physical keyboard events.
bool keyboardInputAllowed = true;
bool get keyboardPerm => parent.target!.ffiModel.keyboard; bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? ''; String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform; String? get peerPlatform => parent.target?.ffiModel.pi.platform;

View File

@@ -1,5 +1,7 @@
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use arboard::{ClipboardData, ClipboardFormat}; use arboard::{ClipboardData, ClipboardFormat};
#[cfg(target_os = "linux")]
use arboard::{LinuxClipboardKind, SetExtLinux};
use hbb_common::{bail, log, message_proto::*, ResultType}; use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
@@ -54,6 +56,27 @@ pub fn check_clipboard(
side: ClipboardSide, side: ClipboardSide,
force: bool, force: bool,
) -> Option<Message> { ) -> Option<Message> {
let (msg, clipboards) = read_clipboard_message(ctx, side, force)?;
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
Some(msg)
}
#[cfg(target_os = "linux")]
pub fn peek_clipboard(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<Message> {
let (msg, _) = read_clipboard_message(ctx, side, force)?;
Some(msg)
}
#[cfg(not(target_os = "android"))]
fn read_clipboard_message(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<(Message, MultiClipboards)> {
if ctx.is_none() { if ctx.is_none() {
*ctx = ClipboardContext::new().ok(); *ctx = ClipboardContext::new().ok();
} }
@@ -64,8 +87,7 @@ pub fn check_clipboard(
let mut msg = Message::new(); let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content); let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone()); msg.set_multi_clipboards(clipboards.clone());
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards; return Some((msg, clipboards));
return Some(msg);
} }
} }
Err(e) => { Err(e) => {
@@ -219,10 +241,7 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
} }
} }
if let Some(ctx) = ctx.as_mut() { if let Some(ctx) = ctx.as_mut() {
to_update_data.push(ClipboardData::Special(( to_update_data = append_owner_marker(to_update_data, side);
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
if let Err(e) = ctx.set(&to_update_data) { if let Err(e) = ctx.set(&to_update_data) {
log::debug!("Failed to set clipboard: {}", e); log::debug!("Failed to set clipboard: {}", e);
} else { } else {
@@ -231,6 +250,29 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
} }
} }
#[cfg(not(target_os = "android"))]
fn append_owner_marker(mut data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<ClipboardData> {
data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
data
}
#[cfg(target_os = "linux")]
pub fn set_text_clipboard_with_owner_sync(text: &str, side: ClipboardSide) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
*ctx = Some(ClipboardContext::new()?);
}
let clipboard_ctx = match ctx.as_mut() {
Some(ctx) => ctx,
None => bail!("Failed to create clipboard context"),
};
let data = append_owner_marker(vec![ClipboardData::Text(text.to_owned())], side);
clipboard_ctx.set_with_owner_marker_for_linux(&data)
}
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) { pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
std::thread::spawn(move || { std::thread::spawn(move || {
@@ -382,6 +424,24 @@ impl ClipboardContext {
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")]
fn set_with_owner_marker_for_linux(&mut self, data: &[ClipboardData]) -> ResultType<()> {
let _lock = ARBOARD_MTX.lock().unwrap();
self.inner
.set()
.clipboard(LinuxClipboardKind::Clipboard)
.formats(data)?;
if let Err(e) = self
.inner
.set()
.clipboard(LinuxClipboardKind::Primary)
.formats(data)
{
log::warn!("Failed to set PRIMARY clipboard with owner marker: {}", e);
}
Ok(())
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] #[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
fn get_file_urls_set_by_rustdesk( fn get_file_urls_set_by_rustdesk(
data: Vec<ClipboardData>, data: Vec<ClipboardData>,

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", "服务器要求显式部署此设备。是否立即部署?"), ("server_requires_deployment_tip", "服务器要求显式部署此设备。是否立即部署?"),
("The server does not require explicit deployment.", "服务器不需要显式部署。"), ("The server does not require explicit deployment.", "服务器不需要显式部署。"),
("Unknown response.", "未知响应。"), ("Unknown response.", "未知响应。"),
("wayland-keyboard-input-disabled-tip", "允许键盘输入?"),
("wayland-keyboard-input-consent-tip", "你在这台远程电脑上输入的内容(包括密码)可能被远程电脑上的其他程序读取。"),
("wayland-keyboard-input-applies-to-tip", "此选择适用于:"),
("wayland-soft-keyboard-input-label", "软键盘输入"),
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
("Why this happens", "了解原因"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -276,5 +276,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("preset-password-in-use-tip", "Preset password is currently in use."), ("preset-password-in-use-tip", "Preset password is currently in use."),
("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"), ("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"),
("server_requires_deployment_tip", "The server requires this device to be deployed explicitly. Deploy now?"), ("server_requires_deployment_tip", "The server requires this device to be deployed explicitly. Deploy now?"),
("wayland-keyboard-input-disabled-tip", "Allow keyboard input?"),
("wayland-keyboard-input-consent-tip", "What you type on this remote computer (including passwords) could be read by other apps on it."),
("wayland-keyboard-input-applies-to-tip", "This choice applies to:"),
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", "Il server richiede che questo dispositivo venga distribuito in modo esplicito.\nVuoi distribuirlo?"), ("server_requires_deployment_tip", "Il server richiede che questo dispositivo venga distribuito in modo esplicito.\nVuoi distribuirlo?"),
("The server does not require explicit deployment.", "Il server non richiede una distribuzione esplicita."), ("The server does not require explicit deployment.", "Il server non richiede una distribuzione esplicita."),
("Unknown response.", "Risposta sconosciuta"), ("Unknown response.", "Risposta sconosciuta"),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""), ("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""), ("The server does not require explicit deployment.", ""),
("Unknown response.", ""), ("Unknown response.", ""),
("wayland-keyboard-input-disabled-tip", ""),
("wayland-keyboard-input-consent-tip", ""),
("wayland-keyboard-input-applies-to-tip", ""),
("wayland-soft-keyboard-input-label", ""),
("wayland-keyboard-input-reset-choice-tip", ""),
("remember-wayland-keyboard-choice-tip", ""),
("Why this happens", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -2,7 +2,7 @@ use super::*;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
use crate::clipboard::clipboard_listener; use crate::clipboard::clipboard_listener;
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; pub use crate::clipboard::{ClipboardContext, ClipboardSide};
pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME};
#[cfg(windows)] #[cfg(windows)]
use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data};
@@ -109,6 +109,62 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")]
const WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES: usize =
super::input_service::WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS * 4;
#[cfg(target_os = "linux")]
fn decode_utf8_prefix(bytes: &[u8]) -> Option<String> {
let end = bytes.len().min(WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
let slice = &bytes[..end];
match std::str::from_utf8(slice) {
Ok(text) => Some(text.to_owned()),
Err(e) => {
if e.error_len().is_some() {
return None;
}
let valid_up_to = e.valid_up_to();
std::str::from_utf8(&slice[..valid_up_to])
.ok()
.map(ToOwned::to_owned)
}
}
}
#[cfg(target_os = "linux")]
fn decode_text_clipboard(clipboard: &Clipboard) -> Option<String> {
if clipboard.format.enum_value() != Ok(ClipboardFormat::Text) {
return None;
}
if clipboard.compress {
let bytes = hbb_common::compress::decompress(&clipboard.content);
return decode_utf8_prefix(&bytes);
}
decode_utf8_prefix(&clipboard.content)
}
#[cfg(target_os = "linux")]
fn should_skip_wayland_clipboard_sync(msg: &Message) -> bool {
if crate::platform::linux::is_x11() {
return false;
}
let is_recent_wayland_input = |clipboard: &Clipboard| -> bool {
let Some(text) = decode_text_clipboard(clipboard) else {
return false;
};
super::input_service::is_recent_wayland_clipboard_input(&text)
};
match &msg.union {
Some(message::Union::Clipboard(clipboard)) => is_recent_wayland_input(clipboard),
Some(message::Union::MultiClipboards(multi_clipboards)) => multi_clipboards
.clipboards
.iter()
.any(is_recent_wayland_input),
_ => false,
}
}
#[cfg(not(target_os = "android"))] #[cfg(not(target_os = "android"))]
impl Handler { impl Handler {
#[cfg(feature = "unix-file-copy-paste")] #[cfg(feature = "unix-file-copy-paste")]
@@ -172,7 +228,19 @@ impl Handler {
} }
} }
check_clipboard(&mut self.ctx, ClipboardSide::Host, false) #[cfg(target_os = "linux")]
{
let msg = crate::clipboard::peek_clipboard(&mut self.ctx, ClipboardSide::Host, false)?;
if should_skip_wayland_clipboard_sync(&msg) {
log::debug!("Skip clipboard sync for recent Wayland keyboard injection");
return None;
}
return Some(msg);
}
#[cfg(not(target_os = "linux"))]
{
crate::clipboard::check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
}
} }
// Read clipboard data from cm using ipc. // Read clipboard data from cm using ipc.
@@ -272,3 +340,46 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst); CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst);
Ok(()) Ok(())
} }
#[cfg(test)]
#[cfg(target_os = "linux")]
mod tests {
use super::{decode_utf8_prefix, WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES};
#[test]
fn decode_utf8_prefix_returns_text_for_valid_utf8() {
let text = "hello-مرحبا";
assert_eq!(decode_utf8_prefix(text.as_bytes()), Some(text.to_owned()));
}
#[test]
fn decode_utf8_prefix_returns_none_for_invalid_utf8_sequence() {
let bytes = b"ab\xffcd";
assert_eq!(decode_utf8_prefix(bytes), None);
}
#[test]
fn decode_utf8_prefix_trims_incomplete_utf8_suffix() {
let bytes = vec![b'a', 0xE4, 0xB8];
assert_eq!(decode_utf8_prefix(&bytes), Some("a".to_owned()));
}
#[test]
fn decode_utf8_prefix_applies_max_bytes_limit() {
let bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES + 8];
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
assert_eq!(result.len(), WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
}
#[test]
fn decode_utf8_prefix_keeps_utf8_boundary_when_limited() {
let mut bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1];
bytes.extend_from_slice("ا".as_bytes());
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
assert_eq!(
result.len(),
WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1
);
assert!(result.chars().all(|c| c == 'a'));
}
}

View File

@@ -457,6 +457,12 @@ lazy_static::lazy_static! {
static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default(); static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default();
} }
#[cfg(target_os = "linux")]
lazy_static::lazy_static! {
static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc<Mutex<Vec<(Instant, String)>>> =
Default::default();
}
#[inline] #[inline]
fn set_relative_mouse_active(conn: i32, active: bool) { fn set_relative_mouse_active(conn: i32, active: bool) {
let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap(); let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap();
@@ -1594,15 +1600,28 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
} }
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) { fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
// On Wayland with uinput mode, use clipboard for character input // On Wayland with uinput mode:
// - ASCII printable: input via key events (custom keyboard path, e.g. portal keysym)
// - Non-ASCII: input via clipboard paste
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() { if !crate::platform::linux::is_x11() && wayland_use_uinput() {
// Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed) // Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed)
if !is_hotkey_modifier_pressed(en) { if !is_hotkey_modifier_pressed(en) {
if down { if let Ok(c) = char::try_from(chr) {
if let Ok(c) = char::try_from(chr) { if is_ascii_printable(c) {
if down {
en.key_down(Key::Layout(c)).ok();
} else {
en.key_up(Key::Layout(c));
}
} else if down {
input_char_via_clipboard_server(en, c); input_char_via_clipboard_server(en, c);
} }
} else {
log::warn!(
"Ignore invalid unicode scalar in Wayland+uinput path: {}",
chr
);
} }
return; return;
} }
@@ -1637,11 +1656,17 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
} }
fn process_unicode(en: &mut Enigo, chr: u32) { fn process_unicode(en: &mut Enigo, chr: u32) {
// On Wayland with uinput mode, use clipboard for character input // On Wayland with uinput mode:
// - ASCII printable: input via key sequence (custom keyboard path)
// - Non-ASCII: input via clipboard paste
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() { if !crate::platform::linux::is_x11() && wayland_use_uinput() {
if let Ok(c) = char::try_from(chr) { if let Ok(c) = char::try_from(chr) {
input_char_via_clipboard_server(en, c); if is_ascii_printable(c) {
en.key_sequence(&c.to_string());
} else {
input_char_via_clipboard_server(en, c);
}
} }
return; return;
} }
@@ -1652,10 +1677,16 @@ fn process_unicode(en: &mut Enigo, chr: u32) {
} }
fn process_seq(en: &mut Enigo, sequence: &str) { fn process_seq(en: &mut Enigo, sequence: &str) {
// On Wayland with uinput mode, use clipboard for text input // On Wayland with uinput mode:
// - pure ASCII printable sequence: input via key sequence (custom keyboard path)
// - any non-ASCII present: input whole sequence via clipboard to preserve order
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() { if !crate::platform::linux::is_x11() && wayland_use_uinput() {
input_text_via_clipboard_server(en, sequence); if sequence.chars().all(is_ascii_printable) {
en.key_sequence(sequence);
} else {
input_text_via_clipboard_server(en, sequence);
}
return; return;
} }
@@ -1668,40 +1699,103 @@ fn process_seq(en: &mut Enigo, sequence: &str) {
/// this delay may be insufficient, but there is no reliable alternative mechanism. /// this delay may be insufficient, but there is no reliable alternative mechanism.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
const CLIPBOARD_SYNC_DELAY_MS: u64 = 50; const CLIPBOARD_SYNC_DELAY_MS: u64 = 50;
#[cfg(target_os = "linux")]
const WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW: Duration = Duration::from_secs(1);
#[cfg(target_os = "linux")]
const WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS: usize = 256;
#[cfg(target_os = "linux")]
pub(super) const WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS: usize = 1024;
#[cfg(target_os = "linux")]
fn cleanup_wayland_clipboard_input_records(records: &mut Vec<(Instant, String)>, now: Instant) {
records.retain(|(created_at, _)| {
now.saturating_duration_since(*created_at) <= WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW
});
let len = records.len();
if len > WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS {
records.drain(0..(len - WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS));
}
}
#[cfg(target_os = "linux")]
#[inline]
fn normalize_wayland_clipboard_input_text(text: &str) -> String {
text.chars()
.take(WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS)
.collect()
}
#[cfg(target_os = "linux")]
#[inline]
fn get_wayland_clipboard_input_normalized_text(text: &str) -> Option<String> {
let normalized = normalize_wayland_clipboard_input_text(text);
if normalized.is_empty() {
return None;
}
Some(normalized)
}
#[cfg(target_os = "linux")]
#[inline]
fn record_wayland_clipboard_input_for_sync_filter(text: &str) -> Option<(Instant, String)> {
if text.is_empty() || crate::platform::linux::is_x11() {
return None;
}
let normalized = get_wayland_clipboard_input_normalized_text(text)?;
let now = Instant::now();
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
cleanup_wayland_clipboard_input_records(&mut records, now);
records.push((now, normalized.clone()));
Some((now, normalized))
}
#[cfg(target_os = "linux")]
#[inline]
fn rollback_wayland_clipboard_input_record(record: (Instant, String)) {
let (created_at, normalized) = record;
let now = Instant::now();
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
cleanup_wayland_clipboard_input_records(&mut records, now);
if let Some(pos) = records
.iter()
.rposition(|(record_created_at, record_normalized)| {
*record_created_at == created_at && *record_normalized == normalized
})
{
records.remove(pos);
}
}
#[cfg(target_os = "linux")]
pub(super) fn is_recent_wayland_clipboard_input(text: &str) -> bool {
if text.is_empty() || crate::platform::linux::is_x11() {
return false;
}
let Some(normalized) = get_wayland_clipboard_input_normalized_text(text) else {
return false;
};
let now = Instant::now();
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
cleanup_wayland_clipboard_input_records(&mut records, now);
records
.iter()
.any(|(_, record_normalized)| record_normalized == &normalized)
}
/// Internal: Set clipboard content without delay. /// Internal: Set clipboard content without delay.
/// Returns true if clipboard was set successfully. /// Returns true if clipboard was set successfully.
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn set_clipboard_content(text: &str) -> bool { fn set_clipboard_content(text: &str) -> bool {
use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux}; if let Err(e) = crate::clipboard::set_text_clipboard_with_owner_sync(
text,
let mut clipboard = match Clipboard::new() { crate::clipboard::ClipboardSide::Host,
Ok(cb) => cb, ) {
Err(e) => { log::error!(
log::error!("set_clipboard_content: failed to create clipboard: {:?}", e); "set_clipboard_content: failed to set clipboard with owner marker: {:?}",
return false; e
} );
};
// 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; 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 true
} }
@@ -1714,7 +1808,11 @@ fn set_clipboard_content(text: &str) -> bool {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[inline] #[inline]
pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool { pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool {
let record = record_wayland_clipboard_input_for_sync_filter(text);
if !set_clipboard_content(text) { if !set_clipboard_content(text) {
if let Some(record) = record {
rollback_wayland_clipboard_input_record(record);
}
return false; return false;
} }
std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS)); std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS));
@@ -1916,49 +2014,53 @@ fn translate_process_code(code: u32, down: bool) {
fn translate_keyboard_mode(evt: &KeyEvent) { fn translate_keyboard_mode(evt: &KeyEvent) {
match &evt.union { match &evt.union {
Some(key_event::Union::Seq(seq)) => { Some(key_event::Union::Seq(seq)) => {
// On Wayland, handle character input directly in this (--server) process using clipboard. // On Wayland:
// This function runs in the --server process (logged-in user session), which has // - uinput mode (--service): keep clipboard handling in this process because
// WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here. // clipboard is unreliable in root service context.
// // - rdp_input mode (--server): forward sequence to custom keyboard handler so
// Why not let it go through uinput IPC: // ASCII can use Portal keysym and non-ASCII can use clipboard.
// 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")] #[cfg(target_os = "linux")]
if !crate::platform::linux::is_x11() { if !crate::platform::linux::is_x11() {
let mut en = ENIGO.lock().unwrap(); let mut en = ENIGO.lock().unwrap();
if wayland_use_rdp_input() {
// Check if this is a hotkey (Ctrl/Alt/Meta pressed) release_shift_for_char_input(&mut en);
// For hotkeys, we send character-based key events via Enigo instead of en.key_sequence(seq);
// 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; return;
} }
// Normal text input: release Shift and use clipboard if wayland_use_uinput() {
release_shift_for_char_input(&mut en); // 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;
}
input_text_via_clipboard_server(&mut en, seq); // Normal text input: release Shift and use clipboard
return; release_shift_for_char_input(&mut en);
if seq.chars().all(is_ascii_printable) {
en.key_sequence(seq);
} else {
input_text_via_clipboard_server(&mut en, seq);
}
return;
}
} }
// Fr -> US // Fr -> US

View File

@@ -118,6 +118,23 @@ pub mod client {
} }
fn key_sequence(&mut self, s: &str) { fn key_sequence(&mut self, s: &str) {
if s.is_empty() {
return;
}
// Keep ordering deterministic:
// - pure ASCII printable: send via Portal keysym
// - any non-ASCII present (including mixed ASCII/non-ASCII): send whole
// sequence via clipboard as one atomic paste
let ascii_only = s.chars().all(|c| {
let keysym = char_to_keysym(c);
can_input_via_keysym(c, keysym)
});
if !ascii_only {
input_text_via_clipboard(s, self.conn.clone(), &self.session);
return;
}
for c in s.chars() { for c in s.chars() {
let keysym = char_to_keysym(c); let keysym = char_to_keysym(c);
// ASCII characters: use keysym // ASCII characters: use keysym
@@ -128,9 +145,6 @@ pub mod client {
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) { if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e); 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);
} }
} }
} }
@@ -167,8 +181,7 @@ pub mod client {
// ASCII characters: send keysym up if we also sent it on key_down // ASCII characters: send keysym up if we also sent it on key_down
let keysym = char_to_keysym(chr); let keysym = char_to_keysym(chr);
if can_input_via_keysym(chr, keysym) { if can_input_via_keysym(chr, keysym) {
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
{
log::error!("Failed to send keysym up: {:?}", e); log::error!("Failed to send keysym up: {:?}", e);
} }
} }