Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/web
Steps to reproduce
Use a checkout of the commit under Version or commit with its lockfile dependencies installed. Save the script below outside the checkout as repro.sh and run sh /path/to/repro.sh from the checkout root (macOS or Linux shell). It creates a private HOME, TMPDIR and T3CODE_HOME under a mktemp directory, writes one temporary test at apps/web/src/terminal/ghostty/tmpLayoutCopyRepro.test.ts (refusing to overwrite an existing file) that reuses the existing DOM harness from surface.test.ts, runs it from apps/web with env -u NODE_ENV vp test run, and removes the test and fixture on exit.
The test creates the production GhosttyTerminalSurface with its real keydown listener and the pinned Ghostty WASM core, then for each platform value Linux x86_64 and Win32:
- Write
SELECTED_OUTPUTinto the terminal and select all of it. - Latin control: dispatch keydown
key: "C", code: "KeyC", ctrlKey: true, shiftKey: trueto the terminal input. Record copy-event writes andonDataterminal input. - Failing input: on a fresh surface, dispatch keydown
key: "С", code: "KeyC", ctrlKey: true, shiftKey: true, whereСis the uppercase Cyrillic letter Es (U+0421) that a Russian layout reports on the physical C key with Shift held. - After the failing chord, dispatch a
copyevent directly (as menu Copy does), then press Ctrl+Insert, and record both results.
Only the DOM and canvas environment, document.execCommand("copy") delivery and the clipboardData sink are test doubles; the copy predicate and key encoder are the production code. The script exits 0 only when the Latin control copies, the Cyrillic chord sends ESC[1057;5u without copying, and both workaround paths copy.
To reproduce interactively (not done for this report): on Linux or Windows, switch to a Russian keyboard layout, select output in the integrated terminal and press Ctrl+Shift+C. The clipboard keeps its old content and the shell receives the encoded key.
Reproduction script: repro.sh
#!/bin/sh
# Run from the root of an installed-dependency t3code checkout at the reported commit.
set -eu
repo=$(cd "${1:-.}" && pwd)
fixture=$(mktemp -d "${TMPDIR:-/tmp}/terminal-layout-repro.XXXXXX")
export HOME="$fixture/fake-home" TMPDIR="$fixture/tmp" T3CODE_HOME="$fixture/t3home"
mkdir -p "$HOME" "$TMPDIR" "$T3CODE_HOME"
test="$repo/apps/web/src/terminal/ghostty/tmpLayoutCopyRepro.test.ts"
if [ -e "$test" ]; then echo "Refusing to replace existing $test"; exit 1; fi
trap 'rm -f "$test"; rm -rf "$fixture"' EXIT INT TERM
# Reuse the existing surface DOM harness (everything before its first ordinary test) and append one test.
python3 - "$repo" "$test" <<'PYTEST'
from pathlib import Path
import sys
repo, target = map(Path, sys.argv[1:])
source = (repo / "apps/web/src/terminal/ghostty/surface.test.ts").read_text()
prefix = source.split(' it("preserves shell Ctrl+A and ordinary application navigation"')[0]
test = r''' it("repro: Ctrl+Shift+C with a Latin and a Cyrillic key on physical KeyC", async () => {
for (const platform of ["Linux x86_64", "Win32"]) {
for (const letter of ["C", "С"]) {
vi.stubGlobal("navigator", { platform });
const harness = createHarness();
const surface = await harness.create({ beforeKey: () => true });
Object.assign(surface.input, { select() {} });
surface.write("SELECTED_OUTPUT");
surface.selectAll();
const copies: string[] = [];
const dispatchCopy = () =>
surface.input.dispatchEvent(Object.assign(new Event("copy", { cancelable: true }), {
clipboardData: { setData: (_type: string, value: string) => copies.push(value) },
}));
Object.assign(document, { execCommand: vi.fn(() => { dispatchCopy(); return true; }) });
const event = key(surface, letter, "KeyC", { ctrlKey: true, shiftKey: true });
const sent = harness.onData.mock.calls.map(([data]) => data);
console.log(JSON.stringify({ platform, key: letter, copies, sent, prevented: event.defaultPrevented, selection: surface.getSelection() }));
if (letter === "C") {
expect(copies).toEqual(["SELECTED_OUTPUT"]);
expect(sent).toEqual([]);
} else {
expect(copies).toEqual([]);
expect(sent).toEqual(["\x1b[1057;5u"]);
expect(event.defaultPrevented).toBe(true);
expect(surface.getSelection()).toBe("SELECTED_OUTPUT");
// Menu/native Copy dispatches a copy event directly, bypassing the key predicate.
dispatchCopy();
expect(copies).toEqual(["SELECTED_OUTPUT"]);
console.log(`MENU_COPY_RECOVERY ${platform}: ${copies[0]}`);
copies.length = 0;
key(surface, "Insert", "Insert", { ctrlKey: true });
expect(copies).toEqual(["SELECTED_OUTPUT"]);
expect(harness.onData.mock.calls.map(([data]) => data)).toEqual(sent);
console.log(`CTRL_INSERT_RECOVERY ${platform}: ${copies[0]}`);
}
surface.dispose();
surfaces.delete(surface);
vi.restoreAllMocks();
}
}
console.log("REPRO_OK: Latin Ctrl+Shift+C copies; Cyrillic Ctrl+Shift+C sends CSI-u terminal input; copy-event and Ctrl+Insert recovery work");
});
});
'''
target.write_text(prefix + test)
PYTEST
cd "$repo/apps/web"
env -u NODE_ENV "$repo/node_modules/.bin/vp" test run src/terminal/ghostty/tmpLayoutCopyRepro.test.ts --silent=falseExpected behavior
Ctrl+Shift+C copies the terminal selection and sends no terminal input, whatever the active keyboard layout. The terminal's copy branch implements Ctrl+Shift+C as a copy chord on Linux and Windows, and the Latin control copies SELECTED_OUTPUT with no terminal input. The project's non-Latin shortcut convention keeps the layout's Latin letter when there is one and otherwise falls back to the physical letter key: see resolveEventKeys in the shared matcher, its tests, #4469, and the repairs that applied it to other hardcoded shortcuts, #13409 (5d707bb, Mod+B) and #21508 (e836ab1, right panel launcher letters). The terminal's existing test that "uses the produced character instead of the physical key position" covers only Latin C and j (Dvorak-style Latin layouts), and that Latin-precedence behavior should stay.
Actual behavior
Summary. On Linux and Windows with a keyboard layout that produces a non-Latin letter on the physical C key under Ctrl+Shift (for example a standard Russian layout, which reports Cyrillic С), pressing Ctrl+Shift+C in the integrated terminal with output selected does not copy the selection. The terminal's copy predicate isTerminalCopyShortcut compares only event.key with Latin c, so the chord falls through to the terminal key encoder, which prevents the default action and sends ESC[1057;5u to the shell as terminal input. The same chord with a Latin key copies and sends nothing. The app's shared shortcut matcher already falls back to the physical letter key when the layout produces a non-Latin character, so this hardcoded terminal predicate misses that convention. macOS Cmd+C is not claimed to be affected. Ctrl+Insert and menu Copy still copy and serve as workarounds.
With the Cyrillic chord, the production listener performs no copy write, prevents the default action, keeps the selection, and calls onData with ESC[1057;5u. The copy predicate lowercases event.key to с, which is not c, so the event reaches the encoder and onData. Because the listener prevents the default, the browser's own copy does not run either. The terminal drawer creates this same surface and its data callback forwards that input to the terminal session. The Latin control copies SELECTED_OUTPUT and sends nothing. Results were the same for the Linux x86_64 and Win32 platform values. How a particular shell or program reacts to the CSI-u bytes was not observed.
Reachability: Chromium's Linux XKB key resolution and Windows layout-map lookup report the active layout's printable character for Ctrl chords, dropping Ctrl for the lookup and keeping Shift, which gives key: "С" with code: "KeyC" on a Russian layout. This is source reading; physical keyboard entry on Linux or Windows was not observed, and other browsers and every non-Latin layout are not asserted. macOS is not claimed: the author of #33698 tested real Russian, Greek and Hebrew layouts and reports that macOS produces Latin letters while Command is held.
Desktop app: Electron's native Copy menu role uses CommandOrControl+C (Electron v44.10.1 roles), so plain Ctrl+C may still copy through native editing on desktop, but it does not provide Ctrl+Shift+C. The terminal handles menu Copy as a copy event independently of keydown, and that path copied after the failed chord in the reproduction. Packaged Electron, native accelerator interception and OS clipboard contents were not exercised.
By source reading only, the neighboring paste, select-all and clear predicates also compare raw event.key letters; native paste events and shell Ctrl+L input may cover some of those cases, and none of them was exercised, so this report does not claim them as verified failures.
Related work, not duplicates: #26836 (open) makes terminal copy and paste shortcuts configurable but keeps this raw-key default predicate; #34109 (open) adds optional copy-on-select, off by default; #24034 (open) handles macOS Option matching for shared shortcuts. #7361 and #8541 introduced the Linux Ctrl+Shift+C and Ctrl+Insert copy behavior. #2189, #13427 and #33385 cover other shortcut entry points, #16440 covered browser-preview editing shortcuts, and #8361 concerns broader Windows selection copy and OSC 52 symptoms rather than this layout-conditioned chord.
Evidence
- Expected source: the Ctrl+Shift+C copy branch at
apps/web/src/terminal/ghostty/surface.tslines 1158-1170 with the passing Latin control in the reproduction, and the non-Latin physical-key fallback inpackages/shared/src/keybindingMatching.tslines 71-80 with its tests inapps/web/src/keybindings.test.tslines 760-792 and the fix(web): Mod+B bolds on non-Latin layouts #13409 and fix(web): right panel launcher letters work on non-Latin keyboard layouts #21508 repairs linked under Expected behavior. - Failure source: the observed reproduction output under Logs or stack traces, and the
event.key-only comparison inisTerminalCopyShortcutatapps/web/src/terminal/ghostty/surface.tslines 315-325. - Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
The reproduction drives the production terminal listener and encoder with synthetic keyboard events; it does not switch a physical OS layout or write the OS clipboard. Reachability of the Cyrillic event shape on Linux and Windows rests on the Chromium source cited above.
The existing focused suites src/terminal/ghostty/surface.test.ts and src/keybindings.test.ts also pass (92 tests in two files).
Restoration check
With the script's input, the Cyrillic Ctrl+Shift+C chord on physical KeyC copies SELECTED_OUTPUT and sends no terminal input on both platform values, matching the Latin control. As controls, Latin Ctrl+Shift+C still copies, a Latin-layout letter still wins over the physical key (the existing Latin C/j terminal test keeps passing), Ctrl+Insert and menu Copy still copy, and plain non-Latin keys without the copy chord still reach the shell. The script asserts the current failing result, so its Cyrillic expectations need updating to the copying result for this check.
Impact
Minor bug or occasional failure
Version or commit
0.1.9 at d9805b2, rerun from a clean checkout; surface.ts, keybindingMatching.ts and ThreadTerminalDrawer.tsx are byte-identical on main at fc6c7c8 by blob comparison only
Environment
Darwin arm64 host, Node v26.12.1, repository-pinned Vitest 5.0.5, Electron pin 44.10.1; production terminal listener and Ghostty WASM core driven by synthetic events with Linux x86_64 and Win32 platform values; no physical Linux or Windows keyboard, packaged desktop app, OS clipboard or shell
Logs or stack traces
Reproduction output, keeping the result lines and test summary:
{"platform":"Linux x86_64","key":"C","copies":["SELECTED_OUTPUT"],"sent":[],"prevented":true,"selection":"SELECTED_OUTPUT"}
{"platform":"Linux x86_64","key":"С","copies":[],"sent":["\u001b[1057;5u"],"prevented":true,"selection":"SELECTED_OUTPUT"}
MENU_COPY_RECOVERY Linux x86_64: SELECTED_OUTPUT
CTRL_INSERT_RECOVERY Linux x86_64: SELECTED_OUTPUT
{"platform":"Win32","key":"C","copies":["SELECTED_OUTPUT"],"sent":[],"prevented":true,"selection":"SELECTED_OUTPUT"}
{"platform":"Win32","key":"С","copies":[],"sent":["\u001b[1057;5u"],"prevented":true,"selection":"SELECTED_OUTPUT"}
MENU_COPY_RECOVERY Win32: SELECTED_OUTPUT
CTRL_INSERT_RECOVERY Win32: SELECTED_OUTPUT
REPRO_OK: Latin Ctrl+Shift+C copies; Cyrillic Ctrl+Shift+C sends CSI-u terminal input; copy-event and Ctrl+Insert recovery work
Test Files 1 passed (1)
Tests 1 passed (1)
Screenshots, recordings, or supporting files
No response
Workaround
Press Ctrl+Insert, or use the Copy menu item, to copy the terminal selection while a non-Latin layout is active. Both take layout-independent copy paths and copied the selection after the failed chord in the reproduction; OS clipboard contents and packaged native menu behavior were not exercised. Switching to a Latin layout before Ctrl+Shift+C also avoids the failure.
Activity