import { describe, expect, it, vi } from "vite-plus/test"; import type { GhosttyCell, GhosttyRow } from "./core";import { DEFAULT_TERMINAL_FONT_FAMILY, DEFAULT_TERMINAL_FONT_SIZE, advanceTerminalSelectionClickSequence, ghosttyMouseButton, isTerminalAltGraphText, isTerminalCompositionCommitInput, isTerminalCopyShortcut, isTerminalLinkPointerGesture, isTerminalPasteShortcut, loadTerminalFontFamily, shouldBlinkTerminalCursor, shouldReportTerminalMouse, shouldShowTerminalLinkHover, terminalGridCellAt, terminalScrollbarGeometry, terminalScrollbarOffsetAtPointer, terminalLinkAtColumn, terminalLinkAtPosition, terminalLinkAtPositionWithRange, terminalContentOriginY, terminalFontFamily, terminalFontSize, terminalWheelArrowData, terminalWheelDeltaRows,} from "./surface"; const cell = (text: string): GhosttyCell => ({ text, wide: 0, foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, bold: false, italic: false, invisible: false, strikethrough: false, overline: false, underline: false, selected: false,}); describe("isTerminalAltGraphText", () => { it("defers printable AltGr output to the textarea input event", () => { expect( isTerminalAltGraphText({ key: "@", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(true); expect( isTerminalAltGraphText({ key: "ArrowRight", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(false); });}); describe("terminalGridCellAt", () => { const options = { bounds: { left: 100, top: 200 }, cols: 3, rows: 2, metrics: { width: 10, height: 20 }, padding: 4, originY: 24, }; it("maps points inside the rendered grid without clamping its padding", () => { expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 224 })).toEqual({ x: 0, y: 0, }); expect(terminalGridCellAt({ ...options, clientX: 133, clientY: 263 })).toEqual({ x: 2, y: 1, }); expect(terminalGridCellAt({ ...options, clientX: 103, clientY: 224 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 223 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 134, clientY: 224 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 264 })).toBeNull(); });}); describe("shouldBlinkTerminalCursor", () => { const blinking = { focused: true, cursorBlinking: true, cursorVisible: true, reducedMotion: false, }; it("blinks a focused visible cursor the terminal asked to blink", () => { expect(shouldBlinkTerminalCursor(blinking)).toBe(true); }); it("holds the cursor steady when blinking would be unwanted", () => { // Unfocused surfaces draw a steady hollow cursor, DECSCUSR steady styles and // DEC mode 12 turn blinking off, a hidden cursor has nothing to toggle, and // reduced-motion readers get no permanently animating element. expect(shouldBlinkTerminalCursor({ ...blinking, focused: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, cursorBlinking: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, cursorVisible: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, reducedMotion: true })).toBe(false); });}); describe("terminalLinkAtColumn", () => { it("maps terminal cells to UTF-16 offsets after a wide emoji", () => { const cells = [ cell("π"), cell(""), ...Array.from("https://t3.codes", (character) => cell(character)), ]; const row: GhosttyRow = { cells, text: cells .map((value) => value.text || " ") .join("") .trimEnd(), isWrapContinuation: false, wrapsToNext: false, }; expect(terminalLinkAtColumn(row, 2)).toBe("https://t3.codes"); expect(terminalLinkAtColumn(row, cells.length - 1)).toBe("https://t3.codes"); expect(terminalLinkAtColumn(row, 0)).toBeNull(); expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({ start: { x: 2, y: 0 }, end: { x: cells.length - 1, y: 0 }, }); }); it("uses shared path matching and reconstructs soft-wrapped links", () => { const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ cells: Array.from(text.padEnd(16), (character) => cell(character)), text: text.trimEnd(), isWrapContinuation, wrapsToNext, }); const rows = [ row("https://example.", false), row("com/reference", true), row("~/project/file", false), row("C:\\repo\\file.ts", false), ]; expect(terminalLinkAtPosition(rows, 0, 8)).toBe("https://example.com/reference"); expect(terminalLinkAtPosition(rows, 1, 4)).toBe("https://example.com/reference"); expect(terminalLinkAtPosition(rows, 2, 2)).toBe("~/project/file"); expect(terminalLinkAtPosition(rows, 3, 4)).toBe("C:\\repo\\file.ts"); expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({ text: "https://example.com/reference", range: { start: { x: 0, y: 0 }, end: { x: 12, y: 1 }, }, }); }); it("refuses links truncated at the viewport edges instead of mis-resolving", () => { const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ cells: Array.from(text.padEnd(16), (character) => cell(character)), text: text.trimEnd(), isWrapContinuation, wrapsToNext, }); // The head of the wrapped line scrolled above the viewport. const headCut = [row("ple.com/missing", true), row("head", true)]; expect(terminalLinkAtPosition(headCut, 0, 4)).toBeNull(); // The bottom row soft-wraps on below the viewport. const tailCut = [row("https://t3.codes", false, true)]; expect(terminalLinkAtPosition(tailCut, 0, 8)).toBeNull(); // A partial bottom row is provably complete and still resolves. const complete = [row("https://t3.codes", false), row("", false)]; expect(terminalLinkAtPosition(complete, 0, 8)).toBe("https://t3.codes"); // A wide grapheme earlier in the row must not break truncation detection: // the soft-wrap flag decides, not string-length-versus-cell-count. const wideFull: GhosttyRow = { cells: [ { ...cell("π"), wide: 1 }, { ...cell(""), wide: 2 }, ...Array.from("https://t3.code", (character) => cell(character)), ], text: "π https://t3.code", isWrapContinuation: false, wrapsToNext: true, }; expect(terminalLinkAtPosition([wideFull], 0, 8)).toBeNull(); // Unwritten trailing cells prove the bottom row is complete. const unwrittenTail: GhosttyRow = { cells: [ ...Array.from("https://t3.codes", (character) => cell(character)), cell(""), cell(""), ], text: "https://t3.codes", isWrapContinuation: false, wrapsToNext: false, }; expect(terminalLinkAtPosition([unwrittenTail], 0, 8)).toBe("https://t3.codes"); });}); describe("isTerminalCopyShortcut", () => { const event = (overrides: Partial<Parameters<typeof isTerminalCopyShortcut>[0]> = {}) => ({ ctrlKey: false, key: "c", metaKey: false, shiftKey: false, ...overrides, }); it("keeps Ctrl+C available for SIGINT on macOS", () => { expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); expect(isTerminalCopyShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); }); it("copies with Ctrl+C and Ctrl+Shift+C elsewhere", () => { expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(true); expect(isTerminalCopyShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( true, ); expect(isTerminalCopyShortcut(event({}), "Linux x86_64")).toBe(false); }); it("uses the produced character instead of the physical key position", () => { expect(isTerminalCopyShortcut(event({ key: "C", metaKey: true }), "MacIntel")).toBe(true); expect(isTerminalCopyShortcut(event({ key: "j", metaKey: true }), "MacIntel")).toBe(false); });}); describe("isTerminalPasteShortcut", () => { const event = (overrides: Partial<Parameters<typeof isTerminalPasteShortcut>[0]> = {}) => ({ ctrlKey: false, key: "v", metaKey: false, shiftKey: false, ...overrides, }); it("uses Cmd+V on macOS", () => { expect(isTerminalPasteShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); }); it("preserves Ctrl+V and uses Ctrl+Shift+V elsewhere", () => { expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false); expect(isTerminalPasteShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( true, ); }); it("supports the conventional Shift+Insert paste shortcut", () => { expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "Linux x86_64")).toBe( true, ); expect(isTerminalPasteShortcut(event({ key: "Insert" }), "Linux x86_64")).toBe(false); expect( isTerminalPasteShortcut( event({ key: "Insert", ctrlKey: true, shiftKey: true }), "Linux x86_64", ), ).toBe(false); expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "MacIntel")).toBe( false, ); });}); describe("isTerminalCompositionCommitInput", () => { it("identifies browser composition follow-up input", () => { expect(isTerminalCompositionCommitInput({ inputType: "" })).toBe(true); expect(isTerminalCompositionCommitInput({ inputType: "insertCompositionText" })).toBe(true); expect(isTerminalCompositionCommitInput({ inputType: "insertFromComposition" })).toBe(true); }); it("keeps a fast repeated input as legitimate text", () => { expect(isTerminalCompositionCommitInput({ inputType: "insertText" })).toBe(false); });}); describe("application mouse reporting", () => { const event = (overrides: Partial<Parameters<typeof shouldReportTerminalMouse>[1]> = {}) => ({ ctrlKey: false, metaKey: false, shiftKey: false, ...overrides, }); it("keeps Shift and link activation modifiers available to the browser", () => { expect(shouldReportTerminalMouse(true, event())).toBe(true); expect(shouldReportTerminalMouse(true, event({ shiftKey: true }))).toBe(false); expect(shouldReportTerminalMouse(true, event({ ctrlKey: true }))).toBe(false); expect(shouldReportTerminalMouse(true, event({ metaKey: true }))).toBe(false); expect(shouldReportTerminalMouse(false, event())).toBe(false); }); it("maps browser buttons to Ghostty's button enum", () => { expect([0, 1, 2, 3, 4, 5].map(ghosttyMouseButton)).toEqual([1, 3, 2, 4, 5, null]); }); it("only shows link hover during mouse tracking when the link modifier is held", () => { expect(shouldShowTerminalLinkHover(false, false)).toBe(true); expect(shouldShowTerminalLinkHover(false, true)).toBe(true); expect(shouldShowTerminalLinkHover(true, false)).toBe(false); expect(shouldShowTerminalLinkHover(true, true)).toBe(true); });}); describe("terminal font resolution", () => { it("validates the requested face after its styles load", async () => { let loaded = false; const load = vi.fn(async () => { loaded = true; return []; }); const resolve = vi.fn(() => { expect(loaded).toBe(true); return DEFAULT_TERMINAL_FONT_FAMILY; }); await expect( loadTerminalFontFamily("Proportional Test", 12, { load, resolve, }), ).resolves.toBe(DEFAULT_TERMINAL_FONT_FAMILY); expect(load).toHaveBeenCalledTimes(4); expect(resolve).toHaveBeenCalledWith("Proportional Test"); }); it("keeps the glyph fallbacks behind a custom text face", () => { expect(terminalFontFamily()).toBe(DEFAULT_TERMINAL_FONT_FAMILY); expect(terminalFontFamily(" ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY); const custom = terminalFontFamily('"Fira Code"'); expect(custom.startsWith('"Fira Code", ')).toBe(true); expect(custom).toContain('"Symbols Nerd Font Mono"'); expect(custom.endsWith("monospace")).toBe(true); }); it("ignores proportional families the cell grid cannot lay out", () => { // jsdom has no canvas metrics, so the probe answers "monospace" and the // family is kept; the guard is exercised in the browser instead. Assert the // shape stays intact so a rejected face still yields a usable stack. const stack = terminalFontFamily("Helvetica Neue"); expect(stack.endsWith("monospace")).toBe(true); }); it("quotes families the canvas font shorthand would otherwise reject", () => { expect(terminalFontFamily("3270 Nerd Font").startsWith('"3270 Nerd Font", ')).toBe(true); expect(terminalFontFamily("M+ 1m").startsWith('"M+ 1m", ')).toBe(true); expect(terminalFontFamily("Cascadia Code, Menlo").startsWith('"Cascadia Code", Menlo, ')).toBe( true, ); expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY); }); it("clamps requested font sizes to the supported range", () => { expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE); expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE); expect(terminalFontSize(13.4)).toBe(13); expect(terminalFontSize(2)).toBe(6); expect(terminalFontSize(90)).toBe(32); });}); describe("terminalContentOriginY", () => { it("stays top-anchored like a fresh terminal until scrollback exists", () => { expect(terminalContentOriginY(100, 4, 5, 16, false)).toBe(4); }); it("pins the grid to the bottom by moving the sub-row slack above row 0", () => { // 100px mount, 4px padding, 5 rows of 16px: 92 - 80 = 12px slack on top. expect(terminalContentOriginY(100, 4, 5, 16, true)).toBe(16); // Exact fit keeps the origin at the padding. expect(terminalContentOriginY(88, 4, 5, 16, true)).toBe(4); // A mount smaller than the grid never pushes the origin above the padding. expect(terminalContentOriginY(80, 4, 5, 16, true)).toBe(4); }); it("keeps the prompt stationary while a drag crosses row boundaries", () => { // Growing the mount pixel by pixel: the bottom edge (origin + rows*cell) // tracks the mount bottom exactly until a new row fits. for (let height = 88; height < 104; height += 1) { const rows = Math.max(1, Math.floor((height - 8) / 16)); const origin = terminalContentOriginY(height, 4, rows, 16, true); expect(origin + rows * 16).toBe(height - 4); } });}); describe("terminalWheelDeltaRows", () => { it("converts line-mode deltas into whole rows", () => { const result = terminalWheelDeltaRows({ deltaY: 3, deltaMode: 1 }, 16, 24, 0); expect(result.rows).toBe(3); expect(result.remainder).toBe(0); }); it("accumulates fractional pixel deltas across events", () => { let remainder = 0; let scrolled = 0; for (let index = 0; index < 4; index += 1) { const result = terminalWheelDeltaRows({ deltaY: 5, deltaMode: 0 }, 16, 24, remainder); remainder = result.remainder; scrolled += result.rows; } expect(scrolled).toBe(1); expect(remainder).toBeCloseTo(4 / 16); }); it("keeps direction for negative page-mode deltas", () => { const result = terminalWheelDeltaRows({ deltaY: -1, deltaMode: 2 }, 16, 24, 0); expect(result.rows).toBe(-24); expect(result.remainder).toBe(0); });}); describe("terminalWheelArrowData", () => { it("emits one arrow per row honoring application cursor keys", () => { expect(terminalWheelArrowData(-2, false)).toBe("\u001b[A\u001b[A"); expect(terminalWheelArrowData(3, false)).toBe("\u001b[B\u001b[B\u001b[B"); expect(terminalWheelArrowData(-1, true)).toBe("\u001bOA"); expect(terminalWheelArrowData(0, true)).toBe(""); });}); describe("isTerminalLinkPointerGesture", () => { it("uses Command on macOS and Control elsewhere", () => { expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "MacIntel")).toBe(true); expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "MacIntel")).toBe(false); expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "Linux x86_64")).toBe( true, ); expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "Linux x86_64")).toBe( false, ); });}); describe("advanceTerminalSelectionClickSequence", () => { it("recognizes stationary double and triple pointer presses without PointerEvent.detail", () => { const first = advanceTerminalSelectionClickSequence(null, { clientX: 20, clientY: 30, timeStamp: 1_000, }); const second = advanceTerminalSelectionClickSequence(first, { clientX: 22, clientY: 29, timeStamp: 1_200, }); const third = advanceTerminalSelectionClickSequence(second, { clientX: 21, clientY: 31, timeStamp: 1_400, }); expect([first.count, second.count, third.count]).toEqual([1, 2, 3]); }); it("starts over after movement, delay, or a completed triple click", () => { const previous = { count: 3, time: 1_000, x: 20, y: 30 }; expect( advanceTerminalSelectionClickSequence(previous, { clientX: 20, clientY: 30, timeStamp: 1_100, }).count, ).toBe(1); expect( advanceTerminalSelectionClickSequence(previous, { clientX: 30, clientY: 30, timeStamp: 1_100, }).count, ).toBe(1); expect( advanceTerminalSelectionClickSequence(previous, { clientX: 20, clientY: 30, timeStamp: 1_501, }).count, ).toBe(1); });}); describe("terminal scrollbar", () => { it("maps Ghostty's viewport state to a proportional thumb", () => { expect(terminalScrollbarGeometry({ total: 100, offset: 40, len: 20 }, 200)).toEqual({ thumbHeight: 40, thumbTop: 80, maxOffset: 80, }); expect(terminalScrollbarGeometry({ total: 100, offset: 0, len: 100 }, 200)).toBeNull(); }); Lines 1β500 of 510. Load more
import { describe, expect, it, vi } from "vite-plus/test"; import type { GhosttyCell, GhosttyRow } from "./core"; import { DEFAULT_TERMINAL_FONT_FAMILY, DEFAULT_TERMINAL_FONT_SIZE, advanceTerminalSelectionClickSequence, ghosttyMouseButton, isTerminalAltGraphText, isTerminalCompositionCommitInput, isTerminalCopyShortcut, isTerminalLinkPointerGesture, isTerminalPasteShortcut, loadTerminalFontFamily, shouldBlinkTerminalCursor, shouldReportTerminalMouse, shouldShowTerminalLinkHover, terminalGridCellAt, terminalScrollbarGeometry, terminalScrollbarOffsetAtPointer, terminalLinkAtColumn, terminalLinkAtPosition, terminalLinkAtPositionWithRange, terminalContentOriginY, terminalFontFamily, terminalFontSize, terminalWheelArrowData, terminalWheelDeltaRows, } from "./surface"; const cell = (text: string): GhosttyCell => ({ text, wide: 0, foreground: { r: 255, g: 255, b: 255 }, background: { r: 0, g: 0, b: 0 }, bold: false, italic: false, invisible: false, strikethrough: false, overline: false, underline: false, selected: false, }); describe("isTerminalAltGraphText", () => { it("defers printable AltGr output to the textarea input event", () => { expect( isTerminalAltGraphText({ key: "@", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(true); expect( isTerminalAltGraphText({ key: "ArrowRight", getModifierState: (modifier) => modifier === "AltGraph", }), ).toBe(false); }); }); describe("terminalGridCellAt", () => { const options = { bounds: { left: 100, top: 200 }, cols: 3, rows: 2, metrics: { width: 10, height: 20 }, padding: 4, originY: 24, }; it("maps points inside the rendered grid without clamping its padding", () => { expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 224 })).toEqual({ x: 0, y: 0, }); expect(terminalGridCellAt({ ...options, clientX: 133, clientY: 263 })).toEqual({ x: 2, y: 1, }); expect(terminalGridCellAt({ ...options, clientX: 103, clientY: 224 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 223 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 134, clientY: 224 })).toBeNull(); expect(terminalGridCellAt({ ...options, clientX: 104, clientY: 264 })).toBeNull(); }); }); describe("shouldBlinkTerminalCursor", () => { const blinking = { focused: true, cursorBlinking: true, cursorVisible: true, reducedMotion: false, }; it("blinks a focused visible cursor the terminal asked to blink", () => { expect(shouldBlinkTerminalCursor(blinking)).toBe(true); }); it("holds the cursor steady when blinking would be unwanted", () => { // Unfocused surfaces draw a steady hollow cursor, DECSCUSR steady styles and // DEC mode 12 turn blinking off, a hidden cursor has nothing to toggle, and // reduced-motion readers get no permanently animating element. expect(shouldBlinkTerminalCursor({ ...blinking, focused: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, cursorBlinking: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, cursorVisible: false })).toBe(false); expect(shouldBlinkTerminalCursor({ ...blinking, reducedMotion: true })).toBe(false); }); }); describe("terminalLinkAtColumn", () => { it("maps terminal cells to UTF-16 offsets after a wide emoji", () => { const cells = [ cell("π"), cell(""), ...Array.from("https://t3.codes", (character) => cell(character)), ]; const row: GhosttyRow = { cells, text: cells .map((value) => value.text || " ") .join("") .trimEnd(), isWrapContinuation: false, wrapsToNext: false, }; expect(terminalLinkAtColumn(row, 2)).toBe("https://t3.codes"); expect(terminalLinkAtColumn(row, cells.length - 1)).toBe("https://t3.codes"); expect(terminalLinkAtColumn(row, 0)).toBeNull(); expect(terminalLinkAtPositionWithRange([row], 0, 8)?.range).toEqual({ start: { x: 2, y: 0 }, end: { x: cells.length - 1, y: 0 }, }); }); it("uses shared path matching and reconstructs soft-wrapped links", () => { const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ cells: Array.from(text.padEnd(16), (character) => cell(character)), text: text.trimEnd(), isWrapContinuation, wrapsToNext, }); const rows = [ row("https://example.", false), row("com/reference", true), row("~/project/file", false), row("C:\\repo\\file.ts", false), ]; expect(terminalLinkAtPosition(rows, 0, 8)).toBe("https://example.com/reference"); expect(terminalLinkAtPosition(rows, 1, 4)).toBe("https://example.com/reference"); expect(terminalLinkAtPosition(rows, 2, 2)).toBe("~/project/file"); expect(terminalLinkAtPosition(rows, 3, 4)).toBe("C:\\repo\\file.ts"); expect(terminalLinkAtPositionWithRange(rows, 1, 4)).toEqual({ text: "https://example.com/reference", range: { start: { x: 0, y: 0 }, end: { x: 12, y: 1 }, }, }); }); it("refuses links truncated at the viewport edges instead of mis-resolving", () => { const row = (text: string, isWrapContinuation: boolean, wrapsToNext = false): GhosttyRow => ({ cells: Array.from(text.padEnd(16), (character) => cell(character)), text: text.trimEnd(), isWrapContinuation, wrapsToNext, }); // The head of the wrapped line scrolled above the viewport. const headCut = [row("ple.com/missing", true), row("head", true)]; expect(terminalLinkAtPosition(headCut, 0, 4)).toBeNull(); // The bottom row soft-wraps on below the viewport. const tailCut = [row("https://t3.codes", false, true)]; expect(terminalLinkAtPosition(tailCut, 0, 8)).toBeNull(); // A partial bottom row is provably complete and still resolves. const complete = [row("https://t3.codes", false), row("", false)]; expect(terminalLinkAtPosition(complete, 0, 8)).toBe("https://t3.codes"); // A wide grapheme earlier in the row must not break truncation detection: // the soft-wrap flag decides, not string-length-versus-cell-count. const wideFull: GhosttyRow = { cells: [ { ...cell("π"), wide: 1 }, { ...cell(""), wide: 2 }, ...Array.from("https://t3.code", (character) => cell(character)), ], text: "π https://t3.code", isWrapContinuation: false, wrapsToNext: true, }; expect(terminalLinkAtPosition([wideFull], 0, 8)).toBeNull(); // Unwritten trailing cells prove the bottom row is complete. const unwrittenTail: GhosttyRow = { cells: [ ...Array.from("https://t3.codes", (character) => cell(character)), cell(""), cell(""), ], text: "https://t3.codes", isWrapContinuation: false, wrapsToNext: false, }; expect(terminalLinkAtPosition([unwrittenTail], 0, 8)).toBe("https://t3.codes"); }); }); describe("isTerminalCopyShortcut", () => { const event = (overrides: Partial<Parameters<typeof isTerminalCopyShortcut>[0]> = {}) => ({ ctrlKey: false, key: "c", metaKey: false, shiftKey: false, ...overrides, }); it("keeps Ctrl+C available for SIGINT on macOS", () => { expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); expect(isTerminalCopyShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); }); it("copies with Ctrl+C and Ctrl+Shift+C elsewhere", () => { expect(isTerminalCopyShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(true); expect(isTerminalCopyShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( true, ); expect(isTerminalCopyShortcut(event({}), "Linux x86_64")).toBe(false); }); it("uses the produced character instead of the physical key position", () => { expect(isTerminalCopyShortcut(event({ key: "C", metaKey: true }), "MacIntel")).toBe(true); expect(isTerminalCopyShortcut(event({ key: "j", metaKey: true }), "MacIntel")).toBe(false); }); }); describe("isTerminalPasteShortcut", () => { const event = (overrides: Partial<Parameters<typeof isTerminalPasteShortcut>[0]> = {}) => ({ ctrlKey: false, key: "v", metaKey: false, shiftKey: false, ...overrides, }); it("uses Cmd+V on macOS", () => { expect(isTerminalPasteShortcut(event({ metaKey: true }), "MacIntel")).toBe(true); expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "MacIntel")).toBe(false); }); it("preserves Ctrl+V and uses Ctrl+Shift+V elsewhere", () => { expect(isTerminalPasteShortcut(event({ ctrlKey: true }), "Linux x86_64")).toBe(false); expect(isTerminalPasteShortcut(event({ ctrlKey: true, shiftKey: true }), "Linux x86_64")).toBe( true, ); }); it("supports the conventional Shift+Insert paste shortcut", () => { expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "Linux x86_64")).toBe( true, ); expect(isTerminalPasteShortcut(event({ key: "Insert" }), "Linux x86_64")).toBe(false); expect( isTerminalPasteShortcut( event({ key: "Insert", ctrlKey: true, shiftKey: true }), "Linux x86_64", ), ).toBe(false); expect(isTerminalPasteShortcut(event({ key: "Insert", shiftKey: true }), "MacIntel")).toBe( false, ); }); }); describe("isTerminalCompositionCommitInput", () => { it("identifies browser composition follow-up input", () => { expect(isTerminalCompositionCommitInput({ inputType: "" })).toBe(true); expect(isTerminalCompositionCommitInput({ inputType: "insertCompositionText" })).toBe(true); expect(isTerminalCompositionCommitInput({ inputType: "insertFromComposition" })).toBe(true); }); it("keeps a fast repeated input as legitimate text", () => { expect(isTerminalCompositionCommitInput({ inputType: "insertText" })).toBe(false); }); }); describe("application mouse reporting", () => { const event = (overrides: Partial<Parameters<typeof shouldReportTerminalMouse>[1]> = {}) => ({ ctrlKey: false, metaKey: false, shiftKey: false, ...overrides, }); it("keeps Shift and link activation modifiers available to the browser", () => { expect(shouldReportTerminalMouse(true, event())).toBe(true); expect(shouldReportTerminalMouse(true, event({ shiftKey: true }))).toBe(false); expect(shouldReportTerminalMouse(true, event({ ctrlKey: true }))).toBe(false); expect(shouldReportTerminalMouse(true, event({ metaKey: true }))).toBe(false); expect(shouldReportTerminalMouse(false, event())).toBe(false); }); it("maps browser buttons to Ghostty's button enum", () => { expect([0, 1, 2, 3, 4, 5].map(ghosttyMouseButton)).toEqual([1, 3, 2, 4, 5, null]); }); it("only shows link hover during mouse tracking when the link modifier is held", () => { expect(shouldShowTerminalLinkHover(false, false)).toBe(true); expect(shouldShowTerminalLinkHover(false, true)).toBe(true); expect(shouldShowTerminalLinkHover(true, false)).toBe(false); expect(shouldShowTerminalLinkHover(true, true)).toBe(true); }); }); describe("terminal font resolution", () => { it("validates the requested face after its styles load", async () => { let loaded = false; const load = vi.fn(async () => { loaded = true; return []; }); const resolve = vi.fn(() => { expect(loaded).toBe(true); return DEFAULT_TERMINAL_FONT_FAMILY; }); await expect( loadTerminalFontFamily("Proportional Test", 12, { load, resolve, }), ).resolves.toBe(DEFAULT_TERMINAL_FONT_FAMILY); expect(load).toHaveBeenCalledTimes(4); expect(resolve).toHaveBeenCalledWith("Proportional Test"); }); it("keeps the glyph fallbacks behind a custom text face", () => { expect(terminalFontFamily()).toBe(DEFAULT_TERMINAL_FONT_FAMILY); expect(terminalFontFamily(" ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY); const custom = terminalFontFamily('"Fira Code"'); expect(custom.startsWith('"Fira Code", ')).toBe(true); expect(custom).toContain('"Symbols Nerd Font Mono"'); expect(custom.endsWith("monospace")).toBe(true); }); it("ignores proportional families the cell grid cannot lay out", () => { // jsdom has no canvas metrics, so the probe answers "monospace" and the // family is kept; the guard is exercised in the browser instead. Assert the // shape stays intact so a rejected face still yields a usable stack. const stack = terminalFontFamily("Helvetica Neue"); expect(stack.endsWith("monospace")).toBe(true); }); it("quotes families the canvas font shorthand would otherwise reject", () => { expect(terminalFontFamily("3270 Nerd Font").startsWith('"3270 Nerd Font", ')).toBe(true); expect(terminalFontFamily("M+ 1m").startsWith('"M+ 1m", ')).toBe(true); expect(terminalFontFamily("Cascadia Code, Menlo").startsWith('"Cascadia Code", Menlo, ')).toBe( true, ); expect(terminalFontFamily(" , ")).toBe(DEFAULT_TERMINAL_FONT_FAMILY); }); it("clamps requested font sizes to the supported range", () => { expect(terminalFontSize()).toBe(DEFAULT_TERMINAL_FONT_SIZE); expect(terminalFontSize(Number.NaN)).toBe(DEFAULT_TERMINAL_FONT_SIZE); expect(terminalFontSize(13.4)).toBe(13); expect(terminalFontSize(2)).toBe(6); expect(terminalFontSize(90)).toBe(32); }); }); describe("terminalContentOriginY", () => { it("stays top-anchored like a fresh terminal until scrollback exists", () => { expect(terminalContentOriginY(100, 4, 5, 16, false)).toBe(4); }); it("pins the grid to the bottom by moving the sub-row slack above row 0", () => { // 100px mount, 4px padding, 5 rows of 16px: 92 - 80 = 12px slack on top. expect(terminalContentOriginY(100, 4, 5, 16, true)).toBe(16); // Exact fit keeps the origin at the padding. expect(terminalContentOriginY(88, 4, 5, 16, true)).toBe(4); // A mount smaller than the grid never pushes the origin above the padding. expect(terminalContentOriginY(80, 4, 5, 16, true)).toBe(4); }); it("keeps the prompt stationary while a drag crosses row boundaries", () => { // Growing the mount pixel by pixel: the bottom edge (origin + rows*cell) // tracks the mount bottom exactly until a new row fits. for (let height = 88; height < 104; height += 1) { const rows = Math.max(1, Math.floor((height - 8) / 16)); const origin = terminalContentOriginY(height, 4, rows, 16, true); expect(origin + rows * 16).toBe(height - 4); } }); }); describe("terminalWheelDeltaRows", () => { it("converts line-mode deltas into whole rows", () => { const result = terminalWheelDeltaRows({ deltaY: 3, deltaMode: 1 }, 16, 24, 0); expect(result.rows).toBe(3); expect(result.remainder).toBe(0); }); it("accumulates fractional pixel deltas across events", () => { let remainder = 0; let scrolled = 0; for (let index = 0; index < 4; index += 1) { const result = terminalWheelDeltaRows({ deltaY: 5, deltaMode: 0 }, 16, 24, remainder); remainder = result.remainder; scrolled += result.rows; } expect(scrolled).toBe(1); expect(remainder).toBeCloseTo(4 / 16); }); it("keeps direction for negative page-mode deltas", () => { const result = terminalWheelDeltaRows({ deltaY: -1, deltaMode: 2 }, 16, 24, 0); expect(result.rows).toBe(-24); expect(result.remainder).toBe(0); }); }); describe("terminalWheelArrowData", () => { it("emits one arrow per row honoring application cursor keys", () => { expect(terminalWheelArrowData(-2, false)).toBe("\u001b[A\u001b[A"); expect(terminalWheelArrowData(3, false)).toBe("\u001b[B\u001b[B\u001b[B"); expect(terminalWheelArrowData(-1, true)).toBe("\u001bOA"); expect(terminalWheelArrowData(0, true)).toBe(""); }); }); describe("isTerminalLinkPointerGesture", () => { it("uses Command on macOS and Control elsewhere", () => { expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "MacIntel")).toBe(true); expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "MacIntel")).toBe(false); expect(isTerminalLinkPointerGesture({ ctrlKey: true, metaKey: false }, "Linux x86_64")).toBe( true, ); expect(isTerminalLinkPointerGesture({ ctrlKey: false, metaKey: true }, "Linux x86_64")).toBe( false, ); }); }); describe("advanceTerminalSelectionClickSequence", () => { it("recognizes stationary double and triple pointer presses without PointerEvent.detail", () => { const first = advanceTerminalSelectionClickSequence(null, { clientX: 20, clientY: 30, timeStamp: 1_000, }); const second = advanceTerminalSelectionClickSequence(first, { clientX: 22, clientY: 29, timeStamp: 1_200, }); const third = advanceTerminalSelectionClickSequence(second, { clientX: 21, clientY: 31, timeStamp: 1_400, }); expect([first.count, second.count, third.count]).toEqual([1, 2, 3]); }); it("starts over after movement, delay, or a completed triple click", () => { const previous = { count: 3, time: 1_000, x: 20, y: 30 }; expect( advanceTerminalSelectionClickSequence(previous, { clientX: 20, clientY: 30, timeStamp: 1_100, }).count, ).toBe(1); expect( advanceTerminalSelectionClickSequence(previous, { clientX: 30, clientY: 30, timeStamp: 1_100, }).count, ).toBe(1); expect( advanceTerminalSelectionClickSequence(previous, { clientX: 20, clientY: 30, timeStamp: 1_501, }).count, ).toBe(1); }); }); describe("terminal scrollbar", () => { it("maps Ghostty's viewport state to a proportional thumb", () => { expect(terminalScrollbarGeometry({ total: 100, offset: 40, len: 20 }, 200)).toEqual({ thumbHeight: 40, thumbTop: 80, maxOffset: 80, }); expect(terminalScrollbarGeometry({ total: 100, offset: 0, len: 100 }, 200)).toBeNull(); });