Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from "bun:test";
import {
shouldBubbleClipboardShortcut,
shouldSelectAllShortcut,
} from "./clipboardShortcuts";

function makeEvent(
overrides: Partial<{
code: string;
metaKey: boolean;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
}>,
) {
return {
code: "KeyC",
metaKey: false,
ctrlKey: false,
altKey: false,
shiftKey: false,
...overrides,
};
}

describe("shouldBubbleClipboardShortcut", () => {
it("matches the VS Code terminal clipboard bindings", () => {
const cases = [
{
name: "macOS Cmd+V",
event: makeEvent({ code: "KeyV", metaKey: true }),
options: { isMac: true, isWindows: false, hasSelection: false },
expected: true,
},
{
name: "macOS Cmd+C with selection",
event: makeEvent({ code: "KeyC", metaKey: true }),
options: { isMac: true, isWindows: false, hasSelection: true },
expected: true,
},
{
name: "windows Ctrl+V",
event: makeEvent({ code: "KeyV", ctrlKey: true }),
options: { isMac: false, isWindows: true, hasSelection: false },
expected: true,
},
{
name: "windows Ctrl+Shift+V",
event: makeEvent({ code: "KeyV", ctrlKey: true, shiftKey: true }),
options: { isMac: false, isWindows: true, hasSelection: false },
expected: true,
},
{
name: "windows Ctrl+C with selection",
event: makeEvent({ code: "KeyC", ctrlKey: true }),
options: { isMac: false, isWindows: true, hasSelection: true },
expected: true,
},
{
name: "linux Ctrl+Shift+C with selection",
event: makeEvent({ code: "KeyC", ctrlKey: true, shiftKey: true }),
options: { isMac: false, isWindows: false, hasSelection: true },
expected: true,
},
{
name: "linux Ctrl+Shift+V",
event: makeEvent({ code: "KeyV", ctrlKey: true, shiftKey: true }),
options: { isMac: false, isWindows: false, hasSelection: false },
expected: true,
},
{
name: "linux Shift+Insert",
event: makeEvent({ code: "Insert", shiftKey: true }),
options: { isMac: false, isWindows: false, hasSelection: false },
expected: true,
},
{
name: "macOS Cmd+C without selection",
event: makeEvent({ code: "KeyC", metaKey: true }),
options: { isMac: true, isWindows: false, hasSelection: false },
expected: false,
},
{
name: "windows Ctrl+C without selection",
event: makeEvent({ code: "KeyC", ctrlKey: true }),
options: { isMac: false, isWindows: true, hasSelection: false },
expected: false,
},
{
name: "linux Ctrl+Shift+C without selection",
event: makeEvent({ code: "KeyC", ctrlKey: true, shiftKey: true }),
options: { isMac: false, isWindows: false, hasSelection: false },
expected: false,
},
{
name: "linux Ctrl+Insert stays with the PTY",
event: makeEvent({ code: "Insert", ctrlKey: true }),
options: { isMac: false, isWindows: false, hasSelection: false },
expected: false,
},
{
name: "macOS does not inherit linux fallback chords",
event: makeEvent({ code: "KeyV", ctrlKey: true, shiftKey: true }),
options: { isMac: true, isWindows: false, hasSelection: false },
expected: false,
},
{
name: "macOS Shift+Insert stays with the PTY",
event: makeEvent({ code: "Insert", shiftKey: true }),
options: { isMac: true, isWindows: false, hasSelection: false },
expected: false,
},
];

for (const { name, event, options, expected } of cases) {
expect(shouldBubbleClipboardShortcut(event, options), name).toBe(
expected,
);
}
});
});

describe("shouldSelectAllShortcut", () => {
it("matches only the VS Code macOS terminal select-all binding", () => {
const cases = [
{
name: "macOS Cmd+A",
event: makeEvent({ code: "KeyA", metaKey: true }),
isMac: true,
expected: true,
},
{
name: "windows Ctrl+A is not intercepted",
event: makeEvent({ code: "KeyA", ctrlKey: true }),
isMac: false,
expected: false,
},
{
name: "macOS Cmd+Shift+A is not intercepted",
event: makeEvent({ code: "KeyA", metaKey: true, shiftKey: true }),
isMac: true,
expected: false,
},
];

for (const { name, event, isMac, expected } of cases) {
expect(shouldSelectAllShortcut(event, isMac), name).toBe(expected);
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
export interface ClipboardShortcutEvent {
code: string;
metaKey: boolean;
ctrlKey: boolean;
altKey: boolean;
shiftKey: boolean;
}

export interface ClipboardShortcutOptions {
isMac: boolean;
isWindows: boolean;
hasSelection: boolean;
}

/** Match VS Code's macOS terminal `Cmd+A` binding. */
export function shouldSelectAllShortcut(
event: ClipboardShortcutEvent,
isMac: boolean,
): boolean {
return (
isMac &&
event.code === "KeyA" &&
event.metaKey &&
!event.ctrlKey &&
!event.altKey &&
!event.shiftKey
);
}

/**
* Mirror VS Code terminal clipboard bindings so host copy/paste can run before
* xterm's kitty keyboard handler turns the chord into CSI-u input.
*/
export function shouldBubbleClipboardShortcut(
event: ClipboardShortcutEvent,
options: ClipboardShortcutOptions,
): boolean {
const { isMac, isWindows, hasSelection } = options;

const onlyMeta =
event.metaKey && !event.ctrlKey && !event.altKey && !event.shiftKey;
const onlyCtrl =
event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
const ctrlShiftOnly =
event.ctrlKey && event.shiftKey && !event.metaKey && !event.altKey;
const onlyShift =
event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey;

if (isMac && onlyMeta) {
return event.code === "KeyV" || (hasSelection && event.code === "KeyC");
}

if (isWindows) {
if (event.code === "KeyV" && (onlyCtrl || ctrlShiftOnly)) {
return true;
}

if (hasSelection && event.code === "KeyC" && (onlyCtrl || ctrlShiftOnly)) {
return true;
}

return false;
}

if (!isMac) {
return (
(event.code === "KeyV" && ctrlShiftOnly) ||
(event.code === "Insert" && onlyShift) ||
(hasSelection && event.code === "KeyC" && ctrlShiftOnly)
);
}

return false;
}
Comment thread
Kitenite marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import {
DEFAULT_THEME_ID,
getTerminalColors,
} from "shared/themes";
import {
shouldBubbleClipboardShortcut,
shouldSelectAllShortcut,
} from "./clipboardShortcuts";
import { TERMINAL_OPTIONS } from "./config";
import { suppressQueryResponses } from "./suppressQueryResponses";

Expand Down Expand Up @@ -574,6 +578,26 @@ export function setupKeyboardHandler(
return false;
}

if (shouldSelectAllShortcut(event, isMac)) {
if (event.type === "keydown") {
event.preventDefault();
xterm.selectAll();
}
return false;
}

// Mirror VS Code terminal clipboard bindings so host copy/paste happens
// before kitty CSI-u handling in xterm consumes the command chord.
if (
shouldBubbleClipboardShortcut(event, {
isMac,
isWindows,
hasSelection: xterm.hasSelection(),
})
) {
return false;
}

// Terminal-reserved chords (ctrl+c/d/z/s/q) always go to xterm
if (isTerminalReservedEvent(event)) return true;

Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading