diff --git a/packages/core/src/renderables/EmbeddedTerminal.test.ts b/packages/core/src/renderables/EmbeddedTerminal.test.ts index de5f9741f..6075a6d3b 100644 --- a/packages/core/src/renderables/EmbeddedTerminal.test.ts +++ b/packages/core/src/renderables/EmbeddedTerminal.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { createTestRenderer, type TestRendererSetup } from "../testing/test-renderer.js" import { KeyEvent } from "../lib/KeyHandler.js" +import { parseKeypress } from "../lib/parse.keypress.js" import { RGBA } from "../lib/RGBA.js" import { resolveRenderLib } from "../zig.js" import { EmbeddedTerminalRenderable } from "./EmbeddedTerminal.js" @@ -276,6 +277,45 @@ describe("EmbeddedTerminalRenderable", () => { ).toBe("\x1b[27u") }) + test.each([ + ["plain Dvorak u", 1, "\x1b[117::102;1u", "u"], + ["plain Dvorak d", 1, "\x1b[100::104;1u", "d"], + ["Dvorak Ctrl+U", 1, "\x1b[117::102;5u", "\x1b[117;5u"], + ["Dvorak Ctrl+D", 1, "\x1b[100::104;5u", "\x1b[100;5u"], + ["Dvorak Ctrl+Shift+U", 1, "\x1b[117:85:102;6u", "\x1b[117;6u"], + ["Cyrillic Ctrl+ф", 1, "\x1b[1092::97;5u", "\x1b[1092;5u"], + ["QWERTY Ctrl+U", 1, "\x1b[117;5u", "\x1b[117;5u"], + ["Dvorak Ctrl+U in legacy mode", 0, "\x1b[117::102;5u", "\x15"], + ["Dvorak Ctrl+D in legacy mode", 0, "\x1b[100::104;5u", "\x04"], + ["Dvorak Ctrl+U with its base-layout alternative", 5, "\x1b[117::102;5u", "\x1b[117::102;5u"], + ["Dvorak Ctrl+D with its base-layout alternative", 5, "\x1b[100::104;5u", "\x1b[100::104;5u"], + ])("preserves %s", (_label, flags, raw, expected) => { + const terminal = new EmbeddedTerminalRenderable(setup.renderer, { width: 20, height: 4 }) + setup.renderer.root.add(terminal) + terminal.write(`\x1b[>${flags}u`) + + const parsed = parseKeypress(raw, { useKittyKeyboard: true })! + expect(new TextDecoder().decode(terminal.encodeKey(new KeyEvent(parsed)))).toBe(expected) + }) + + test("forwards Dvorak press, repeat, and release with the same active-layout key", () => { + const output: string[] = [] + const terminal = new EmbeddedTerminalRenderable(setup.renderer, { + width: 20, + height: 4, + onData: (data) => output.push(new TextDecoder().decode(data)), + }) + setup.renderer.root.add(terminal) + terminal.write("\x1b[>3u") + terminal.focus() + + for (const raw of ["\x1b[117::102;5u", "\x1b[117::102;5:2u", "\x1b[117::102;5:3u"]) { + setup.renderer.keyInput.processParsedKey(parseKeypress(raw, { useKittyKeyboard: true })!) + } + + expect(output).toEqual(["\x1b[117;5u", "\x1b[117;5:2u", "\x1b[117;5:3u"]) + }) + test("drains the preserved response prefix after overflow", () => { const lib = resolveRenderLib() const handle = lib.createEmbeddedTerminal({ cols: 20, rows: 4 }) diff --git a/packages/core/src/renderables/EmbeddedTerminal.ts b/packages/core/src/renderables/EmbeddedTerminal.ts index 28056a1d5..a2941856b 100644 --- a/packages/core/src/renderables/EmbeddedTerminal.ts +++ b/packages/core/src/renderables/EmbeddedTerminal.ts @@ -148,7 +148,7 @@ export class EmbeddedTerminalRenderable extends Renderable { key: physical, mods: modifiers(key), text, - unshiftedCodepoint: key.baseCode ?? physicalUnshiftedCodepoint(physical), + unshiftedCodepoint: unshiftedCodepoint(key, physical), }) } @@ -382,8 +382,9 @@ function modifiers(input: { function physicalKey(key: KeyEvent) { if (key.code && !key.code.startsWith("[")) return key.code - if (/^[a-z]$/i.test(key.name)) return `Key${key.name.toUpperCase()}` - if (/^[0-9]$/.test(key.name)) return `Digit${key.name}` + const name = key.baseCode === undefined ? key.name : String.fromCodePoint(key.baseCode) + if (/^[a-z]$/i.test(name)) return `Key${name.toUpperCase()}` + if (/^[0-9]$/.test(name)) return `Digit${name}` return ( { backspace: "Backspace", @@ -413,7 +414,10 @@ function textualKey(key: KeyEvent) { if ([...key.name].length === 1 || /[^\x00-\x7f]/.test(key.name)) return key.name } -function physicalUnshiftedCodepoint(code: string | undefined) { +function unshiftedCodepoint(key: KeyEvent, code: string | undefined) { + // Kitty's baseCode is a physical-layout alternative, not the active layout's character. + if (key.name === "space") return 32 + if ([...key.name].length === 1) return key.name.codePointAt(0)! if (code?.startsWith("Key") && code.length === 4) return code.charCodeAt(3) + 32 if (code?.startsWith("Digit") && code.length === 6) return code.charCodeAt(5) return 0