diff --git a/addons/addon-image/src/ImageAddon.ts b/addons/addon-image/src/ImageAddon.ts index fb3368f40..7af0e519c 100644 --- a/addons/addon-image/src/ImageAddon.ts +++ b/addons/addon-image/src/ImageAddon.ts @@ -5,6 +5,7 @@ import type { ITerminalAddon, IDisposable } from '@xterm/xterm'; import type { ImageAddon as IImageApi } from '@xterm/addon-image'; +import { Emitter, type IEvent } from 'common/Event'; import { IIPHandler } from './IIPHandler'; import { ImageRenderer } from './ImageRenderer'; import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage'; @@ -62,6 +63,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi { private _disposables: IDisposable[] = []; private _terminal: ITerminalExt | undefined; private _handlers: Map = new Map(); + private readonly _onImageAdded = new Emitter(); + public readonly onImageAdded: IEvent = this._onImageAdded.event; constructor(opts?: Partial) { this._opts = Object.assign({}, DEFAULT_OPTIONS, opts); @@ -74,6 +77,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi { } this._disposables.length = 0; this._handlers.clear(); + this._onImageAdded.dispose(); } private _disposeLater(...args: IDisposable[]): void { @@ -88,6 +92,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi { // internal data structures this._renderer = new ImageRenderer(terminal); this._storage = new ImageStorage(terminal, this._renderer, this._opts); + this._storage.onImageAdded = () => this._onImageAdded.fire(); // enable size reports if (this._opts.enableSizeReports) { diff --git a/addons/addon-image/src/ImageStorage.ts b/addons/addon-image/src/ImageStorage.ts index 2809c0fbd..8c89a16b0 100644 --- a/addons/addon-image/src/ImageStorage.ts +++ b/addons/addon-image/src/ImageStorage.ts @@ -124,6 +124,7 @@ export class ImageStorage implements IDisposable { private _pixelLimit: number = 2500000; private _viewportMetrics: { cols: number, rows: number }; + public onImageAdded: (() => void) | undefined; public onImageDeleted: ((storageId: number) => void) | undefined; constructor( @@ -336,6 +337,7 @@ export class ImageStorage implements IDisposable { // finally add the image this._images.set(imageId, imgSpec); + this.onImageAdded?.(); return imageId; } diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts index d6178ff50..67769204f 100644 --- a/addons/addon-image/test/ImageAddon.test.ts +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -196,6 +196,18 @@ test.describe('ImageAddon', () => { }); test.describe('image lifecycle & eviction', () => { + test('onImageAdded fires for each image', async () => { + await ctx.page.evaluate(` + window._imageAddedCount = 0; + window.imageAddon.onImageAdded(() => { window._imageAddedCount++; }); + `); + await ctx.proxy.write(SIXEL_SEQ_0); + await pollFor(ctx.page, 'window._imageAddedCount', 1); + await ctx.proxy.write(SIXEL_SEQ_0); + await pollFor(ctx.page, 'window._imageAddedCount', 2); + await ctx.proxy.write(SIXEL_SEQ_0); + await pollFor(ctx.page, 'window._imageAddedCount', 3); + }); test('delete image once scrolled off', async () => { await ctx.proxy.write(SIXEL_SEQ_0); pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); diff --git a/addons/addon-image/test/KittyGraphics.test.ts b/addons/addon-image/test/KittyGraphics.test.ts index b28c3422a..315538aa1 100644 --- a/addons/addon-image/test/KittyGraphics.test.ts +++ b/addons/addon-image/test/KittyGraphics.test.ts @@ -2159,6 +2159,19 @@ test.describe('Kitty Graphics Protocol', () => { strictEqual(await ctx.page.evaluate(`window.imageAddon._storage._images.has(${oldStorageId})`), false); }); }); + + test.describe('onImageAdded callback', () => { + test('onImageAdded fires for each kitty image', async () => { + await ctx.page.evaluate(` + window._imageAddedCount = 0; + window.imageAddon.onImageAdded(() => { window._imageAddedCount++; }); + `); + await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`); + await pollFor(ctx.page, 'window._imageAddedCount', 1); + await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_RGB_3X1_BASE64}\x1b\\`); + await pollFor(ctx.page, 'window._imageAddedCount', 2); + }); + }); }); /** diff --git a/addons/addon-image/typings/addon-image.d.ts b/addons/addon-image/typings/addon-image.d.ts index 856063e3e..a3fc9e1c8 100644 --- a/addons/addon-image/typings/addon-image.d.ts +++ b/addons/addon-image/typings/addon-image.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm'; declare module '@xterm/addon-image' { export interface IImageAddonOptions { @@ -116,6 +116,11 @@ declare module '@xterm/addon-image' { */ public showPlaceholder: boolean; + /** + * Event fired whenever a new image is added to storage. + */ + public readonly onImageAdded: IEvent; + /** * Get original image canvas at buffer position. */