diff --git a/flutter/lib/models/terminal_copy_shortcut.dart b/flutter/lib/models/terminal_copy_shortcut.dart new file mode 100644 index 000000000..a526c16b4 --- /dev/null +++ b/flutter/lib/models/terminal_copy_shortcut.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:xterm/xterm.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'); + } +} + +Map? platformTerminalShortcuts() { + if (defaultTargetPlatform != TargetPlatform.linux) return null; + return { + for (final entry in defaultTerminalShortcuts.entries) + if (!_isControlVShortcut(entry.key)) entry.key: entry.value, + _controlShiftVPasteShortcut: + const PasteTextIntent(SelectionChangedCause.keyboard), + }; +} + +bool _isControlVShortcut(ShortcutActivator shortcut) => + shortcut is SingleActivator && + shortcut.trigger == LogicalKeyboardKey.keyV && + shortcut.control && + !shortcut.shift && + !shortcut.alt && + !shortcut.meta; + +FocusOnKeyEventCallback terminalCopyHandler( + Terminal terminal, + TerminalController controller, +) => + (_, event) { + if (!_isWindowsCopyShortcut(event)) return KeyEventResult.ignored; + final selection = controller.selection; + if (selection == null || selection.isCollapsed) { + return KeyEventResult.ignored; + } + if (event is KeyDownEvent) { + final text = terminal.buffer.getText(selection); + unawaited(writeTerminalClipboard(text)); + } + return KeyEventResult.handled; + }; + +bool _isWindowsCopyShortcut(KeyEvent event) { + final keyboard = HardwareKeyboard.instance; + return defaultTargetPlatform == TargetPlatform.windows && + (event is KeyDownEvent || event is KeyRepeatEvent) && + event.logicalKey == LogicalKeyboardKey.keyC && + keyboard.isControlPressed && + !keyboard.isShiftPressed && + !keyboard.isAltPressed && + !keyboard.isMetaPressed; +} diff --git a/flutter/lib/models/terminal_mouse_handler.dart b/flutter/lib/models/terminal_mouse_handler.dart index 6c5638793..76d84a2d4 100644 --- a/flutter/lib/models/terminal_mouse_handler.dart +++ b/flutter/lib/models/terminal_mouse_handler.dart @@ -4,6 +4,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:xterm/xterm.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 @@ -291,6 +292,8 @@ class _TerminalMouseInteractionState extends State { focusNode: widget.focusNode, backgroundOpacity: widget.backgroundOpacity, padding: widget.padding, + shortcuts: platformTerminalShortcuts(), + onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller), onSecondaryTapDown: widget.onSecondaryTapDown, ), ); diff --git a/flutter/test/terminal_mouse_handler_test.dart b/flutter/test/terminal_mouse_handler_test.dart index 62f19ff44..04af45250 100644 --- a/flutter/test/terminal_mouse_handler_test.dart +++ b/flutter/test/terminal_mouse_handler_test.dart @@ -1,6 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_hbb/models/terminal_mouse_handler.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xterm/xterm.dart'; @@ -38,6 +40,34 @@ void main() { ..onOutput = output.add; }); + testWidgets('Linux Ctrl+V is not paste', (tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.linux; + try { + final messenger = tester.binding.defaultBinaryMessenger; + messenger.setMockMethodCallHandler( + SystemChannels.platform, + (_) async => {'text': 'clipboard'}, + ); + final controller = TerminalController(); + addTearDown(controller.dispose); + await tester.pumpWidget(_terminalHarness(terminal, controller)); + await tester.tap(find.byType(TerminalView)); + await tester.pump(kDoubleTapTimeout); + await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.keyV); + final controlVOutput = List.of(output); + output.clear(); + await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); + await tester.sendKeyEvent(LogicalKeyboardKey.keyV); + await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); + await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); + expect(controlVOutput, ['\x16']); + expect(output, ['clipboard']); + } finally { + debugDefaultTargetPlatformOverride = null; + } + }); + String? report( TerminalMouseButton button, [ TerminalMouseButtonState state = TerminalMouseButtonState.down,