From 618bf37debba4ec812e415742d0695a572b2423e Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 5 Sep 2026 13:21:49 +0800 Subject: [PATCH] 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 --------- Signed-off-by: fufesou --- .../lib/common/widgets/setting_widgets.dart | 27 +++ flutter/lib/consts.dart | 5 + .../desktop/pages/desktop_setting_page.dart | 13 +- flutter/lib/desktop/pages/terminal_page.dart | 6 + .../lib/desktop/pages/terminal_tab_page.dart | 197 ++++++++++++++++- flutter/lib/mobile/pages/settings_page.dart | 10 +- flutter/lib/mobile/pages/terminal_page.dart | 206 +++++++++++++++++- flutter/lib/models/rustdesk_terminal.dart | 103 ++++++++- .../lib/models/terminal_clipboard_writer.dart | 15 ++ .../models/terminal_clipboard_writer_web.dart | 29 +++ .../lib/models/terminal_copy_shortcut.dart | 122 ++++++++++- flutter/lib/models/terminal_model.dart | 49 ++++- .../models/terminal_mouse_drag_reporter.dart | 102 +++++++-- .../lib/models/terminal_mouse_handler.dart | 143 ++++++------ .../models/terminal_mouse_handler_input.dart | 162 ++++++++++++++ .../terminal_web_clipboard_gesture.dart | 56 +++++ src/lang/ar.rs | 3 + src/lang/be.rs | 3 + src/lang/bg.rs | 3 + src/lang/ca.rs | 3 + src/lang/cn.rs | 3 + src/lang/cs.rs | 3 + src/lang/da.rs | 3 + src/lang/de.rs | 3 + src/lang/el.rs | 3 + src/lang/en.rs | 1 + src/lang/eo.rs | 3 + src/lang/es.rs | 3 + src/lang/et.rs | 3 + src/lang/eu.rs | 3 + src/lang/fa.rs | 3 + src/lang/fi.rs | 3 + src/lang/fr.rs | 3 + src/lang/ge.rs | 3 + src/lang/gu.rs | 3 + src/lang/he.rs | 3 + src/lang/hi.rs | 3 + src/lang/hr.rs | 3 + src/lang/hu.rs | 3 + src/lang/id.rs | 3 + src/lang/it.rs | 3 + src/lang/ja.rs | 3 + src/lang/ko.rs | 3 + src/lang/kz.rs | 3 + src/lang/lt.rs | 3 + src/lang/lv.rs | 3 + src/lang/ml.rs | 3 + src/lang/nb.rs | 3 + src/lang/nl.rs | 3 + src/lang/pl.rs | 3 + src/lang/pt_PT.rs | 3 + src/lang/ptbr.rs | 3 + src/lang/ro.rs | 3 + src/lang/ru.rs | 3 + src/lang/sc.rs | 3 + src/lang/sk.rs | 3 + src/lang/sl.rs | 3 + src/lang/sq.rs | 3 + src/lang/sr.rs | 3 + src/lang/sv.rs | 3 + src/lang/ta.rs | 3 + src/lang/template.rs | 3 + src/lang/th.rs | 3 + src/lang/tr.rs | 3 + src/lang/tw.rs | 3 + src/lang/uk.rs | 3 + src/lang/ur.rs | 2 + src/lang/vi.rs | 3 + 68 files changed, 1279 insertions(+), 119 deletions(-) create mode 100644 flutter/lib/models/terminal_clipboard_writer.dart create mode 100644 flutter/lib/models/terminal_clipboard_writer_web.dart create mode 100644 flutter/lib/models/terminal_mouse_handler_input.dart create mode 100644 flutter/lib/models/terminal_web_clipboard_gesture.dart diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index 9449c3624..3941ed050 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -244,11 +244,38 @@ List<(String, String)> otherDefaultSettings() { kKeyUseAllMyDisplaysForTheRemoteSession ), ('Keep terminal sessions on disconnect', kOptionTerminalPersistent), + ( + 'Allow terminal apps to copy to clipboard', + kOptionAllowTerminalClipboardWrite + ), ]; return v; } +String getOtherDefaultSettingOption(String key) { + if (key == kOptionAllowTerminalClipboardWrite) { + return bind.mainGetLocalOption(key: key); + } + return bind.mainGetUserDefaultOption(key: key); +} + +Future 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 { final SimpleWrapper value; // If null, no debouncer will be applied. diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index c6f9d9d6b..ba1eff7d7 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -115,6 +115,11 @@ const String kOptionEnableAudio = "enable-audio"; const String kOptionEnableCamera = "enable-camera"; const String kOptionEnableTerminal = "enable-terminal"; 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 kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 256e3923f..baa632892 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2080,14 +2080,13 @@ class _DisplayState extends State<_Display> { } Widget otherRow(String label, String key) { - final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; - final isOptFixed = isOptionFixed(key); + final value = getOtherDefaultSettingOption(key) == 'Y'; + final isOptFixed = isOtherDefaultSettingReadOnly(key); onChanged(bool b) async { - await bind.mainSetUserDefaultOption( - key: key, - value: b - ? 'Y' - : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo)); + await setOtherDefaultSettingOption( + key, + b ? 'Y' : (key == kOptionEnableFileCopyPaste ? 'N' : defaultOptionNo), + ); setState(() {}); } diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index f193bb23a..b6ea485a1 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -19,6 +19,8 @@ class TerminalPage extends StatefulWidget { required this.tabKey, this.forceRelay, this.connToken, + this.onClipboardWriteBlocked, + this.onClipboardWriteSucceeded, }) : super(key: key); final String id; final String? password; @@ -26,6 +28,8 @@ class TerminalPage extends StatefulWidget { final bool? forceRelay; final bool? isSharedPassword; final String? connToken; + final ValueChanged? onClipboardWriteBlocked; + final ValueChanged? onClipboardWriteSucceeded; final int terminalId; /// Tab key for focus management, passed from parent to avoid duplicate construction @@ -71,6 +75,8 @@ class _TerminalPageState extends State // Create terminal model with specific terminal ID _terminalModel = TerminalModel(_ffi, widget.terminalId); + _terminalModel.onClipboardWriteBlocked = widget.onClipboardWriteBlocked; + _terminalModel.onClipboardWriteSucceeded = widget.onClipboardWriteSucceeded; debugPrint( '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 63289e94d..2612c9d35 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; 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/utils/multi_window_manager.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 '../../models/platform_model.dart'; @@ -19,6 +22,12 @@ import '../widgets/material_mod_popup_menu.dart' as mod_menu; import '../widgets/popup_menu.dart'; import 'package:bot_toast/bot_toast.dart'; +typedef _TerminalClipboardSource = ({ + String peerId, + int terminalId, + String tabKey, +}); + class TerminalTabPage extends StatefulWidget { final Map params; @@ -30,6 +39,18 @@ class TerminalTabPage extends StatefulWidget { class _TerminalTabPageState extends State { DesktopTabController get tabController => Get.find(); + 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 unselectedIcon = Icons.terminal_outlined; @@ -38,6 +59,9 @@ class _TerminalTabPageState extends State { final Set _closingTabs = {}; // When true, all session cleanup should persist (window-level close in progress) bool _windowClosing = false; + CancelFunc? _terminalClipboardNoticeCancel; + final _terminalClipboardNotice = + TerminalClipboardNoticeCoordinator<_TerminalClipboardSource>(); _TerminalTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); @@ -45,7 +69,10 @@ class _TerminalTabPageState extends State { WindowController.fromWindowId(windowId()) .setTitle(getWindowNameWithId(id)); }; - tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.onRemoved = (_, id) { + _closeTerminalClipboardNoticeForTab(id); + onRemoveId(id); + }; tabController.onCloseWindow = _closeWindowFromConnection; final terminalId = params['terminalId'] ?? _nextTerminalId++; tabController.add(_createTerminalTab( @@ -70,6 +97,11 @@ class _TerminalTabPageState extends State { final alias = bind.mainGetPeerOptionSync(id: peerId, key: 'alias'); final tabLabel = alias.isNotEmpty ? '$alias #$terminalId' : '$peerId #$terminalId'; + final clipboardSource = ( + peerId: peerId, + terminalId: terminalId, + tabKey: tabKey, + ); return TabInfo( key: tabKey, label: tabLabel, @@ -86,10 +118,169 @@ class _TerminalTabPageState extends State { tabController: tabController, forceRelay: forceRelay, 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 _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 _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). /// Shows audit dialog, cleans up session if not persistent, then removes the UI tab. Future _closeTab(String tabKey) async { @@ -147,6 +338,8 @@ class _TerminalTabPageState extends State { // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) // Keep the cleanup target lookup below synchronous before its first await: // it relies on the current frame still retaining each TerminalPage's FFI/model. + _terminalClipboardNotice.clear(); + _terminalClipboardNoticeCancel?.call(); tabController.clear(); // 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. @@ -357,6 +550,8 @@ class _TerminalTabPageState extends State { @override void dispose() { HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + _terminalClipboardNotice.clear(); + _terminalClipboardNoticeCancel?.call(); super.dispose(); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index cbc99fd2b..434cff501 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1269,16 +1269,18 @@ class __DisplayPageState extends State<_DisplayPage> { } SettingsTile otherRow(String label, String key) { - final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; - final isOptFixed = isOptionFixed(key); + final value = getOtherDefaultSettingOption(key) == 'Y'; + final isOptFixed = isOtherDefaultSettingReadOnly(key); return SettingsTile.switchTile( initialValue: value, title: Text(translate(label)), onToggle: isOptFixed ? null : (b) async { - await bind.mainSetUserDefaultOption( - key: key, value: b ? 'Y' : defaultOptionNo); + await setOtherDefaultSettingOption( + key, + b ? 'Y' : defaultOptionNo, + ); setState(() {}); }, ); diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 7a8c03ebb..ac2ca42f5 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/models/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_model.dart'; +import 'package:flutter_hbb/models/terminal_mouse_handler.dart'; import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart'; import 'package:flutter_hbb/web/dummy.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 '../../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? 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 { const TerminalPage({ Key? key, @@ -41,6 +85,19 @@ class TerminalPage extends StatefulWidget { class _TerminalPageState extends State 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 TerminalModel _terminalModel; double? _cellHeight; @@ -57,6 +114,9 @@ class _TerminalPageState extends State // For iOS edge swipe gesture double _swipeStartX = 0; double _swipeCurrentX = 0; + ScaffoldFeatureController? + _terminalClipboardNoticeController; + final _terminalClipboardNotice = TerminalClipboardNoticeCoordinator(); // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -89,6 +149,12 @@ class _TerminalPageState extends State // Create terminal model with specific terminal ID _terminalModel = TerminalModel(_ffi, widget.terminalId); + if (_canHandleTerminalClipboardWriteRequest) { + _terminalModel.onClipboardWriteBlocked = + _handleTerminalClipboardWriteBlocked; + _terminalModel.onClipboardWriteSucceeded = + _handleTerminalClipboardWriteSucceeded; + } debugPrint( '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); @@ -134,12 +200,144 @@ class _TerminalPageState extends State _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 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((_) { + 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 _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 _completeTerminalClipboardWrite( + TerminalClipboardNoticeRequest 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 void dispose() { // Unregister terminal model from FFI _ffi.unregisterTerminalModel(widget.terminalId); _terminalModel.dispose(); _keyboardDebounce?.cancel(); + _terminalClipboardNotice.clear(); + _terminalClipboardNoticeController?.close(); WidgetsBinding.instance.removeObserver(this); super.dispose(); TerminalConnectionManager.releaseConnection(widget.id); @@ -234,12 +432,12 @@ class _TerminalPageState extends State child: LayoutBuilder( builder: (context, constraints) { final heightPx = constraints.maxHeight; - return TerminalView( - _terminalModel.terminal, + return _buildTerminalViewForPlatform( + reportMouseInput: isWebDesktop || isAndroid, + reportTouchInput: isIOS, + terminal: _terminalModel.terminal, controller: _terminalModel.terminalController, - autofocus: true, textStyle: _getTerminalStyle(), - backgroundOpacity: 0.7, // The following comment is from xterm.dart source code: // Workaround to detect delete key for platforms and IMEs that do not // emit a hardware delete event. Preferred on mobile platforms. [false] by diff --git a/flutter/lib/models/rustdesk_terminal.dart b/flutter/lib/models/rustdesk_terminal.dart index 6e3f35dfd..452cba8dc 100644 --- a/flutter/lib/models/rustdesk_terminal.dart +++ b/flutter/lib/models/rustdesk_terminal.dart @@ -1,7 +1,108 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; import 'package:xterm/xterm.dart'; +enum TerminalClipboardWritePermission { denied, unconfigured, allowed } + class RustDeskTerminal extends Terminal { - RustDeskTerminal({super.maxLines}); + RustDeskTerminal({ + super.maxLines, + required TerminalClipboardWritePermission Function() + clipboardWritePermission, + required Future Function(String) onClipboardWrite, + ValueChanged? onClipboardWriteBlocked, + ValueChanged? 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 Function(String) _onClipboardWrite; + final ValueChanged? _onClipboardWriteBlocked; + final ValueChanged? _onClipboardWriteSucceeded; + + bool get isClipboardWriteAllowed => + _clipboardWritePermission() == TerminalClipboardWritePermission.allowed; + + void _handlePrivateOsc(String code, List 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 _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 void eraseScrollbackOnly() { diff --git a/flutter/lib/models/terminal_clipboard_writer.dart b/flutter/lib/models/terminal_clipboard_writer.dart new file mode 100644 index 000000000..57d4378a4 --- /dev/null +++ b/flutter/lib/models/terminal_clipboard_writer.dart @@ -0,0 +1,15 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +Future 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; + } +} diff --git a/flutter/lib/models/terminal_clipboard_writer_web.dart b/flutter/lib/models/terminal_clipboard_writer_web.dart new file mode 100644 index 000000000..56cd076e6 --- /dev/null +++ b/flutter/lib/models/terminal_clipboard_writer_web.dart @@ -0,0 +1,29 @@ +import 'dart:js_interop'; + +import 'package:flutter/foundation.dart'; + +const _writeTerminalClipboardCommand = 'write_terminal_clipboard'; + +@JS('setByName') +external JSPromise _setByName( + JSString name, + JSString value, + JSBoolean userInitiated, +); + +Future 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; + } +} diff --git a/flutter/lib/models/terminal_copy_shortcut.dart b/flutter/lib/models/terminal_copy_shortcut.dart index 242586e6b..0a9295c43 100644 --- a/flutter/lib/models/terminal_copy_shortcut.dart +++ b/flutter/lib/models/terminal_copy_shortcut.dart @@ -3,20 +3,130 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:xterm/xterm.dart'; +import 'terminal_clipboard_writer.dart' + if (dart.library.html) 'terminal_clipboard_writer_web.dart'; + const _controlShiftVPasteShortcut = SingleActivator( LogicalKeyboardKey.keyV, control: true, shift: true, ); -Future writeTerminalClipboard(String text) async { - try { - await Clipboard.setData(ClipboardData(text: text)); - } catch (error) { - debugPrint('[Terminal] Failed to write clipboard: $error'); +typedef TerminalClipboardWriter = Future Function( + String text, { + required bool userInitiated, +}); + +class TerminalClipboardNoticeRequest { + 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 extends ChangeNotifier { + TerminalClipboardNoticeRequest? _current; + bool _noticeVisible = false; + bool _actionInProgress = false; + + TerminalClipboardNoticeRequest? get current => _current; + bool get canClaimAction => + _noticeVisible && !_actionInProgress && _current != null; + + TerminalClipboardNoticeRequest? currentForSource(T source) { + final current = _current; + if (current == null || current.source != source) return null; + return current; } + + TerminalClipboardNoticeRequest? 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? 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 writeTerminalClipboard( + String text, { + bool userInitiated = false, +}) => + writeTerminalClipboardPlatform(text, userInitiated: userInitiated); + +Future completeTerminalClipboardWrite({ + required String clipboardText, + required bool Function() canWrite, + required TerminalClipboardWriter writeClipboard, + Future Function()? persistAllowed, +}) async { + if (!canWrite()) return false; + if (!await writeClipboard(clipboardText, userInitiated: true)) return false; + await persistAllowed?.call(); + return true; } Map? platformTerminalShortcuts() { @@ -68,7 +178,7 @@ FocusOnKeyEventCallback terminalCopyHandler( if (selection != null && !selection.isCollapsed) { if (event is KeyDownEvent) { final text = terminal.buffer.getText(selection); - unawaited(writeTerminalClipboard(text)); + unawaited(writeTerminalClipboard(text, userInitiated: true)); } return KeyEventResult.handled; } diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 63e831202..80725b5bc 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -11,8 +11,38 @@ import 'input_modifier_utils.dart'; import 'model.dart'; import 'platform_model.dart'; import 'rustdesk_terminal.dart'; +import 'terminal_copy_shortcut.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 { final String id; // peer id final FFI parent; @@ -62,6 +92,9 @@ class TerminalModel with ChangeNotifier { /// The listener (typically TerminalPage) can use this to auto-close the tab/page. VoidCallback? onClosed; + ValueChanged? onClipboardWriteBlocked; + ValueChanged? onClipboardWriteSucceeded; + Future _handleInput(String data) async { // xterm can complete asynchronous input after the Flutter page has gone // 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 { - 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(); terminalController = TerminalController(); @@ -593,6 +638,8 @@ class TerminalModel with ChangeNotifier { clearAltLock = null; onResizeExternal = null; onClosed = null; + onClipboardWriteBlocked = null; + onClipboardWriteSucceeded = null; // Clear buffers to free memory _inputBuffer.clear(); _pendingOutputChunks.clear(); diff --git a/flutter/lib/models/terminal_mouse_drag_reporter.dart b/flutter/lib/models/terminal_mouse_drag_reporter.dart index d08fc9afd..33aa9d26b 100644 --- a/flutter/lib/models/terminal_mouse_drag_reporter.dart +++ b/flutter/lib/models/terminal_mouse_drag_reporter.dart @@ -62,13 +62,17 @@ class TerminalMouseDragReporter { var _ownsControllerSuspension = false; var _releasePending = false; var _reporting = false; + var _dragged = false; bool handleDown( PointerDownEvent event, Terminal terminal, - TerminalViewState? terminalView, - ) { - if (!_isPrimaryMouse(event) || !_reportsDrag(terminal.mouseMode)) { + TerminalViewState? terminalView, { + bool reportTouchInput = false, + bool deferReport = false, + }) { + if (!_isPrimaryPointer(event, reportTouchInput) || + !_reportsDrag(terminal.mouseMode)) { return false; } if (terminalView == null || terminalView.widget.readOnly) return false; @@ -83,14 +87,33 @@ class TerminalMouseDragReporter { _pointerId = event.pointer; _controller = controller; _ownsControllerSuspension = true; - _releasePending = true; - _reporting = true; + _releasePending = !deferReport; + _reporting = !deferReport; + _dragged = false; controller.setSuspendPointerInput(true); _clearSelection(controller); final position = _cellAt(event, terminalView); _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( - _report(terminal.mouseReportMode, position), + _report(terminal.mouseReportMode, _lastReportedPosition), ); return true; } @@ -98,26 +121,36 @@ class TerminalMouseDragReporter { bool handleMove( PointerMoveEvent event, Terminal terminal, - TerminalViewState? terminalView, - ) { + TerminalViewState? terminalView, { + void Function(bool dragged)? beforeRelease, + void Function()? onCancel, + }) { if (event.pointer != _pointerId) return false; if (terminalView == null) { + onCancel?.call(); cancel(); return true; } final reportsDrag = _reportsDrag(terminal.mouseMode); - if (!_isPrimaryMouse(event)) { + if (!_hasPrimaryButton(event)) { if (_releasePending && reportsDrag) { - _reportRelease( + _finishRelease( + event, terminal, - _reporting ? _cellAt(event, terminalView) : _lastReportedPosition, + terminalView, + beforeRelease: beforeRelease, ); + } else { + onCancel?.call(); } cancel(); return true; } if (!_reporting || !reportsDrag) { - if (!reportsDrag) _releasePending = false; + if (!reportsDrag && _releasePending) { + _releasePending = false; + onCancel?.call(); + } _reporting = false; // Keep ownership until the matching end event to suppress local selection. final controller = _controller; @@ -126,7 +159,7 @@ class TerminalMouseDragReporter { } final position = _cellAt(event, terminalView); - _lastReportedPosition = position; + _recordPosition(position); terminal.textInput( _report(terminal.mouseReportMode, position, motion: true), ); @@ -138,16 +171,22 @@ class TerminalMouseDragReporter { bool handleEnd( PointerEvent event, Terminal terminal, - TerminalViewState? terminalView, - ) { + TerminalViewState? terminalView, { + void Function(bool dragged)? beforeRelease, + void Function()? onCancel, + }) { if (event.pointer != _pointerId) return false; if (terminalView != null && _releasePending && _reportsDrag(terminal.mouseMode)) { - _reportRelease( + _finishRelease( + event, terminal, - _reporting ? _cellAt(event, terminalView) : _lastReportedPosition, + terminalView, + beforeRelease: beforeRelease, ); + } else { + onCancel?.call(); } _clearSelection(_controller); final controller = _controller; @@ -172,6 +211,7 @@ class TerminalMouseDragReporter { _ownsControllerSuspension = false; _releasePending = false; _reporting = false; + _dragged = false; } 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) { final renderTerminal = terminalView.renderTerminal; return renderTerminal.getCellOffset( @@ -210,9 +268,13 @@ class TerminalMouseDragReporter { ); } - bool _isPrimaryMouse(PointerEvent event) => - event.kind == PointerDeviceKind.mouse && - (event.buttons & kPrimaryMouseButton) == kPrimaryMouseButton; + bool _isPrimaryPointer(PointerEvent event, bool reportTouchInput) => + (event.kind == PointerDeviceKind.mouse || + reportTouchInput && event.kind == PointerDeviceKind.touch) && + _hasPrimaryButton(event); + + bool _hasPrimaryButton(PointerEvent event) => + (event.buttons & kPrimaryButton) == kPrimaryButton; bool _reportsDrag(MouseMode mode) => mode == MouseMode.upDownScrollDrag || mode == MouseMode.upDownScrollMove; diff --git a/flutter/lib/models/terminal_mouse_handler.dart b/flutter/lib/models/terminal_mouse_handler.dart index 76d84a2d4..d1ed191ad 100644 --- a/flutter/lib/models/terminal_mouse_handler.dart +++ b/flutter/lib/models/terminal_mouse_handler.dart @@ -1,45 +1,17 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:xterm/xterm.dart'; +import 'platform_model.dart'; +import 'rustdesk_terminal.dart'; import 'terminal_copy_shortcut.dart'; import 'terminal_mouse_drag_reporter.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}); - - 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, - ); - } -} +part 'terminal_mouse_handler_input.dart'; +part 'terminal_web_clipboard_gesture.dart'; class TerminalMouseInteraction extends StatefulWidget { const TerminalMouseInteraction( @@ -47,6 +19,12 @@ class TerminalMouseInteraction extends StatefulWidget { super.key, required this.controller, this.focusNode, + this.autofocus = false, + this.textStyle = const TerminalStyle(), + this.deleteDetection = false, + this.reportTouchInput = false, + this.shortcuts, + this.onKeyEvent, this.backgroundOpacity = 1, this.padding, this.onSecondaryTapDown, @@ -55,6 +33,12 @@ class TerminalMouseInteraction extends StatefulWidget { final Terminal terminal; final TerminalController controller; final FocusNode? focusNode; + final bool autofocus; + final TerminalStyle textStyle; + final bool deleteDetection; + final bool reportTouchInput; + final Map? shortcuts; + final FocusOnKeyEventCallback? onKeyEvent; final double backgroundOpacity; final EdgeInsets? padding; final void Function(TapDownDetails, CellOffset)? onSecondaryTapDown; @@ -81,8 +65,13 @@ class _TerminalMouseInteractionState extends State { Buffer? _selectionBuffer; int? _selectionPointerId; Timer? _selectionScrollTimer; + Timer? _pendingTouchMouseTimer; + PointerDownEvent? _pendingTouchMouseDown; var _selectionHasScrolled = false; 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; @override @@ -90,6 +79,7 @@ class _TerminalMouseInteractionState extends State { super.initState(); _mouseHandler = WheelButtonFixMouseHandler( positionProvider: _cellAtPointer, + suppressLeftButton: kIsWeb ? _consumeXtermLeftButtonSuppression : null, ); _installMouseHandler(widget.terminal); } @@ -100,10 +90,15 @@ class _TerminalMouseInteractionState extends State { final terminalChanged = !identical(oldWidget.terminal, widget.terminal); final controllerChanged = !identical(oldWidget.controller, widget.controller); + final touchInputChanged = + oldWidget.reportTouchInput != widget.reportTouchInput; + if (!terminalChanged && !controllerChanged && !touchInputChanged) return; + _cancelPendingTouchMouseDrag(); if (!terminalChanged && !controllerChanged) return; if (controllerChanged && !terminalChanged) { _mouseDrag.updateController(widget.controller); } else { + _discardPendingTerminalClipboardWrites(); _mouseDrag.cancel(); } _clearSelectionDrag(); @@ -123,46 +118,18 @@ class _TerminalMouseInteractionState extends State { } } - 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) { _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.kind != PointerDeviceKind.mouse || (event.buttons & kPrimaryMouseButton) != kPrimaryMouseButton) { @@ -241,8 +208,28 @@ class _TerminalMouseInteractionState extends State { void _handlePointerEnd(PointerEvent event) { _updatePointerPosition(event); - if (!_mouseDrag.handleEnd(event, widget.terminal, _terminalView) && - event.pointer != _selectionPointerId) return; + final pendingTouch = _pendingTouchMouseDown; + 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); _clearSelectionDrag(); } @@ -265,6 +252,8 @@ class _TerminalMouseInteractionState extends State { @override void dispose() { + _discardPendingTerminalClipboardWrites(); + _cancelPendingTouchMouseDrag(); _mouseDrag.cancel(); _clearSelectionDrag(); _restoreMouseHandler(widget.terminal); @@ -290,10 +279,14 @@ class _TerminalMouseInteractionState extends State { controller: widget.controller, scrollController: _scrollController, focusNode: widget.focusNode, + autofocus: widget.autofocus, + textStyle: widget.textStyle, + deleteDetection: widget.deleteDetection, backgroundOpacity: widget.backgroundOpacity, padding: widget.padding, - shortcuts: platformTerminalShortcuts(), - onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller), + shortcuts: widget.shortcuts ?? platformTerminalShortcuts(), + onKeyEvent: widget.onKeyEvent ?? + terminalCopyHandler(widget.terminal, widget.controller), onSecondaryTapDown: widget.onSecondaryTapDown, ), ); diff --git a/flutter/lib/models/terminal_mouse_handler_input.dart b/flutter/lib/models/terminal_mouse_handler_input.dart new file mode 100644 index 000000000..b0ed06dc8 --- /dev/null +++ b/flutter/lib/models/terminal_mouse_handler_input.dart @@ -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; + } +} diff --git a/flutter/lib/models/terminal_web_clipboard_gesture.dart b/flutter/lib/models/terminal_web_clipboard_gesture.dart new file mode 100644 index 000000000..6b4dedf37 --- /dev/null +++ b/flutter/lib/models/terminal_web_clipboard_gesture.dart @@ -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'); + } + } +} diff --git a/src/lang/ar.rs b/src/lang/ar.rs index da12b2017..8e358caec 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "قفل اللوحة"), ("Sync clipboard between sessions", "مزامنة الحافظة بين الجلسات"), ("sync-clipboard-between-sessions-tip", "النص أو الصور المنسوخة في جلسة بعيدة واحدة تُرسَل أيضًا إلى حافظة جلساتك المتصلة الأخرى."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "تفعيل"), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 2c012fb64..e72c4027f 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Заблакіраваць палатно"), ("Sync clipboard between sessions", "Сінхранізаваць буфер абмену паміж сеансамі"), ("sync-clipboard-between-sessions-tip", "Тэкст або відарысы, скапіяваныя ў адным аддаленым сеансе, таксама адпраўляюцца ў буфер абмену іншых вашых падключаных сеансаў."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Уключыць"), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 4104846ee..98ae557fe 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Заключване на платното"), ("Sync clipboard between sessions", "Синхронизиране на клипборда между сесиите"), ("sync-clipboard-between-sessions-tip", "Текст или изображения, копирани в една отдалечена сесия, се изпращат и към клипборда на другите ви свързани сесии."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Активирай"), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8d73f523b..a0bc96b9f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloca el llenç"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Habilita"), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 03e10097e..bce02e3b0 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "锁定画布"), ("Sync clipboard between sessions", "在会话间同步剪贴板"), ("sync-clipboard-between-sessions-tip", "在一个远程会话中复制的文本或图片也会发送到其他已连接会话的剪贴板。"), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", "允许终端应用复制到剪贴板"), + ("Enable", "启用"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 71b151e41..d3df00e95 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Zamknout zobrazení"), ("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í."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Povolit"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 3ba116072..d8b50b9dd 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Lås lærred"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktivér"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 54a691b7c..4fde0cf8e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Sichtfeld sperren"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktivieren"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e7c7e1d09..a2ff41c99 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Κλείδωμα καμβά"), ("Sync clipboard between sessions", "Συγχρονισμός προχείρου μεταξύ συνεδριών"), ("sync-clipboard-between-sessions-tip", "Κείμενο ή εικόνες που αντιγράφονται σε μία απομακρυσμένη συνεδρία αποστέλλονται και στο πρόχειρο των άλλων συνδεδεμένων συνεδριών σας."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Ενεργοποίηση"), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 3ffc0939c..6256bfc0d 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -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"), ("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."), + ("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(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index e1edc9053..ef7452257 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Ŝlosi kanvason"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Ebligi"), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 1967d56ab..86e10f0a2 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloquear lienzo"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Habilitar"), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 6b8b715f8..bb1c12cff 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Lukusta lõuend"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Luba"), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 06783379a..cbf98dc01 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Blokeatu oihala"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Gaitu"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index dd16ed09a..a2682c0df 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "قفل کردن صفحه"), ("Sync clipboard between sessions", "همگام‌سازی کلیپ‌بورد بین نشست‌ها"), ("sync-clipboard-between-sessions-tip", "متن یا تصاویری که در یک نشست راه دور کپی می‌شوند به کلیپ‌بورد سایر نشست‌های متصل شما نیز ارسال می‌شوند."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "فعال‌سازی"), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 3b00e00b1..150f9d38c 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Lukitse näkymä"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Ota käyttöön"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 11372cc51..19d9ee6e3 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Verrouiller la vue"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Activer"), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index edacbb4f5..e3ca18da4 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "ტილოს დაბლოკვა"), ("Sync clipboard between sessions", "გაცვლის ბუფერის სინქრონიზაცია სესიებს შორის"), ("sync-clipboard-between-sessions-tip", "ერთ დაშორებულ სესიაში დაკოპირებული ტექსტი ან სურათები ასევე იგზავნება თქვენი სხვა დაკავშირებული სესიების გაცვლის ბუფერში."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "ჩართვა"), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index c0d722940..434a3bf01 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "કેનવાસ લોક કરો"), ("Sync clipboard between sessions", "સત્રો વચ્ચે ક્લિપબોર્ડ સિંક કરો"), ("sync-clipboard-between-sessions-tip", "એક રિમોટ સત્રમાં કૉપિ કરેલ ટેક્સ્ટ કે છબીઓ તમારા અન્ય જોડાયેલા સત્રોના ક્લિપબોર્ડ પર પણ મોકલવામાં આવે છે."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "સક્ષમ કરો"), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 8dd783f30..8fb61c405 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "נעל לוח ציור"), ("Sync clipboard between sessions", "סנכרן לוח בין סשנים"), ("sync-clipboard-between-sessions-tip", "טקסט או תמונות שהועתקו בסשן מרוחק אחד נשלחים גם ללוח של שאר הסשנים המחוברים שלך."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "הפעל"), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index f9eebc603..5961bc649 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "कैनवास लॉक करें"), ("Sync clipboard between sessions", "सत्रों के बीच क्लिपबोर्ड सिंक करें"), ("sync-clipboard-between-sessions-tip", "एक रिमोट सत्र में कॉपी किए गए टेक्स्ट या चित्र आपके अन्य जुड़े सत्रों के क्लिपबोर्ड पर भी भेजे जाते हैं।"), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "सक्षम करें"), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index a793e519e..ffd58a08e 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Zaključaj pozadinu"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Omogući"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 4a5a737da..0622a4248 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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-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(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f5a8918bd..9e637b065 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Kunci kanvas"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktifkan"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 73e5e6d83..18c44ce76 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Blocca tela"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Abilita"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index dad7b0557..d63a975c2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "キャンバスをロック"), ("Sync clipboard between sessions", "セッション間でクリップボードを同期"), ("sync-clipboard-between-sessions-tip", "1つのリモートセッションでコピーしたテキストや画像は、接続中の他のセッションのクリップボードにも送信されます。"), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "有効にする"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 2151c9b8b..d83c16c15 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "캔버스 잠금"), ("Sync clipboard between sessions", "세션 간 클립보드 동기화"), ("sync-clipboard-between-sessions-tip", "하나의 원격 세션에서 복사한 텍스트나 이미지는 연결된 다른 세션의 클립보드에도 전송됩니다."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "활성화"), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 27ed4e8d6..b7e55f44c 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Кенепті құлыптау"), ("Sync clipboard between sessions", "Сеанстар арасында көшіру-тақтасын синхрондау"), ("sync-clipboard-between-sessions-tip", "Бір қашықтағы сеанста көшірілген мәтін немесе суреттер басқа қосылған сеанстардың көшіру-тақтасына да жіберіледі."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Қосу"), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 4beb66593..6a94e5125 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Užrakinti drobę"), ("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ę."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Įgalinti"), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 2d7038f19..747475090 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloķēt audeklu"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Iespējot"), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index f7804fa0c..130f4ed6b 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "ക്യാൻവാസ് ലോക്ക് ചെയ്യുക"), ("Sync clipboard between sessions", "സെഷനുകൾക്കിടയിൽ ക്ലിപ്പ്ബോർഡ് സമന്വയിപ്പിക്കുക"), ("sync-clipboard-between-sessions-tip", "ഒരു റിമോട്ട് സെഷനിൽ പകർത്തിയ ടെക്സ്റ്റോ ചിത്രങ്ങളോ നിങ്ങളുടെ മറ്റ് കണക്റ്റുചെയ്ത സെഷനുകളുടെ ക്ലിപ്പ്ബോർഡിലേക്കും അയയ്ക്കപ്പെടും."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "അനുവദിക്കുക"), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 753db92a2..7d77bd7ea 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Lås lerret"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktiver"), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 00626f31c..6bb2f4828 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Canvas vergrendelen"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Inschakelen"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 8b599b2ea..4ff145386 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Zablokuj ekran"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Włącz"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 2b71cc3e2..cbec87b50 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloquear tela"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Ativar"), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f721942fc..130d0bc03 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloquear tela"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Habilitar"), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 6bf368822..30ecd7874 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Blochează ecranul"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Activează"), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 54d388cbe..bd57b91a5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Заблокировать холст"), ("Sync clipboard between sessions", "Синхронизировать буфер обмена между сеансами"), ("sync-clipboard-between-sessions-tip", "Текст или изображения, скопированные в одном удалённом сеансе, также отправляются в буфер обмена других подключённых сеансов."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Включить"), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index e13c07fc9..ada1bc23b 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Bloca sa tela"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Abìlita"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 1795c97ac..640eec48a 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Uzamknúť zobrazenie"), ("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í."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Povoliť"), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 694297a46..8db91c36b 100644 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Zakleni platno"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Omogoči"), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3263993f3..f94e17537 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Kyç canvas"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktivizo"), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 31987bf31..006dfaccb 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Zaključaj pozadinu"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Omogući"), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index a6a5eb404..990e62ca7 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Lås canvas"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Aktivera"), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 2a0e1e0f7..ce4119f8b 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "கேன்வாஸைப் பூட்டு"), ("Sync clipboard between sessions", "அமர்வுகளுக்கு இடையே கிளிப்போர்டை ஒத்திசைக்கவும்"), ("sync-clipboard-between-sessions-tip", "ஒரு தொலை அமர்வில் நகலெடுக்கப்பட்ட உரை அல்லது படங்கள் உங்கள் பிற இணைக்கப்பட்ட அமர்வுகளின் கிளிப்போர்டுக்கும் அனுப்பப்படும்."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "இயக்கு"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index feab1b71e..ce3d245ae 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", ""), ("Sync clipboard between sessions", ""), ("sync-clipboard-between-sessions-tip", ""), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d261884b3..544bcdc04 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "ล็อคแคนวาส"), ("Sync clipboard between sessions", "ซิงค์คลิปบอร์ดระหว่างเซสชัน"), ("sync-clipboard-between-sessions-tip", "ข้อความหรือรูปภาพที่คัดลอกในเซสชันระยะไกลหนึ่งจะถูกส่งไปยังคลิปบอร์ดของเซสชันอื่นที่เชื่อมต่ออยู่ด้วย"), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "เปิดใช้งาน"), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4e5f1bbca..b70a1c44a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Tuvali kilitle"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Etkinleştir"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index bfe6fb15a..2f823cc6b 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "鎖定畫布"), ("Sync clipboard between sessions", "在工作階段間同步剪貼簿"), ("sync-clipboard-between-sessions-tip", "在一個遠端工作階段中複製的文字或圖片也會傳送到其他已連線工作階段的剪貼簿。"), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "啟用"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 97c56ba86..2cca68973 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Блокування полотна"), ("Sync clipboard between sessions", "Синхронізувати буфер обміну між сеансами"), ("sync-clipboard-between-sessions-tip", "Текст або зображення, скопійовані в одному віддаленому сеансі, також надсилаються до буфера обміну інших підключених сеансів."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Увімкнути"), ].iter().cloned().collect(); } diff --git a/src/lang/ur.rs b/src/lang/ur.rs index 5f6e1c235..28465d386 100644 --- a/src/lang/ur.rs +++ b/src/lang/ur.rs @@ -745,6 +745,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "display-name"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 8995ade87..48c652f5a 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -763,5 +763,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Lock canvas", "Khóa khung hình"), ("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."), + ("terminal-clipboard-write-tip", ""), + ("Allow terminal apps to copy to clipboard", ""), + ("Enable", "Bật"), ].iter().cloned().collect(); }