Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ export class MyAddon implements ITerminalAddon {
- Proposed APIs require `allowProposedApi: true` option
- Constructor-only options (cols, rows) cannot be changed after instantiation

**Disposable Management**:
- When a disposable object can be replaced over time, prefer a registered `MutableDisposable` over manual dispose/reassign logic.
- Register it on the owning class (for example, `this._register(new MutableDisposable())`) and assign through `.value`; this automatically disposes the previous value and avoids accidentally leaking resources.

**TypeScript Constants**:
- Prefer `const enum` over top-level `const` declarations for primitive constants when appropriate, since values are inlined and avoid runtime property lookups.

**Testing Utilities**: Use `TestUtils.ts` helpers:
- `openTerminal(ctx, options)` for setup
- `pollFor(page, fn, expectedValue)` for async assertions
Expand Down
10 changes: 6 additions & 4 deletions addons/addon-fit/src/FitAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ interface ITerminalDimensions {
cols: number;
}

const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 1;
const enum Constants {
MINIMUM_COLS = 2,
MINIMUM_ROWS = 1
}

function getWindow(e: Node): Window {
if (e?.ownerDocument?.defaultView) {
Expand Down Expand Up @@ -86,8 +88,8 @@ export class FitAddon implements ITerminalAddon, IFitApi {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
cols: Math.max(Constants.MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(Constants.MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
Expand Down
14 changes: 8 additions & 6 deletions addons/addon-image/src/ImageRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { IDisposable } from '@xterm/xterm';
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';

const PLACEHOLDER_LENGTH = 4096;
const PLACEHOLDER_HEIGHT = 24;
const enum Constants {
PLACEHOLDER_LENGTH = 4096,
PLACEHOLDER_HEIGHT = 24
}

/**
* ImageRenderer - terminal frontend extension:
Expand Down Expand Up @@ -110,7 +112,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
public showPlaceholder(value: boolean): void {
if (value) {
if (!this._placeholder && this.cellSize.height !== -1) {
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
}
} else {
this._placeholderBitmap?.close();
Expand Down Expand Up @@ -249,7 +251,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
}

if (!this._placeholder) {
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
} else if (height >= this._placeholder!.height) {
this._createPlaceHolder(height + 1);
}
Expand Down Expand Up @@ -379,7 +381,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
return this._layers.has(layer);
}

private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;

Expand All @@ -403,7 +405,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
ctx.putImageData(imgData, 0, 0);

// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
Expand Down
55 changes: 27 additions & 28 deletions addons/addon-image/src/kitty/KittyGraphicsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,23 @@ import {
IKittyCommand,
IPendingTransmission,
IKittyImageData,
BYTES_PER_PIXEL_RGB,
BYTES_PER_PIXEL_RGBA,
ALPHA_OPAQUE,
KittyPixelConstants,
parseKittyCommand
} from './KittyGraphicsTypes';

// Memory limit for base64 decoder (4MB, same as IIPHandler)
const DECODER_KEEP_DATA = 4194304;
const DECODER_INITIAL_DATA = 4194304; // 4MB

// Local mirror of const enum (esbuild can't inline const enums from external packages)
const DECODER_OK: DecodeStatus.OK = 0;

// Maximum control data size
const MAX_CONTROL_DATA_SIZE = 512;
const enum Constants {
// Memory limit for base64 decoder (4MB, same as IIPHandler)
DECODER_KEEP_DATA = 4194304,
DECODER_INITIAL_DATA = 4194304, // 4MB
// Local mirror of const enum (esbuild can't inline const enums from external packages)
DECODER_OK = 0,
// Maximum control data size
MAX_CONTROL_DATA_SIZE = 512,
// Semicolon codepoint
SEMICOLON = 0x3B
}

// Semicolon codepoint
const SEMICOLON = 0x3B;
const DECODER_OK = Constants.DECODER_OK as unknown as DecodeStatus.OK;

// Kitty graphics protocol handler with streaming base64 decoding.
export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDisposable {
Expand All @@ -50,7 +49,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
private _inControlData = true;

// Buffer for control data.
private _controlData = new Uint32Array(MAX_CONTROL_DATA_SIZE);
private _controlData = new Uint32Array(Constants.MAX_CONTROL_DATA_SIZE);
private _controlLength = 0;

// Pre-calculated encoded size limit
Expand All @@ -77,7 +76,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Convert decoded size limit -> max encoded bytes.
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
// ensure we preallocate more than configured limit while using 4mb initial size.
this._initialEncodedBytes = Math.min(DECODER_INITIAL_DATA, this._maxEncodedBytes);
this._initialEncodedBytes = Math.min(Constants.DECODER_INITIAL_DATA, this._maxEncodedBytes);
}

public reset(): void {
Expand Down Expand Up @@ -129,7 +128,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Scan for semicolon
let controlEnd = end;
for (let i = start; i < end; i++) {
if (data[i] === SEMICOLON) {
if (data[i] === Constants.SEMICOLON) {
this._inControlData = false;
controlEnd = i;
break;
Expand All @@ -138,7 +137,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos

// Copy control data
const copyLength = controlEnd - start;
if (this._controlLength + copyLength > MAX_CONTROL_DATA_SIZE) {
if (this._controlLength + copyLength > Constants.MAX_CONTROL_DATA_SIZE) {
this._aborted = true;
return;
}
Expand Down Expand Up @@ -201,7 +200,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
this._activeDecoder = pending.decoder;
}
if (!this._activeDecoder) {
this._activeDecoder = new Base64Decoder(DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
this._activeDecoder = new Base64Decoder(Constants.DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
this._activeDecoder.init();
}

Expand Down Expand Up @@ -484,7 +483,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
return true;
}

const bytesPerPixel = format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
const bytesPerPixel = format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;

if (bytes.length < expectedBytes) {
Expand Down Expand Up @@ -723,7 +722,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
throw new Error('Width and height required for raw pixel data');
}

const bytesPerPixel = image.format === KittyFormat.RGBA ? BYTES_PER_PIXEL_RGBA : BYTES_PER_PIXEL_RGB;
const bytesPerPixel = image.format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;

if (bytes.length < expectedBytes) {
Expand All @@ -734,13 +733,13 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos

if (image.format === KittyFormat.RGBA) {
// RGBA: use bytes directly — no copy needed
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * BYTES_PER_PIXEL_RGBA), width, height));
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA), width, height));
}

// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
// Assumes little-endian (all modern browsers/Node.js).
const data = new Uint8ClampedArray(pixelCount * BYTES_PER_PIXEL_RGBA);
const data = new Uint8ClampedArray(pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA);
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
const dst32 = new Uint32Array(data.buffer);
const alignedPixels = pixelCount & ~3; // round down to multiple of 4
Expand All @@ -759,15 +758,15 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
}

// Handle remaining 1–3 pixels
let srcByte = alignedPixels * BYTES_PER_PIXEL_RGB;
let dstByte = alignedPixels * BYTES_PER_PIXEL_RGBA;
let srcByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGB;
let dstByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
for (let i = alignedPixels; i < pixelCount; i++) {
data[dstByte] = bytes[srcByte];
data[dstByte + 1] = bytes[srcByte + 1];
data[dstByte + 2] = bytes[srcByte + 2];
data[dstByte + 3] = ALPHA_OPAQUE;
srcByte += BYTES_PER_PIXEL_RGB;
dstByte += BYTES_PER_PIXEL_RGBA;
data[dstByte + 3] = KittyPixelConstants.ALPHA_OPAQUE;
srcByte += KittyPixelConstants.BYTES_PER_PIXEL_RGB;
dstByte += KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
}

return createImageBitmap(new ImageData(data, width, height));
Expand Down
8 changes: 5 additions & 3 deletions addons/addon-image/src/kitty/KittyGraphicsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ export const enum KittyKey {
}

// Pixel format constants
export const BYTES_PER_PIXEL_RGB = 3;
export const BYTES_PER_PIXEL_RGBA = 4;
export const ALPHA_OPAQUE = 255;
export const enum KittyPixelConstants {
BYTES_PER_PIXEL_RGB = 3,
BYTES_PER_PIXEL_RGBA = 4,
ALPHA_OPAQUE = 255
}

// Parsed Kitty graphics command.
export interface IKittyCommand {
Expand Down
9 changes: 8 additions & 1 deletion addons/addon-serialize/test/SerializeAddon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ test.describe('SerializeAddon', () => {
for (let i = 0; i < buffer.length; i++) {
// Do this intentionally to get content of underlining source
const bufferLine = buffer.getLine(i)._line;
lines.push(JSON.stringify(bufferLine));
lines.push(JSON.stringify(bufferLine, (key, value) => {
// BufferLine caches are internal/transient and can legitimately differ
// across equivalent terminal states.
if (key === '_stringCache' || key === '_stringCacheEntryRef') {
return undefined;
}
return value;
}));
}
return {
x: buffer.cursorX,
Expand Down
34 changes: 18 additions & 16 deletions addons/addon-webgl/src/GlyphRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@ void main() {
}`);
}

const INDICES_PER_CELL = 11;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
const enum Constants {
INDICES_PER_CELL = 11,
BYTES_PER_CELL = INDICES_PER_CELL * 4/* Float32Array.BYTES_PER_ELEMENT */,
CELL_POSITION_INDICES = 2
}

// Work variables to avoid garbage collection
let $i = 0;
Expand Down Expand Up @@ -160,22 +162,22 @@ export class GlyphRenderer extends Disposable {
this._register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 0);
gl.vertexAttribDivisor(VertexAttribLocations.OFFSET, 1);
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, Constants.BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, Constants.BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);

// Setup static uniforms
Expand Down Expand Up @@ -222,12 +224,12 @@ export class GlyphRenderer extends Disposable {
}

private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
$i = (y * this._terminal.cols + x) * Constants.INDICES_PER_CELL;

// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
array.fill(0, $i, $i + Constants.INDICES_PER_CELL - 1 - Constants.CELL_POSITION_INDICES);
return;
}

Expand Down Expand Up @@ -288,7 +290,7 @@ export class GlyphRenderer extends Disposable {

public clear(): void {
const terminal = this._terminal;
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
const newCount = terminal.cols * terminal.rows * Constants.INDICES_PER_CELL;

// Clear vertices
if (this._vertices.count !== newCount) {
Expand All @@ -310,7 +312,7 @@ export class GlyphRenderer extends Disposable {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 9] = x / terminal.cols;
this._vertices.attributes[i + 10] = y / terminal.rows;
i += INDICES_PER_CELL;
i += Constants.INDICES_PER_CELL;
}
}
}
Expand Down Expand Up @@ -346,8 +348,8 @@ export class GlyphRenderer extends Disposable {
// - So we don't send vertices for all the line-ending whitespace to the GPU
let bufferLength = 0;
for (let y = 0; y < renderModel.lineLengths.length; y++) {
const si = y * this._terminal.cols * INDICES_PER_CELL;
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
const si = y * this._terminal.cols * Constants.INDICES_PER_CELL;
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * Constants.INDICES_PER_CELL);
activeBuffer.set(sub, bufferLength);
bufferLength += sub.length;
}
Expand All @@ -364,7 +366,7 @@ export class GlyphRenderer extends Disposable {
}

// Draw the viewport
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / Constants.INDICES_PER_CELL);
}

public setAtlas(atlas: ITextureAtlas): void {
Expand Down
8 changes: 4 additions & 4 deletions addons/addon-webgl/src/RectangleRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IColor } from 'common/Types';
import { Terminal } from '@xterm/xterm';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { RenderModelConstants } from './RenderModel';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
Expand Down Expand Up @@ -217,9 +217,9 @@ export class RectangleRenderer extends Disposable {
currentFg = 0;
currentInverse = false;
for (x = 0; x < terminal.cols; x++) {
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
modelIndex = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
bg = model.cells[modelIndex + RenderModelConstants.BG_OFFSET];
fg = model.cells[modelIndex + RenderModelConstants.FG_OFFSET];
inverse = !!(fg & FgFlags.INVERSE);
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
// A rectangle needs to be drawn if going from non-default to another color
Expand Down
Loading
Loading