fix(flutter): align terminal shortcuts with platform conventions (#15970)

* fix(flutter): align terminal shortcuts with platform conventions

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

* fix(flutter): handle Linux terminal paste with modifier locks

Detect platform-specific paste shortcuts so Ctrl+Shift+V bypasses
virtual Ctrl/Alt modifiers on Linux. Add regression coverage.

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

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-08-27 16:33:58 +08:00
committed by GitHub
parent 1fe451c2e8
commit 03a7fc5992
4 changed files with 110 additions and 27 deletions

View File

@@ -117,10 +117,11 @@ String prepareTerminalInputPayload(
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
///
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
/// one-character paste as normal text when bracketed paste mode is disabled.
/// xterm already handles each platform's paste shortcut in the common case.
/// Only intercept while a virtual Ctrl/Alt lock is active, because xterm can
/// emit a one-character paste as normal text when bracketed paste mode is off.
bool shouldHandleTerminalPasteShortcut({
required TargetPlatform platform,
required LogicalKeyboardKey logicalKey,
required bool isKeyDown,
required bool isKeyRepeat,
@@ -133,8 +134,18 @@ bool shouldHandleTerminalPasteShortcut({
if (!modifierLockActive) return false;
if (!isKeyDown && !isKeyRepeat) return false;
if (logicalKey != LogicalKeyboardKey.keyV) return false;
if (altPressed || shiftPressed) return false;
return controlPressed != metaPressed;
if (altPressed) return false;
switch (platform) {
case TargetPlatform.linux:
return controlPressed && !metaPressed && shiftPressed;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return !controlPressed && metaPressed && !shiftPressed;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.windows:
return controlPressed && !metaPressed && !shiftPressed;
}
}
/// Returns true when collapsing Row3 should also clear hidden modifier state.

View File

@@ -20,43 +20,68 @@ Future<void> writeTerminalClipboard(String text) async {
}
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
if (defaultTargetPlatform != TargetPlatform.linux) return null;
final platform = defaultTargetPlatform;
if (platform == TargetPlatform.linux) {
return {
for (final entry in defaultTerminalShortcuts.entries)
if (!_isControlShortcut(entry.key, LogicalKeyboardKey.keyV))
entry.key: entry.value,
_controlShiftVPasteShortcut:
const PasteTextIntent(SelectionChangedCause.keyboard),
};
}
if (platform != TargetPlatform.windows &&
platform != TargetPlatform.android) {
return null;
}
return {
for (final entry in defaultTerminalShortcuts.entries)
if (!_isControlVShortcut(entry.key)) entry.key: entry.value,
_controlShiftVPasteShortcut:
const PasteTextIntent(SelectionChangedCause.keyboard),
if (!_isControlShortcut(
entry.key,
LogicalKeyboardKey.keyC,
shift: true,
))
entry.key: entry.value,
};
}
bool _isControlVShortcut(ShortcutActivator shortcut) =>
bool _isControlShortcut(
ShortcutActivator shortcut,
LogicalKeyboardKey key, {
bool shift = false,
}) =>
shortcut is SingleActivator &&
shortcut.trigger == LogicalKeyboardKey.keyV &&
shortcut.trigger == key &&
shortcut.control &&
!shortcut.shift &&
shortcut.shift == 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;
TerminalController controller, {
FocusOnKeyEventCallback? fallback,
}) =>
(focusNode, event) {
if (_isSelectionCopyShortcut(event)) {
final selection = controller.selection;
if (selection != null && !selection.isCollapsed) {
if (event is KeyDownEvent) {
final text = terminal.buffer.getText(selection);
unawaited(writeTerminalClipboard(text));
}
return KeyEventResult.handled;
}
}
if (event is KeyDownEvent) {
final text = terminal.buffer.getText(selection);
unawaited(writeTerminalClipboard(text));
}
return KeyEventResult.handled;
return fallback?.call(focusNode, event) ?? KeyEventResult.ignored;
};
bool _isWindowsCopyShortcut(KeyEvent event) {
bool _isSelectionCopyShortcut(KeyEvent event) {
final keyboard = HardwareKeyboard.instance;
return defaultTargetPlatform == TargetPlatform.windows &&
final platform = defaultTargetPlatform;
final usesControlCopy =
platform == TargetPlatform.windows || platform == TargetPlatform.android;
return usesControlCopy &&
(event is KeyDownEvent || event is KeyRepeatEvent) &&
event.logicalKey == LogicalKeyboardKey.keyC &&
keyboard.isControlPressed &&