Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
97a5069
Refactor so isWrapped isn't public writeable.
PerBothner Mar 31, 2026
1c58f1f
Merge branch 'master' into setWrapped
PerBothner Apr 6, 2026
e9ea22f
Fix MockBuffer.setWrapped.
PerBothner Apr 6, 2026
bd79ccc
Move "data" from BufferLine to new LogicalLine class.
PerBothner Apr 7, 2026
e7d9acd
Proof-of-concept attaching markers and decorations to LogicalLine.
PerBothner Apr 28, 2026
dbc9c2b
Fix some testsuite regressions.
PerBothner May 2, 2026
894cf71
Fix test so it works with new marker implementation.
PerBothner May 2, 2026
45d9f7a
Merge branch 'master' into LogicalLine
PerBothner May 4, 2026
05ba0d6
Merge branch 'LogicalLine' into markdecor
PerBothner May 4, 2026
4f3a863
Cleanup after merge.
PerBothner May 5, 2026
345bf14
Faster and siompler buffer reflow.
PerBothner May 17, 2026
3c70ed2
Merge branch 'master' into markdecor
PerBothner May 18, 2026
2a9096c
Change image storage to use new ExtendedAttrs payload field.
PerBothner May 13, 2026
686035e
Remove no-longer working test for private accessors.
PerBothner May 18, 2026
ed1c48a
Make BufferLine _logicalLine property be private.
PerBothner May 18, 2026
3f7db79
Update inspectBuffer in serialize testing for LogicalLine
PerBothner May 18, 2026
baf1794
Tweak to previous checkin.
PerBothner May 18, 2026
f15c974
Remove a debug remnant.
PerBothner May 18, 2026
41ec6d3
New new forEachDecorationAtCellLine to speed up webgl decorator handling
PerBothner May 18, 2026
d049311
Fix Marker.line slowdown.
PerBothner May 23, 2026
28cbc95
Merge branch 'master' into markdecor
PerBothner May 23, 2026
3c6b153
For decorations, use a plain doubly-linked list instead of SortedList.
PerBothner May 23, 2026
9702d78
Remove no-longer-used SortedList.ts.
PerBothner May 23, 2026
423af94
Use 'import type' in addons/addon-image/src/ImageStorage.ts
PerBothner May 21, 2026
3dbd370
Add 'type' to another import in ImageStorage.ts.
PerBothner May 23, 2026
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
204 changes: 67 additions & 137 deletions addons/addon-image/src/ImageStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

import { IDisposable } from '@xterm/xterm';
import { ImageRenderer } from './ImageRenderer';
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
import type { ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, ImageLayer } from './Types';
import type { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';



// fallback default cell size
Expand All @@ -14,93 +17,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
height: 14
};

/**
* Extend extended attribute to also hold image tile information.
*
* Object definition is copied from base repo to fully mimick its behavior.
* Image data is added as additional public properties `imageId` and `tileId`.
*/
class ExtendedAttrsImage implements IExtendedAttrsImage {
private _ext: number = 0;
public get ext(): number {
if (this._urlId) {
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }

public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
}

public get underlineColor(): number {
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public set underlineColor(value: number) {
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}

public get underlineVariantOffset(): number {
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
if (val < 0) {
return val ^ 0xFFFFFFF8;
}
return val;
}
public set underlineVariantOffset(value: number) {
this._ext &= ~ExtFlags.VARIANT_OFFSET;
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
}

private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}

class ImageTileInfo {
constructor(
ext: number = 0,
urlId: number = 0,
public imageId = -1,
public tileId = -1
) {
this._ext = ext;
this._urlId = urlId;
}

public clone(): IExtendedAttrsImage {
/**
* Technically we dont need a clone variant of ExtendedAttrsImage,
* as we never clone a cell holding image data.
* Note: Clone is only meant to be used by the InputHandler for
* sticky attributes, which is never the case for image data.
* We still provide a proper clone method to reflect the full ext attr
* state in case there are future use cases for clone.
*/
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
}

public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
public tileId = -1) {
}
}
const EMPTY_ATTRS = new ExtendedAttrsImage();


