feat(terminal): add opt-in OSC 52 clipboard writes (#16072)

* feat(terminal): add opt-in OSC 52 clipboard writes

* Remove dup tr

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-09-05 13:21:49 +08:00
committed by GitHub
parent c1a587cfa4
commit 618bf37deb
68 changed files with 1279 additions and 119 deletions

View File

@@ -244,11 +244,38 @@ List<(String, String)> otherDefaultSettings() {
kKeyUseAllMyDisplaysForTheRemoteSession kKeyUseAllMyDisplaysForTheRemoteSession
), ),
('Keep terminal sessions on disconnect', kOptionTerminalPersistent), ('Keep terminal sessions on disconnect', kOptionTerminalPersistent),
(
'Allow terminal apps to copy to clipboard',
kOptionAllowTerminalClipboardWrite
),
]; ];
return v; return v;
} }
String getOtherDefaultSettingOption(String key) {
if (key == kOptionAllowTerminalClipboardWrite) {
return bind.mainGetLocalOption(key: key);
}
return bind.mainGetUserDefaultOption(key: key);
}
Future<void> setOtherDefaultSettingOption(String key, String value) {
if (key == kOptionAllowTerminalClipboardWrite) {
return bind.mainSetLocalOption(
key: key,
value: value == kTerminalClipboardWriteAllowed
? kTerminalClipboardWriteAllowed
: kTerminalClipboardWriteDenied,
);
}
return bind.mainSetUserDefaultOption(key: key, value: value);
}
bool isOtherDefaultSettingReadOnly(String key) =>
isOptionFixed(key) ||
(key == kOptionAllowTerminalClipboardWrite && bind.isDisableSettings());
class TrackpadSpeedWidget extends StatefulWidget { class TrackpadSpeedWidget extends StatefulWidget {
final SimpleWrapper<int> value; final SimpleWrapper<int> value;
// If null, no debouncer will be applied. // If null, no debouncer will be applied.

View File

@@ -115,6 +115,11 @@ const String kOptionEnableAudio = "enable-audio";
const String kOptionEnableCamera = "enable-camera"; const String kOptionEnableCamera = "enable-camera";
const String kOptionEnableTerminal = "enable-terminal"; const String kOptionEnableTerminal = "enable-terminal";
const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionTerminalPersistent = "terminal-persistent";
const String kOptionAllowTerminalClipboardWrite =
"allow-terminal-clipboard-write";
const String kTerminalClipboardWriteUnconfigured = "";
const String kTerminalClipboardWriteAllowed = "Y";
const String kTerminalClipboardWriteDenied = "N";
const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableTunnel = "enable-tunnel";
const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableRemoteRestart = "enable-remote-restart";
const String kOptionEnableBlockInput = "enable-block-input"; const String kOptionEnableBlockInput = "enable-block-input";

View File

@@ -2080,14 +2080,13 @@ class _DisplayState extends State<_Display> {
} }
Widget otherRow(String label, String key) { Widget otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; final value = getOtherDefaultSettingOption(key) == 'Y';
final isOptFixed = isOptionFixed(key); final isOptFixed = isOtherDefaultSettingReadOnly(key);
onChanged(bool b) async { onChanged(bool b) async {
await bind.mainSetUserDefaultOption( await setOtherDefaultSettingOption(
key: key, key,
value: b b ? 'Y' : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo),
? 'Y' );
: (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo));
setState(() {}); setState(() {});
} }

View File

@@ -19,6 +19,8 @@ class TerminalPage extends StatefulWidget {
required this.tabKey, required this.tabKey,
this.forceRelay, this.forceRelay,
this.connToken, this.connToken,
this.onClipboardWriteBlocked,
this.onClipboardWriteSucceeded,
}) : super(key: key); }) : super(key: key);
final String id; final String id;
final String? password; final String? password;
@@ -26,6 +28,8 @@ class TerminalPage extends StatefulWidget {
final bool? forceRelay; final bool? forceRelay;
final bool? isSharedPassword; final bool? isSharedPassword;
final String? connToken; final String? connToken;
final ValueChanged<String>? onClipboardWriteBlocked;
final ValueChanged<String>? onClipboardWriteSucceeded;
final int terminalId; final int terminalId;
/// Tab key for focus management, passed from parent to avoid duplicate construction /// Tab key for focus management, passed from parent to avoid duplicate construction
@@ -71,6 +75,8 @@ class _TerminalPageState extends State<TerminalPage>
// Create terminal model with specific terminal ID // Create terminal model with specific terminal ID
_terminalModel = TerminalModel(_ffi, widget.terminalId); _terminalModel = TerminalModel(_ffi, widget.terminalId);
_terminalModel.onClipboardWriteBlocked = widget.onClipboardWriteBlocked;
_terminalModel.onClipboardWriteSucceeded = widget.onClipboardWriteSucceeded;
debugPrint( debugPrint(
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); '[TerminalPage] Terminal model created for terminal ${widget.terminalId}');

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart';
@@ -10,6 +11,8 @@ import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/terminal_copy_shortcut.dart';
import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
@@ -19,6 +22,12 @@ import '../widgets/material_mod_popup_menu.dart' as mod_menu;
import '../widgets/popup_menu.dart'; import '../widgets/popup_menu.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
typedef _TerminalClipboardSource = ({
String peerId,
int terminalId,
String tabKey,
});
class TerminalTabPage extends StatefulWidget { class TerminalTabPage extends StatefulWidget {
final Map<String, dynamic> params; final Map<String, dynamic> params;
@@ -30,6 +39,18 @@ class TerminalTabPage extends StatefulWidget {
class _TerminalTabPageState extends State<TerminalTabPage> { class _TerminalTabPageState extends State<TerminalTabPage> {
DesktopTabController get tabController => Get.find<DesktopTabController>(); DesktopTabController get tabController => Get.find<DesktopTabController>();
bool get _canConfigureTerminalClipboardPermission =>
canConfigureTerminalClipboardPermission(
settingsDisabled: bind.isDisableSettings(),
optionFixed: isOptionFixed(kOptionAllowTerminalClipboardWrite),
);
bool get _canHandleTerminalClipboardWriteRequest =>
canHandleTerminalClipboardWriteRequest(
localOption: bind.mainGetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
),
canConfigurePermission: _canConfigureTerminalClipboardPermission,
);
static const IconData selectedIcon = Icons.terminal; static const IconData selectedIcon = Icons.terminal;
static const IconData unselectedIcon = Icons.terminal_outlined; static const IconData unselectedIcon = Icons.terminal_outlined;
@@ -38,6 +59,9 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
final Set<String> _closingTabs = {}; final Set<String> _closingTabs = {};
// When true, all session cleanup should persist (window-level close in progress) // When true, all session cleanup should persist (window-level close in progress)
bool _windowClosing = false; bool _windowClosing = false;
CancelFunc? _terminalClipboardNoticeCancel;
final _terminalClipboardNotice =
TerminalClipboardNoticeCoordinator<_TerminalClipboardSource>();
_TerminalTabPageState(Map<String, dynamic> params) { _TerminalTabPageState(Map<String, dynamic> params) {
Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); Get.put(DesktopTabController(tabType: DesktopTabType.terminal));
@@ -45,7 +69,10 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
WindowController.fromWindowId(windowId()) WindowController.fromWindowId(windowId())
.setTitle(getWindowNameWithId(id)); .setTitle(getWindowNameWithId(id));
}; };
tabController.onRemoved = (_, id) => onRemoveId(id); tabController.onRemoved = (_, id) {
_closeTerminalClipboardNoticeForTab(id);
onRemoveId(id);
};
tabController.onCloseWindow = _closeWindowFromConnection; tabController.onCloseWindow = _closeWindowFromConnection;
final terminalId = params['terminalId'] ?? _nextTerminalId++; final terminalId = params['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab( tabController.add(_createTerminalTab(
@@ -70,6 +97,11 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias'); final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias');
final tabLabel = final tabLabel =
alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId'; alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId';
final clipboardSource = (
peerId: peerId,
terminalId: terminalId,
tabKey: tabKey,
);
return TabInfo( return TabInfo(
key: tabKey, key: tabKey,
label: tabLabel, label: tabLabel,
@@ -86,10 +118,169 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
tabController: tabController, tabController: tabController,
forceRelay: forceRelay, forceRelay: forceRelay,
connToken: connToken, connToken: connToken,
onClipboardWriteBlocked: _canHandleTerminalClipboardWriteRequest
? (text) => _handleTerminalClipboardWriteBlocked(
clipboardSource,
text,
)
: null,
onClipboardWriteSucceeded: (_) {
_handleTerminalClipboardWriteSucceeded(clipboardSource);
},
), ),
); );
} }
void _handleTerminalClipboardWriteBlocked(
_TerminalClipboardSource source,
String clipboardText,
) {
if (!mounted) return;
final option = bind.mainGetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
);
final request = _terminalClipboardNotice.recordBlocked(
source: source,
text: clipboardText,
option: option,
canWrite: _canWriteTerminalClipboard,
);
if (request != null) _showTerminalClipboardNotice(request);
}
void _showTerminalClipboardNotice(
TerminalClipboardNoticeRequest<_TerminalClipboardSource> request,
) {
_terminalClipboardNoticeCancel = BotToast.showCustomNotification(
duration: null,
enableSlideOff: false,
onlyOne: true,
onClose: _handleTerminalClipboardNoticeClosed,
toastBuilder: (_) => AnimatedBuilder(
animation: _terminalClipboardNotice,
builder: (_, __) => MaterialBanner(
leading: const Icon(Icons.content_copy_outlined),
content: Text(translate(kTerminalClipboardNoticeMessageKey)),
actions: [
TextButton(
onPressed: _terminalClipboardNotice.canClaimAction
? _handleTerminalClipboardNegativeAction
: null,
child: Text(translate(request.negativeActionKey)),
),
TextButton(
onPressed: _terminalClipboardNotice.canClaimAction
? _handleTerminalClipboardPositiveAction
: null,
child: Text(translate(request.actionKey)),
),
],
),
),
);
}
void _handleTerminalClipboardNegativeAction() {
final request = _terminalClipboardNotice.claimCurrentAction();
if (request == null) return;
if (request.persistAllowed) {
unawaited(_declineTerminalClipboardWrite());
} else {
_closeTerminalClipboardNotice();
}
}
void _handleTerminalClipboardPositiveAction() {
final request = _terminalClipboardNotice.claimCurrentAction();
if (request == null) return;
unawaited(_completeTerminalClipboardWrite(request));
}
void _handleTerminalClipboardNoticeClosed() {
_terminalClipboardNoticeCancel = null;
_terminalClipboardNotice.noticeClosed();
}
bool _canWriteTerminalClipboard(
_TerminalClipboardSource source,
) {
if (!_canHandleTerminalClipboardWriteRequest) return false;
final ffi = TerminalConnectionManager.getExistingConnection(source.peerId);
return ffi != null &&
!ffi.closed &&
ffi.ffiModel.permissions['clipboard'] != false &&
tabController.state.value.tabs.any((tab) => tab.key == source.tabKey) &&
ffi.terminalModels.containsKey(source.terminalId);
}
void _handleTerminalClipboardWriteSucceeded(
_TerminalClipboardSource source,
) {
final request = _terminalClipboardNotice.currentForSource(source);
if (request == null) return;
_closeTerminalClipboardNotice();
}
Future<void> _declineTerminalClipboardWrite() async {
try {
await bind.mainSetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
value: kTerminalClipboardWriteDenied,
);
} catch (error) {
debugPrint(
'[TerminalTabPage] Failed to save terminal clipboard permission: $error');
return;
} finally {
_terminalClipboardNotice.releaseAction();
}
_closeTerminalClipboardNotice();
}
Future<void> _completeTerminalClipboardWrite(
TerminalClipboardNoticeRequest<_TerminalClipboardSource> request,
) async {
final source = request.source;
var completed = false;
try {
completed = await completeTerminalClipboardWrite(
clipboardText: request.text,
canWrite: () => _canWriteTerminalClipboard(source),
writeClipboard: writeTerminalClipboard,
persistAllowed: request.persistAllowed
? () => bind.mainSetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
value: kTerminalClipboardWriteAllowed,
)
: null,
);
} catch (error) {
debugPrint(
'[TerminalTabPage] Failed to complete terminal clipboard write: $error');
} finally {
_terminalClipboardNotice.releaseAction();
}
if (!completed) return;
_closeTerminalClipboardNotice();
}
void _closeTerminalClipboardNoticeForTab(String tabKey) {
final current = _terminalClipboardNotice.current;
if (current?.source.tabKey != tabKey) return;
_closeTerminalClipboardNotice();
}
void _closeTerminalClipboardNotice() {
if (!_terminalClipboardNotice.beginClose()) return;
final cancel = _terminalClipboardNoticeCancel;
if (cancel == null) {
debugPrint('[TerminalTabPage] Clipboard notice controller is missing');
_terminalClipboardNotice.noticeClosed();
return;
}
cancel();
}
/// Unified tab close handler for all close paths (button, shortcut, programmatic). /// Unified tab close handler for all close paths (button, shortcut, programmatic).
/// Shows audit dialog, cleans up session if not persistent, then removes the UI tab. /// Shows audit dialog, cleans up session if not persistent, then removes the UI tab.
Future<void> _closeTab(String tabKey) async { Future<void> _closeTab(String tabKey) async {
@@ -147,6 +338,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
// Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) // Remove all UI tabs immediately (same instant behavior as the old tabController.clear())
// Keep the cleanup target lookup below synchronous before its first await: // Keep the cleanup target lookup below synchronous before its first await:
// it relies on the current frame still retaining each TerminalPage's FFI/model. // it relies on the current frame still retaining each TerminalPage's FFI/model.
_terminalClipboardNotice.clear();
_terminalClipboardNoticeCancel?.call();
tabController.clear(); tabController.clear();
// Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout). // Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout).
// Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls. // Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls.
@@ -357,6 +550,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
@override @override
void dispose() { void dispose() {
HardwareKeyboard.instance.removeHandler(_handleKeyEvent); HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
_terminalClipboardNotice.clear();
_terminalClipboardNoticeCancel?.call();
super.dispose(); super.dispose();
} }

View File

@@ -1269,16 +1269,18 @@ class __DisplayPageState extends State<_DisplayPage> {
} }
SettingsTile otherRow(String label, String key) { SettingsTile otherRow(String label, String key) {
final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; final value = getOtherDefaultSettingOption(key) == 'Y';
final isOptFixed = isOptionFixed(key); final isOptFixed = isOtherDefaultSettingReadOnly(key);
return SettingsTile.switchTile( return SettingsTile.switchTile(
initialValue: value, initialValue: value,
title: Text(translate(label)), title: Text(translate(label)),
onToggle: isOptFixed onToggle: isOptFixed
? null ? null
: (b) async { : (b) async {
await bind.mainSetUserDefaultOption( await setOtherDefaultSettingOption(
key: key, value: b ? 'Y' : defaultOptionNo); key,
b ? 'Y' : defaultOptionNo,
);
setState(() {}); setState(() {});
}, },
); );

View File

@@ -11,6 +11,7 @@ import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/terminal_copy_shortcut.dart'; import 'package:flutter_hbb/models/terminal_copy_shortcut.dart';
import 'package:flutter_hbb/models/terminal_model.dart'; import 'package:flutter_hbb/models/terminal_model.dart';
import 'package:flutter_hbb/models/terminal_mouse_handler.dart';
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart'; import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
import 'package:flutter_hbb/web/dummy.dart' import 'package:flutter_hbb/web/dummy.dart'
if (dart.library.html) 'package:flutter_hbb/web/terminal_font.dart'; if (dart.library.html) 'package:flutter_hbb/web/terminal_font.dart';
@@ -19,6 +20,49 @@ import 'package:xterm/xterm.dart';
import '../../desktop/pages/terminal_connection_manager.dart'; import '../../desktop/pages/terminal_connection_manager.dart';
import '../../consts.dart'; import '../../consts.dart';
const _terminalBackgroundOpacity = 0.7;
Widget _buildTerminalViewForPlatform({
required bool reportMouseInput,
required bool reportTouchInput,
required Terminal terminal,
required TerminalController controller,
required TerminalStyle textStyle,
required EdgeInsets padding,
required bool deleteDetection,
required Map<ShortcutActivator, Intent>? shortcuts,
required FocusOnKeyEventCallback onKeyEvent,
required void Function(TapDownDetails, CellOffset) onSecondaryTapDown,
}) {
if (reportMouseInput || reportTouchInput) {
return TerminalMouseInteraction(
terminal,
controller: controller,
autofocus: true,
textStyle: textStyle,
deleteDetection: deleteDetection,
reportTouchInput: reportTouchInput,
shortcuts: shortcuts,
onKeyEvent: onKeyEvent,
backgroundOpacity: _terminalBackgroundOpacity,
padding: padding,
onSecondaryTapDown: onSecondaryTapDown,
);
}
return TerminalView(
terminal,
controller: controller,
autofocus: true,
textStyle: textStyle,
deleteDetection: deleteDetection,
shortcuts: shortcuts,
onKeyEvent: onKeyEvent,
backgroundOpacity: _terminalBackgroundOpacity,
padding: padding,
onSecondaryTapDown: onSecondaryTapDown,
);
}
class TerminalPage extends StatefulWidget { class TerminalPage extends StatefulWidget {
const TerminalPage({ const TerminalPage({
Key? key, Key? key,
@@ -41,6 +85,19 @@ class TerminalPage extends StatefulWidget {
class _TerminalPageState extends State<TerminalPage> class _TerminalPageState extends State<TerminalPage>
with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { with AutomaticKeepAliveClientMixin, WidgetsBindingObserver {
bool get _canConfigureTerminalClipboardPermission =>
canConfigureTerminalClipboardPermission(
settingsDisabled: bind.isDisableSettings(),
optionFixed: isOptionFixed(kOptionAllowTerminalClipboardWrite),
);
bool get _canHandleTerminalClipboardWriteRequest =>
canHandleTerminalClipboardWriteRequest(
localOption: bind.mainGetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
),
canConfigurePermission: _canConfigureTerminalClipboardPermission,
);
late FFI _ffi; late FFI _ffi;
late TerminalModel _terminalModel; late TerminalModel _terminalModel;
double? _cellHeight; double? _cellHeight;
@@ -57,6 +114,9 @@ class _TerminalPageState extends State<TerminalPage>
// For iOS edge swipe gesture // For iOS edge swipe gesture
double _swipeStartX = 0; double _swipeStartX = 0;
double _swipeCurrentX = 0; double _swipeCurrentX = 0;
ScaffoldFeatureController<MaterialBanner, MaterialBannerClosedReason>?
_terminalClipboardNoticeController;
final _terminalClipboardNotice = TerminalClipboardNoticeCoordinator<int>();
// For web only. // For web only.
// 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety.
@@ -89,6 +149,12 @@ class _TerminalPageState extends State<TerminalPage>
// Create terminal model with specific terminal ID // Create terminal model with specific terminal ID
_terminalModel = TerminalModel(_ffi, widget.terminalId); _terminalModel = TerminalModel(_ffi, widget.terminalId);
if (_canHandleTerminalClipboardWriteRequest) {
_terminalModel.onClipboardWriteBlocked =
_handleTerminalClipboardWriteBlocked;
_terminalModel.onClipboardWriteSucceeded =
_handleTerminalClipboardWriteSucceeded;
}
debugPrint( debugPrint(
'[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); '[TerminalPage] Terminal model created for terminal ${widget.terminalId}');
@@ -134,12 +200,144 @@ class _TerminalPageState extends State<TerminalPage>
_ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id);
} }
void _handleTerminalClipboardWriteBlocked(String clipboardText) {
if (!mounted) return;
final option = bind.mainGetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
);
final request = _terminalClipboardNotice.recordBlocked(
source: widget.terminalId,
text: clipboardText,
option: option,
canWrite: (_) => _canWriteTerminalClipboard,
);
if (request != null) _showTerminalClipboardNotice(request);
}
void _showTerminalClipboardNotice(
TerminalClipboardNoticeRequest<int> request,
) {
final controller = ScaffoldMessenger.of(context).showMaterialBanner(
MaterialBanner(
leading: const Icon(Icons.content_copy_outlined),
content: Text(translate(kTerminalClipboardNoticeMessageKey)),
actions: [
AnimatedBuilder(
animation: _terminalClipboardNotice,
builder: (_, __) => TextButton(
onPressed: _terminalClipboardNotice.canClaimAction
? _handleTerminalClipboardNegativeAction
: null,
child: Text(translate(request.negativeActionKey)),
),
),
AnimatedBuilder(
animation: _terminalClipboardNotice,
builder: (_, __) => TextButton(
onPressed: _terminalClipboardNotice.canClaimAction
? _handleTerminalClipboardPositiveAction
: null,
child: Text(translate(request.actionKey)),
),
),
],
),
);
_terminalClipboardNoticeController = controller;
unawaited(controller.closed.then<void>((_) {
if (identical(_terminalClipboardNoticeController, controller)) {
_terminalClipboardNoticeController = null;
_terminalClipboardNotice.noticeClosed();
}
}));
}
void _handleTerminalClipboardNegativeAction() {
final request = _terminalClipboardNotice.claimCurrentAction();
if (request == null) return;
if (request.persistAllowed) {
unawaited(_declineTerminalClipboardWrite());
} else {
_closeTerminalClipboardNotice();
}
}
void _handleTerminalClipboardPositiveAction() {
final request = _terminalClipboardNotice.claimCurrentAction();
if (request == null) return;
unawaited(_completeTerminalClipboardWrite(request));
}
bool get _canWriteTerminalClipboard =>
_canHandleTerminalClipboardWriteRequest &&
!_ffi.closed &&
_ffi.ffiModel.permissions['clipboard'] != false;
void _handleTerminalClipboardWriteSucceeded(String _) {
_closeTerminalClipboardNotice();
}
Future<void> _declineTerminalClipboardWrite() async {
try {
await bind.mainSetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
value: kTerminalClipboardWriteDenied,
);
} catch (error) {
debugPrint(
'[TerminalPage] Failed to save terminal clipboard permission: $error');
return;
} finally {
_terminalClipboardNotice.releaseAction();
}
_closeTerminalClipboardNotice();
}
Future<void> _completeTerminalClipboardWrite(
TerminalClipboardNoticeRequest<int> request,
) async {
var completed = false;
try {
completed = await completeTerminalClipboardWrite(
clipboardText: request.text,
canWrite: () => _canWriteTerminalClipboard,
writeClipboard: writeTerminalClipboard,
persistAllowed: request.persistAllowed
? () => bind.mainSetLocalOption(
key: kOptionAllowTerminalClipboardWrite,
value: kTerminalClipboardWriteAllowed,
)
: null,
);
} catch (error) {
debugPrint(
'[TerminalPage] Failed to complete terminal clipboard write: $error');
} finally {
_terminalClipboardNotice.releaseAction();
}
if (!completed) return;
_closeTerminalClipboardNotice();
}
void _closeTerminalClipboardNotice() {
if (!_terminalClipboardNotice.beginClose()) return;
final controller = _terminalClipboardNoticeController;
if (controller == null) {
debugPrint('[TerminalPage] Clipboard notice controller is missing');
_terminalClipboardNotice.noticeClosed();
return;
}
controller.close();
}
@override @override
void dispose() { void dispose() {
// Unregister terminal model from FFI // Unregister terminal model from FFI
_ffi.unregisterTerminalModel(widget.terminalId); _ffi.unregisterTerminalModel(widget.terminalId);
_terminalModel.dispose(); _terminalModel.dispose();
_keyboardDebounce?.cancel(); _keyboardDebounce?.cancel();
_terminalClipboardNotice.clear();
_terminalClipboardNoticeController?.close();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
super.dispose(); super.dispose();
TerminalConnectionManager.releaseConnection(widget.id); TerminalConnectionManager.releaseConnection(widget.id);
@@ -234,12 +432,12 @@ class _TerminalPageState extends State<TerminalPage>
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final heightPx = constraints.maxHeight; final heightPx = constraints.maxHeight;
return TerminalView( return _buildTerminalViewForPlatform(
_terminalModel.terminal, reportMouseInput: isWebDesktop || isAndroid,
reportTouchInput: isIOS,
terminal: _terminalModel.terminal,
controller: _terminalModel.terminalController, controller: _terminalModel.terminalController,
autofocus: true,
textStyle: _getTerminalStyle(), textStyle: _getTerminalStyle(),
backgroundOpacity: 0.7,
// The following comment is from xterm.dart source code: // The following comment is from xterm.dart source code:
// Workaround to detect delete key for platforms and IMEs that do not // Workaround to detect delete key for platforms and IMEs that do not
// emit a hardware delete event. Preferred on mobile platforms. [false] by // emit a hardware delete event. Preferred on mobile platforms. [false] by

View File

@@ -1,7 +1,108 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:xterm/xterm.dart'; import 'package:xterm/xterm.dart';
enum TerminalClipboardWritePermission { denied, unconfigured, allowed }
class RustDeskTerminal extends Terminal { class RustDeskTerminal extends Terminal {
RustDeskTerminal({super.maxLines}); RustDeskTerminal({
super.maxLines,
required TerminalClipboardWritePermission Function()
clipboardWritePermission,
required Future<bool> Function(String) onClipboardWrite,
ValueChanged<String>? onClipboardWriteBlocked,
ValueChanged<String>? onClipboardWriteSucceeded,
}) : _clipboardWritePermission = clipboardWritePermission,
_onClipboardWrite = onClipboardWrite,
_onClipboardWriteBlocked = onClipboardWriteBlocked,
_onClipboardWriteSucceeded = onClipboardWriteSucceeded {
onPrivateOSC = _handlePrivateOsc;
}
static const _clipboardOscCode = '52';
static const _systemClipboardSelection = 'c';
// Match the terminal helper's existing payload safety ceiling.
static const _maxClipboardWriteBytes = 16 * 1024 * 1024;
static const _base64InputBytesPerBlock = 3;
static const _base64EncodedCharsPerBlock = 4;
static final _osc52Selection = RegExp(r'^[cpqs0-7]*$');
final TerminalClipboardWritePermission Function() _clipboardWritePermission;
final Future<bool> Function(String) _onClipboardWrite;
final ValueChanged<String>? _onClipboardWriteBlocked;
final ValueChanged<String>? _onClipboardWriteSucceeded;
bool get isClipboardWriteAllowed =>
_clipboardWritePermission() == TerminalClipboardWritePermission.allowed;
void _handlePrivateOsc(String code, List<String> args) {
if (code != _clipboardOscCode) return;
if (args.length != 2 || !_osc52Selection.hasMatch(args.first)) {
debugPrint('[RustDeskTerminal] Rejected malformed OSC 52 command');
return;
}
if (args.last == '?') {
debugPrint('[RustDeskTerminal] Rejected OSC 52 clipboard query');
return;
}
final permission = _clipboardWritePermission();
if (permission == TerminalClipboardWritePermission.denied) {
debugPrint('[RustDeskTerminal] Rejected unauthorized OSC 52 write');
return;
}
final selection = args.first;
if (selection.isNotEmpty &&
!selection.contains(_systemClipboardSelection)) {
debugPrint('[RustDeskTerminal] Ignored unsupported OSC 52 selection');
return;
}
if (selection.replaceAll(_systemClipboardSelection, '').isNotEmpty) {
debugPrint('[RustDeskTerminal] Ignored unsupported OSC 52 selections');
}
final text = _decodeClipboardPayload(args.last);
if (text == null) return;
if (permission == TerminalClipboardWritePermission.unconfigured) {
debugPrint('[RustDeskTerminal] Blocked OSC 52 write pending consent');
_onClipboardWriteBlocked?.call(text);
return;
}
unawaited(_writeClipboard(text));
}
Future<void> _writeClipboard(String text) async {
final succeeded = await _onClipboardWrite(text);
if (succeeded) {
_onClipboardWriteSucceeded?.call(text);
return;
}
debugPrint(
'[RustDeskTerminal] OSC 52 clipboard write requires interaction');
_onClipboardWriteBlocked?.call(text);
}
String? _decodeClipboardPayload(String payload) {
if (payload.length > _maxBase64EncodedLength(_maxClipboardWriteBytes)) {
debugPrint('[RustDeskTerminal] Rejected oversized OSC 52 payload');
return null;
}
try {
final bytes = base64.decode(payload);
if (bytes.length > _maxClipboardWriteBytes) {
debugPrint('[RustDeskTerminal] Rejected oversized OSC 52 payload');
return null;
}
return utf8.decode(bytes);
} on FormatException {
debugPrint('[RustDeskTerminal] Rejected malformed OSC 52 payload');
return null;
}
}
static int _maxBase64EncodedLength(int maxBytes) =>
((maxBytes + _base64InputBytesPerBlock - 1) ~/
_base64InputBytesPerBlock) *
_base64EncodedCharsPerBlock;
@override @override
void eraseScrollbackOnly() { void eraseScrollbackOnly() {

View File

@@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
Future<bool> writeTerminalClipboardPlatform(
String text, {
bool userInitiated = false,
}) async {
try {
await Clipboard.setData(ClipboardData(text: text));
return true;
} catch (error) {
debugPrint('[Terminal] Failed to write clipboard: $error');
return false;
}
}

View File

@@ -0,0 +1,29 @@
import 'dart:js_interop';
import 'package:flutter/foundation.dart';
const _writeTerminalClipboardCommand = 'write_terminal_clipboard';
@JS('setByName')
external JSPromise<JSBoolean> _setByName(
JSString name,
JSString value,
JSBoolean userInitiated,
);
Future<bool> writeTerminalClipboardPlatform(
String text, {
bool userInitiated = false,
}) async {
try {
final result = await _setByName(
_writeTerminalClipboardCommand.toJS,
text.toJS,
userInitiated.toJS,
).toDart;
return result.toDart;
} catch (error) {
debugPrint('[Terminal] Failed to write Web clipboard: $error');
return false;
}
}

View File

@@ -3,20 +3,130 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:xterm/xterm.dart'; import 'package:xterm/xterm.dart';
import 'terminal_clipboard_writer.dart'
if (dart.library.html) 'terminal_clipboard_writer_web.dart';
const _controlShiftVPasteShortcut = SingleActivator( const _controlShiftVPasteShortcut = SingleActivator(
LogicalKeyboardKey.keyV, LogicalKeyboardKey.keyV,
control: true, control: true,
shift: true, shift: true,
); );
Future<void> writeTerminalClipboard(String text) async { typedef TerminalClipboardWriter = Future<bool> Function(
try { String text, {
await Clipboard.setData(ClipboardData(text: text)); required bool userInitiated,
} catch (error) { });
debugPrint('[Terminal] Failed to write clipboard: $error');
class TerminalClipboardNoticeRequest<T> {
const TerminalClipboardNoticeRequest({
required this.source,
required this.text,
required this.persistAllowed,
});
final T source;
final String text;
final bool persistAllowed;
String get actionKey => persistAllowed ? 'Enable' : 'Copy to clipboard';
String get negativeActionKey => persistAllowed ? 'Decline' : 'Dismiss';
}
const kTerminalClipboardNoticeMessageKey = 'terminal-clipboard-write-tip';
class TerminalClipboardNoticeCoordinator<T> extends ChangeNotifier {
TerminalClipboardNoticeRequest<T>? _current;
bool _noticeVisible = false;
bool _actionInProgress = false;
TerminalClipboardNoticeRequest<T>? get current => _current;
bool get canClaimAction =>
_noticeVisible && !_actionInProgress && _current != null;
TerminalClipboardNoticeRequest<T>? currentForSource(T source) {
final current = _current;
if (current == null || current.source != source) return null;
return current;
} }
TerminalClipboardNoticeRequest<T>? recordBlocked({
required T source,
required String text,
required String option,
required bool Function(T source) canWrite,
}) {
if (!canWrite(source)) return null;
final requestAllowsPersistence =
option == kTerminalClipboardWriteUnconfigured;
if (option != kTerminalClipboardWriteAllowed && !requestAllowsPersistence) {
return null;
}
if (_noticeVisible && _actionInProgress) return null;
final wasVisible = _noticeVisible;
final persistAllowed =
wasVisible ? _current?.persistAllowed : requestAllowsPersistence;
final request = TerminalClipboardNoticeRequest(
source: source,
text: text,
persistAllowed: persistAllowed ?? requestAllowsPersistence,
);
_current = request;
if (wasVisible) return null;
_noticeVisible = true;
return request;
}
TerminalClipboardNoticeRequest<T>? claimCurrentAction() {
if (!canClaimAction) return null;
final current = _current;
if (current == null) return null;
_actionInProgress = true;
notifyListeners();
return current;
}
void releaseAction() {
if (!_actionInProgress) return;
_actionInProgress = false;
notifyListeners();
}
bool beginClose() {
if (!_noticeVisible) return false;
_actionInProgress = true;
notifyListeners();
return true;
}
void noticeClosed() => clear();
void clear() {
_current = null;
_noticeVisible = false;
_actionInProgress = false;
}
}
Future<bool> writeTerminalClipboard(
String text, {
bool userInitiated = false,
}) =>
writeTerminalClipboardPlatform(text, userInitiated: userInitiated);
Future<bool> completeTerminalClipboardWrite({
required String clipboardText,
required bool Function() canWrite,
required TerminalClipboardWriter writeClipboard,
Future<void> Function()? persistAllowed,
}) async {
if (!canWrite()) return false;
if (!await writeClipboard(clipboardText, userInitiated: true)) return false;
await persistAllowed?.call();
return true;
} }
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() { Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
@@ -68,7 +178,7 @@ FocusOnKeyEventCallback terminalCopyHandler(
if (selection != null && !selection.isCollapsed) { if (selection != null && !selection.isCollapsed) {
if (event is KeyDownEvent) { if (event is KeyDownEvent) {
final text = terminal.buffer.getText(selection); final text = terminal.buffer.getText(selection);
unawaited(writeTerminalClipboard(text)); unawaited(writeTerminalClipboard(text, userInitiated: true));
} }
return KeyEventResult.handled; return KeyEventResult.handled;
} }

View File

@@ -11,8 +11,38 @@ import 'input_modifier_utils.dart';
import 'model.dart'; import 'model.dart';
import 'platform_model.dart'; import 'platform_model.dart';
import 'rustdesk_terminal.dart'; import 'rustdesk_terminal.dart';
import 'terminal_copy_shortcut.dart';
import 'terminal_mouse_handler.dart'; import 'terminal_mouse_handler.dart';
bool canConfigureTerminalClipboardPermission({
required bool settingsDisabled,
required bool optionFixed,
}) =>
!settingsDisabled && !optionFixed;
bool canHandleTerminalClipboardWriteRequest({
required String localOption,
required bool canConfigurePermission,
}) =>
canConfigurePermission || localOption == kTerminalClipboardWriteAllowed;
TerminalClipboardWritePermission terminalClipboardWritePermission(
String localOption, {
required bool remoteClipboardEnabled,
bool canRequestConsent = true,
}) {
if (!remoteClipboardEnabled) {
return TerminalClipboardWritePermission.denied;
}
if (localOption == kTerminalClipboardWriteAllowed) {
return TerminalClipboardWritePermission.allowed;
}
if (localOption == kTerminalClipboardWriteUnconfigured && canRequestConsent) {
return TerminalClipboardWritePermission.unconfigured;
}
return TerminalClipboardWritePermission.denied;
}
class TerminalModel with ChangeNotifier { class TerminalModel with ChangeNotifier {
final String id; // peer id final String id; // peer id
final FFI parent; final FFI parent;
@@ -62,6 +92,9 @@ class TerminalModel with ChangeNotifier {
/// The listener (typically TerminalPage) can use this to auto-close the tab/page. /// The listener (typically TerminalPage) can use this to auto-close the tab/page.
VoidCallback? onClosed; VoidCallback? onClosed;
ValueChanged<String>? onClipboardWriteBlocked;
ValueChanged<String>? onClipboardWriteSucceeded;
Future<void> _handleInput(String data) async { Future<void> _handleInput(String data) async {
// xterm can complete asynchronous input after the Flutter page has gone // xterm can complete asynchronous input after the Flutter page has gone
// away. Stop before reading or clearing widget-owned modifier state. // away. Stop before reading or clearing widget-owned modifier state.
@@ -130,7 +163,19 @@ class TerminalModel with ChangeNotifier {
} }
TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id { TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id {
terminal = RustDeskTerminal(maxLines: 10000); terminal = RustDeskTerminal(
maxLines: 10000,
onClipboardWrite: writeTerminalClipboard,
clipboardWritePermission: () => terminalClipboardWritePermission(
bind.mainGetLocalOption(key: kOptionAllowTerminalClipboardWrite),
remoteClipboardEnabled:
parent.ffiModel.permissions['clipboard'] != false,
canRequestConsent: onClipboardWriteBlocked != null,
),
onClipboardWriteBlocked: (text) => onClipboardWriteBlocked?.call(text),
onClipboardWriteSucceeded: (text) =>
onClipboardWriteSucceeded?.call(text),
);
terminal.mouseHandler = const WheelButtonFixMouseHandler(); terminal.mouseHandler = const WheelButtonFixMouseHandler();
terminalController = TerminalController(); terminalController = TerminalController();
@@ -593,6 +638,8 @@ class TerminalModel with ChangeNotifier {
clearAltLock = null; clearAltLock = null;
onResizeExternal = null; onResizeExternal = null;
onClosed = null; onClosed = null;
onClipboardWriteBlocked = null;
onClipboardWriteSucceeded = null;
// Clear buffers to free memory // Clear buffers to free memory
_inputBuffer.clear(); _inputBuffer.clear();
_pendingOutputChunks.clear(); _pendingOutputChunks.clear();

View File

@@ -62,13 +62,17 @@ class TerminalMouseDragReporter {
var _ownsControllerSuspension = false; var _ownsControllerSuspension = false;
var _releasePending = false; var _releasePending = false;
var _reporting = false; var _reporting = false;
var _dragged = false;
bool handleDown( bool handleDown(
PointerDownEvent event, PointerDownEvent event,
Terminal terminal, Terminal terminal,
TerminalViewState? terminalView, TerminalViewState? terminalView, {
) { bool reportTouchInput = false,
if (!_isPrimaryMouse(event) || !_reportsDrag(terminal.mouseMode)) { bool deferReport = false,
}) {
if (!_isPrimaryPointer(event, reportTouchInput) ||
!_reportsDrag(terminal.mouseMode)) {
return false; return false;
} }
if (terminalView == null || terminalView.widget.readOnly) return false; if (terminalView == null || terminalView.widget.readOnly) return false;
@@ -83,14 +87,33 @@ class TerminalMouseDragReporter {
_pointerId = event.pointer; _pointerId = event.pointer;
_controller = controller; _controller = controller;
_ownsControllerSuspension = true; _ownsControllerSuspension = true;
_releasePending = true; _releasePending = !deferReport;
_reporting = true; _reporting = !deferReport;
_dragged = false;
controller.setSuspendPointerInput(true); controller.setSuspendPointerInput(true);
_clearSelection(controller); _clearSelection(controller);
final position = _cellAt(event, terminalView); final position = _cellAt(event, terminalView);
_lastReportedPosition = position; _lastReportedPosition = position;
if (!deferReport) {
terminal.textInput(
_report(terminal.mouseReportMode, position),
);
}
return true;
}
bool activateDeferredDown(Terminal terminal) {
if (_pointerId == null ||
_controller == null ||
_releasePending ||
!_reportsDrag(terminal.mouseMode)) {
return false;
}
_releasePending = true;
_reporting = true;
_clearSelection(_controller);
terminal.textInput( terminal.textInput(
_report(terminal.mouseReportMode, position), _report(terminal.mouseReportMode, _lastReportedPosition),
); );
return true; return true;
} }
@@ -98,26 +121,36 @@ class TerminalMouseDragReporter {
bool handleMove( bool handleMove(
PointerMoveEvent event, PointerMoveEvent event,
Terminal terminal, Terminal terminal,
TerminalViewState? terminalView, TerminalViewState? terminalView, {
) { void Function(bool dragged)? beforeRelease,
void Function()? onCancel,
}) {
if (event.pointer != _pointerId) return false; if (event.pointer != _pointerId) return false;
if (terminalView == null) { if (terminalView == null) {
onCancel?.call();
cancel(); cancel();
return true; return true;
} }
final reportsDrag = _reportsDrag(terminal.mouseMode); final reportsDrag = _reportsDrag(terminal.mouseMode);
if (!_isPrimaryMouse(event)) { if (!_hasPrimaryButton(event)) {
if (_releasePending && reportsDrag) { if (_releasePending && reportsDrag) {
_reportRelease( _finishRelease(
event,
terminal, terminal,
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition, terminalView,
beforeRelease: beforeRelease,
); );
} else {
onCancel?.call();
} }
cancel(); cancel();
return true; return true;
} }
if (!_reporting || !reportsDrag) { if (!_reporting || !reportsDrag) {
if (!reportsDrag) _releasePending = false; if (!reportsDrag && _releasePending) {
_releasePending = false;
onCancel?.call();
}
_reporting = false; _reporting = false;
// Keep ownership until the matching end event to suppress local selection. // Keep ownership until the matching end event to suppress local selection.
final controller = _controller; final controller = _controller;
@@ -126,7 +159,7 @@ class TerminalMouseDragReporter {
} }
final position = _cellAt(event, terminalView); final position = _cellAt(event, terminalView);
_lastReportedPosition = position; _recordPosition(position);
terminal.textInput( terminal.textInput(
_report(terminal.mouseReportMode, position, motion: true), _report(terminal.mouseReportMode, position, motion: true),
); );
@@ -138,16 +171,22 @@ class TerminalMouseDragReporter {
bool handleEnd( bool handleEnd(
PointerEvent event, PointerEvent event,
Terminal terminal, Terminal terminal,
TerminalViewState? terminalView, TerminalViewState? terminalView, {
) { void Function(bool dragged)? beforeRelease,
void Function()? onCancel,
}) {
if (event.pointer != _pointerId) return false; if (event.pointer != _pointerId) return false;
if (terminalView != null && if (terminalView != null &&
_releasePending && _releasePending &&
_reportsDrag(terminal.mouseMode)) { _reportsDrag(terminal.mouseMode)) {
_reportRelease( _finishRelease(
event,
terminal, terminal,
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition, terminalView,
beforeRelease: beforeRelease,
); );
} else {
onCancel?.call();
} }
_clearSelection(_controller); _clearSelection(_controller);
final controller = _controller; final controller = _controller;
@@ -172,6 +211,7 @@ class TerminalMouseDragReporter {
_ownsControllerSuspension = false; _ownsControllerSuspension = false;
_releasePending = false; _releasePending = false;
_reporting = false; _reporting = false;
_dragged = false;
} }
void updateController(TerminalController controller) { void updateController(TerminalController controller) {
@@ -203,6 +243,24 @@ class TerminalMouseDragReporter {
); );
} }
void _finishRelease(
PointerEvent event,
Terminal terminal,
TerminalViewState terminalView, {
void Function(bool dragged)? beforeRelease,
}) {
final position =
_reporting ? _cellAt(event, terminalView) : _lastReportedPosition;
if (_reporting) _recordPosition(position);
beforeRelease?.call(_dragged);
_reportRelease(terminal, position);
}
void _recordPosition(CellOffset position) {
_dragged = _dragged || position != _lastReportedPosition;
_lastReportedPosition = position;
}
CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) { CellOffset _cellAt(PointerEvent event, TerminalViewState terminalView) {
final renderTerminal = terminalView.renderTerminal; final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset( return renderTerminal.getCellOffset(
@@ -210,9 +268,13 @@ class TerminalMouseDragReporter {
); );
} }
bool _isPrimaryMouse(PointerEvent event) => bool _isPrimaryPointer(PointerEvent event, bool reportTouchInput) =>
event.kind == PointerDeviceKind.mouse && (event.kind == PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) == kPrimaryMouseButton; reportTouchInput && event.kind == PointerDeviceKind.touch) &&
_hasPrimaryButton(event);
bool _hasPrimaryButton(PointerEvent event) =>
(event.buttons & kPrimaryButton) == kPrimaryButton;
bool _reportsDrag(MouseMode mode) => bool _reportsDrag(MouseMode mode) =>
mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove; mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove;

View File

@@ -1,45 +1,17 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:xterm/xterm.dart'; import 'package:xterm/xterm.dart';
import 'platform_model.dart';
import 'rustdesk_terminal.dart';
import 'terminal_copy_shortcut.dart'; import 'terminal_copy_shortcut.dart';
import 'terminal_mouse_drag_reporter.dart'; import 'terminal_mouse_drag_reporter.dart';
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift part 'terminal_mouse_handler_input.dart';
/// modifier, so strict full-screen apps ignore the report and never scroll. part 'terminal_web_clipboard_gesture.dart';
/// Upstream fix: TerminalStudio/xterm.dart#238.
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
const WheelButtonFixMouseHandler({this.positionProvider});
final CellOffset? Function()? positionProvider;
@override
String? call(TerminalMouseEvent event) {
if (!event.button.isWheel) {
return defaultMouseHandler(event);
}
// Same gate as UpDownMouseHandler: only the scroll modes report a wheel,
// and a wheel release is never reported, so the report is always a press.
if (!event.state.mouseMode.reportScroll ||
event.buttonState == TerminalMouseButtonState.up) {
return null;
}
return _reportWheel(event);
}
String _reportWheel(TerminalMouseEvent event) {
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
final button = event.button.id - 4;
final position = positionProvider?.call() ?? event.position;
return encodeTerminalMouseReport(
event.state.mouseReportMode,
button,
position,
);
}
}
class TerminalMouseInteraction extends StatefulWidget { class TerminalMouseInteraction extends StatefulWidget {
const TerminalMouseInteraction( const TerminalMouseInteraction(
@@ -47,6 +19,12 @@ class TerminalMouseInteraction extends StatefulWidget {
super.key, super.key,
required this.controller, required this.controller,
this.focusNode, this.focusNode,
this.autofocus = false,
this.textStyle = const TerminalStyle(),
this.deleteDetection = false,
this.reportTouchInput = false,
this.shortcuts,
this.onKeyEvent,
this.backgroundOpacity = 1, this.backgroundOpacity = 1,
this.padding, this.padding,
this.onSecondaryTapDown, this.onSecondaryTapDown,
@@ -55,6 +33,12 @@ class TerminalMouseInteraction extends StatefulWidget {
final Terminal terminal; final Terminal terminal;
final TerminalController controller; final TerminalController controller;
final FocusNode? focusNode; final FocusNode? focusNode;
final bool autofocus;
final TerminalStyle textStyle;
final bool deleteDetection;
final bool reportTouchInput;
final Map<ShortcutActivator, Intent>? shortcuts;
final FocusOnKeyEventCallback? onKeyEvent;
final double backgroundOpacity; final double backgroundOpacity;
final EdgeInsets? padding; final EdgeInsets? padding;
final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown; final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown;
@@ -81,8 +65,13 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
Buffer? _selectionBuffer; Buffer? _selectionBuffer;
int? _selectionPointerId; int? _selectionPointerId;
Timer? _selectionScrollTimer; Timer? _selectionScrollTimer;
Timer? _pendingTouchMouseTimer;
PointerDownEvent? _pendingTouchMouseDown;
var _selectionHasScrolled = false; var _selectionHasScrolled = false;
var _scrollDirection = _noScroll; var _scrollDirection = _noScroll;
// xterm can finish its tap callbacks after the raw drag was reported.
var _suppressXtermLeftButton = false;
var _terminalClipboardGesturePrepared = false;
TerminalViewState? get _terminalView => _terminalViewKey.currentState; TerminalViewState? get _terminalView => _terminalViewKey.currentState;
@override @override
@@ -90,6 +79,7 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
super.initState(); super.initState();
_mouseHandler = WheelButtonFixMouseHandler( _mouseHandler = WheelButtonFixMouseHandler(
positionProvider: _cellAtPointer, positionProvider: _cellAtPointer,
suppressLeftButton: kIsWeb ? _consumeXtermLeftButtonSuppression : null,
); );
_installMouseHandler(widget.terminal); _installMouseHandler(widget.terminal);
} }
@@ -100,10 +90,15 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
final terminalChanged = !identical(oldWidget.terminal, widget.terminal); final terminalChanged = !identical(oldWidget.terminal, widget.terminal);
final controllerChanged = final controllerChanged =
!identical(oldWidget.controller, widget.controller); !identical(oldWidget.controller, widget.controller);
final touchInputChanged =
oldWidget.reportTouchInput != widget.reportTouchInput;
if (!terminalChanged && !controllerChanged && !touchInputChanged) return;
_cancelPendingTouchMouseDrag();
if (!terminalChanged && !controllerChanged) return; if (!terminalChanged && !controllerChanged) return;
if (controllerChanged && !terminalChanged) { if (controllerChanged && !terminalChanged) {
_mouseDrag.updateController(widget.controller); _mouseDrag.updateController(widget.controller);
} else { } else {
_discardPendingTerminalClipboardWrites();
_mouseDrag.cancel(); _mouseDrag.cancel();
} }
_clearSelectionDrag(); _clearSelectionDrag();
@@ -123,46 +118,18 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
} }
} }
CellOffset? _cellAtPointer() {
final terminalView = _terminalView;
final pointerPosition = _pointerPosition;
if (terminalView == null || pointerPosition == null) return null;
final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset(
renderTerminal.globalToLocal(pointerPosition),
);
}
void _updatePointerPosition(PointerEvent event) =>
_pointerPosition = event.position;
void _handlePointerDown(PointerDownEvent event) {
_updatePointerPosition(event);
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
_clearSelectionDrag();
return;
}
if (event.kind != PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
return;
}
_clearSelectionDrag();
final terminalView = _terminalView;
if (terminalView == null) return;
final renderTerminal = terminalView.renderTerminal;
final localPosition = renderTerminal.globalToLocal(event.position);
final selectionBuffer = widget.terminal.buffer;
_selectionPointerId = event.pointer;
_selectionBase = selectionBuffer.createAnchorFromOffset(
renderTerminal.getCellOffset(localPosition),
);
_selectionBuffer = selectionBuffer;
_selectionPointer = localPosition;
}
void _handlePointerMove(PointerMoveEvent event) { void _handlePointerMove(PointerMoveEvent event) {
_updatePointerPosition(event); _updatePointerPosition(event);
if (_mouseDrag.handleMove(event, widget.terminal, _terminalView)) return; if (_handlePendingTouchMove(event)) return;
if (_mouseDrag.handleMove(
event,
widget.terminal,
_terminalView,
beforeRelease: _finishTerminalClipboardWrite,
onCancel: _cancelTerminalClipboardWrite,
)) {
return;
}
if (event.pointer != _selectionPointerId) return; if (event.pointer != _selectionPointerId) return;
if (event.kind != PointerDeviceKind.mouse || if (event.kind != PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) { (event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
@@ -241,8 +208,28 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
void _handlePointerEnd(PointerEvent event) { void _handlePointerEnd(PointerEvent event) {
_updatePointerPosition(event); _updatePointerPosition(event);
if (!_mouseDrag.handleEnd(event, widget.terminal, _terminalView) && final pendingTouch = _pendingTouchMouseDown;
event.pointer != _selectionPointerId) return; if (pendingTouch != null && pendingTouch.pointer == event.pointer) {
final movedBeyondSlop =
(event.position - pendingTouch.position).distance > kTouchSlop;
if (event is PointerUpEvent && !movedBeyondSlop) {
_activatePendingTouchMouseDrag(cancelOnFailure: false);
} else {
_takePendingTouchMouseDrag(pointer: event.pointer);
}
}
final handledByMouseDrag = _mouseDrag.handleEnd(
event,
widget.terminal,
_terminalView,
beforeRelease: event is PointerUpEvent
? _finishTerminalClipboardWrite
: (_) => _cancelTerminalClipboardWrite(),
onCancel: _cancelTerminalClipboardWrite,
);
if (!handledByMouseDrag && event.pointer != _selectionPointerId) {
return;
}
if (_selectionHasScrolled) _scrollSelection(scroll: false); if (_selectionHasScrolled) _scrollSelection(scroll: false);
_clearSelectionDrag(); _clearSelectionDrag();
} }
@@ -265,6 +252,8 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
@override @override
void dispose() { void dispose() {
_discardPendingTerminalClipboardWrites();
_cancelPendingTouchMouseDrag();
_mouseDrag.cancel(); _mouseDrag.cancel();
_clearSelectionDrag(); _clearSelectionDrag();
_restoreMouseHandler(widget.terminal); _restoreMouseHandler(widget.terminal);
@@ -290,10 +279,14 @@ class _TerminalMouseInteractionState extends State<TerminalMouseInteraction> {
controller: widget.controller, controller: widget.controller,
scrollController: _scrollController, scrollController: _scrollController,
focusNode: widget.focusNode, focusNode: widget.focusNode,
autofocus: widget.autofocus,
textStyle: widget.textStyle,
deleteDetection: widget.deleteDetection,
backgroundOpacity: widget.backgroundOpacity, backgroundOpacity: widget.backgroundOpacity,
padding: widget.padding, padding: widget.padding,
shortcuts: platformTerminalShortcuts(), shortcuts: widget.shortcuts ?? platformTerminalShortcuts(),
onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller), onKeyEvent: widget.onKeyEvent ??
terminalCopyHandler(widget.terminal, widget.controller),
onSecondaryTapDown: widget.onSecondaryTapDown, onSecondaryTapDown: widget.onSecondaryTapDown,
), ),
); );

View File

@@ -0,0 +1,162 @@
part of 'terminal_mouse_handler.dart';
/// xterm 4.0.0 encodes wheel buttons as 68..71; the extra bit reads as a Shift
/// modifier, so strict full-screen apps ignore the report and never scroll.
/// Upstream fix: TerminalStudio/xterm.dart#238.
class WheelButtonFixMouseHandler implements TerminalMouseHandler {
const WheelButtonFixMouseHandler({
this.positionProvider,
this.suppressLeftButton,
});
final CellOffset? Function()? positionProvider;
final bool Function(TerminalMouseButtonState)? suppressLeftButton;
@override
String? call(TerminalMouseEvent event) {
if (!event.button.isWheel) {
if (event.button == TerminalMouseButton.left &&
suppressLeftButton?.call(event.buttonState) == true) {
return null;
}
return defaultMouseHandler(event);
}
// Same gate as UpDownMouseHandler: only the scroll modes report a wheel,
// and a wheel release is never reported, so the report is always a press.
if (!event.state.mouseMode.reportScroll ||
event.buttonState == TerminalMouseButtonState.up) {
return null;
}
return _reportWheel(event);
}
String _reportWheel(TerminalMouseEvent event) {
// Wheel buttons 4..7 go on the wire as 64..67, but `id` is 64 + 4..7.
final button = event.button.id - 4;
final position = positionProvider?.call() ?? event.position;
return encodeTerminalMouseReport(
event.state.mouseReportMode,
button,
position,
);
}
}
extension _TerminalMouseInput on _TerminalMouseInteractionState {
CellOffset? _cellAtPointer() {
final terminalView = _terminalView;
final pointerPosition = _pointerPosition;
if (terminalView == null || pointerPosition == null) return null;
final renderTerminal = terminalView.renderTerminal;
return renderTerminal.getCellOffset(
renderTerminal.globalToLocal(pointerPosition),
);
}
void _updatePointerPosition(PointerEvent event) =>
_pointerPosition = event.position;
void _handlePointerDown(PointerDownEvent event) {
_updatePointerPosition(event);
_suppressXtermLeftButton = false;
if (_startPendingTouchMouseDrag(event)) return;
if (_mouseDrag.handleDown(event, widget.terminal, _terminalView)) {
_prepareTerminalClipboardWrite();
if (kIsWeb) _suppressXtermLeftButton = true;
_clearSelectionDrag();
return;
}
if (event.kind != PointerDeviceKind.mouse ||
(event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) {
return;
}
_clearSelectionDrag();
final terminalView = _terminalView;
if (terminalView == null) return;
final renderTerminal = terminalView.renderTerminal;
final localPosition = renderTerminal.globalToLocal(event.position);
final selectionBuffer = widget.terminal.buffer;
_selectionPointerId = event.pointer;
_selectionBase = selectionBuffer.createAnchorFromOffset(
renderTerminal.getCellOffset(localPosition),
);
_selectionBuffer = selectionBuffer;
_selectionPointer = localPosition;
}
bool _startPendingTouchMouseDrag(PointerDownEvent event) {
if (!widget.reportTouchInput ||
event.kind != PointerDeviceKind.touch ||
!_mouseDrag.handleDown(
event,
widget.terminal,
_terminalView,
reportTouchInput: true,
deferReport: true,
)) {
return false;
}
_pendingTouchMouseDown = event;
_pendingTouchMouseTimer = Timer(
kLongPressTimeout,
_activatePendingTouchMouseDrag,
);
return true;
}
bool _activatePendingTouchMouseDrag({
bool cancelOnFailure = true,
}) {
if (_takePendingTouchMouseDrag() == null) return false;
if (_mouseDrag.activateDeferredDown(widget.terminal)) {
_prepareTerminalClipboardWrite();
_clearSelectionDrag();
return true;
}
if (cancelOnFailure) _mouseDrag.cancel();
return false;
}
PointerDownEvent? _takePendingTouchMouseDrag({int? pointer}) {
final pending = _pendingTouchMouseDown;
if (pending == null || pointer != null && pointer != pending.pointer) {
return null;
}
_pendingTouchMouseTimer?.cancel();
_pendingTouchMouseTimer = null;
_pendingTouchMouseDown = null;
return pending;
}
void _cancelPendingTouchMouseDrag({
int? pointer,
bool deferCancel = false,
}) {
if (_takePendingTouchMouseDrag(pointer: pointer) == null) return;
if (deferCancel) {
scheduleMicrotask(_mouseDrag.cancel);
} else {
_mouseDrag.cancel();
}
}
bool _handlePendingTouchMove(PointerMoveEvent event) {
final pending = _pendingTouchMouseDown;
if (pending == null || pending.pointer != event.pointer) return false;
if ((event.position - pending.position).distance > kTouchSlop) {
_cancelPendingTouchMouseDrag(
pointer: event.pointer,
deferCancel: true,
);
}
return true;
}
bool _consumeXtermLeftButtonSuppression(TerminalMouseButtonState state) {
final suppress = _suppressXtermLeftButton;
if (state == TerminalMouseButtonState.up) {
_suppressXtermLeftButton = false;
}
return suppress;
}
}

View File

@@ -0,0 +1,56 @@
part of 'terminal_mouse_handler.dart';
const _prepareTerminalClipboardCommand = 'prepare_terminal_clipboard';
const _finishTerminalClipboardCommand = 'finish_terminal_clipboard';
const _cancelTerminalClipboardCommand = 'cancel_terminal_clipboard';
extension _TerminalWebClipboardGesture on _TerminalMouseInteractionState {
void _prepareTerminalClipboardWrite() {
if (!kIsWeb) return;
_cancelTerminalClipboardWrite();
final terminal = widget.terminal;
if (terminal is! RustDeskTerminal || !terminal.isClipboardWriteAllowed) {
return;
}
try {
ffiSetByName(_prepareTerminalClipboardCommand);
_terminalClipboardGesturePrepared = true;
} catch (error) {
debugPrint('[Terminal] Failed to prepare Web clipboard write: $error');
}
}
void _finishTerminalClipboardWrite(bool responseExpected) {
if (!_terminalClipboardGesturePrepared) return;
_terminalClipboardGesturePrepared = false;
if (!kIsWeb) return;
try {
ffiSetByName(
_finishTerminalClipboardCommand,
responseExpected ? 'true' : 'false',
);
} catch (error) {
debugPrint('[Terminal] Failed to finish Web clipboard write: $error');
}
}
void _cancelTerminalClipboardWrite() {
if (!_terminalClipboardGesturePrepared) return;
_terminalClipboardGesturePrepared = false;
_sendTerminalClipboardCancel();
}
void _discardPendingTerminalClipboardWrites() {
_cancelTerminalClipboardWrite();
_sendTerminalClipboardCancel();
}
void _sendTerminalClipboardCancel() {
if (!kIsWeb) return;
try {
ffiSetByName(_cancelTerminalClipboardCommand);
} catch (error) {
debugPrint('[Terminal] Failed to cancel Web clipboard write: $error');
}
}
}

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "قفل اللوحة"), ("Lock canvas", "قفل اللوحة"),
("Sync clipboard between sessions", "مزامنة الحافظة بين الجلسات"), ("Sync clipboard between sessions", "مزامنة الحافظة بين الجلسات"),
("sync-clipboard-between-sessions-tip", "النص أو الصور المنسوخة في جلسة بعيدة واحدة تُرسَل أيضًا إلى حافظة جلساتك المتصلة الأخرى."), ("sync-clipboard-between-sessions-tip", "النص أو الصور المنسوخة في جلسة بعيدة واحدة تُرسَل أيضًا إلى حافظة جلساتك المتصلة الأخرى."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "تفعيل"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Заблакіраваць палатно"), ("Lock canvas", "Заблакіраваць палатно"),
("Sync clipboard between sessions", "Сінхранізаваць буфер абмену паміж сеансамі"), ("Sync clipboard between sessions", "Сінхранізаваць буфер абмену паміж сеансамі"),
("sync-clipboard-between-sessions-tip", "Тэкст або відарысы, скапіяваныя ў адным аддаленым сеансе, таксама адпраўляюцца ў буфер абмену іншых вашых падключаных сеансаў."), ("sync-clipboard-between-sessions-tip", "Тэкст або відарысы, скапіяваныя ў адным аддаленым сеансе, таксама адпраўляюцца ў буфер абмену іншых вашых падключаных сеансаў."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Уключыць"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Заключване на платното"), ("Lock canvas", "Заключване на платното"),
("Sync clipboard between sessions", "Синхронизиране на клипборда между сесиите"), ("Sync clipboard between sessions", "Синхронизиране на клипборда между сесиите"),
("sync-clipboard-between-sessions-tip", "Текст или изображения, копирани в една отдалечена сесия, се изпращат и към клипборда на другите ви свързани сесии."), ("sync-clipboard-between-sessions-tip", "Текст или изображения, копирани в една отдалечена сесия, се изпращат и към клипборда на другите ви свързани сесии."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Активирай"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloca el llenç"), ("Lock canvas", "Bloca el llenç"),
("Sync clipboard between sessions", "Sincronitza el porta-retalls entre sessions"), ("Sync clipboard between sessions", "Sincronitza el porta-retalls entre sessions"),
("sync-clipboard-between-sessions-tip", "El text o les imatges copiats en una sessió remota també s'envien al porta-retalls de les altres sessions connectades."), ("sync-clipboard-between-sessions-tip", "El text o les imatges copiats en una sessió remota també s'envien al porta-retalls de les altres sessions connectades."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Habilita"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "锁定画布"), ("Lock canvas", "锁定画布"),
("Sync clipboard between sessions", "在会话间同步剪贴板"), ("Sync clipboard between sessions", "在会话间同步剪贴板"),
("sync-clipboard-between-sessions-tip", "在一个远程会话中复制的文本或图片也会发送到其他已连接会话的剪贴板。"), ("sync-clipboard-between-sessions-tip", "在一个远程会话中复制的文本或图片也会发送到其他已连接会话的剪贴板。"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", "允许终端应用复制到剪贴板"),
("Enable", "启用"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Zamknout zobrazení"), ("Lock canvas", "Zamknout zobrazení"),
("Sync clipboard between sessions", "Synchronizovat schránku mezi relacemi"), ("Sync clipboard between sessions", "Synchronizovat schránku mezi relacemi"),
("sync-clipboard-between-sessions-tip", "Text nebo obrázky zkopírované v jedné vzdálené relaci se odešlou i do schránky ostatních připojených relací."), ("sync-clipboard-between-sessions-tip", "Text nebo obrázky zkopírované v jedné vzdálené relaci se odešlou i do schránky ostatních připojených relací."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Povolit"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Lås lærred"), ("Lock canvas", "Lås lærred"),
("Sync clipboard between sessions", "Synkroniser udklipsholder mellem sessioner"), ("Sync clipboard between sessions", "Synkroniser udklipsholder mellem sessioner"),
("sync-clipboard-between-sessions-tip", "Tekst eller billeder, der kopieres i én fjernsession, sendes også til udklipsholderen i dine andre forbundne sessioner."), ("sync-clipboard-between-sessions-tip", "Tekst eller billeder, der kopieres i én fjernsession, sendes også til udklipsholderen i dine andre forbundne sessioner."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktivér"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Sichtfeld sperren"), ("Lock canvas", "Sichtfeld sperren"),
("Sync clipboard between sessions", "Zwischenablage zwischen Sitzungen synchronisieren"), ("Sync clipboard between sessions", "Zwischenablage zwischen Sitzungen synchronisieren"),
("sync-clipboard-between-sessions-tip", "In einer Remote-Sitzung kopierter Text oder kopierte Bilder werden auch an die Zwischenablage Ihrer anderen verbundenen Sitzungen gesendet."), ("sync-clipboard-between-sessions-tip", "In einer Remote-Sitzung kopierter Text oder kopierte Bilder werden auch an die Zwischenablage Ihrer anderen verbundenen Sitzungen gesendet."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktivieren"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Κλείδωμα καμβά"), ("Lock canvas", "Κλείδωμα καμβά"),
("Sync clipboard between sessions", "Συγχρονισμός προχείρου μεταξύ συνεδριών"), ("Sync clipboard between sessions", "Συγχρονισμός προχείρου μεταξύ συνεδριών"),
("sync-clipboard-between-sessions-tip", "Κείμενο ή εικόνες που αντιγράφονται σε μία απομακρυσμένη συνεδρία αποστέλλονται και στο πρόχειρο των άλλων συνδεδεμένων συνεδριών σας."), ("sync-clipboard-between-sessions-tip", "Κείμενο ή εικόνες που αντιγράφονται σε μία απομακρυσμένη συνεδρία αποστέλλονται και στο πρόχειρο των άλλων συνδεδεμένων συνεδριών σας."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Ενεργοποίηση"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -276,5 +276,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("whitelist_cidr_tip", "CIDR notation is supported, e.g. 192.168.1.0/24"), ("whitelist_cidr_tip", "CIDR notation is supported, e.g. 192.168.1.0/24"),
("Your ip is blocked by the peer", "Your IP is blocked by the peer"), ("Your ip is blocked by the peer", "Your IP is blocked by the peer"),
("sync-clipboard-between-sessions-tip", "Text or images copied in one remote session are also sent to the clipboard of your other connected sessions."), ("sync-clipboard-between-sessions-tip", "Text or images copied in one remote session are also sent to the clipboard of your other connected sessions."),
("terminal-clipboard-write-tip", "An app in the terminal wants to copy text to this device's clipboard. If granted, this permission applies to terminal apps in all connections until you turn it off in Settings. Manual copy and paste are unaffected."),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Ŝlosi kanvason"), ("Lock canvas", "Ŝlosi kanvason"),
("Sync clipboard between sessions", "Sinkronigi poŝon inter seancoj"), ("Sync clipboard between sessions", "Sinkronigi poŝon inter seancoj"),
("sync-clipboard-between-sessions-tip", "Teksto aŭ bildoj kopiitaj en unu fora seanco ankaŭ sendiĝas al la poŝo de viaj aliaj konektitaj seancoj."), ("sync-clipboard-between-sessions-tip", "Teksto aŭ bildoj kopiitaj en unu fora seanco ankaŭ sendiĝas al la poŝo de viaj aliaj konektitaj seancoj."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Ebligi"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloquear lienzo"), ("Lock canvas", "Bloquear lienzo"),
("Sync clipboard between sessions", "Sincronizar portapapeles entre sesiones"), ("Sync clipboard between sessions", "Sincronizar portapapeles entre sesiones"),
("sync-clipboard-between-sessions-tip", "El texto o las imágenes copiados en una sesión remota también se envían al portapapeles de tus otras sesiones conectadas."), ("sync-clipboard-between-sessions-tip", "El texto o las imágenes copiados en una sesión remota también se envían al portapapeles de tus otras sesiones conectadas."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Habilitar"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Lukusta lõuend"), ("Lock canvas", "Lukusta lõuend"),
("Sync clipboard between sessions", "Sünkrooni lõikelaud seansside vahel"), ("Sync clipboard between sessions", "Sünkrooni lõikelaud seansside vahel"),
("sync-clipboard-between-sessions-tip", "Ühes kaugseansis kopeeritud tekst või pildid saadetakse ka teiste ühendatud seansside lõikelauale."), ("sync-clipboard-between-sessions-tip", "Ühes kaugseansis kopeeritud tekst või pildid saadetakse ka teiste ühendatud seansside lõikelauale."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Luba"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Blokeatu oihala"), ("Lock canvas", "Blokeatu oihala"),
("Sync clipboard between sessions", "Sinkronizatu arbela saioen artean"), ("Sync clipboard between sessions", "Sinkronizatu arbela saioen artean"),
("sync-clipboard-between-sessions-tip", "Urruneko saio batean kopiatutako testua edo irudiak konektatutako beste saioen arbelera ere bidaltzen dira."), ("sync-clipboard-between-sessions-tip", "Urruneko saio batean kopiatutako testua edo irudiak konektatutako beste saioen arbelera ere bidaltzen dira."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Gaitu"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "قفل کردن صفحه"), ("Lock canvas", "قفل کردن صفحه"),
("Sync clipboard between sessions", "همگام‌سازی کلیپ‌بورد بین نشست‌ها"), ("Sync clipboard between sessions", "همگام‌سازی کلیپ‌بورد بین نشست‌ها"),
("sync-clipboard-between-sessions-tip", "متن یا تصاویری که در یک نشست راه دور کپی می‌شوند به کلیپ‌بورد سایر نشست‌های متصل شما نیز ارسال می‌شوند."), ("sync-clipboard-between-sessions-tip", "متن یا تصاویری که در یک نشست راه دور کپی می‌شوند به کلیپ‌بورد سایر نشست‌های متصل شما نیز ارسال می‌شوند."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "فعال‌سازی"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Lukitse näkymä"), ("Lock canvas", "Lukitse näkymä"),
("Sync clipboard between sessions", "Synkronoi leikepöytä istuntojen välillä"), ("Sync clipboard between sessions", "Synkronoi leikepöytä istuntojen välillä"),
("sync-clipboard-between-sessions-tip", "Yhdessä etäistunnossa kopioitu teksti tai kuvat lähetetään myös muiden yhdistettyjen istuntojen leikepöydälle."), ("sync-clipboard-between-sessions-tip", "Yhdessä etäistunnossa kopioitu teksti tai kuvat lähetetään myös muiden yhdistettyjen istuntojen leikepöydälle."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Ota käyttöön"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Verrouiller la vue"), ("Lock canvas", "Verrouiller la vue"),
("Sync clipboard between sessions", "Synchroniser le presse-papiers entre les sessions"), ("Sync clipboard between sessions", "Synchroniser le presse-papiers entre les sessions"),
("sync-clipboard-between-sessions-tip", "Le texte ou les images copiés dans une session distante sont également envoyés au presse-papiers de vos autres sessions connectées."), ("sync-clipboard-between-sessions-tip", "Le texte ou les images copiés dans une session distante sont également envoyés au presse-papiers de vos autres sessions connectées."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Activer"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "ტილოს დაბლოკვა"), ("Lock canvas", "ტილოს დაბლოკვა"),
("Sync clipboard between sessions", "გაცვლის ბუფერის სინქრონიზაცია სესიებს შორის"), ("Sync clipboard between sessions", "გაცვლის ბუფერის სინქრონიზაცია სესიებს შორის"),
("sync-clipboard-between-sessions-tip", "ერთ დაშორებულ სესიაში დაკოპირებული ტექსტი ან სურათები ასევე იგზავნება თქვენი სხვა დაკავშირებული სესიების გაცვლის ბუფერში."), ("sync-clipboard-between-sessions-tip", "ერთ დაშორებულ სესიაში დაკოპირებული ტექსტი ან სურათები ასევე იგზავნება თქვენი სხვა დაკავშირებული სესიების გაცვლის ბუფერში."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "ჩართვა"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "કેનવાસ લોક કરો"), ("Lock canvas", "કેનવાસ લોક કરો"),
("Sync clipboard between sessions", "સત્રો વચ્ચે ક્લિપબોર્ડ સિંક કરો"), ("Sync clipboard between sessions", "સત્રો વચ્ચે ક્લિપબોર્ડ સિંક કરો"),
("sync-clipboard-between-sessions-tip", "એક રિમોટ સત્રમાં કૉપિ કરેલ ટેક્સ્ટ કે છબીઓ તમારા અન્ય જોડાયેલા સત્રોના ક્લિપબોર્ડ પર પણ મોકલવામાં આવે છે."), ("sync-clipboard-between-sessions-tip", "એક રિમોટ સત્રમાં કૉપિ કરેલ ટેક્સ્ટ કે છબીઓ તમારા અન્ય જોડાયેલા સત્રોના ક્લિપબોર્ડ પર પણ મોકલવામાં આવે છે."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "સક્ષમ કરો"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "נעל לוח ציור"), ("Lock canvas", "נעל לוח ציור"),
("Sync clipboard between sessions", "סנכרן לוח בין סשנים"), ("Sync clipboard between sessions", "סנכרן לוח בין סשנים"),
("sync-clipboard-between-sessions-tip", "טקסט או תמונות שהועתקו בסשן מרוחק אחד נשלחים גם ללוח של שאר הסשנים המחוברים שלך."), ("sync-clipboard-between-sessions-tip", "טקסט או תמונות שהועתקו בסשן מרוחק אחד נשלחים גם ללוח של שאר הסשנים המחוברים שלך."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "הפעל"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "कैनवास लॉक करें"), ("Lock canvas", "कैनवास लॉक करें"),
("Sync clipboard between sessions", "सत्रों के बीच क्लिपबोर्ड सिंक करें"), ("Sync clipboard between sessions", "सत्रों के बीच क्लिपबोर्ड सिंक करें"),
("sync-clipboard-between-sessions-tip", "एक रिमोट सत्र में कॉपी किए गए टेक्स्ट या चित्र आपके अन्य जुड़े सत्रों के क्लिपबोर्ड पर भी भेजे जाते हैं।"), ("sync-clipboard-between-sessions-tip", "एक रिमोट सत्र में कॉपी किए गए टेक्स्ट या चित्र आपके अन्य जुड़े सत्रों के क्लिपबोर्ड पर भी भेजे जाते हैं।"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "सक्षम करें"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Zaključaj pozadinu"), ("Lock canvas", "Zaključaj pozadinu"),
("Sync clipboard between sessions", "Sinkroniziraj međuspremnik između sesija"), ("Sync clipboard between sessions", "Sinkroniziraj međuspremnik između sesija"),
("sync-clipboard-between-sessions-tip", "Tekst ili slike kopirani u jednoj udaljenoj sesiji šalju se i u međuspremnik vaših ostalih povezanih sesija."), ("sync-clipboard-between-sessions-tip", "Tekst ili slike kopirani u jednoj udaljenoj sesiji šalju se i u međuspremnik vaših ostalih povezanih sesija."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Omogući"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Nézet zárolása"), ("Lock canvas", "Nézet zárolása"),
("Sync clipboard between sessions", "Vágólap szinkronizálása a munkamenetek között"), ("Sync clipboard between sessions", "Vágólap szinkronizálása a munkamenetek között"),
("sync-clipboard-between-sessions-tip", "Az egyik távoli munkamenetben másolt szöveg vagy kép a többi csatlakoztatott munkamenet vágólapjára is elküldésre kerül."), ("sync-clipboard-between-sessions-tip", "Az egyik távoli munkamenetben másolt szöveg vagy kép a többi csatlakoztatott munkamenet vágólapjára is elküldésre kerül."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Engedélyezés"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Kunci kanvas"), ("Lock canvas", "Kunci kanvas"),
("Sync clipboard between sessions", "Sinkronkan papan klip antar sesi"), ("Sync clipboard between sessions", "Sinkronkan papan klip antar sesi"),
("sync-clipboard-between-sessions-tip", "Teks atau gambar yang disalin di satu sesi jarak jauh juga dikirim ke papan klip sesi terhubung Anda yang lain."), ("sync-clipboard-between-sessions-tip", "Teks atau gambar yang disalin di satu sesi jarak jauh juga dikirim ke papan klip sesi terhubung Anda yang lain."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktifkan"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Blocca tela"), ("Lock canvas", "Blocca tela"),
("Sync clipboard between sessions", "Sincronizza gli appunti tra le sessioni"), ("Sync clipboard between sessions", "Sincronizza gli appunti tra le sessioni"),
("sync-clipboard-between-sessions-tip", "Il testo o le immagini copiati in una sessione remota vengono inviati anche agli appunti delle altre sessioni connesse."), ("sync-clipboard-between-sessions-tip", "Il testo o le immagini copiati in una sessione remota vengono inviati anche agli appunti delle altre sessioni connesse."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Abilita"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "キャンバスをロック"), ("Lock canvas", "キャンバスをロック"),
("Sync clipboard between sessions", "セッション間でクリップボードを同期"), ("Sync clipboard between sessions", "セッション間でクリップボードを同期"),
("sync-clipboard-between-sessions-tip", "1つのリモートセッションでコピーしたテキストや画像は、接続中の他のセッションのクリップボードにも送信されます。"), ("sync-clipboard-between-sessions-tip", "1つのリモートセッションでコピーしたテキストや画像は、接続中の他のセッションのクリップボードにも送信されます。"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "有効にする"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "캔버스 잠금"), ("Lock canvas", "캔버스 잠금"),
("Sync clipboard between sessions", "세션 간 클립보드 동기화"), ("Sync clipboard between sessions", "세션 간 클립보드 동기화"),
("sync-clipboard-between-sessions-tip", "하나의 원격 세션에서 복사한 텍스트나 이미지는 연결된 다른 세션의 클립보드에도 전송됩니다."), ("sync-clipboard-between-sessions-tip", "하나의 원격 세션에서 복사한 텍스트나 이미지는 연결된 다른 세션의 클립보드에도 전송됩니다."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "활성화"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Кенепті құлыптау"), ("Lock canvas", "Кенепті құлыптау"),
("Sync clipboard between sessions", "Сеанстар арасында көшіру-тақтасын синхрондау"), ("Sync clipboard between sessions", "Сеанстар арасында көшіру-тақтасын синхрондау"),
("sync-clipboard-between-sessions-tip", "Бір қашықтағы сеанста көшірілген мәтін немесе суреттер басқа қосылған сеанстардың көшіру-тақтасына да жіберіледі."), ("sync-clipboard-between-sessions-tip", "Бір қашықтағы сеанста көшірілген мәтін немесе суреттер басқа қосылған сеанстардың көшіру-тақтасына да жіберіледі."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Қосу"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Užrakinti drobę"), ("Lock canvas", "Užrakinti drobę"),
("Sync clipboard between sessions", "Sinchronizuoti iškarpinę tarp seansų"), ("Sync clipboard between sessions", "Sinchronizuoti iškarpinę tarp seansų"),
("sync-clipboard-between-sessions-tip", "Viename nuotoliniame seanse nukopijuotas tekstas ar vaizdai taip pat siunčiami į kitų prijungtų seansų iškarpinę."), ("sync-clipboard-between-sessions-tip", "Viename nuotoliniame seanse nukopijuotas tekstas ar vaizdai taip pat siunčiami į kitų prijungtų seansų iškarpinę."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Įgalinti"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloķēt audeklu"), ("Lock canvas", "Bloķēt audeklu"),
("Sync clipboard between sessions", "Sinhronizēt starpliktuvi starp sesijām"), ("Sync clipboard between sessions", "Sinhronizēt starpliktuvi starp sesijām"),
("sync-clipboard-between-sessions-tip", "Vienā attālajā sesijā nokopētais teksts vai attēli tiek nosūtīti arī uz pārējo pievienoto sesiju starpliktuvi."), ("sync-clipboard-between-sessions-tip", "Vienā attālajā sesijā nokopētais teksts vai attēli tiek nosūtīti arī uz pārējo pievienoto sesiju starpliktuvi."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Iespējot"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "ക്യാൻവാസ് ലോക്ക് ചെയ്യുക"), ("Lock canvas", "ക്യാൻവാസ് ലോക്ക് ചെയ്യുക"),
("Sync clipboard between sessions", "സെഷനുകൾക്കിടയിൽ ക്ലിപ്പ്ബോർഡ് സമന്വയിപ്പിക്കുക"), ("Sync clipboard between sessions", "സെഷനുകൾക്കിടയിൽ ക്ലിപ്പ്ബോർഡ് സമന്വയിപ്പിക്കുക"),
("sync-clipboard-between-sessions-tip", "ഒരു റിമോട്ട് സെഷനിൽ പകർത്തിയ ടെക്സ്റ്റോ ചിത്രങ്ങളോ നിങ്ങളുടെ മറ്റ് കണക്റ്റുചെയ്ത സെഷനുകളുടെ ക്ലിപ്പ്ബോർഡിലേക്കും അയയ്ക്കപ്പെടും."), ("sync-clipboard-between-sessions-tip", "ഒരു റിമോട്ട് സെഷനിൽ പകർത്തിയ ടെക്സ്റ്റോ ചിത്രങ്ങളോ നിങ്ങളുടെ മറ്റ് കണക്റ്റുചെയ്ത സെഷനുകളുടെ ക്ലിപ്പ്ബോർഡിലേക്കും അയയ്ക്കപ്പെടും."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "അനുവദിക്കുക"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Lås lerret"), ("Lock canvas", "Lås lerret"),
("Sync clipboard between sessions", "Synkroniser utklippstavlen mellom økter"), ("Sync clipboard between sessions", "Synkroniser utklippstavlen mellom økter"),
("sync-clipboard-between-sessions-tip", "Tekst eller bilder som kopieres i én ekstern økt, sendes også til utklippstavlen i de andre tilkoblede øktene dine."), ("sync-clipboard-between-sessions-tip", "Tekst eller bilder som kopieres i én ekstern økt, sendes også til utklippstavlen i de andre tilkoblede øktene dine."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktiver"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Canvas vergrendelen"), ("Lock canvas", "Canvas vergrendelen"),
("Sync clipboard between sessions", "Klembord synchroniseren tussen sessies"), ("Sync clipboard between sessions", "Klembord synchroniseren tussen sessies"),
("sync-clipboard-between-sessions-tip", "Tekst of afbeeldingen die in één externe sessie worden gekopieerd, worden ook naar het klembord van uw andere verbonden sessies gestuurd."), ("sync-clipboard-between-sessions-tip", "Tekst of afbeeldingen die in één externe sessie worden gekopieerd, worden ook naar het klembord van uw andere verbonden sessies gestuurd."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Inschakelen"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Zablokuj ekran"), ("Lock canvas", "Zablokuj ekran"),
("Sync clipboard between sessions", "Synchronizuj schowek między sesjami"), ("Sync clipboard between sessions", "Synchronizuj schowek między sesjami"),
("sync-clipboard-between-sessions-tip", "Tekst lub obrazy skopiowane w jednej sesji zdalnej są wysyłane także do schowka pozostałych połączonych sesji."), ("sync-clipboard-between-sessions-tip", "Tekst lub obrazy skopiowane w jednej sesji zdalnej są wysyłane także do schowka pozostałych połączonych sesji."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Włącz"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloquear tela"), ("Lock canvas", "Bloquear tela"),
("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"), ("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"),
("sync-clipboard-between-sessions-tip", "O texto ou as imagens copiados numa sessão remota também são enviados para a área de transferência das suas outras sessões ligadas."), ("sync-clipboard-between-sessions-tip", "O texto ou as imagens copiados numa sessão remota também são enviados para a área de transferência das suas outras sessões ligadas."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Ativar"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloquear tela"), ("Lock canvas", "Bloquear tela"),
("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"), ("Sync clipboard between sessions", "Sincronizar área de transferência entre sessões"),
("sync-clipboard-between-sessions-tip", "Texto ou imagens copiados em uma sessão remota também são enviados para a área de transferência das suas outras sessões conectadas."), ("sync-clipboard-between-sessions-tip", "Texto ou imagens copiados em uma sessão remota também são enviados para a área de transferência das suas outras sessões conectadas."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Habilitar"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Blochează ecranul"), ("Lock canvas", "Blochează ecranul"),
("Sync clipboard between sessions", "Sincronizează clipboardul între sesiuni"), ("Sync clipboard between sessions", "Sincronizează clipboardul între sesiuni"),
("sync-clipboard-between-sessions-tip", "Textul sau imaginile copiate într-o sesiune la distanță sunt trimise și în clipboardul celorlalte sesiuni conectate."), ("sync-clipboard-between-sessions-tip", "Textul sau imaginile copiate într-o sesiune la distanță sunt trimise și în clipboardul celorlalte sesiuni conectate."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Activează"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Заблокировать холст"), ("Lock canvas", "Заблокировать холст"),
("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"), ("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"),
("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."), ("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Включить"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Bloca sa tela"), ("Lock canvas", "Bloca sa tela"),
("Sync clipboard between sessions", "Sincroniza sa punta de billete intre is sessiones"), ("Sync clipboard between sessions", "Sincroniza sa punta de billete intre is sessiones"),
("sync-clipboard-between-sessions-tip", "Su testu o is immàgines copiadas in una sessione remota sunt imbiadas fintzas a sa punta de billete de is àteras sessiones connètidas."), ("sync-clipboard-between-sessions-tip", "Su testu o is immàgines copiadas in una sessione remota sunt imbiadas fintzas a sa punta de billete de is àteras sessiones connètidas."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Abìlita"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Uzamknúť zobrazenie"), ("Lock canvas", "Uzamknúť zobrazenie"),
("Sync clipboard between sessions", "Synchronizovať schránku medzi reláciami"), ("Sync clipboard between sessions", "Synchronizovať schránku medzi reláciami"),
("sync-clipboard-between-sessions-tip", "Text alebo obrázky skopírované v jednej vzdialenej relácii sa odošlú aj do schránky ostatných pripojených relácií."), ("sync-clipboard-between-sessions-tip", "Text alebo obrázky skopírované v jednej vzdialenej relácii sa odošlú aj do schránky ostatných pripojených relácií."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Povoliť"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Zakleni platno"), ("Lock canvas", "Zakleni platno"),
("Sync clipboard between sessions", "Sinhroniziraj odložišče med sejami"), ("Sync clipboard between sessions", "Sinhroniziraj odložišče med sejami"),
("sync-clipboard-between-sessions-tip", "Besedilo ali slike, kopirane v eni oddaljeni seji, se pošljejo tudi v odložišče vaših drugih povezanih sej."), ("sync-clipboard-between-sessions-tip", "Besedilo ali slike, kopirane v eni oddaljeni seji, se pošljejo tudi v odložišče vaših drugih povezanih sej."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Omogoči"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Kyç canvas"), ("Lock canvas", "Kyç canvas"),
("Sync clipboard between sessions", "Sinkronizo clipboard-in midis sesioneve"), ("Sync clipboard between sessions", "Sinkronizo clipboard-in midis sesioneve"),
("sync-clipboard-between-sessions-tip", "Teksti ose imazhet e kopjuara në një sesion të largët dërgohen edhe në clipboard-in e sesioneve të tjera të lidhura."), ("sync-clipboard-between-sessions-tip", "Teksti ose imazhet e kopjuara në një sesion të largët dërgohen edhe në clipboard-in e sesioneve të tjera të lidhura."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktivizo"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Zaključaj pozadinu"), ("Lock canvas", "Zaključaj pozadinu"),
("Sync clipboard between sessions", "Sinhronizuj klipbord između sesija"), ("Sync clipboard between sessions", "Sinhronizuj klipbord između sesija"),
("sync-clipboard-between-sessions-tip", "Tekst ili slike kopirane u jednoj udaljenoj sesiji šalju se i u klipbord vaših ostalih povezanih sesija."), ("sync-clipboard-between-sessions-tip", "Tekst ili slike kopirane u jednoj udaljenoj sesiji šalju se i u klipbord vaših ostalih povezanih sesija."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Omogući"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Lås canvas"), ("Lock canvas", "Lås canvas"),
("Sync clipboard between sessions", "Synkronisera urklipp mellan sessioner"), ("Sync clipboard between sessions", "Synkronisera urklipp mellan sessioner"),
("sync-clipboard-between-sessions-tip", "Text eller bilder som kopieras i en fjärrsession skickas även till urklipp i dina andra anslutna sessioner."), ("sync-clipboard-between-sessions-tip", "Text eller bilder som kopieras i en fjärrsession skickas även till urklipp i dina andra anslutna sessioner."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Aktivera"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "கேன்வாஸைப் பூட்டு"), ("Lock canvas", "கேன்வாஸைப் பூட்டு"),
("Sync clipboard between sessions", "அமர்வுகளுக்கு இடையே கிளிப்போர்டை ஒத்திசைக்கவும்"), ("Sync clipboard between sessions", "அமர்வுகளுக்கு இடையே கிளிப்போர்டை ஒத்திசைக்கவும்"),
("sync-clipboard-between-sessions-tip", "ஒரு தொலை அமர்வில் நகலெடுக்கப்பட்ட உரை அல்லது படங்கள் உங்கள் பிற இணைக்கப்பட்ட அமர்வுகளின் கிளிப்போர்டுக்கும் அனுப்பப்படும்."), ("sync-clipboard-between-sessions-tip", "ஒரு தொலை அமர்வில் நகலெடுக்கப்பட்ட உரை அல்லது படங்கள் உங்கள் பிற இணைக்கப்பட்ட அமர்வுகளின் கிளிப்போர்டுக்கும் அனுப்பப்படும்."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "இயக்கு"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", ""), ("Lock canvas", ""),
("Sync clipboard between sessions", ""), ("Sync clipboard between sessions", ""),
("sync-clipboard-between-sessions-tip", ""), ("sync-clipboard-between-sessions-tip", ""),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "ล็อคแคนวาส"), ("Lock canvas", "ล็อคแคนวาส"),
("Sync clipboard between sessions", "ซิงค์คลิปบอร์ดระหว่างเซสชัน"), ("Sync clipboard between sessions", "ซิงค์คลิปบอร์ดระหว่างเซสชัน"),
("sync-clipboard-between-sessions-tip", "ข้อความหรือรูปภาพที่คัดลอกในเซสชันระยะไกลหนึ่งจะถูกส่งไปยังคลิปบอร์ดของเซสชันอื่นที่เชื่อมต่ออยู่ด้วย"), ("sync-clipboard-between-sessions-tip", "ข้อความหรือรูปภาพที่คัดลอกในเซสชันระยะไกลหนึ่งจะถูกส่งไปยังคลิปบอร์ดของเซสชันอื่นที่เชื่อมต่ออยู่ด้วย"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "เปิดใช้งาน"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Tuvali kilitle"), ("Lock canvas", "Tuvali kilitle"),
("Sync clipboard between sessions", "Oturumlar arasında panoyu senkronize et"), ("Sync clipboard between sessions", "Oturumlar arasında panoyu senkronize et"),
("sync-clipboard-between-sessions-tip", "Bir uzak oturumda kopyalanan metin veya görseller, bağlı diğer oturumlarınızın panosuna da gönderilir."), ("sync-clipboard-between-sessions-tip", "Bir uzak oturumda kopyalanan metin veya görseller, bağlı diğer oturumlarınızın panosuna da gönderilir."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Etkinleştir"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "鎖定畫布"), ("Lock canvas", "鎖定畫布"),
("Sync clipboard between sessions", "在工作階段間同步剪貼簿"), ("Sync clipboard between sessions", "在工作階段間同步剪貼簿"),
("sync-clipboard-between-sessions-tip", "在一個遠端工作階段中複製的文字或圖片也會傳送到其他已連線工作階段的剪貼簿。"), ("sync-clipboard-between-sessions-tip", "在一個遠端工作階段中複製的文字或圖片也會傳送到其他已連線工作階段的剪貼簿。"),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "啟用"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Блокування полотна"), ("Lock canvas", "Блокування полотна"),
("Sync clipboard between sessions", "Синхронізувати буфер обміну між сеансами"), ("Sync clipboard between sessions", "Синхронізувати буфер обміну між сеансами"),
("sync-clipboard-between-sessions-tip", "Текст або зображення, скопійовані в одному віддаленому сеансі, також надсилаються до буфера обміну інших підключених сеансів."), ("sync-clipboard-between-sessions-tip", "Текст або зображення, скопійовані в одному віддаленому сеансі, також надсилаються до буфера обміну інших підключених сеансів."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Увімкнути"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -745,6 +745,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "display-name"), ("Display Name", "display-name"),
("password-hidden-tip", ""), ("password-hidden-tip", ""),
("preset-password-in-use-tip", ""), ("preset-password-in-use-tip", ""),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Lock canvas", "Khóa khung hình"), ("Lock canvas", "Khóa khung hình"),
("Sync clipboard between sessions", "Đồng bộ clipboard giữa các phiên"), ("Sync clipboard between sessions", "Đồng bộ clipboard giữa các phiên"),
("sync-clipboard-between-sessions-tip", "Văn bản hoặc hình ảnh được sao chép trong một phiên từ xa cũng được gửi đến clipboard của các phiên đã kết nối khác."), ("sync-clipboard-between-sessions-tip", "Văn bản hoặc hình ảnh được sao chép trong một phiên từ xa cũng được gửi đến clipboard của các phiên đã kết nối khác."),
("terminal-clipboard-write-tip", ""),
("Allow terminal apps to copy to clipboard", ""),
("Enable", "Bật"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }