mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
fix(gui-extensions): stop Command chords from typing letters in the terminal (#57650)
This commit is contained in:
committed by
GitHub
Unverified
parent
28dce534b7
commit
899ccee996
@@ -8,15 +8,24 @@ const workspace = { name: "TerminalInput", pty: {} } satisfies WorkspaceInput
|
||||
|
||||
test.use({ viewport: { width: 1440, height: 900 } })
|
||||
|
||||
test("clears the terminal line with Command+Delete", async ({ page }) => {
|
||||
test("clears the terminal line with Command+Delete and suppresses printable Command chords", async ({ page }) => {
|
||||
const { pty } = await openSession(page, workspace)
|
||||
const terminal = page.locator('[data-component="terminal"]')
|
||||
await page.keyboard.press("Control+Backquote")
|
||||
await expect(terminal.locator("textarea")).toHaveCount(1)
|
||||
await expect.poll(() => pty.sockets.length).toBe(1)
|
||||
|
||||
await page.keyboard.press("Meta+k")
|
||||
await page.keyboard.press("Meta+j")
|
||||
await terminal.locator("textarea").evaluate((textarea) => {
|
||||
const clipboardData = new DataTransfer()
|
||||
clipboardData.setData("text/plain", "echo pasted")
|
||||
textarea.dispatchEvent(new ClipboardEvent("paste", { clipboardData, bubbles: true, cancelable: true }))
|
||||
})
|
||||
await expect.poll(() => pty.sockets[0]!.input.join("")).toBe("echo pasted")
|
||||
|
||||
await page.keyboard.press("Meta+Backspace")
|
||||
await expect.poll(() => pty.sockets[0]!.input.join("")).toBe("\x15")
|
||||
await expect.poll(() => pty.sockets[0]!.input.join("")).toBe("echo pasted\x15")
|
||||
})
|
||||
|
||||
test("reveals the terminal after its first output and hides the native caret", async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { consumeTerminalMetaKey, terminalKeyInput } from "./terminal-key-event"
|
||||
|
||||
function keyEvent(init: KeyboardEventInit) {
|
||||
return new KeyboardEvent("keydown", { cancelable: true, ...init })
|
||||
}
|
||||
|
||||
describe("terminalKeyEvent", () => {
|
||||
test("maps unmodified Command+Backspace to line kill", () => {
|
||||
expect(terminalKeyInput(keyEvent({ key: "Backspace", code: "Backspace", metaKey: true }))).toBe("\x15")
|
||||
expect(terminalKeyInput(keyEvent({ key: "Backspace", code: "Backspace", metaKey: true, shiftKey: true }))).toBeUndefined()
|
||||
})
|
||||
|
||||
test("consumes printable Command chords but leaves physical paste and copy to ghostty-web", () => {
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "k", code: "KeyK", metaKey: true }))).toBe(true)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "a", code: "KeyA", metaKey: true }))).toBe(true)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "v", code: "KeyV", metaKey: true }))).toBe(false)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "c", code: "KeyC", metaKey: true }))).toBe(false)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "ArrowLeft", code: "ArrowLeft", metaKey: true }))).toBe(false)
|
||||
})
|
||||
|
||||
test("keeps paste and copy working on RTL and non-QWERTY layouts", () => {
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "ر", code: "KeyV", metaKey: true }))).toBe(false)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "ؤ", code: "KeyC", metaKey: true }))).toBe(false)
|
||||
|
||||
let pasted = 0
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "v", code: "Period", metaKey: true }), { paste: () => void (pasted += 1) })).toBe(true)
|
||||
expect(pasted).toBe(1)
|
||||
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "c", code: "KeyI", metaKey: true }), { copy: () => true })).toBe(true)
|
||||
})
|
||||
|
||||
test("never consumes Ctrl chords so they reach the shell", () => {
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "c", code: "KeyC", ctrlKey: true }))).toBe(false)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "a", code: "KeyA", ctrlKey: true }))).toBe(false)
|
||||
expect(consumeTerminalMetaKey(keyEvent({ key: "k", code: "KeyK", ctrlKey: true, metaKey: true }))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -5,3 +5,30 @@ export function terminalKeyInput(event: KeyboardEvent) {
|
||||
|
||||
return "\x15"
|
||||
}
|
||||
|
||||
// ghostty-web sends the raw character to the PTY for Command chords, so consume them here.
|
||||
// Returning true makes ghostty-web preventDefault, so physical KeyV/KeyC are left alone
|
||||
// (whatever character the layout gives them, e.g. RTL) so native paste and copy still run.
|
||||
export function consumeTerminalMetaKey(
|
||||
event: KeyboardEvent,
|
||||
options?: {
|
||||
copy?: () => boolean
|
||||
paste?: () => void
|
||||
},
|
||||
) {
|
||||
if (!event.metaKey || event.ctrlKey || event.altKey) return false
|
||||
|
||||
if (event.code === "KeyV" || (event.code === "KeyC" && !event.shiftKey)) return false
|
||||
|
||||
const key = event.key.toLowerCase()
|
||||
|
||||
if (key === "v") {
|
||||
options?.paste?.()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (key === "c" && !event.shiftKey && options?.copy?.()) return true
|
||||
|
||||
return event.key.length === 1
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { TerminalModel } from "./model"
|
||||
import type { LocalPTY } from "./state"
|
||||
import { SerializeAddon } from "./serialize"
|
||||
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "./runtime-adapters"
|
||||
import { terminalKeyInput } from "./terminal-key-event"
|
||||
import { consumeTerminalMetaKey, terminalKeyInput } from "./terminal-key-event"
|
||||
import { terminalWriter } from "./writer"
|
||||
|
||||
// Serialization on unmount is a synchronous O(rows x cols) walk on the main thread and the
|
||||
@@ -481,7 +481,12 @@ export const Terminal = (props: TerminalProps) => {
|
||||
}
|
||||
|
||||
// allow for toggle terminal keybinds in parent
|
||||
return keybinds.matches("terminal.toggle", event)
|
||||
if (keybinds.matches("terminal.toggle", event)) return true
|
||||
|
||||
return consumeTerminalMetaKey(event, {
|
||||
paste: () => void document.execCommand("paste"),
|
||||
copy: () => t.hasSelection() && (document.execCommand("copy") || t.copySelection()),
|
||||
})
|
||||
})
|
||||
|
||||
const fit = new mod.FitAddon()
|
||||
|
||||
Reference in New Issue
Block a user