mirror of
https://github.com/rustdesk/rustdesk
synced 2026-04-21 13:27:19 +00:00
Merge 4a8a27346a into 1e9c4d04f1
This commit is contained in:
commit
054f2a28cb
57 changed files with 1037 additions and 100 deletions
|
|
@ -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<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
|
||||
|
||||
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<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),
|
||||
],
|
||||
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<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
|
||||
|
|
@ -142,11 +330,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-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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -135,6 +152,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();
|
||||
|
|
@ -163,6 +183,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.
|
||||
|
|
@ -294,7 +348,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);
|
||||
|
|
@ -306,6 +360,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 {
|
||||
|
|
@ -314,6 +371,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 == ' ') {
|
||||
|
|
@ -323,6 +383,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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue