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
commit 3217125dd3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1142 additions and 101 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/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart';
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
// 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) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
List<TTextMenu> v = [];
// elevation
@ -148,11 +371,60 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
Future<void> sendClipboardKeystrokes() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
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
@ -766,7 +1038,8 @@ List<TToggleMenu> toolbarPrivacyMode(
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
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.
// 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) {
final enabled =
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
return TToggleMenu(
value: privacyModeState.isNotEmpty,
onChanged: enabled

View file

@ -101,6 +101,9 @@ class _RemotePageState extends State<RemotePage>
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
Worker? _waylandKeyboardModeWorker;
bool _waylandKeyboardModeNormalized = false;
bool _waylandKeyboardModeNormalizing = false;
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
_ffi.inputModel.onRelativeMouseModeDisabled =
_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
@ -318,6 +363,7 @@ class _RemotePageState extends State<RemotePage>
_pointerLockCenterDebounceTimer?.cancel();
_pointerLockCenterDebounceTimer = null;
_waylandKeyboardModeWorker?.dispose();
// Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
@ -331,6 +377,9 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
if (closeSession) {
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
}
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();

View file

@ -2324,18 +2324,8 @@ class _KeyboardMenu extends StatelessWidget {
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;
}
if (pi.isWayland && mode.key != kKeyMapMode) {
continue;
}
var text = translate(mode.menu);

View file

@ -75,6 +75,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
Worker? _waylandKeyboardGateWorker;
bool _waylandKeyboardGateInitialized = false;
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
@ -121,6 +124,20 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
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
@ -143,6 +160,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
_waylandKeyboardGateWorker?.dispose();
inputModel.keyboardInputAllowed = true;
await gFFI.close();
_timer?.cancel();
_iosKeyboardWorkaroundTimer?.cancel();
@ -171,6 +191,40 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
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.
// 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.
@ -302,7 +356,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
content == '【】')) {
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
bind.sessionInputString(sessionId: sessionId, value: content);
openKeyboard();
_openKeyboardUnlocked();
return;
}
bind.sessionInputString(sessionId: sessionId, value: content);
@ -314,6 +368,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
@ -322,6 +379,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
void inputChar(String char) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (char == '\n') {
char = 'VK_RETURN';
} else if (char == ' ') {
@ -331,6 +391,29 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
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);
// destroy first, so that our _value trick can work
_value = initText;

View file

@ -474,6 +474,10 @@ class InputModel {
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;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;

View file

@ -1,5 +1,7 @@
#[cfg(not(target_os = "android"))]
use arboard::{ClipboardData, ClipboardFormat};
#[cfg(target_os = "linux")]
use arboard::{LinuxClipboardKind, SetExtLinux};
use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{
sync::{Arc, Mutex},
@ -54,6 +56,27 @@ pub fn check_clipboard(
side: ClipboardSide,
force: bool,
) -> 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() {
*ctx = ClipboardContext::new().ok();
}
@ -64,8 +87,7 @@ pub fn check_clipboard(
let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone());
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg);
return Some((msg, clipboards));
}
}
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() {
to_update_data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
to_update_data = append_owner_marker(to_update_data, side);
if let Err(e) = ctx.set(&to_update_data) {
log::debug!("Failed to set clipboard: {}", e);
} 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"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
std::thread::spawn(move || {
@ -382,6 +424,24 @@ impl ClipboardContext {
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"))]
fn get_file_urls_set_by_rustdesk(
data: Vec<ClipboardData>,

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", "服务器要求显式部署此设备。是否立即部署?"),
("The server does not require explicit deployment.", "服务器不需要显式部署。"),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

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."),
("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?"),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

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?"),
("The server does not require explicit deployment.", "Il server non richiede una distribuzione esplicita."),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("server_requires_deployment_tip", ""),
("The server does not require explicit deployment.", ""),
("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();
}

View file

@ -2,7 +2,7 @@ use super::*;
#[cfg(not(target_os = "android"))]
use crate::clipboard::clipboard_listener;
#[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};
#[cfg(windows)]
use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data};
@ -109,6 +109,62 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
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"))]
impl Handler {
#[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.
@ -272,3 +340,46 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst);
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();
}
#[cfg(target_os = "linux")]
lazy_static::lazy_static! {
static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc<Mutex<Vec<(Instant, String)>>> =
Default::default();
}
#[inline]
fn set_relative_mouse_active(conn: i32, active: bool) {
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) {
// 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")]
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) {
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);
}
} else {
log::warn!(
"Ignore invalid unicode scalar in Wayland+uinput path: {}",
chr
);
}
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) {
// 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")]
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
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;
}
@ -1652,10 +1677,16 @@ fn process_unicode(en: &mut Enigo, chr: u32) {
}
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")]
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;
}
@ -1668,40 +1699,103 @@ fn process_seq(en: &mut Enigo, sequence: &str) {
/// this delay may be insufficient, but there is no reliable alternative mechanism.
#[cfg(target_os = "linux")]
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.
/// 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);
if let Err(e) = crate::clipboard::set_text_clipboard_with_owner_sync(
text,
crate::clipboard::ClipboardSide::Host,
) {
log::error!(
"set_clipboard_content: failed to set clipboard with owner marker: {:?}",
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
}
@ -1714,7 +1808,11 @@ fn set_clipboard_content(text: &str) -> bool {
#[cfg(target_os = "linux")]
#[inline]
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 let Some(record) = record {
rollback_wayland_clipboard_input_record(record);
}
return false;
}
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) {
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.
// On Wayland:
// - uinput mode (--service): keep clipboard handling in this process because
// clipboard is unreliable in root service context.
// - rdp_input mode (--server): forward sequence to custom keyboard handler so
// ASCII can use Portal keysym and non-ASCII can use clipboard.
#[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));
}
if wayland_use_rdp_input() {
release_shift_for_char_input(&mut en);
en.key_sequence(seq);
return;
}
// Normal text input: release Shift and use clipboard
release_shift_for_char_input(&mut en);
if wayland_use_uinput() {
// 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);
return;
// Normal text input: release Shift and use clipboard
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

View file

@ -118,6 +118,23 @@ pub mod client {
}
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() {
let keysym = char_to_keysym(c);
// ASCII characters: use keysym
@ -128,9 +145,6 @@ pub mod client {
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);
}
}
}
@ -167,8 +181,7 @@ pub mod client {
// 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)
{
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
log::error!("Failed to send keysym up: {:?}", e);
}
}