From b4529f317b249605016f644d5ec188fa31fa52cb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 8 Feb 2026 20:18:02 -0800 Subject: [PATCH] . --- .gitmodules | 3 + addons/addon-search/src/SearchEngine.ts | 3 + bin/build_ghostty_wasm.mjs | 42 + bin/test_unit.js | 3 + demo/server/server.ts | 1 + package.json | 1 + src/browser/Terminal.test.ts | 27 +- src/common/InputHandler.ts | 16 +- src/common/buffer/Buffer.test.ts | 20 +- src/common/buffer/Buffer.ts | 61 +- src/common/buffer/BufferReflow.ts | 43 +- src/common/buffer/GhosttyBufferLine.test.ts | 44 ++ src/common/buffer/GhosttyBufferLine.ts | 352 +++++++++ src/common/buffer/GhosttyCircularList.ts | 270 +++++++ src/common/buffer/GhosttyWasmBuffer.ts | 198 +++++ src/common/parser/GhosttyWasmParser.ts | 831 ++++++++++++++++++++ src/common/parser/GhosttyWasmRuntime.ts | 133 ++++ vendor/ghostty | 1 + vendor/ghostty-wasm/ghostty-vt.wasm | Bin 0 -> 153893 bytes 19 files changed, 1993 insertions(+), 56 deletions(-) create mode 100644 bin/build_ghostty_wasm.mjs create mode 100644 src/common/buffer/GhosttyBufferLine.test.ts create mode 100644 src/common/buffer/GhosttyBufferLine.ts create mode 100644 src/common/buffer/GhosttyCircularList.ts create mode 100644 src/common/buffer/GhosttyWasmBuffer.ts create mode 100644 src/common/parser/GhosttyWasmParser.ts create mode 100644 src/common/parser/GhosttyWasmRuntime.ts create mode 160000 vendor/ghostty create mode 100644 vendor/ghostty-wasm/ghostty-vt.wasm diff --git a/.gitmodules b/.gitmodules index e69de29bb..786337100 100644 --- a/.gitmodules +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/ghostty"] + path = vendor/ghostty + url = https://github.com/ghostty-org/ghostty diff --git a/addons/addon-search/src/SearchEngine.ts b/addons/addon-search/src/SearchEngine.ts index b9991974d..240c62e6c 100644 --- a/addons/addon-search/src/SearchEngine.ts +++ b/addons/addon-search/src/SearchEngine.ts @@ -64,6 +64,9 @@ export class SearchEngine { throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`); } + startRow = Math.max(0, startRow); + startCol = Math.max(0, startCol); + this._lineCache.initLinesCache(); const searchPosition: ISearchPosition = { diff --git a/bin/build_ghostty_wasm.mjs b/bin/build_ghostty_wasm.mjs new file mode 100644 index 000000000..9ef4c0f91 --- /dev/null +++ b/bin/build_ghostty_wasm.mjs @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { spawnSync } from 'child_process'; +import { copyFileSync, mkdirSync, renameSync } from 'fs'; +import path from 'path'; +import process from 'process'; +import { fileURLToPath } from 'url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const ghosttyDir = path.join(repoRoot, 'vendor', 'ghostty'); +const wasmOutDir = path.join(repoRoot, 'vendor', 'ghostty-wasm'); +const wasmBinDir = path.join(wasmOutDir, 'bin'); +const wasmFile = path.join(wasmOutDir, 'ghostty-vt.wasm'); +const wasmBinFile = path.join(wasmBinDir, 'ghostty-vt.wasm'); + +mkdirSync(wasmOutDir, { recursive: true }); + +const result = spawnSync('zig', [ + 'build', + 'lib-vt', + '-Dtarget=wasm32-freestanding', + '-Doptimize=ReleaseSmall', + '-p', + path.relative(ghosttyDir, wasmOutDir) +], { + cwd: ghosttyDir, + stdio: 'inherit', + shell: true +}); + +if (result.status !== 0) { + process.exit(result.status ?? 1); +} + +try { + renameSync(wasmBinFile, wasmFile); +} catch { + copyFileSync(wasmBinFile, wasmFile); +} diff --git a/bin/test_unit.js b/bin/test_unit.js index 7ec61a814..159db5d15 100644 --- a/bin/test_unit.js +++ b/bin/test_unit.js @@ -6,6 +6,9 @@ const cp = require('child_process'); const path = require('path'); +globalThis.require = require; +globalThis.XTERM_GHOSTTY_WASM_URL = path.resolve(__dirname, '../vendor/ghostty-wasm/ghostty-vt.wasm'); + const COVERAGE_LINES_THRESHOLD = 40; // Add `out` to the NODE_PATH so absolute paths can be resolved. diff --git a/demo/server/server.ts b/demo/server/server.ts index 0f4a79b3d..9617696c3 100644 --- a/demo/server/server.ts +++ b/demo/server/server.ts @@ -46,6 +46,7 @@ function startServer(): void { }); app.use('/fonts', express.static(demoRoot + '/fonts')); + app.use('/vendor/ghostty-wasm', express.static(demoRoot + '/../vendor/ghostty-wasm')); app.use('/dist', express.static(demoRoot + '/dist')); app.use('/src', express.static(demoRoot + '/src')); diff --git a/package.json b/package.json index 5ae5a4e7b..cce4f71b9 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "esbuild-demo-client-watch": "node bin/esbuild.mjs --demo-client --watch", "esbuild-demo-server": "node bin/esbuild.mjs --demo-server", "esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch", + "ghostty-wasm": "node bin/build_ghostty_wasm.mjs", "test": "npm run test-unit", "posttest": "npm run lint", "lint": "eslint --max-warnings 0 src/ addons/ demo/ test/", diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index ba2ce6dd5..24cc85aa8 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -17,6 +17,21 @@ const INIT_ROWS = 24; // grab wcwidth from mock unicode service (hardcoded to V6) const wcwidth = (new MockUnicodeService()).wcwidth; +function assertLineEqual(actual: any, expected: any): void { + assert.isOk(actual); + assert.isOk(expected); + assert.equal(actual.length, expected.length); + assert.equal(actual.isWrapped, expected.isWrapped); + const actualCell = new CellData(); + const expectedCell = new CellData(); + for (let i = 0; i < actual.length; i++) { + assert.deepEqual( + actual.loadCell(i, actualCell).getAsCharData(), + expected.loadCell(i, expectedCell).getAsCharData() + ); + } +} + describe('Terminal', () => { let term: TestTerminal; const termOptions = { @@ -183,9 +198,9 @@ describe('Terminal', () => { assert.equal(term.buffer.ybase, 0); assert.equal(term.buffer.ydisp, 0); assert.equal(term.buffer.lines.length, term.rows); - assert.deepEqual(term.buffer.lines.get(0), promptLine); + assertLineEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should clear a buffer larger than rows', async () => { @@ -198,9 +213,9 @@ describe('Terminal', () => { assert.equal(term.buffer.ybase, 0); assert.equal(term.buffer.ydisp, 0); assert.equal(term.buffer.lines.length, term.rows); - assert.deepEqual(term.buffer.lines.get(0), promptLine); + assertLineEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); it('should not break the prompt when cleared twice', () => { @@ -211,9 +226,9 @@ describe('Terminal', () => { assert.equal(term.buffer.ybase, 0); assert.equal(term.buffer.ydisp, 0); assert.equal(term.buffer.lines.length, term.rows); - assert.deepEqual(term.buffer.lines.get(0), promptLine); + assertLineEqual(term.buffer.lines.get(0), promptLine); for (let i = 1; i < term.rows; i++) { - assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); + assertLineEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA)); } }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 1997d9f11..61345b058 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -7,13 +7,13 @@ import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; -import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; +import { GhosttyWasmParser } from 'common/parser/GhosttyWasmParser'; import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; -import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services'; import { UnicodeService } from 'common/services/UnicodeService'; @@ -109,6 +109,7 @@ const SLOW_ASYNC_LIMIT = 5000; // Work variables to avoid garbage collection let $temp = 0; +const $workCell = new CellData(); /** * The terminal's standard implementation of IInputHandler, this handles all @@ -180,7 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _oscLinkService: IOscLinkService, private readonly _coreMouseService: ICoreMouseService, private readonly _unicodeService: IUnicodeService, - private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() + private readonly _parser: IEscapeSequenceParser = new GhosttyWasmParser() ) { super(); this._register(this._parser); @@ -616,11 +617,12 @@ export class InputHandler extends Disposable implements IInputHandler { if (!bufferRow) { return; } - if (oldWidth > 0 && bufferRow instanceof BufferLine) { + if (oldWidth > 0) { // Combining character widens 1 column to 2. // Move old character to next line. - bufferRow.copyCellsFrom(oldRow as BufferLine, - oldCol, 0, oldWidth, false); + for (let copyIndex = 0; copyIndex < oldWidth; copyIndex++) { + bufferRow.setCell(copyIndex, oldRow.loadCell(oldCol + copyIndex, $workCell)); + } } // clear left over cells to the right while (oldCol < cols) { diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index f9e3e1b14..f14d0a409 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -5,7 +5,6 @@ import { assert } from 'chai'; import { Buffer } from 'common/buffer/Buffer'; -import { CircularList } from 'common/CircularList'; import { MockOptionsService, MockBufferService, MockLogService } from 'common/TestUtils.test'; import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; @@ -28,7 +27,6 @@ describe('Buffer', () => { describe('constructor', () => { it('should create a CircularList with max length equal to rows + scrollback, for its lines', () => { - assert.instanceOf(buffer.lines, CircularList); assert.equal(buffer.lines.maxLength, bufferService.rows + INIT_SCROLLBACK); }); it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { @@ -1180,31 +1178,19 @@ describe('Buffer', () => { }); describe('memory cleanup after shrinking', () => { - it('should realign memory from idle task execution', async () => { + it('should keep line sizes consistent after shrinking', async () => { buffer.fillViewportRows(); // shrink more than 2 times to trigger lazy memory cleanup buffer.resize(INIT_COLS / 2 - 1, INIT_ROWS); - // sync for (let i = 0; i < INIT_ROWS; i++) { const line = buffer.lines.get(i)!; - // line memory is still at old size from initialization - assert.equal((line as any)._data.buffer.byteLength, INIT_COLS * 3 * 4); - // array.length and .length get immediately adjusted - assert.equal((line as any)._data.length, (INIT_COLS / 2 - 1) * 3); assert.equal(line.length, INIT_COLS / 2 - 1); + assert.equal(line.cleanupMemory(), 0); } - // wait for a bit to give IdleTaskQueue a chance to kick in - // and finish memory cleaning - await new Promise(r => setTimeout(r, 30)); - - // cleanup should have realigned memory with exact bytelength - for (let i = 0; i < INIT_ROWS; i++) { - const line = buffer.lines.get(i)!; - assert.equal((line as any)._data.buffer.byteLength, (INIT_COLS / 2 - 1) * 3 * 4); - } + await new Promise(r => setTimeout(r, 10)); }); }); }); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 8efefd60a..0a6231888 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CircularList, IInsertEvent } from 'common/CircularList'; +import { IInsertEvent } from 'common/CircularList'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types'; import { ExtendedAttrs } from 'common/buffer/AttributeData'; @@ -13,11 +13,16 @@ import { CellData } from 'common/buffer/CellData'; import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from 'common/buffer/Constants'; import { Marker } from 'common/buffer/Marker'; import { IBuffer } from 'common/buffer/Types'; +import { GhosttyCircularList } from 'common/buffer/GhosttyCircularList'; +import { GhosttyWasmBuffer } from 'common/buffer/GhosttyWasmBuffer'; import { DEFAULT_CHARSET } from 'common/data/Charsets'; import { IBufferService, ILogService, IOptionsService } from 'common/services/Services'; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 +// Work variable to avoid garbage collection during reflow +const $workCell = new CellData(); + /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -26,7 +31,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 * - scroll position */ export class Buffer implements IBuffer { - public lines: CircularList; + public lines: GhosttyCircularList; public ydisp: number = 0; public ybase: number = 0; public y: number = 0; @@ -50,6 +55,7 @@ export class Buffer implements IBuffer { private _isClearing: boolean = false; private _memoryCleanupQueue: InstanceType; private _memoryCleanupPosition = 0; + private _ghosttyBuffer: GhosttyWasmBuffer; constructor( private _hasScrollback: boolean, @@ -59,7 +65,9 @@ export class Buffer implements IBuffer { ) { this._cols = this._bufferService.cols; this._rows = this._bufferService.rows; - this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); + const maxLength = this._getCorrectBufferLength(this._rows); + this._ghosttyBuffer = new GhosttyWasmBuffer(this._cols, this._rows, maxLength); + this.lines = new GhosttyCircularList(this._ghosttyBuffer, maxLength); this.scrollTop = 0; this.scrollBottom = this._rows - 1; this.setupTabStops(); @@ -142,7 +150,10 @@ export class Buffer implements IBuffer { this.ybase = 0; this.y = 0; this.x = 0; - this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); + const maxLength = this._getCorrectBufferLength(this._rows); + this._ghosttyBuffer.dispose(); + this._ghosttyBuffer = new GhosttyWasmBuffer(this._cols, this._rows, maxLength); + this.lines = new GhosttyCircularList(this._ghosttyBuffer, maxLength); this.scrollTop = 0; this.scrollBottom = this._rows - 1; this.setupTabStops(); @@ -156,6 +167,8 @@ export class Buffer implements IBuffer { public resize(newCols: number, newRows: number): void { // store reference to null cell with default attrs const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); + const oldCols = this._cols; + const oldMaxLength = this.lines.maxLength; // count bufferlines with overly big memory to be cleaned afterwards let dirtyMemoryLines = 0; @@ -163,6 +176,14 @@ export class Buffer implements IBuffer { // Increase max length if needed before adjustments to allow space to fill // as required. const newMaxLength = this._getCorrectBufferLength(newRows); + const shouldDelayColResize = this._isReflowEnabled && newCols < oldCols; + const shouldDelayRowResize = newRows < this._rows; + const canDelayResize = newMaxLength <= oldMaxLength; + const shouldDelayResize = canDelayResize && (shouldDelayColResize || shouldDelayRowResize); + const initialCols = shouldDelayColResize ? oldCols : newCols; + const initialRows = shouldDelayRowResize ? this._rows : newRows; + const initialMaxLength = shouldDelayResize ? oldMaxLength : newMaxLength; + this._ghosttyBuffer.resize(initialCols, initialRows, initialMaxLength); if (newMaxLength > this.lines.maxLength) { this.lines.maxLength = newMaxLength; } @@ -263,6 +284,13 @@ export class Buffer implements IBuffer { } } + if (shouldDelayResize) { + if (newRows < this._rows || newMaxLength < oldMaxLength) { + this.lines.normalize(); + } + this._ghosttyBuffer.resize(newCols, newRows, newMaxLength); + } + this._cols = newCols; this._rows = newRows; @@ -304,11 +332,7 @@ export class Buffer implements IBuffer { } private get _isReflowEnabled(): boolean { - const windowsPty = this._optionsService.rawOptions.windowsPty; - if (windowsPty && windowsPty.buildNumber) { - return this._hasScrollback && windowsPty.backend === 'conpty' && windowsPty.buildNumber >= 21376; - } - return this._hasScrollback; + return true; } private _reflow(newCols: number, newRows: number): void { @@ -432,7 +456,19 @@ export class Buffer implements IBuffer { // which would stop the reflow from happening if an exception would throw. break; } - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); + const destStart = destCol - cellsToCopy; + const srcStart = srcCol - cellsToCopy; + const sameLine = wrappedLines[destLineIndex] === wrappedLines[srcLineIndex]; + const overlaps = sameLine && destStart < srcStart + cellsToCopy && srcStart < destStart + cellsToCopy; + if (overlaps && destStart > srcStart) { + for (let cell = cellsToCopy - 1; cell >= 0; cell--) { + wrappedLines[destLineIndex].setCell(destStart + cell, wrappedLines[srcLineIndex].loadCell(srcStart + cell, $workCell)); + } + } else { + for (let cell = 0; cell < cellsToCopy; cell++) { + wrappedLines[destLineIndex].setCell(destStart + cell, wrappedLines[srcLineIndex].loadCell(srcStart + cell, $workCell)); + } + } destCol -= cellsToCopy; if (destCol === 0) { destLineIndex--; @@ -488,7 +524,10 @@ export class Buffer implements IBuffer { // Record original lines so they don't get overridden when we rearrange the list const originalLines: BufferLine[] = []; for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); + const line = this.lines.get(i); + if (line) { + originalLines.push(line.clone() as BufferLine); + } } const originalLinesLength = this.lines.length; diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts index 44aa0976f..2ac4c4293 100644 --- a/src/common/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { BufferLine } from 'common/buffer/BufferLine'; -import { CircularList } from 'common/CircularList'; -import { IBufferLine, ICellData } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; +import { ICircularList, IBufferLine, ICellData } from 'common/Types'; export interface INewLayoutResult { layout: number[]; @@ -22,7 +21,7 @@ export interface INewLayoutResult { * @param nullCell The cell data to use when filling in empty cells. * @param reflowCursorLine Whether to reflow the line containing the cursor. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] { +export function reflowLargerGetLinesToRemove(lines: ICircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -30,16 +29,16 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o for (let y = 0; y < lines.length - 1; y++) { // Check if this row is wrapped let i = y; - let nextLine = lines.get(++i) as BufferLine; + let nextLine = lines.get(++i) as IBufferLine; if (!nextLine.isWrapped) { continue; } // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine]; + const wrappedLines: IBufferLine[] = [lines.get(y) as IBufferLine]; while (i < lines.length && nextLine.isWrapped) { wrappedLines.push(nextLine); - nextLine = lines.get(++i) as BufferLine; + nextLine = lines.get(++i) as IBufferLine; } if (!reflowCursorLine) { @@ -62,7 +61,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o const destRemainingCells = newCols - destCol; const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); + copyCells(wrappedLines[srcLineIndex], wrappedLines[destLineIndex], srcCol, destCol, cellsToCopy); destCol += cellsToCopy; if (destCol === newCols) { @@ -78,7 +77,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o // Make sure the last cell isn't wide, if it is copy it to the current dest if (destCol === 0 && destLineIndex !== 0) { if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); + copyCells(wrappedLines[destLineIndex - 1], wrappedLines[destLineIndex], newCols - 1, destCol++, 1); // Null out the end of the last row wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell); } @@ -113,7 +112,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, o * @param lines The buffer lines. * @param toRemove The indexes to remove. */ -export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { +export function reflowLargerCreateNewLayout(lines: ICircularList, toRemove: number[]): INewLayoutResult { const layout: number[] = []; // First iterate through the list and get the actual indexes to use for rows let nextToRemoveIndex = 0; @@ -148,11 +147,11 @@ export function reflowLargerCreateNewLayout(lines: CircularList, to * @param lines The buffer lines. * @param newLayout The new layout to apply. */ -export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { +export function reflowLargerApplyNewLayout(lines: ICircularList, newLayout: number[]): void { // Record original lines so they don't get overridden when we rearrange the list - const newLayoutLines: BufferLine[] = []; + const newLayoutLines: IBufferLine[] = []; for (let i = 0; i < newLayout.length; i++) { - newLayoutLines.push(lines.get(newLayout[i]) as BufferLine); + newLayoutLines.push((lines.get(newLayout[i]) as IBufferLine).clone()); } // Rearrange the list @@ -176,7 +175,7 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new * @param oldCols The columns before resize. * @param newCols The columns after resize. */ -export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { +export function reflowSmallerGetNewLineLengths(wrappedLines: IBufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c); @@ -209,7 +208,7 @@ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCo return newLineLengths; } -export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number { +export function getWrappedLineTrimmedLength(lines: IBufferLine[], i: number, cols: number): number { // If this is the last row in the wrapped line, get the actual trimmed length if (i === lines.length - 1) { return lines[i].getTrimmedLength(); @@ -224,3 +223,17 @@ export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols } return cols; } + +const workCell = new CellData(); + +function copyCells(src: IBufferLine, dest: IBufferLine, srcCol: number, destCol: number, length: number): void { + if (src === dest && destCol > srcCol) { + for (let i = length - 1; i >= 0; i--) { + dest.setCell(destCol + i, src.loadCell(srcCol + i, workCell)); + } + return; + } + for (let i = 0; i < length; i++) { + dest.setCell(destCol + i, src.loadCell(srcCol + i, workCell)); + } +} diff --git a/src/common/buffer/GhosttyBufferLine.test.ts b/src/common/buffer/GhosttyBufferLine.test.ts new file mode 100644 index 000000000..fdfa20129 --- /dev/null +++ b/src/common/buffer/GhosttyBufferLine.test.ts @@ -0,0 +1,44 @@ +import { assert } from 'chai'; +import { GhosttyWasmBuffer } from 'common/buffer/GhosttyWasmBuffer'; +import { GhosttyBufferLine } from 'common/buffer/GhosttyBufferLine'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; + +const WASM_PATH = 'vendor/ghostty-wasm/ghostty-vt.wasm'; + +describe('GhosttyBufferLine', () => { + before(() => { + (globalThis as any).XTERM_GHOSTTY_WASM_URL = WASM_PATH; + }); + + it('writes and reads simple cells', () => { + const buffer = new GhosttyWasmBuffer(5, 2, 5); + const line = new GhosttyBufferLine(buffer, 0); + + line.setCellFromCodepoint(0, 'A'.charCodeAt(0), 1, DEFAULT_ATTR_DATA); + line.setCellFromCodepoint(1, 'B'.charCodeAt(0), 1, DEFAULT_ATTR_DATA); + + assert.equal(line.translateToString(true), 'AB'); + assert.equal(line.getCodePoint(0), 'A'.charCodeAt(0)); + assert.equal(line.getCodePoint(1), 'B'.charCodeAt(0)); + + buffer.dispose(); + }); + + it('supports insert and delete', () => { + const buffer = new GhosttyWasmBuffer(5, 1, 5); + const line = new GhosttyBufferLine(buffer, 0); + const fill = new CellData(); + + line.setCellFromCodepoint(0, 'A'.charCodeAt(0), 1, DEFAULT_ATTR_DATA); + line.setCellFromCodepoint(1, 'B'.charCodeAt(0), 1, DEFAULT_ATTR_DATA); + + line.insertCells(1, 1, fill); + assert.equal(line.translateToString(true), 'A B'); + + line.deleteCells(1, 1, fill); + assert.equal(line.translateToString(true), 'AB'); + + buffer.dispose(); + }); +}); diff --git a/src/common/buffer/GhosttyBufferLine.ts b/src/common/buffer/GhosttyBufferLine.ts new file mode 100644 index 000000000..4988e1bfe --- /dev/null +++ b/src/common/buffer/GhosttyBufferLine.ts @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; +import { BufferLine } from 'common/buffer/BufferLine'; +import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; +import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { GhosttyWasmBuffer } from 'common/buffer/GhosttyWasmBuffer'; + +// Work variables to avoid garbage collection +const workCell = new CellData(); + +export class GhosttyBufferLine implements IBufferLine { + public get length(): number { + return this._buffer.cols; + } + + constructor( + private readonly _buffer: GhosttyWasmBuffer, + private readonly _row: number + ) { + } + + public get row(): number { + return this._row; + } + + public get _extendedAttrs(): { [key: number]: IExtendedAttrs | undefined } { + const rowMap = this._buffer.getExtendedRow(this._row); + if (!rowMap) { + return {}; + } + const result: { [key: number]: IExtendedAttrs | undefined } = {}; + for (const [col, ext] of rowMap) { + result[col] = ext; + } + return result; + } + + public get isWrapped(): boolean { + return this._buffer.getRowWrap(this._row); + } + + public set isWrapped(value: boolean) { + this._buffer.setRowWrap(this._row, value); + } + + public get(index: number): CharData { + const [content, fg] = this._buffer.getCell(this._row, index); + const cp = content & Content.CODEPOINT_MASK; + const combined = (content & Content.IS_COMBINED_MASK) + ? this._buffer.getCombined(this._row, index) ?? '' + : ''; + return [ + fg, + (content & Content.IS_COMBINED_MASK) + ? combined + : (cp) ? stringFromCodePoint(cp) : '', + content >> Content.WIDTH_SHIFT, + (content & Content.IS_COMBINED_MASK) + ? (combined.length ? combined.charCodeAt(combined.length - 1) : 0) + : cp + ]; + } + + public set(index: number, value: CharData): void { + const fg = value[CHAR_DATA_ATTR_INDEX]; + const chars = value[CHAR_DATA_CHAR_INDEX]; + if (chars.length > 1) { + this._buffer.setCombined(this._row, index, chars); + this._buffer.setCell(this._row, index, index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT), fg, 0); + } else { + this._buffer.clearCombined(this._row, index); + this._buffer.setCell(this._row, index, chars.charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT), fg, 0); + } + } + + public loadCell(index: number, cell: ICellData): ICellData { + const [content, fg, bg] = this._buffer.getCell(this._row, index); + cell.content = content; + cell.fg = fg; + cell.bg = bg; + if (cell.content & Content.IS_COMBINED_MASK) { + cell.combinedData = this._buffer.getCombined(this._row, index) ?? ''; + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + cell.extended = this._buffer.getExtended(this._row, index)!; + } + return cell; + } + + public setCell(index: number, cell: ICellData): void { + if (cell.content & Content.IS_COMBINED_MASK) { + this._buffer.setCombined(this._row, index, cell.combinedData); + } else { + this._buffer.clearCombined(this._row, index); + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + this._buffer.setExtended(this._row, index, cell.extended); + } else { + this._buffer.clearExtended(this._row, index); + } + this._buffer.setCell(this._row, index, cell.content, cell.fg, cell.bg); + } + + public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { + if (attrs.bg & BgFlags.HAS_EXTENDED) { + this._buffer.setExtended(this._row, index, attrs.extended); + } else { + this._buffer.clearExtended(this._row, index); + } + this._buffer.clearCombined(this._row, index); + this._buffer.setCell(this._row, index, codePoint | (width << Content.WIDTH_SHIFT), attrs.fg, attrs.bg); + } + + public addCodepointToCell(index: number, codePoint: number, width: number): void { + const [content, fg, bg] = this._buffer.getCell(this._row, index); + let newContent = content; + if (content & Content.IS_COMBINED_MASK) { + const existing = this._buffer.getCombined(this._row, index) ?? ''; + this._buffer.setCombined(this._row, index, existing + stringFromCodePoint(codePoint)); + } else { + if (content & Content.CODEPOINT_MASK) { + const existing = stringFromCodePoint(content & Content.CODEPOINT_MASK); + this._buffer.setCombined(this._row, index, existing + stringFromCodePoint(codePoint)); + newContent &= ~Content.CODEPOINT_MASK; + newContent |= Content.IS_COMBINED_MASK; + } else { + newContent = codePoint | (1 << Content.WIDTH_SHIFT); + } + } + if (width) { + newContent &= ~Content.WIDTH_MASK; + newContent |= width << Content.WIDTH_SHIFT; + } + this._buffer.setCell(this._row, index, newContent, fg, bg); + } + + public insertCells(pos: number, n: number, fillCellData: ICellData): void { + pos %= this.length; + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); + } + + if (n < this.length - pos) { + for (let i = this.length - pos - n - 1; i >= 0; --i) { + this.setCell(pos + n + i, this.loadCell(pos + i, workCell)); + } + for (let i = 0; i < n; ++i) { + this.setCell(pos + i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + if (this.getWidth(this.length - 1) === 2) { + this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData); + } + } + + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + pos %= this.length; + if (n < this.length - pos) { + for (let i = 0; i < this.length - pos - n; ++i) { + this.setCell(pos + i, this.loadCell(pos + n + i, workCell)); + } + for (let i = this.length - n; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodepoint(pos, 0, 1, fillCellData); + } + } + + public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { + if (respectProtect) { + if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) { + this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); + } + if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) { + this.setCellFromCodepoint(end, 0, 1, fillCellData); + } + while (start < end && start < this.length) { + if (!this.isProtected(start)) { + this.setCell(start, fillCellData); + } + start++; + } + return; + } + + if (start && this.getWidth(start - 1) === 2) { + this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); + } + if (end < this.length && this.getWidth(end - 1) === 2) { + this.setCellFromCodepoint(end, 0, 1, fillCellData); + } + + while (start < end && start < this.length) { + this.setCell(start++, fillCellData); + } + } + + public resize(_cols: number, _fillCellData: ICellData): boolean { + return false; + } + + public cleanupMemory(): number { + return 0; + } + + public fill(fillCellData: ICellData, respectProtect: boolean = false): void { + if (respectProtect) { + for (let i = 0; i < this.length; ++i) { + if (!this.isProtected(i)) { + this.setCell(i, fillCellData); + } + } + return; + } + for (let i = 0; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + public copyFrom(line: IBufferLine): void { + for (let i = 0; i < this.length; ++i) { + this.setCell(i, line.loadCell(i, workCell)); + } + this.isWrapped = line.isWrapped; + } + + public clone(): IBufferLine { + const clone = new BufferLine(this.length); + for (let i = 0; i < this.length; ++i) { + clone.setCell(i, this.loadCell(i, workCell)); + } + clone.isWrapped = this.isWrapped; + return clone; + } + + public getTrimmedLength(): number { + for (let i = this.length - 1; i >= 0; --i) { + if ((this._buffer.getCell(this._row, i)[0] & Content.HAS_CONTENT_MASK)) { + return i + (this._buffer.getCell(this._row, i)[0] >> Content.WIDTH_SHIFT); + } + } + return 0; + } + + public getNoBgTrimmedLength(): number { + for (let i = this.length - 1; i >= 0; --i) { + const [content, , bg] = this._buffer.getCell(this._row, i); + if ((content & Content.HAS_CONTENT_MASK) || (bg & Attributes.CM_MASK)) { + return i + (content >> Content.WIDTH_SHIFT); + } + } + return 0; + } + + public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string { + startCol = startCol ?? 0; + endCol = endCol ?? this.length; + if (trimRight) { + endCol = Math.min(endCol, this.getTrimmedLength()); + } + if (outColumns) { + outColumns.length = 0; + } + let result = ''; + while (startCol < endCol) { + const content = this._buffer.getCell(this._row, startCol)[0]; + const cp = content & Content.CODEPOINT_MASK; + const chars = (content & Content.IS_COMBINED_MASK) + ? (this._buffer.getCombined(this._row, startCol) ?? '') + : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + result += chars; + if (outColumns) { + for (let i = 0; i < chars.length; ++i) { + outColumns.push(startCol); + } + } + startCol += (content >> Content.WIDTH_SHIFT) || 1; + } + if (outColumns) { + outColumns.push(startCol); + } + return result; + } + + public getWidth(index: number): number { + return this._buffer.getCell(this._row, index)[0] >> Content.WIDTH_SHIFT; + } + + public hasWidth(index: number): number { + return this._buffer.getCell(this._row, index)[0] & Content.WIDTH_MASK; + } + + public getFg(index: number): number { + return this._buffer.getCell(this._row, index)[1]; + } + + public getBg(index: number): number { + return this._buffer.getCell(this._row, index)[2]; + } + + public hasContent(index: number): number { + return this._buffer.getCell(this._row, index)[0] & Content.HAS_CONTENT_MASK; + } + + public getCodePoint(index: number): number { + const content = this._buffer.getCell(this._row, index)[0]; + if (content & Content.IS_COMBINED_MASK) { + const combined = this._buffer.getCombined(this._row, index) ?? ''; + return combined.charCodeAt(combined.length - 1); + } + return content & Content.CODEPOINT_MASK; + } + + public isCombined(index: number): number { + return this._buffer.getCell(this._row, index)[0] & Content.IS_COMBINED_MASK; + } + + public getString(index: number): string { + const content = this._buffer.getCell(this._row, index)[0]; + if (content & Content.IS_COMBINED_MASK) { + return this._buffer.getCombined(this._row, index) ?? ''; + } + if (content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(content & Content.CODEPOINT_MASK); + } + return ''; + } + + public isProtected(index: number): number { + return this._buffer.getCell(this._row, index)[2] & BgFlags.PROTECTED; + } +} diff --git a/src/common/buffer/GhosttyCircularList.ts b/src/common/buffer/GhosttyCircularList.ts new file mode 100644 index 000000000..d45429af5 --- /dev/null +++ b/src/common/buffer/GhosttyCircularList.ts @@ -0,0 +1,270 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICircularList, IBufferLine } from 'common/Types'; +import { Disposable } from 'common/Lifecycle'; +import { Emitter } from 'common/Event'; +import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; +import { GhosttyWasmBuffer } from 'common/buffer/GhosttyWasmBuffer'; +import { GhosttyBufferLine } from 'common/buffer/GhosttyBufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { BgFlags, Content } from 'common/buffer/Constants'; + +export class GhosttyCircularList extends Disposable implements ICircularList { + private _startIndex: number = 0; + private _length: number = 0; + private _lines: GhosttyBufferLine[]; + + public readonly onDeleteEmitter = this._register(new Emitter()); + public readonly onDelete = this.onDeleteEmitter.event; + public readonly onInsertEmitter = this._register(new Emitter()); + public readonly onInsert = this.onInsertEmitter.event; + public readonly onTrimEmitter = this._register(new Emitter()); + public readonly onTrim = this.onTrimEmitter.event; + + private _workCell = new CellData(); + + constructor( + private _buffer: GhosttyWasmBuffer, + private _maxLength: number + ) { + super(); + this._lines = new Array(_maxLength); + for (let i = 0; i < _maxLength; i++) { + this._lines[i] = new GhosttyBufferLine(this._buffer, i); + } + } + + public get maxLength(): number { + return this._maxLength; + } + + public set maxLength(newMaxLength: number) { + if (this._maxLength === newMaxLength) { + return; + } + + const nextLength = Math.min(newMaxLength, this.length); + const oldLines = this._getLogicalLines(nextLength); + const newLines = new Array(newMaxLength); + for (let i = 0; i < newMaxLength; i++) { + newLines[i] = new GhosttyBufferLine(this._buffer, i); + } + for (let i = 0; i < nextLength; i++) { + this._buffer.copyRow(oldLines[i].row, newLines[i].row); + } + + this._lines = newLines; + this._maxLength = newMaxLength; + this._startIndex = 0; + this._length = nextLength; + } + + public get length(): number { + return this._length; + } + + public set length(newLength: number) { + this._length = newLength; + } + + public get isFull(): boolean { + return this._length === this._maxLength; + } + + public get(index: number): IBufferLine | undefined { + if (index < 0 || index >= this._length) { + return undefined; + } + return this._lines[this._getCyclicIndex(index)]; + } + + public set(index: number, value: IBufferLine): void { + if (index < 0) { + return; + } + if (index >= this._length) { + this._length = Math.min(index + 1, this._maxLength); + } + this._setIndex(index, value); + } + + public push(value: IBufferLine): void { + if (this._length === this._maxLength) { + this._startIndex = ++this._startIndex % this._maxLength; + this.onTrimEmitter.fire(1); + } else { + this._length++; + } + this.set(this._length - 1, value); + } + + public recycle(): GhosttyBufferLine { + if (!this.isFull) { + throw new Error('recycle is only supported when buffer is full'); + } + this._startIndex = ++this._startIndex % this._maxLength; + this.onTrimEmitter.fire(1); + return this._lines[this._getCyclicIndex(this._length - 1)]; + } + + public pop(): IBufferLine | undefined { + if (this._length === 0) { + return undefined; + } + const value = this._lines[this._getCyclicIndex(this._length - 1)]; + this._length--; + return value; + } + + public splice(start: number, deleteCount: number, ...items: IBufferLine[]): void { + if (deleteCount === 0 && items.length === 0) { + return; + } + + const before = Math.max(0, Math.min(start, this._length)); + const actualDeleteCount = Math.min(deleteCount, this._length - before); + + if (actualDeleteCount) { + for (let i = before; i < this._length - actualDeleteCount; i++) { + const src = this.get(i + actualDeleteCount); + if (src) { + this._setIndex(i, src); + } + } + this._length -= actualDeleteCount; + this.onDeleteEmitter.fire({ index: before, amount: actualDeleteCount }); + } + + if (items.length) { + for (let i = this._length - 1; i >= before; i--) { + const src = this.get(i); + if (src) { + this._setIndex(i + items.length, src); + } + } + for (let i = 0; i < items.length; i++) { + this._setIndex(before + i, items[i]); + } + this.onInsertEmitter.fire({ index: before, amount: items.length }); + + if (this._length + items.length > this._maxLength) { + const countToTrim = this._length + items.length - this._maxLength; + this._startIndex = (this._startIndex + countToTrim) % this._maxLength; + this._length = this._maxLength; + this.onTrimEmitter.fire(countToTrim); + } else { + this._length += items.length; + } + } + } + + public trimStart(count: number): void { + if (count <= 0) { + return; + } + count = Math.min(count, this._length); + if (count > 0) { + this._startIndex = (this._startIndex + count) % this._maxLength; + this._length -= count; + this.onTrimEmitter.fire(count); + } + } + + public shiftElements(start: number, count: number, offset: number): void { + if (count <= 0) { + return; + } + if (start < 0 || start >= this._length) { + throw new Error('start argument out of range'); + } + if (start + offset < 0) { + throw new Error('Cannot shift elements in list beyond index 0'); + } + + if (offset > 0) { + for (let i = count - 1; i >= 0; i--) { + const srcLine = this.get(start + i); + if (srcLine) { + this._setIndex(start + i + offset, srcLine); + } + } + const expandListBy = (start + count + offset) - this._length; + if (expandListBy > 0) { + this._length += expandListBy; + while (this._length > this._maxLength) { + this._length--; + this._startIndex = (this._startIndex + 1) % this._maxLength; + this.onTrimEmitter.fire(1); + } + } + } else if (offset < 0) { + for (let i = 0; i < count; i++) { + const srcLine = this.get(start + i); + if (srcLine) { + this._setIndex(start + i + offset, srcLine); + } + } + } + } + + public normalize(): void { + if (this._startIndex === 0) { + return; + } + const snapshot = this._getLogicalLines(this._length).map(line => line.clone()); + for (let i = 0; i < snapshot.length; i++) { + this._copyInto(this._lines[i].row, snapshot[i]); + } + this._startIndex = 0; + } + + private _setIndex(index: number, value: IBufferLine): void { + const line = this._lines[this._getCyclicIndex(index)]; + this._copyInto(line.row, value); + } + + private _getCyclicIndex(index: number): number { + return (this._startIndex + index) % this._maxLength; + } + + private _copyInto(rowIndex: number, value: IBufferLine): void { + const nullContent = 1 << Content.WIDTH_SHIFT; + if (value instanceof GhosttyBufferLine) { + this._buffer.copyRow(value.row, rowIndex); + return; + } + + const colsToCopy = Math.min(this._buffer.cols, value.length); + for (let col = 0; col < colsToCopy; col++) { + const cell = value.loadCell(col, this._workCell); + this._buffer.setCell(rowIndex, col, cell.content, cell.fg, cell.bg); + if (cell.content & Content.IS_COMBINED_MASK) { + this._buffer.setCombined(rowIndex, col, cell.combinedData); + } else { + this._buffer.clearCombined(rowIndex, col); + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + this._buffer.setExtended(rowIndex, col, cell.extended); + } else { + this._buffer.clearExtended(rowIndex, col); + } + } + for (let col = colsToCopy; col < this._buffer.cols; col++) { + this._buffer.setCell(rowIndex, col, nullContent, 0, 0); + this._buffer.clearCombined(rowIndex, col); + this._buffer.clearExtended(rowIndex, col); + } + this._buffer.setRowWrap(rowIndex, value.isWrapped); + } + + private _getLogicalLines(count: number): GhosttyBufferLine[] { + const result: GhosttyBufferLine[] = []; + for (let i = 0; i < count; i++) { + result.push(this._lines[this._getCyclicIndex(i)]); + } + return result; + } +} diff --git a/src/common/buffer/GhosttyWasmBuffer.ts b/src/common/buffer/GhosttyWasmBuffer.ts new file mode 100644 index 000000000..36c7d9d15 --- /dev/null +++ b/src/common/buffer/GhosttyWasmBuffer.ts @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { GhosttyWasmRuntime, IGhosttyWasmExports } from 'common/parser/GhosttyWasmRuntime'; +import { IExtendedAttrs } from 'common/Types'; + +const CELL_STRIDE = 3; + +export class GhosttyWasmBuffer { + private readonly _wasm: IGhosttyWasmExports; + private _handle: number; + private _cols: number; + private _rows: number; + private _maxRows: number; + + private _combined = new Map>(); + private _extended = new Map>(); + + private _cellScratchPtr: number; + + constructor(cols: number, rows: number, maxRows: number) { + this._wasm = GhosttyWasmRuntime.getInstanceSync(); + this._cols = cols; + this._rows = rows; + this._maxRows = maxRows; + + this._handle = this._wasm.ghostty_xterm_buffer_new(cols, rows, maxRows); + if (!this._handle) { + throw new Error('Failed to allocate Ghostty xterm buffer'); + } + + this._cellScratchPtr = this._wasm.ghostty_wasm_alloc_u8_array(CELL_STRIDE * 4); + if (!this._cellScratchPtr) { + this._wasm.ghostty_xterm_buffer_free(this._handle); + this._handle = 0; + throw new Error('Failed to allocate Ghostty cell scratch buffer'); + } + } + + public dispose(): void { + if (this._handle) { + this._wasm.ghostty_xterm_buffer_free(this._handle); + this._handle = 0; + } + if (this._cellScratchPtr) { + this._wasm.ghostty_wasm_free_u8_array(this._cellScratchPtr, CELL_STRIDE * 4); + this._cellScratchPtr = 0; + } + } + + public get cols(): number { + return this._cols; + } + + public get rows(): number { + return this._rows; + } + + public get maxRows(): number { + return this._maxRows; + } + + public resize(cols: number, rows: number, maxRows: number): void { + const oldCols = this._cols; + if (!this._wasm.ghostty_xterm_buffer_resize(this._handle, cols, rows, maxRows)) { + throw new Error('Failed to resize Ghostty xterm buffer'); + } + this._cols = cols; + this._rows = rows; + this._maxRows = maxRows; + + if (cols < oldCols) { + for (const [row, rowMap] of this._combined) { + for (const col of rowMap.keys()) { + if (col >= cols) { + rowMap.delete(col); + } + } + if (rowMap.size === 0) { + this._combined.delete(row); + } + } + for (const [row, rowMap] of this._extended) { + for (const col of rowMap.keys()) { + if (col >= cols) { + rowMap.delete(col); + } + } + if (rowMap.size === 0) { + this._extended.delete(row); + } + } + } + } + + public clear(): void { + this._wasm.ghostty_xterm_buffer_clear(this._handle); + this._combined.clear(); + this._extended.clear(); + } + + public getCell(row: number, col: number): [number, number, number] { + this._wasm.ghostty_xterm_buffer_get_cell(this._handle, row, col, this._cellScratchPtr); + const view = new DataView(this._wasm.memory.buffer, this._cellScratchPtr, CELL_STRIDE * 4); + const content = view.getUint32(0, true); + const fg = view.getUint32(4, true); + const bg = view.getUint32(8, true); + return [content, fg, bg]; + } + + public setCell(row: number, col: number, content: number, fg: number, bg: number): void { + this._wasm.ghostty_xterm_buffer_set_cell(this._handle, row, col, content, fg, bg); + } + + public clearRow(row: number, content: number, fg: number, bg: number): void { + this._wasm.ghostty_xterm_buffer_clear_row(this._handle, row, content, fg, bg); + this._combined.delete(row); + this._extended.delete(row); + } + + public copyRow(srcRow: number, dstRow: number): void { + this._wasm.ghostty_xterm_buffer_copy_row(this._handle, srcRow, dstRow); + const combined = this._combined.get(srcRow); + if (combined) { + this._combined.set(dstRow, new Map(combined)); + } else { + this._combined.delete(dstRow); + } + const extended = this._extended.get(srcRow); + if (extended) { + this._extended.set(dstRow, new Map(extended)); + } else { + this._extended.delete(dstRow); + } + } + + public getRowWrap(row: number): boolean { + return this._wasm.ghostty_xterm_buffer_get_row_wrap(this._handle, row) !== 0; + } + + public setRowWrap(row: number, wrap: boolean): void { + this._wasm.ghostty_xterm_buffer_set_row_wrap(this._handle, row, wrap ? 1 : 0); + } + + public getCombined(row: number, col: number): string | undefined { + return this._combined.get(row)?.get(col); + } + + public setCombined(row: number, col: number, data: string): void { + let rowMap = this._combined.get(row); + if (!rowMap) { + rowMap = new Map(); + this._combined.set(row, rowMap); + } + rowMap.set(col, data); + } + + public clearCombined(row: number, col: number): void { + const rowMap = this._combined.get(row); + if (!rowMap) { + return; + } + rowMap.delete(col); + if (rowMap.size === 0) { + this._combined.delete(row); + } + } + + public getExtended(row: number, col: number): IExtendedAttrs | undefined { + return this._extended.get(row)?.get(col); + } + + public getExtendedRow(row: number): Map | undefined { + return this._extended.get(row); + } + + public setExtended(row: number, col: number, data: IExtendedAttrs): void { + let rowMap = this._extended.get(row); + if (!rowMap) { + rowMap = new Map(); + this._extended.set(row, rowMap); + } + rowMap.set(col, data); + } + + public clearExtended(row: number, col: number): void { + const rowMap = this._extended.get(row); + if (!rowMap) { + return; + } + rowMap.delete(col); + if (rowMap.size === 0) { + this._extended.delete(row); + } + } +} diff --git a/src/common/parser/GhosttyWasmParser.ts b/src/common/parser/GhosttyWasmParser.ts new file mode 100644 index 000000000..083fd8856 --- /dev/null +++ b/src/common/parser/GhosttyWasmParser.ts @@ -0,0 +1,831 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Disposable, toDisposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, IApcHandler, IApcParser, ApcFallbackHandlerType } from 'common/parser/Types'; +import { OscParser } from 'common/parser/OscParser'; +import { DcsParser } from 'common/parser/DcsParser'; +import { ApcParser } from 'common/parser/ApcParser'; +import { Params } from 'common/parser/Params'; +import { StringToUtf32 } from 'common/input/TextDecoder'; +import { GhosttyWasmRuntime, IGhosttyWasmExports } from 'common/parser/GhosttyWasmRuntime'; + +const enum RawEventTag { + PRINT = 0, + EXECUTE = 1, + ESC = 2, + CSI = 3, + OSC = 4, + DCS_HOOK = 5, + DCS_PUT = 6, + DCS_UNHOOK = 7, + APC_START = 8, + APC_PUT = 9, + APC_END = 10 +} + +export class GhosttyWasmParser extends Disposable implements IEscapeSequenceParser { + public precedingJoinState = 0; + + private _printHandler: PrintHandlerType; + private _executeHandlers: { [flag: number]: ExecuteHandlerType }; + private _csiHandlers: IHandlerCollection; + private _escHandlers: IHandlerCollection; + private readonly _oscParser: OscParser; + private readonly _dcsParser: IDcsParser; + private readonly _apcParser: IApcParser; + private _errorHandler: (state: IParsingState) => IParsingState; + + private _printHandlerFb: PrintFallbackHandlerType; + private _executeHandlerFb: ExecuteFallbackHandlerType; + private _csiHandlerFb: CsiFallbackHandlerType; + private _escHandlerFb: EscFallbackHandlerType; + private _errorHandlerFb: (state: IParsingState) => IParsingState; + + private _parseStack: IParserStackState = { + state: ParserStackType.NONE, + handlers: [], + handlerPos: 0, + transition: 0, + chunkPos: 0 + }; + + private _wasm?: IGhosttyWasmExports; + private _parserHandle = 0; + private _wasmReady?: Promise; + + private _eventSize = 0; + private _eventMaxParams = 0; + private _eventMaxIntermediates = 0; + private readonly _ptrSize = 4; + + private _eventIndex = 0; + private _pendingEvents: { ptr: number, len: number } | undefined; + private _resumeParams: IParams | undefined; + + private _textEncoder = new TextEncoder(); + private _textDecoder = new TextDecoder('utf-8'); + private _utf32Scratch = new Uint32Array(4096); + private _oscBuffer = new StringToUtf32(); + private _dcsPutBuffer = new Uint32Array(1); + private _apcPutBuffer = new Uint32Array(1); + + constructor() { + super(); + + this._printHandlerFb = () => { }; + this._executeHandlerFb = () => { }; + this._csiHandlerFb = () => { }; + this._escHandlerFb = () => { }; + this._errorHandlerFb = (state: IParsingState): IParsingState => state; + + this._printHandler = this._printHandlerFb; + this._executeHandlers = Object.create(null); + this._csiHandlers = Object.create(null); + this._escHandlers = Object.create(null); + + this._oscParser = this._register(new OscParser()); + this._dcsParser = this._register(new DcsParser()); + this._apcParser = this._register(new ApcParser()); + this._errorHandler = this._errorHandlerFb; + + this._register(toDisposable(() => { + this._csiHandlers = Object.create(null); + this._executeHandlers = Object.create(null); + this._escHandlers = Object.create(null); + })); + + this._register(toDisposable(() => { + if (this._wasm && this._parserHandle) { + this._wasm.ghostty_raw_parser_free(this._parserHandle); + } + this._parserHandle = 0; + })); + + this.registerEscHandler({ final: '\\' }, () => true); + } + + public identToString(ident: number): string { + const res: string[] = []; + while (ident) { + res.push(String.fromCharCode(ident & 0xFF)); + ident >>= 8; + } + return res.reverse().join(''); + } + + public setPrintHandler(handler: PrintHandlerType): void { + this._printHandler = handler; + } + + public clearPrintHandler(): void { + this._printHandler = this._printHandlerFb; + } + + public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable { + const ident = this._identifier(id, [0x30, 0x7e]); + this._escHandlers[ident] ??= []; + const handlerList = this._escHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + + public clearEscHandler(id: IFunctionIdentifier): void { + if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; + } + + public setEscHandlerFallback(handler: EscFallbackHandlerType): void { + this._escHandlerFb = handler; + } + + public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void { + this._executeHandlers[flag.charCodeAt(0)] = handler; + } + + public clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + + public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void { + this._executeHandlerFb = handler; + } + + public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable { + const ident = this._identifier(id); + this._csiHandlers[ident] ??= []; + const handlerList = this._csiHandlers[ident]; + handlerList.push(handler); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(handler); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } + + public clearCsiHandler(id: IFunctionIdentifier): void { + if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; + } + + public setCsiHandlerFallback(callback: CsiFallbackHandlerType): void { + this._csiHandlerFb = callback; + } + + public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + return this._dcsParser.registerHandler(this._identifier(id), handler); + } + + public clearDcsHandler(id: IFunctionIdentifier): void { + this._dcsParser.clearHandler(this._identifier(id)); + } + + public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void { + this._dcsParser.setHandlerFallback(handler); + } + + public registerOscHandler(ident: number, handler: IOscHandler): IDisposable { + return this._oscParser.registerHandler(ident, handler); + } + + public clearOscHandler(ident: number): void { + this._oscParser.clearHandler(ident); + } + + public setOscHandlerFallback(handler: OscFallbackHandlerType): void { + this._oscParser.setHandlerFallback(handler); + } + + public registerApcHandler(ident: number, handler: IApcHandler): IDisposable { + return this._apcParser.registerHandler(ident, handler); + } + + public clearApcHandler(ident: number): void { + this._apcParser.clearHandler(ident); + } + + public setApcHandlerFallback(handler: ApcFallbackHandlerType): void { + this._apcParser.setHandlerFallback(handler); + } + + public setErrorHandler(handler: (state: IParsingState) => IParsingState): void { + this._errorHandler = handler; + } + + public clearErrorHandler(): void { + this._errorHandler = this._errorHandlerFb; + } + + public reset(): void { + this._oscParser.reset(); + this._dcsParser.reset(); + this._apcParser.reset(); + this.precedingJoinState = 0; + if (this._wasm && this._parserHandle) { + this._wasm.ghostty_raw_parser_reset(this._parserHandle); + } + if (this._parseStack.state !== ParserStackType.NONE) { + this._parseStack.state = ParserStackType.RESET; + this._parseStack.handlers = []; + } + } + + public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise { + if (!this._wasm) { + this._wasmReady ??= this._initWasm(); + return this._wasmReady; + } + + if (this._parseStack.state !== ParserStackType.NONE) { + return this._resume(promiseResult); + } + + const bytes = this._encodeUtf32(data, length); + const result = this._writeToWasm(bytes); + if (result) { + return result; + } + return this._drainEvents(); + } + + private _resume(promiseResult?: boolean): void | Promise { + if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) { + this._parseStack.state = ParserStackType.FAIL; + throw new Error('improper continuation due to previous async handler, giving up parsing'); + } + + switch (this._parseStack.state) { + case ParserStackType.CSI: + case ParserStackType.ESC: + return this._resumeHandlerLoop(promiseResult); + case ParserStackType.OSC: + return this._resumeOsc(promiseResult); + case ParserStackType.DCS: + return this._resumeDcs(promiseResult); + case ParserStackType.APC: + return this._resumeApc(promiseResult); + case ParserStackType.RESET: + this._parseStack.state = ParserStackType.NONE; + return; + default: + this._parseStack.state = ParserStackType.NONE; + return; + } + } + + private _resumeHandlerLoop(promiseResult: boolean): void | Promise { + const handlers = this._parseStack.handlers as CsiHandlerType[] | EscHandlerType[]; + let handlerPos = this._parseStack.handlerPos - 1; + if (promiseResult === false && handlerPos > -1) { + if (this._parseStack.state === ParserStackType.CSI) { + const params = this._resumeParams ?? new Params(); + for (; handlerPos >= 0; handlerPos--) { + const handlerResult = (handlers as CsiHandlerType[])[handlerPos](params); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } else { + for (; handlerPos >= 0; handlerPos--) { + const handlerResult = (handlers as EscHandlerType[])[handlerPos](); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.handlerPos = handlerPos; + return handlerResult; + } + } + } + } + + this._resumeParams = undefined; + this._parseStack.handlers = []; + this._parseStack.state = ParserStackType.NONE; + return this._continueAfterAsync(); + } + + private _resumeOsc(promiseResult: boolean): void | Promise { + const handlerResult = this._oscParser.end(true, promiseResult); + if (handlerResult) { + return handlerResult as Promise; + } + this._parseStack.state = ParserStackType.NONE; + return this._continueAfterAsync(); + } + + private _resumeDcs(promiseResult: boolean): void | Promise { + const handlerResult = this._dcsParser.unhook(true, promiseResult); + if (handlerResult) { + return handlerResult as Promise; + } + this._parseStack.state = ParserStackType.NONE; + return this._continueAfterAsync(); + } + + private _resumeApc(promiseResult: boolean): void | Promise { + const handlerResult = this._apcParser.end(true, promiseResult); + if (handlerResult) { + return handlerResult as Promise; + } + this._parseStack.state = ParserStackType.NONE; + return this._continueAfterAsync(); + } + + private _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number { + let res = 0; + if (id.prefix) { + if (id.prefix.length > 1) { + throw new Error('only one byte as prefix supported'); + } + res = id.prefix.charCodeAt(0); + if (res && 0x3c > res || res > 0x3f) { + throw new Error('prefix must be in range 0x3c .. 0x3f'); + } + } + if (id.intermediates) { + if (id.intermediates.length > 2) { + throw new Error('only two bytes as intermediates are supported'); + } + for (let i = 0; i < id.intermediates.length; ++i) { + const intermediate = id.intermediates.charCodeAt(i); + if (0x20 > intermediate || intermediate > 0x2f) { + throw new Error('intermediate must be in range 0x20 .. 0x2f'); + } + res <<= 8; + res |= intermediate; + } + } + if (id.final.length !== 1) { + throw new Error('final must be a single byte'); + } + const finalCode = id.final.charCodeAt(0); + if (finalRange[0] > finalCode || finalCode > finalRange[1]) { + throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`); + } + res <<= 8; + res |= finalCode; + + return res; + } + + private async _initWasm(): Promise { + this._wasm = await GhosttyWasmRuntime.getInstance(); + this._parserHandle = this._wasm.ghostty_raw_parser_new(0); + this._eventSize = this._wasm.ghostty_raw_parser_event_size(); + this._eventMaxParams = this._wasm.ghostty_raw_parser_max_params(); + this._eventMaxIntermediates = this._wasm.ghostty_raw_parser_max_intermediates(); + return true; + } + + private _encodeUtf32(data: Uint32Array, length: number): Uint8Array { + if (length === 0) { + return new Uint8Array(0); + } + const bytes: number[] = []; + for (let i = 0; i < length; i++) { + const codePoint = data[i]; + if (codePoint === 0x9c) { + bytes.push(0x1b, 0x5c); + continue; + } + if (codePoint <= 0x7f || (codePoint >= 0x80 && codePoint <= 0x9f)) { + bytes.push(codePoint); + } else if (codePoint <= 0x7ff) { + bytes.push( + 0xc0 | (codePoint >> 6), + 0x80 | (codePoint & 0x3f) + ); + } else if (codePoint <= 0xffff) { + bytes.push( + 0xe0 | (codePoint >> 12), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f) + ); + } else { + bytes.push( + 0xf0 | (codePoint >> 18), + 0x80 | ((codePoint >> 12) & 0x3f), + 0x80 | ((codePoint >> 6) & 0x3f), + 0x80 | (codePoint & 0x3f) + ); + } + } + return new Uint8Array(bytes); + } + + private _writeToWasm(bytes: Uint8Array): void | Promise { + if (!this._wasm || !this._parserHandle) { + return; + } + if (!bytes.length) { + return; + } + + const dataPtr = this._wasm.ghostty_wasm_alloc_u8_array(bytes.length); + if (!dataPtr) { + return; + } + + const mem = new Uint8Array(this._wasm.memory.buffer, dataPtr, bytes.length); + mem.set(bytes); + this._wasm.ghostty_raw_parser_write(this._parserHandle, dataPtr, bytes.length); + this._wasm.ghostty_wasm_free_u8_array(dataPtr, bytes.length); + } + + private _drainEvents(): void | Promise { + if (!this._wasm || !this._parserHandle) { + return; + } + + const eventsPtr = this._wasm.ghostty_raw_parser_events_ptr(this._parserHandle); + const eventsLen = this._wasm.ghostty_raw_parser_events_len(this._parserHandle); + if (!eventsPtr || !eventsLen) { + this._wasm.ghostty_raw_parser_events_clear(this._parserHandle); + return; + } + + this._pendingEvents = { ptr: eventsPtr, len: eventsLen }; + this._eventIndex = 0; + + const result = this._processEvents(); + if (result) { + return result; + } + + this._wasm.ghostty_raw_parser_events_clear(this._parserHandle); + this._pendingEvents = undefined; + } + + private _continueAfterAsync(): void | Promise { + if (!this._wasm || !this._parserHandle || !this._pendingEvents) { + return; + } + + this._eventIndex++; + const result = this._processEvents(); + if (result) { + return result; + } + + this._wasm.ghostty_raw_parser_events_clear(this._parserHandle); + this._pendingEvents = undefined; + } + + private _processEvents(): void | Promise { + if (!this._pendingEvents || !this._wasm) { + return; + } + + const { ptr, len } = this._pendingEvents; + const mem = this._wasm.memory.buffer; + const view = new DataView(mem); + + let textStart = 0; + let textLen = 0; + + for (; this._eventIndex < len; this._eventIndex++) { + const base = ptr + this._eventIndex * this._eventSize; + const tag = view.getInt32(base, true); + + if (tag !== RawEventTag.PRINT && textLen > 0) { + this._flushText(view, ptr, textStart, textLen); + textLen = 0; + } + + switch (tag) { + case RawEventTag.PRINT: { + if (textLen === 0) { + textStart = this._eventIndex; + } + textLen++; + break; + } + case RawEventTag.EXECUTE: { + const code = view.getUint32(base + 4, true); + this._handleExecute(code); + break; + } + case RawEventTag.ESC: { + const event = this._readEvent(view, base); + const ident = this._buildIdent(event.intermediates, event.final, false, [0x30, 0x7e]); + const handlers = this._escHandlers[ident]; + const handlerResult = this._callEscHandlers(handlers, ident); + if (handlerResult) { + return handlerResult; + } + break; + } + case RawEventTag.CSI: { + const event = this._readEvent(view, base); + const params = this._buildParams(event.params, event.paramsLen, event.paramsSep); + const ident = this._buildIdent(event.intermediates, event.final, true); + const handlers = this._csiHandlers[ident]; + const handlerResult = this._callCsiHandlers(handlers, ident, params); + if (handlerResult) { + return handlerResult; + } + break; + } + case RawEventTag.OSC: { + const event = this._readEvent(view, base); + const oscResult = this._handleOsc(event.code, event.dataPtr, event.dataLen); + if (oscResult) { + return oscResult; + } + break; + } + case RawEventTag.DCS_HOOK: { + const event = this._readEvent(view, base); + const params = this._buildParams(event.params, event.paramsLen, event.paramsSep); + const ident = this._buildIdent(event.intermediates, event.final, true); + this._dcsParser.hook(ident, params); + this.precedingJoinState = 0; + break; + } + case RawEventTag.DCS_PUT: { + const code = view.getUint32(base + 4, true); + this._dcsPutBuffer[0] = code; + this._dcsParser.put(this._dcsPutBuffer, 0, 1); + break; + } + case RawEventTag.DCS_UNHOOK: { + const dcsResult = this._dcsParser.unhook(true); + if (dcsResult) { + this._parseStack.state = ParserStackType.DCS; + this.precedingJoinState = 0; + return dcsResult as Promise; + } + this.precedingJoinState = 0; + break; + } + case RawEventTag.APC_START: { + this._apcParser.start(); + this.precedingJoinState = 0; + break; + } + case RawEventTag.APC_PUT: { + const code = view.getUint32(base + 4, true); + this._apcPutBuffer[0] = code; + this._apcParser.put(this._apcPutBuffer, 0, 1); + break; + } + case RawEventTag.APC_END: { + const apcResult = this._apcParser.end(true); + if (apcResult) { + this._parseStack.state = ParserStackType.APC; + this.precedingJoinState = 0; + return apcResult as Promise; + } + this.precedingJoinState = 0; + break; + } + } + } + + if (textLen > 0) { + this._flushText(view, ptr, textStart, textLen); + } + + return; + } + + private _flushText(view: DataView, ptr: number, start: number, count: number): void { + let remaining = count; + let offset = 0; + while (remaining > 0) { + const chunk = Math.min(remaining, this._utf32Scratch.length); + for (let i = 0; i < chunk; i++) { + const base = ptr + (start + offset + i) * this._eventSize; + const code = view.getUint32(base + 4, true); + this._utf32Scratch[i] = code; + } + this._printHandler(this._utf32Scratch, 0, chunk); + remaining -= chunk; + offset += chunk; + } + } + + private _handleExecute(code: number): void { + if (this._executeHandlers[code]) { + this._executeHandlers[code](); + } else { + this._executeHandlerFb(code); + } + this.precedingJoinState = 0; + } + + private _callEscHandlers(handlers: EscHandlerType[] | undefined, ident: number): void | Promise { + if (!handlers) { + this._escHandlerFb(ident); + this.precedingJoinState = 0; + return; + } + let j = handlers.length - 1; + for (; j >= 0; j--) { + const handlerResult = handlers[j](); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.state = ParserStackType.ESC; + this._parseStack.handlers = handlers; + this._parseStack.handlerPos = j; + this.precedingJoinState = 0; + return handlerResult; + } + } + if (j < 0) { + this._escHandlerFb(ident); + } + this.precedingJoinState = 0; + } + + private _callCsiHandlers(handlers: CsiHandlerType[] | undefined, ident: number, params: IParams): void | Promise { + if (!handlers) { + this._csiHandlerFb(ident, params); + this.precedingJoinState = 0; + return; + } + let j = handlers.length - 1; + for (; j >= 0; j--) { + const handlerResult = handlers[j](params); + if (handlerResult === true) { + break; + } else if (handlerResult instanceof Promise) { + this._parseStack.state = ParserStackType.CSI; + this._resumeParams = params; + this._parseStack.handlers = handlers; + this._parseStack.handlerPos = j; + this.precedingJoinState = 0; + return handlerResult; + } + } + if (j < 0) { + this._csiHandlerFb(ident, params); + } + this.precedingJoinState = 0; + } + + private _handleOsc(oscId: number, dataPtr: number, dataLen: number): void | Promise { + console.log('handle osc'); + if (!this._wasm) { + return; + } + + let payload = ''; + if (dataLen > 0) { + if (!dataPtr || dataPtr + dataLen > this._wasm.memory.buffer.byteLength) { + return; + } + const bytes = new Uint8Array(this._wasm.memory.buffer, dataPtr, dataLen); + payload = this._textDecoder.decode(bytes); + } + if (payload.indexOf('\u0000') !== -1) { + if (oscId === 8) { + const parts = payload.split('\u0000').filter((part, index, all) => { + if (part.length > 0) { + return true; + } + return index < all.length - 1; + }); + if (parts.length > 0) { + const uri = parts[parts.length - 1] ?? ''; + const params = parts.slice(0, -1).join(':'); + payload = `${params};${uri}`; + } else { + payload = ''; + } + } else { + payload = payload.replace(/\u0000+/g, ''); + } + } + const oscPrefix = `${oscId};`; + const rawPayload = payload.startsWith(oscPrefix) + ? payload.slice(oscPrefix.length) + : payload; + let oscPayload = rawPayload; + if (oscId === 8 && oscPayload && !oscPayload.startsWith(';')) { + const sepIndex = oscPayload.indexOf(';'); + const paramsPart = sepIndex === -1 ? oscPayload : oscPayload.slice(0, sepIndex); + if (!paramsPart.includes('=')) { + oscPayload = `;${oscPayload}`; + } + } + const oscString = `${oscPrefix}${oscPayload}`; + + this._oscParser.start(); + const length = this._decodeToScratch(oscString); + this._oscParser.put(this._utf32Scratch, 0, length); + const handlerResult = this._oscParser.end(true); + if (handlerResult) { + this._parseStack.state = ParserStackType.OSC; + this.precedingJoinState = 0; + return handlerResult as Promise; + } + this.precedingJoinState = 0; + } + + private _buildParams(params: Uint16Array, length: number, sepBits: number): Params { + const result = new Params(this._eventMaxParams, 256); + if (length === 0) { + result.addParam(0); + return result; + } + + let i = 0; + while (i < length) { + result.addParam(params[i]); + if (sepBits & (1 << i)) { + i++; + while (i < length) { + result.addSubParam(params[i]); + if ((sepBits & (1 << i)) === 0) { + break; + } + i++; + } + } + i++; + } + + return result; + } + + private _buildIdent(intermediates: string, final: string, allowPrefix: boolean, finalRange: number[] = [0x40, 0x7e]): number { + let prefix: string | undefined; + let inters = intermediates; + if (allowPrefix && intermediates.length) { + const first = intermediates.charCodeAt(0); + if (first >= 0x3c && first <= 0x3f) { + prefix = intermediates.charAt(0); + inters = intermediates.substring(1); + } + } + return this._identifier({ prefix, intermediates: inters || undefined, final }, finalRange); + } + + private _readEvent(view: DataView, base: number): { + final: string; + intermediates: string; + params: Uint16Array; + paramsLen: number; + paramsSep: number; + code: number; + dataPtr: number; + dataLen: number; + } { + const code = view.getUint32(base + 4, true); + const final = String.fromCharCode(view.getUint8(base + 8)); + const intermediatesLen = Math.min(view.getUint8(base + 9), this._eventMaxIntermediates); + const paramsLen = Math.min(view.getUint8(base + 10), this._eventMaxParams); + const paramsSep = view.getUint32(base + 12, true); + + let intermediates = ''; + const interOffset = base + 16; + for (let i = 0; i < intermediatesLen; i++) { + intermediates += String.fromCharCode(view.getUint8(interOffset + i)); + } + + const paramsOffset = base + 16 + this._eventMaxIntermediates; + const params = new Uint16Array(paramsLen); + for (let i = 0; i < paramsLen; i++) { + params[i] = view.getUint16(paramsOffset + i * 2, true); + } + + const rawDataPtrOffset = paramsOffset + this._eventMaxParams * 2; + const dataPtrOffset = (rawDataPtrOffset + (this._ptrSize - 1)) & ~(this._ptrSize - 1); + const dataPtr = view.getUint32(dataPtrOffset, true); + const dataLen = view.getUint32(dataPtrOffset + this._ptrSize, true); + + return { + final, + intermediates, + params, + paramsLen, + paramsSep, + code, + dataPtr, + dataLen + }; + } + + private _decodeToScratch(text: string): number { + if (this._utf32Scratch.length < text.length * 2) { + this._utf32Scratch = new Uint32Array(text.length * 2); + } + return this._oscBuffer.decode(text, this._utf32Scratch); + } +} diff --git a/src/common/parser/GhosttyWasmRuntime.ts b/src/common/parser/GhosttyWasmRuntime.ts new file mode 100644 index 000000000..558d6cc27 --- /dev/null +++ b/src/common/parser/GhosttyWasmRuntime.ts @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2026 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/* eslint-disable @typescript-eslint/naming-convention */ +export interface IGhosttyWasmExports { + memory: WebAssembly.Memory; + ghostty_raw_parser_new(allocPtr: number): number; + ghostty_raw_parser_free(parser: number): void; + ghostty_raw_parser_reset(parser: number): void; + ghostty_raw_parser_write(parser: number, dataPtr: number, len: number): void; + ghostty_raw_parser_events_ptr(parser: number): number; + ghostty_raw_parser_events_len(parser: number): number; + ghostty_raw_parser_events_clear(parser: number): void; + ghostty_raw_parser_event_size(): number; + ghostty_raw_parser_max_params(): number; + ghostty_raw_parser_max_intermediates(): number; + ghostty_wasm_alloc_u8_array(len: number): number; + ghostty_wasm_free_u8_array(ptr: number, len: number): void; + ghostty_xterm_buffer_new(cols: number, rows: number, maxRows: number): number; + ghostty_xterm_buffer_free(buffer: number): void; + ghostty_xterm_buffer_resize(buffer: number, cols: number, rows: number, maxRows: number): number; + ghostty_xterm_buffer_clear(buffer: number): void; + ghostty_xterm_buffer_get_cols(buffer: number): number; + ghostty_xterm_buffer_get_rows(buffer: number): number; + ghostty_xterm_buffer_get_max_rows(buffer: number): number; + ghostty_xterm_buffer_get_cell(buffer: number, row: number, col: number, outPtr: number): void; + ghostty_xterm_buffer_set_cell(buffer: number, row: number, col: number, content: number, fg: number, bg: number): void; + ghostty_xterm_buffer_clear_row(buffer: number, row: number, content: number, fg: number, bg: number): void; + ghostty_xterm_buffer_copy_row(buffer: number, srcRow: number, dstRow: number): void; + ghostty_xterm_buffer_get_row_wrap(buffer: number, row: number): number; + ghostty_xterm_buffer_set_row_wrap(buffer: number, row: number, wrap: number): void; +} +/* eslint-enable @typescript-eslint/naming-convention */ + +export class GhosttyWasmRuntime { + private static _instancePromise: Promise | undefined; + private static _instanceSync: IGhosttyWasmExports | undefined; + + public static getInstance(): Promise { + this._instancePromise ??= this._loadAsync(); + return this._instancePromise; + } + + public static getInstanceSync(): IGhosttyWasmExports { + this._instanceSync ??= this._loadSync(); + return this._instanceSync; + } + + private static async _loadAsync(): Promise { + const wasmUrl = GhosttyWasmRuntime._getWasmUrl(); + const isNode = typeof globalThis !== 'undefined' && !!(globalThis as any).process?.versions?.node; + if (isNode) { + const requireFn = (globalThis as any).require + ?? (globalThis as any).process?.mainModule?.require + ?? (globalThis as any).__xtermRequire; + const fs = requireFn?.('fs') as { readFile(path: string, cb: (err: Error | null, data?: Uint8Array) => void): void } | undefined; + if (!fs) { + throw new Error('Node.js async WASM loading requires require("fs")'); + } + const wasmBytes = await new Promise((resolve, reject) => { + fs.readFile(wasmUrl, (err, data) => { + if (err || !data) { + reject(err ?? new Error(`Failed to load Ghostty WASM from ${wasmUrl}`)); + return; + } + resolve(data); + }); + }); + const env = { log: () => {} }; + const { instance } = await WebAssembly.instantiate(wasmBytes, { env }); + return instance.exports as unknown as IGhosttyWasmExports; + } + + const response = await fetch(wasmUrl); + if (!response.ok) { + throw new Error(`Failed to load Ghostty WASM from ${wasmUrl}`); + } + const wasmBytes = await response.arrayBuffer(); + const env = { log: () => {} }; + const { instance } = await WebAssembly.instantiate(wasmBytes, { env }); + return instance.exports as unknown as IGhosttyWasmExports; + } + + private static _loadSync(): IGhosttyWasmExports { + const wasmUrl = GhosttyWasmRuntime._getWasmUrl(); + const isNode = typeof globalThis !== 'undefined' && !!(globalThis as any).process?.versions?.node; + if (!isNode && typeof document !== 'undefined') { + const request = new XMLHttpRequest(); + request.open('GET', wasmUrl, false); + request.overrideMimeType('text/plain; charset=x-user-defined'); + request.send(null); + if (request.status !== 200 || !request.responseText) { + throw new Error(`Failed to load Ghostty WASM from ${wasmUrl}`); + } + const binary = request.responseText; + const wasmBytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + wasmBytes[i] = binary.charCodeAt(i) & 0xFF; + } + const module = new WebAssembly.Module(wasmBytes); + const env = { log: () => {} }; + const instance = new WebAssembly.Instance(module, { env }); + return instance.exports as unknown as IGhosttyWasmExports; + } + + // Node.js sync load for tests and headless usage. + const requireFn = (globalThis as any).require + ?? (globalThis as any).process?.mainModule?.require + ?? (globalThis as any).__xtermRequire; + const fs = requireFn?.('fs') as { readFileSync(path: string): Uint8Array } | undefined; + if (!fs) { + throw new Error('Node.js sync WASM loading requires require("fs")'); + } + const wasmBytes = fs.readFileSync(wasmUrl); + const module = new WebAssembly.Module(wasmBytes); + const env = { log: () => {} }; + const instance = new WebAssembly.Instance(module, { env }); + return instance.exports as unknown as IGhosttyWasmExports; + } + + private static _getWasmUrl(): string { + const override = (globalThis as any).XTERM_GHOSTTY_WASM_URL as string | undefined; + if (override) { + return override; + } + if (typeof document !== 'undefined') { + return 'vendor/ghostty-wasm/ghostty-vt.wasm'; + } + return 'vendor/ghostty-wasm/ghostty-vt.wasm'; + } +} diff --git a/vendor/ghostty b/vendor/ghostty new file mode 160000 index 000000000..6c0e02d11 --- /dev/null +++ b/vendor/ghostty @@ -0,0 +1 @@ +Subproject commit 6c0e02d1138d5376e4ac2f5e311b225494514a97 diff --git a/vendor/ghostty-wasm/ghostty-vt.wasm b/vendor/ghostty-wasm/ghostty-vt.wasm new file mode 100644 index 0000000000000000000000000000000000000000..350491bc4a1d6d0d5f8afcf267520d39aa3eadb3 GIT binary patch literal 153893 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