Files
openchamber/packages/ui/src/lib/ghostty/keyCodes.test.ts
T
Bohdan Triapitsyn 39fa8c1917 feat(terminal): replace ghostty-web with an in-repo libghostty-vt adapter
The terminal ran on the ghostty-web npm package plus a hand-written patch,
and every rendering bug (recycled rows, duplicated reflow fragments, prompt
artifacts) had to be worked around from outside. The emulator now is the
official libghostty-vt C ABI compiled to WebAssembly, driven by a browser
adapter ported from T3 Code (MIT, notice in LICENSE-T3CODE) and owned in
packages/ui/src/lib/ghostty. The artifact is reproducible with
scripts/build-libghostty-wasm.sh, including a workaround for Zig 0.15.2 on
macOS 27 SDKs.

On top of the port: one WASM instance per page with every tab kept mounted
and hidden tabs paused; history replayed at the PTY size it was drawn for;
shells spawned only after the first fitted grid so zsh never prints the
PROMPT_SP marker; box drawing, block elements and Powerline arrows drawn
procedurally to the exact cell so TUI borders and block logos have no gaps
between rows; a software-rasterized canvas so Gecko renders every tab's text
with the same smoothing; the symbols-only Nerd Font bundled instead of a CDN
fetch; touch selection and scrolling driven through the surface API; a copy
button in the tab strip for touch hosts; localized aria labels.

Testing: bun tests run the real WASM (reflow, palette, replay isolation,
recycled rows, box glyph geometry); viewport and view tests use a surface
double; verified in Chromium and Zen (windowed and headless) for crisp text,
new tabs, panel reopen, resize and box glyph rendering; package type-check,
oxlint/eslint on new files, web build.
2026-09-07 12:18:22 +03:00

68 lines
2.8 KiB
TypeScript

// Adapted from T3 Code's libghostty-vt browser adapter tests (MIT, T3 Tools Inc.).
// See LICENSE-T3CODE in this directory.
import { describe, expect, test } from 'bun:test';
import { ghosttyConsumedMods, ghosttyKeyForCode, ghosttyUnshiftedCodepoint } from './keyCodes';
describe('ghosttyKeyForCode', () => {
test('keeps the tail of the pinned Ghostty key enum in order', () => {
expect(ghosttyKeyForCode('F25')).toBe(ghosttyKeyForCode('F24') + 1);
expect(ghosttyKeyForCode('PrintScreen')).toBe(ghosttyKeyForCode('FnLock') + 1);
expect(ghosttyKeyForCode('Pause')).toBe(ghosttyKeyForCode('ScrollLock') + 1);
expect(ghosttyKeyForCode('Paste')).toBe(ghosttyKeyForCode('Cut') + 1);
});
});
describe('ghosttyConsumedMods', () => {
const shifted = { altKey: false, ctrlKey: false, key: '@', metaKey: false, shiftKey: true };
test('only consumes a lone Shift producing a character', () => {
expect(ghosttyConsumedMods(shifted)).toBe(1);
expect(ghosttyConsumedMods({ ...shifted, ctrlKey: true })).toBe(0);
expect(ghosttyConsumedMods({ ...shifted, key: 'Tab' })).toBe(0);
// Deliberate: Shift+Space collapses to Space so it still types one.
expect(ghosttyConsumedMods({ ...shifted, key: ' ' })).toBe(1);
});
});
describe('ghosttyUnshiftedCodepoint', () => {
test('provides the logical base character for Kitty keyboard encoding', () => {
expect(ghosttyUnshiftedCodepoint({ code: 'KeyC', key: 'c', shiftKey: false })).toBe(
'c'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'KeyC', key: 'C', shiftKey: true })).toBe(
'c'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'Digit1', key: '!', shiftKey: true })).toBe(
'1'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'Slash', key: '?', shiftKey: true })).toBe(
'/'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'Digit1', key: '&', shiftKey: false })).toBe(
'&'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'Enter', key: 'Enter', shiftKey: false })).toBe(0);
});
test('reports unknown instead of the shifted character without layout data', () => {
expect(ghosttyUnshiftedCodepoint({ code: 'Digit7', key: '/', shiftKey: true })).toBe(0);
expect(ghosttyUnshiftedCodepoint({ code: 'KeyD', key: 'Д', shiftKey: true })).toBe(
'д'.codePointAt(0),
);
});
test('prefers the active browser layout over US physical key positions', () => {
const layoutMap = new Map([
['Digit1', '&'],
['KeyC', 'j'],
]);
expect(ghosttyUnshiftedCodepoint({ code: 'Digit1', key: '1', shiftKey: true }, layoutMap)).toBe(
'&'.codePointAt(0),
);
expect(ghosttyUnshiftedCodepoint({ code: 'KeyC', key: 'J', shiftKey: true }, layoutMap)).toBe(
'j'.codePointAt(0),
);
});
});