.
This commit is contained in:
@@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+50
-11
@@ -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<IBufferLine>;
|
||||
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<typeof IdleTaskQueue>;
|
||||
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<IBufferLine>(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<IBufferLine>(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;
|
||||
|
||||
|
||||
@@ -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<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] {
|
||||
export function reflowLargerGetLinesToRemove(lines: ICircularList<IBufferLine>, 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<IBufferLine>, 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<IBufferLine>, 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<IBufferLine>, 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<IBufferLine>, o
|
||||
* @param lines The buffer lines.
|
||||
* @param toRemove The indexes to remove.
|
||||
*/
|
||||
export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, toRemove: number[]): INewLayoutResult {
|
||||
export function reflowLargerCreateNewLayout(lines: ICircularList<IBufferLine>, 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<IBufferLine>, to
|
||||
* @param lines The buffer lines.
|
||||
* @param newLayout The new layout to apply.
|
||||
*/
|
||||
export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, newLayout: number[]): void {
|
||||
export function reflowLargerApplyNewLayout(lines: ICircularList<IBufferLine>, 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<IBufferLine>, 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<IBufferLine> {
|
||||
private _startIndex: number = 0;
|
||||
private _length: number = 0;
|
||||
private _lines: GhosttyBufferLine[];
|
||||
|
||||
public readonly onDeleteEmitter = this._register(new Emitter<IDeleteEvent>());
|
||||
public readonly onDelete = this.onDeleteEmitter.event;
|
||||
public readonly onInsertEmitter = this._register(new Emitter<IInsertEvent>());
|
||||
public readonly onInsert = this.onInsertEmitter.event;
|
||||
public readonly onTrimEmitter = this._register(new Emitter<number>());
|
||||
public readonly onTrim = this.onTrimEmitter.event;
|
||||
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
private _buffer: GhosttyWasmBuffer,
|
||||
private _maxLength: number
|
||||
) {
|
||||
super();
|
||||
this._lines = new Array<GhosttyBufferLine>(_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<GhosttyBufferLine>(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;
|
||||
}
|
||||
}
|
||||
@@ -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<number, Map<number, string>>();
|
||||
private _extended = new Map<number, Map<number, IExtendedAttrs>>();
|
||||
|
||||
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<number, IExtendedAttrs> | 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user