/**
* ImageStorage - extension of CoreTerminal:
Expand All @@ -122,6 +44,7 @@ export class ImageStorage implements IDisposable {
private _needsFullClear = false;
// hard limit of stored pixels (fallback limit of 10 MB)
private _pixelLimit: number = 2500000;
private _workCell: CellData = new CellData();

private _viewportMetrics: { cols: number, rows: number };
public onImageAdded: (() => void) | undefined;
Expand Down Expand Up @@ -271,10 +194,10 @@ export class ImageStorage implements IDisposable {

this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(buffer.y + buffer.ybase);
const line = buffer.lines.get(buffer.y + buffer.ybase)!;
for (let col = 0; col < cols; ++col) {
if (offset + col >= termCols) break;
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
this._writeToCell(line, offset + col, imageId, row * cols + col);
tileCount++;
}
if (scrolling) {
Expand Down Expand Up @@ -417,22 +340,18 @@ export class ImageStorage implements IDisposable {
const placeholderCalls: { col: number, row: number, count: number }[] = [];

// walk all cells in viewport and collect tiles found
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
// Note: We check extended directly (not just HAS_EXTENDED flag)
// because text writes clear the BG flag but leave image tile data intact.
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
const line = buffer.lines.get(row + buffer.ydisp);
if (!line) return;
const workCell = this._workCell;
for (let col = 0; col < cols; ++col) {
let e: IExtendedAttrsImage;
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
} else {
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
continue;
}
e = maybeImg;
line.loadCell(col, workCell);
let e = workCell.hasExtendedAttrs() && workCell.extended.payload;
if (! (e instanceof ImageTileInfo)) {
continue;
}
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
Expand All @@ -451,8 +370,9 @@ export class ImageStorage implements IDisposable {
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
*/
while (++col < cols) {
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
line.loadCell(col, workCell);
const nextE = workCell.hasExtendedAttrs() && workCell.extended.payload;
if (! (nextE instanceof ImageTileInfo) || !nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break;
}
e = nextE;
Expand Down Expand Up @@ -503,10 +423,15 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer;
const rows = buffer.lines.length;
const oldCol = this._viewportMetrics.cols - 1;
const workCell = this._workCell;
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
const line = buffer.lines.get(row) !;
line.loadCell(oldCol, workCell);
if (workCell.hasExtendedAttrs()) {
const e = workCell.extended.payload;
if (! (e instanceof ImageTileInfo)) {
continue;
}
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
Expand All @@ -523,7 +448,7 @@ export class ImageStorage implements IDisposable {
// expand only if right side is empty (nothing got wrapped from below)
let hasData = false;
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
if (line.hasContent(rightCol)) {
hasData = true;
break;
}
Expand All @@ -535,7 +460,7 @@ export class ImageStorage implements IDisposable {
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
let lastTile = e.tileId;
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
this._writeToCell(line, expandCol, imageId, ++lastTile);
imgSpec.tileCount++;
}
}
Expand All @@ -549,10 +474,10 @@ export class ImageStorage implements IDisposable {
*/
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const line = buffer.lines.get(y);
if (line && line.loadCell(x, this._workCell).hasExtendedAttrs()) {
const e = this._workCell.extended.payload;
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
Expand All @@ -569,10 +494,10 @@ export class ImageStorage implements IDisposable {
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const line = buffer.lines.get(y);
if (line && line.loadCell(x, this._workCell).hasExtendedAttrs()) {
const e = this._workCell.extended.payload;
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
return this._renderer.extractTile(spec, e.tileId);
Expand Down Expand Up @@ -600,31 +525,34 @@ export class ImageStorage implements IDisposable {
return used - current;
}

private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
old.imageId = imageId;
old.tileId = tileId;
return;
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void {
const workCell = this._workCell;
line.loadCell(x, workCell);
if (this._workCell.hasExtendedAttrs()) {
const old = workCell.extended.payload;
if (old instanceof ImageTileInfo) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
// found a plain ExtendedAttrs instance, clone it to new entry
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
old.imageId = imageId;
old.tileId = tileId;
return;
}
// found a plain ExtendedAttrs instance
workCell.extended.payload = new ImageTileInfo(imageId, tileId);
return;
}
// fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
const extattr = workCell.extended.clone();
extattr.payload = new ImageTileInfo(imageId, tileId);
workCell.extended = extattr;
workCell.updateExtended();
line.setCell(x, workCell);
}

private _evictOnAlternate(): void {
Expand All @@ -637,15 +565,17 @@ export class ImageStorage implements IDisposable {
// re-count tiles on whole buffer
const buffer = this._terminal._core.buffer;
for (let y = 0; y < this._terminal.rows; ++y) {
const line = buffer.lines.get(y) as IBufferLineExt;
const line = buffer.lines.get(y);
if (!line) {
continue;
}
const workCell = this._workCell;
for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs[x]?.imageId;
if (imgId) {
const spec = this._images.get(imgId);
line.loadCell(x, workCell);
if (workCell.hasExtendedAttrs()) {
const payload = workCell.extended.payload;
if (payload instanceof ImageTileInfo) {
const spec = this._images.get(payload.imageId);
if (spec) {
spec.tileCount++;
}
Expand Down
19 changes: 3 additions & 16 deletions addons/addon-image/src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
import type { AttributeData } from 'common/buffer/AttributeData';
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
import type { IInputHandler } from 'common/Types';
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
Expand Down Expand Up @@ -47,26 +47,14 @@ export interface IResetHandler {
reset(): void;
}

/* eslint-disable */
/**
* Stub into private interfaces.
* This should be kept in line with common libs.
* Any change made here should be replayed in the accessors test case to
* have a somewhat reliable testing against code changes in the core repo.
*/

// overloaded IExtendedAttrs to hold image refs
export interface IExtendedAttrsImage extends IExtendedAttrs {
imageId: number;
tileId: number;
clone(): IExtendedAttrsImage;
}

/* eslint-disable */
export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array;
}

interface IInputHandlerExt extends IInputHandler {
_parser: IEscapeSequenceParser;
_curAttrData: AttributeData;
Expand All @@ -77,6 +65,7 @@ interface IInputHandlerExt extends IInputHandler {
};
onRequestReset(handler: () => void): IDisposable;
}
/* eslint-enable */

export interface ICoreTerminalExt extends ITerminal {
_themeService: IThemeService | undefined;
Expand All @@ -88,8 +77,6 @@ export interface ICoreTerminalExt extends ITerminal {
export interface ITerminalExt extends Terminal {
_core: ICoreTerminalExt;
}
/* eslint-enable */


/**
* Some storage definitions.
Expand Down
Loading
Loading