feat(terminal): use platform-native copy and paste shortcuts (#15931)

This commit is contained in:
fufesou
2026-08-21 21:20:39 +08:00
committed by GitHub
parent 61ddade049
commit 92eb137178
3 changed files with 99 additions and 0 deletions
@@ -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<void> writeTerminalClipboard(String text) async {
try {
await Clipboard.setData(ClipboardData(text: text));
} catch (error) {
debugPrint('[Terminal] Failed to write clipboard: $error');
}
}
Map<ShortcutActivator, Intent>? 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;
}
@@ -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<TerminalMouseInteraction> {
focusNode: widget.focusNode,
backgroundOpacity: widget.backgroundOpacity,
padding: widget.padding,
shortcuts: platformTerminalShortcuts(),
onKeyEvent: terminalCopyHandler(widget.terminal, widget.controller),
onSecondaryTapDown: widget.onSecondaryTapDown,
),
);
@@ -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,