diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 1a6160324..0262169bb 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -15,6 +15,54 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; bool isEditOsPassword = false; +const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard'; +const String kWaylandKeyboardIssueUrl = + 'https://github.com/rustdesk/rustdesk/issues/14586'; +final Set _waylandKeyboardPromptSuppressedConnectionIds = {}; + +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), + ), + ); +} class TTextMenu { final Widget child; @@ -87,12 +135,152 @@ handleOsPasswordAction( } } +void showWaylandKeyboardInputWarningDialog( + {required String id, + required String connectionId, + required FFI ffi, + required Future 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 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), + ], + createDialogContent(kWaylandKeyboardIssueUrl).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 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 v = []; // elevation @@ -142,11 +330,60 @@ List 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 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-clear-perm-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 diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 29e710bbc..f4669644b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -101,6 +101,9 @@ class _RemotePageState extends State 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 // 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 _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 _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 _ffi.imageModel.disposeImage(); _ffi.cursorModel.disposeImages(); _rawKeyFocusNode.dispose(); + if (closeSession) { + clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString()); + } await _ffi.close(closeSession: closeSession); _timer?.cancel(); _ffi.dialogManager.dismissAll(); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index ec05c987f..8146e0d6f 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1861,18 +1861,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); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9102d163c..23fd70411 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -75,6 +75,9 @@ class _RemotePageState extends State 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 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 @@ -135,6 +152,9 @@ class _RemotePageState extends State 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(); @@ -163,6 +183,40 @@ class _RemotePageState extends State 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. @@ -294,7 +348,7 @@ class _RemotePageState extends State 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); @@ -306,6 +360,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { // handle mobile virtual keyboard void handleSoftKeyboardInput(String newValue) { + if (!inputModel.keyboardInputAllowed) { + return; + } if (isIOS) { _handleIOSSoftKeyboardInput(newValue); } else { @@ -314,6 +371,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { } void inputChar(String char) { + if (!inputModel.keyboardInputAllowed) { + return; + } if (char == '\n') { char = 'VK_RETURN'; } else if (char == ' ') { @@ -323,6 +383,29 @@ class _RemotePageState extends State 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; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 675a95e42..3095dc4ac 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -396,6 +396,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; diff --git a/src/clipboard.rs b/src/clipboard.rs index cae7d03ac..1300d72d7 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -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 { + 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, + side: ClipboardSide, + force: bool, +) -> Option { + let (msg, _) = read_clipboard_message(ctx, side, force)?; + Some(msg) +} + +#[cfg(not(target_os = "android"))] +fn read_clipboard_message( + ctx: &mut Option, + 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, 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, side: ClipboardS } } +#[cfg(not(target_os = "android"))] +fn append_owner_marker(mut data: Vec, side: ClipboardSide) -> Vec { + 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, 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, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 6d48e34ee..f921f2806 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "اسم العرض"), ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6c6a13315..d394508ac 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 218070291..feb769009 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f1cc8734..a8e6dfe0f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 75d16ff92..8dc0545ef 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "显示名称"), ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), + ("wayland-keyboard-input-disabled-tip", "键盘输入已禁用"), + ("wayland-keyboard-input-consent-tip", "此会话使用 Wayland。键盘输入可能会临时通过被控端剪贴板粘贴到当前应用,剪贴板历史或其他应用可能读取这些内容(包括密码)。\n点击“OK”允许当前会话。"), + ("wayland-keyboard-input-applies-to-tip", "此授权适用于:"), + ("wayland-soft-keyboard-input-label", "软键盘输入"), + ("wayland-keyboard-input-clear-perm-tip", "清除已保存键盘授权"), + ("remember-wayland-keyboard-choice-tip", "在此设备上始终允许键盘输入"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7b3dc7908..8dd7fdb7d 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 06ad254c7..495577824 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 39e077348..3168e5da3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Anzeigename"), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 38e11bfce..683815bc9 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Εμφανιζόμενο όνομα"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 73974a2e5..d93b61a89 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -274,5 +274,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("password-hidden-tip", "Permanent password is set (hidden)."), ("preset-password-in-use-tip", "Preset password is currently in use."), + ("wayland-keyboard-input-disabled-tip", "Keyboard input disabled"), + ("wayland-keyboard-input-consent-tip", "This session uses Wayland. Keyboard input may be temporarily pasted via the remote clipboard, and clipboard history or other apps may read it (including passwords).\nPress OK to allow input for this session."), + ("wayland-keyboard-input-applies-to-tip", "This permission applies to:"), + ("wayland-soft-keyboard-input-label", "Soft keyboard input"), + ("wayland-keyboard-input-clear-perm-tip", "Clear saved keyboard permission"), + ("remember-wayland-keyboard-choice-tip", "Always allow keyboard input on this device"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 921f79612..605efde12 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0f49079a2..05b266572 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index d65cd31c5..ea4b79c1a 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index f12ecf371..df6528d73 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5f6d5f005..fab2f85ee 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 43c033a11..164004203 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0dda7817f..6034c15d2 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nom d’affichage"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index dc78bc0d9..97bb89e38 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 741805e25..df8013110 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 2d596bacc..68554e60d 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e69514e45..6e2ff5416 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Kijelző név"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 356a9ee2d..7cf9fd968 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1b6e49691..05a3cc315 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Visualizza nome"), ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 805898ef9..ddee88b74 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7cc0c9067..3d9a60032 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "표시 이름"), ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e943ff4cd..899d6f4ff 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a4f39f1e4..713397bef 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 838984207..5ab1e1c49 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d9cf6ad38..0fe99e767 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6d140daad..5af8073d9 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2000de2c8..ddf5b0f13 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nazwa wyświetlana"), ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0cdcf93b4..5847ead70 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f9bae32b1..6476f819a 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 0a5ab0299..40673ea24 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 14bc96390..e08e642ed 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Отображаемое имя"), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f2c4fbfa2..1335e7012 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d0e99b2a4..0a40f7f0b 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index aef6b7c66..a185666e7 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5f9d5505b..e90ba60ac 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 19ae6896f..0dce3a908 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7ad257fcb..6982c65cc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 2cee45268..b30fee561 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ff755768c..c10cab19f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2d3eb1d34..47fbac87d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 5acb15221..3847d8f0e 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Görünen Ad"), ("password-hidden-tip", "Şifre gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5211cc92b..669218dc8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "顯示名稱"), ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 2594b7cc3..e2ce258c4 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 6939b2ea1..55f27663e 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -743,5 +743,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("wayland-keyboard-input-disabled-tip", ""), + ("wayland-keyboard-input-consent-tip", ""), + ("wayland-keyboard-input-applies-to-tip", ""), + ("wayland-soft-keyboard-input-label", ""), + ("wayland-keyboard-input-clear-perm-tip", ""), + ("remember-wayland-keyboard-choice-tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 1d2f0a3fb..67086fbc3 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -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 { + 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 { + 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')); + } +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 97dc78755..91a2901dc 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -457,6 +457,12 @@ lazy_static::lazy_static! { static ref RELATIVE_MOUSE_CONNS: Arc>> = Default::default(); } +#[cfg(target_os = "linux")] +lazy_static::lazy_static! { + static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc>> = + 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 { + 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 diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index 5348f2f24..81189159d 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -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); } }