@xterm/addon-image 0.10.0-beta.18 → 0.10.0-beta.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xterm/addon-image",
3
- "version": "0.10.0-beta.18",
3
+ "version": "0.10.0-beta.180",
4
4
  "author": {
5
5
  "name": "The xterm.js authors",
6
6
  "url": "https://xtermjs.org/"
@@ -18,17 +18,17 @@
18
18
  "xterm.js"
19
19
  ],
20
20
  "scripts": {
21
- "prepackage": "../../node_modules/.bin/tsc -p .",
21
+ "prepackage": "../../node_modules/.bin/tsgo -p .",
22
22
  "package": "../../node_modules/.bin/webpack",
23
23
  "prepublishOnly": "npm run package",
24
24
  "start": "node ../../demo/start"
25
25
  },
26
26
  "devDependencies": {
27
27
  "sixel": "^0.16.0",
28
- "xterm-wasm-parts": "^0.1.0"
28
+ "xterm-wasm-parts": "^0.3.0"
29
29
  },
30
- "commit": "a3d6bf671084b659d92502421d7fe1a45c563f15",
30
+ "commit": "783c4a34d296480f29ecddfb8743e4e542e29cf5",
31
31
  "peerDependencies": {
32
- "@xterm/xterm": "^6.1.0-beta.18"
32
+ "@xterm/xterm": "^6.1.0-beta.180"
33
33
  }
34
34
  }
package/src/IIPHandler.ts CHANGED
@@ -4,17 +4,21 @@
4
4
  */
5
5
  import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
6
6
  import { ImageRenderer } from './ImageRenderer';
7
- import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
7
+ import { IIPImageStorage } from './IIPImageStorage';
8
+ import { CELL_SIZE_DEFAULT } from './ImageStorage';
8
9
  import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
9
10
  import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
10
11
  import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
11
12
 
12
-
13
- // eslint-disable-next-line
14
- declare const Buffer: any;
15
-
16
- // limit hold memory in base64 decoder
17
- const KEEP_DATA = 4194304;
13
+ // Local const enum mirror - esbuild can't inline const enums from external packages
14
+ const enum DecoderConst {
15
+ // Limit held memory in base64 decoder (encoded bytes).
16
+ KEEP_DATA = 4194304,
17
+ // Initial buffer allocation for the decoder.
18
+ INITIAL_DATA = 1048576,
19
+ // Local mirror of const enum (esbuild can't inline const enums from external packages)
20
+ OK = 0
21
+ }
18
22
 
19
23
  // default IIP header values
20
24
  const DEFAULT_HEADER: IHeaderFields = {
@@ -31,15 +35,19 @@ export class IIPHandler implements IOscHandler, IResetHandler {
31
35
  private _aborted = false;
32
36
  private _hp = new HeaderParser();
33
37
  private _header: IHeaderFields = DEFAULT_HEADER;
34
- private _dec = new Base64Decoder(KEEP_DATA);
38
+ private _dec: Base64Decoder;
35
39
  private _metrics = UNSUPPORTED_TYPE;
36
40
 
37
41
  constructor(
38
42
  private readonly _opts: IImageAddonOptions,
39
43
  private readonly _renderer: ImageRenderer,
40
- private readonly _storage: ImageStorage,
44
+ private readonly _storage: IIPImageStorage,
41
45
  private readonly _coreTerminal: ITerminalExt
42
- ) {}
46
+ ) {
47
+ const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
48
+ const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
49
+ this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
50
+ }
43
51
 
44
52
  public reset(): void {}
45
53
 
@@ -54,7 +62,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
54
62
  if (this._aborted) return;
55
63
 
56
64
  if (this._hp.state === HeaderState.END) {
57
- if (this._dec.put(data, start, end)) {
65
+ if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
58
66
  this._dec.release();
59
67
  this._aborted = true;
60
68
  }
@@ -70,8 +78,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
70
78
  this._aborted = true;
71
79
  return;
72
80
  }
73
- this._dec.init(this._header.size);
74
- if (this._dec.put(data, dataPos, end)) {
81
+ this._dec.init();
82
+ if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
75
83
  this._dec.release();
76
84
  this._aborted = true;
77
85
  }
@@ -89,13 +97,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
89
97
  let cond: number | boolean = true;
90
98
  if (cond = success) {
91
99
  if (cond = !this._dec.end()) {
92
- this._metrics = imageType(this._dec.data8);
93
- if (cond = this._metrics.mime !== 'unsupported') {
94
- w = this._metrics.width;
95
- h = this._metrics.height;
96
- if (cond = w && h && w * h < this._opts.pixelLimit) {
97
- [w, h] = this._resize(w, h).map(Math.floor);
98
- cond = w && h && w * h < this._opts.pixelLimit;
100
+ if (cond = this._dec.data8.length === this._header.size) {
101
+ this._metrics = imageType(this._dec.data8);
102
+ if (cond = this._metrics.mime !== 'unsupported') {
103
+ w = this._metrics.width;
104
+ h = this._metrics.height;
105
+ if (cond = w && h && w * h < this._opts.pixelLimit) {
106
+ [w, h] = this._resize(w, h).map(Math.floor);
107
+ cond = w && h && w * h < this._opts.pixelLimit;
108
+ }
99
109
  }
100
110
  }
101
111
  }
@@ -105,7 +115,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
105
115
  return true;
106
116
  }
107
117
 
108
- const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
118
+ // HACK: The types on Blob are too restrictive, this is a Uint8Array so the browser accepts it
119
+ const blob = new Blob([this._dec.data8 as Uint8Array<ArrayBuffer>], { type: this._metrics.mime });
109
120
  this._dec.release();
110
121
 
111
122
  if (!window.createImageBitmap) {
@@ -8,6 +8,7 @@ declare const Buffer: any;
8
8
 
9
9
 
10
10
  export interface IHeaderFields {
11
+ [key: string]: number | string | Uint32Array | null | undefined;
11
12
  // base-64 encoded filename. Defaults to "Unnamed file".
12
13
  name: string;
13
14
  // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -81,7 +82,7 @@ function toName(data: Uint32Array): string {
81
82
  return new TextDecoder().decode(b);
82
83
  }
83
84
 
84
- const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
85
+ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
85
86
  inline: toInt,
86
87
  size: toInt,
87
88
  name: toName,
@@ -100,7 +101,7 @@ export class HeaderParser {
100
101
  private _buffer = new Uint32Array(MAX_FIELDCHARS);
101
102
  private _position = 0;
102
103
  private _key = '';
103
- public fields: {[key: string]: any} = {};
104
+ public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
104
105
 
105
106
  public reset(): void {
106
107
  this._buffer.fill(0);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { ImageStorage } from './ImageStorage';
7
+
8
+ /**
9
+ * IIP (iTerm Image Protocol) specific image storage controller.
10
+ *
11
+ * Wraps the shared ImageStorage with IIP protocol semantics:
12
+ * - Always uses scrolling mode (cursor advances with image)
13
+ */
14
+ export class IIPImageStorage {
15
+ constructor(
16
+ private readonly _storage: ImageStorage
17
+ ) {}
18
+
19
+ /**
20
+ * Add an IIP image to storage.
21
+ * Always uses scrolling mode — cursor advances past the image.
22
+ */
23
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
24
+ this._storage.addImage(img, true);
25
+ }
26
+ }
package/src/ImageAddon.ts CHANGED
@@ -5,10 +5,15 @@
5
5
 
6
6
  import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
7
7
  import type { ImageAddon as IImageApi } from '@xterm/addon-image';
8
+ import { Emitter, type IEvent } from 'common/Event';
8
9
  import { IIPHandler } from './IIPHandler';
9
10
  import { ImageRenderer } from './ImageRenderer';
10
11
  import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
12
+ import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
13
+ import { KittyImageStorage } from './kitty/KittyImageStorage';
11
14
  import { SixelHandler } from './SixelHandler';
15
+ import { SixelImageStorage } from './SixelImageStorage';
16
+ import { IIPImageStorage } from './IIPImageStorage';
12
17
  import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
13
18
 
14
19
  // default values of addon ctor options
@@ -22,7 +27,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
22
27
  storageLimit: 128,
23
28
  showPlaceholder: true,
24
29
  iipSupport: true,
25
- iipSizeLimit: 20000000
30
+ iipSizeLimit: 20000000,
31
+ kittySupport: true,
32
+ kittySizeLimit: 20000000
26
33
  };
27
34
 
28
35
  // max palette size supported by the sixel lib (compile time setting)
@@ -48,7 +55,7 @@ const enum GaStatus {
48
55
  }
49
56
 
50
57
 
51
- export class ImageAddon implements ITerminalAddon , IImageApi {
58
+ export class ImageAddon implements ITerminalAddon, IImageApi {
52
59
  private _opts: IImageAddonOptions;
53
60
  private _defaultOpts: IImageAddonOptions;
54
61
  private _storage: ImageStorage | undefined;
@@ -56,6 +63,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
56
63
  private _disposables: IDisposable[] = [];
57
64
  private _terminal: ITerminalExt | undefined;
58
65
  private _handlers: Map<String, IResetHandler> = new Map();
66
+ private readonly _onImageAdded = new Emitter<void>();
67
+ public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
59
68
 
60
69
  constructor(opts?: Partial<IImageAddonOptions>) {
61
70
  this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
@@ -68,6 +77,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
68
77
  }
69
78
  this._disposables.length = 0;
70
79
  this._handlers.clear();
80
+ this._onImageAdded.dispose();
71
81
  }
72
82
 
73
83
  private _disposeLater(...args: IDisposable[]): void {
@@ -82,6 +92,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
82
92
  // internal data structures
83
93
  this._renderer = new ImageRenderer(terminal);
84
94
  this._storage = new ImageStorage(terminal, this._renderer, this._opts);
95
+ this._storage.onImageAdded = () => this._onImageAdded.fire();
85
96
 
86
97
  // enable size reports
87
98
  if (this._opts.enableSizeReports) {
@@ -90,7 +101,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
90
101
  // windowOptions.getCellSizePixels = true;
91
102
  // windowOptions.getWinSizeChars = true;
92
103
  // terminal.setOption('windowOptions', windowOptions);
93
- const windowOps = terminal.options.windowOptions || {};
104
+ const windowOps = terminal.options.windowOptions ?? {};
94
105
  windowOps.getWinSizePixels = true;
95
106
  windowOps.getCellSizePixels = true;
96
107
  windowOps.getWinSizeChars = true;
@@ -129,7 +140,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
129
140
 
130
141
  // SIXEL handler
131
142
  if (this._opts.sixelSupport) {
132
- const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
143
+ const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
144
+ const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
133
145
  this._handlers.set('sixel', sixelHandler);
134
146
  this._disposeLater(
135
147
  terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
@@ -138,12 +150,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
138
150
 
139
151
  // iTerm IIP handler
140
152
  if (this._opts.iipSupport) {
141
- const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
153
+ const iipStorage = new IIPImageStorage(this._storage!);
154
+ const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
142
155
  this._handlers.set('iip', iipHandler);
143
156
  this._disposeLater(
144
157
  terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
145
158
  );
146
159
  }
160
+
161
+ // Kitty graphics handler
162
+ if (this._opts.kittySupport) {
163
+ const kittyStorage = new KittyImageStorage(this._storage!);
164
+ const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
165
+ this._handlers.set('kitty', kittyHandler);
166
+ this._disposeLater(
167
+ kittyStorage,
168
+ kittyHandler,
169
+ terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
170
+ );
171
+ }
147
172
  }
148
173
 
149
174
  // Note: storageLimit is skipped here to not intoduce a surprising side effect.
@@ -5,8 +5,8 @@
5
5
 
6
6
  import { toRGBA8888 } from 'sixel/lib/Colors';
7
7
  import { IDisposable } from '@xterm/xterm';
8
- import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
9
- import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
8
+ import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
9
+ import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
10
10
 
11
11
  const PLACEHOLDER_LENGTH = 4096;
12
12
  const PLACEHOLDER_HEIGHT = 24;
@@ -18,8 +18,9 @@ const PLACEHOLDER_HEIGHT = 24;
18
18
  * - draw image tiles onRender
19
19
  */
20
20
  export class ImageRenderer extends Disposable implements IDisposable {
21
- public canvas: HTMLCanvasElement | undefined;
22
- private _ctx: CanvasRenderingContext2D | null | undefined;
21
+ /** @deprecated Kept for backward compat — points to top layer canvas. */
22
+ public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
23
+ private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
23
24
  private _placeholder: HTMLCanvasElement | undefined;
24
25
  private _placeholderBitmap: ImageBitmap | undefined;
25
26
  private _optionsRefresh = this._register(new MutableDisposable());
@@ -38,7 +39,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
38
39
  * Only the DOM output canvas should be on the terminal's document,
39
40
  * which gets explicitly checked in `insertLayerToDom`.
40
41
  */
41
- const canvas = (localDocument || document).createElement('canvas');
42
+ const canvas = (localDocument ?? document).createElement('canvas');
42
43
  canvas.width = width | 0;
43
44
  canvas.height = height | 0;
44
45
  return canvas;
@@ -86,6 +87,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
86
87
  });
87
88
  this._register(toDisposable(() => {
88
89
  this.removeLayerFromDom();
90
+ this.removeLayerFromDom('bottom');
89
91
  if (this._terminal._core && this._oldOpen) {
90
92
  this._terminal._core.open = this._oldOpen;
91
93
  this._oldOpen = undefined;
@@ -95,8 +97,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
95
97
  this._oldSetRenderer = undefined;
96
98
  }
97
99
  this._renderService = undefined;
98
- this.canvas = undefined;
99
- this._ctx = undefined;
100
+ this._layers.clear();
100
101
  this._placeholderBitmap?.close();
101
102
  this._placeholderBitmap = undefined;
102
103
  this._placeholder = undefined;
@@ -124,7 +125,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
124
125
  * Forwarded from internal render service.
125
126
  */
126
127
  public get dimensions(): IRenderDimensions | undefined {
127
- return this._renderService?.dimensions;
128
+ return this._terminal.dimensions;
128
129
  }
129
130
 
130
131
  /**
@@ -140,27 +141,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
140
141
  /**
141
142
  * Clear a region of the image layer canvas.
142
143
  */
143
- public clearLines(start: number, end: number): void {
144
- this._ctx?.clearRect(
145
- 0,
146
- start * (this.dimensions?.css.cell.height || 0),
147
- this.dimensions?.css.canvas.width || 0,
148
- (++end - start) * (this.dimensions?.css.cell.height || 0)
149
- );
144
+ public clearLines(start: number, end: number, layer?: ImageLayer): void {
145
+ const y = start * (this.dimensions?.css.cell.height || 0);
146
+ const w = this.dimensions?.css.canvas.width || 0;
147
+ const h = (++end - start) * (this.dimensions?.css.cell.height || 0);
148
+ if (!layer || layer === 'top') {
149
+ this._layers.get('top')?.clearRect(0, y, w, h);
150
+ }
151
+ if (!layer || layer === 'bottom') {
152
+ this._layers.get('bottom')?.clearRect(0, y, w, h);
153
+ }
150
154
  }
151
155
 
152
156
  /**
153
157
  * Clear whole image canvas.
154
158
  */
155
- public clearAll(): void {
156
- this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
159
+ public clearAll(layer?: ImageLayer): void {
160
+ if (!layer || layer === 'top') {
161
+ const ctx = this._layers.get('top');
162
+ ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
163
+ }
164
+ if (!layer || layer === 'bottom') {
165
+ const ctx = this._layers.get('bottom');
166
+ ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
167
+ }
157
168
  }
158
169
 
159
170
  /**
160
171
  * Draw neighboring tiles on the image layer canvas.
161
172
  */
162
173
  public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
163
- if (!this._ctx) {
174
+ const ctx = this._layers.get(imgSpec.layer);
175
+ if (!ctx) {
164
176
  return;
165
177
  }
166
178
  const { width, height } = this.cellSize;
@@ -187,7 +199,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
187
199
  // Note: For not pixel perfect aligned cells like in the DOM renderer
188
200
  // this will move a tile slightly to the top/left (subpixel range, thus ignore it).
189
201
  // FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
190
- this._ctx.drawImage(
202
+ ctx.drawImage(
191
203
  img,
192
204
  Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
193
205
  Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
@@ -227,7 +239,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
227
239
  * Draw a line with placeholder on the image layer canvas.
228
240
  */
229
241
  public drawPlaceholder(col: number, row: number, count: number = 1): void {
230
- if (this._ctx) {
242
+ const ctx = this._layers.get('top');
243
+ if (ctx) {
231
244
  const { width, height } = this.cellSize;
232
245
 
233
246
  // Don't try to draw anything, if we cannot get valid renderer metrics.
@@ -241,8 +254,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
241
254
  this._createPlaceHolder(height + 1);
242
255
  }
243
256
  if (!this._placeholder) return;
244
- this._ctx.drawImage(
245
- this._placeholderBitmap || this._placeholder!,
257
+ ctx.drawImage(
258
+ this._placeholderBitmap ?? this._placeholder!,
246
259
  col * width,
247
260
  (row * height) % 2 ? 0 : 1, // needs %2 offset correction
248
261
  width * count,
@@ -260,12 +273,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
260
273
  * Checked once from `ImageStorage.render`.
261
274
  */
262
275
  public rescaleCanvas(): void {
263
- if (!this.canvas) {
264
- return;
265
- }
266
- if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
267
- this.canvas.width = this.dimensions!.css.canvas.width || 0;
268
- this.canvas.height = this.dimensions!.css.canvas.height || 0;
276
+ const w = this.dimensions?.css.canvas.width || 0;
277
+ const h = this.dimensions?.css.canvas.height || 0;
278
+ for (const ctx of this._layers.values()) {
279
+ if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
280
+ ctx.canvas.width = w;
281
+ ctx.canvas.height = h;
282
+ }
269
283
  }
270
284
  }
271
285
 
@@ -304,37 +318,67 @@ export class ImageRenderer extends Disposable implements IDisposable {
304
318
  this._renderService = this._terminal._core._renderService;
305
319
  this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
306
320
  this._renderService.setRenderer = (renderer: any) => {
307
- this.removeLayerFromDom();
321
+ for (const key of [...this._layers.keys()]) {
322
+ this.removeLayerFromDom(key);
323
+ }
308
324
  this._oldSetRenderer?.call(this._renderService, renderer);
309
325
  };
310
326
  }
311
327
 
312
- public insertLayerToDom(): void {
328
+ public insertLayerToDom(layer: ImageLayer = 'top'): void {
313
329
  // make sure that the terminal is attached to a document and to DOM
314
- if (this.document && this._terminal._core.screenElement) {
315
- if (!this.canvas) {
316
- this.canvas = ImageRenderer.createCanvas(
317
- this.document, this.dimensions?.css.canvas.width || 0,
318
- this.dimensions?.css.canvas.height || 0
319
- );
320
- this.canvas.classList.add('xterm-image-layer');
321
- this._terminal._core.screenElement.appendChild(this.canvas);
322
- this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
323
- this.clearAll();
324
- }
325
- } else {
330
+ if (!this.document || !this._terminal._core.screenElement) {
326
331
  console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
332
+ return;
333
+ }
334
+ if (this._layers.has(layer)) {
335
+ return;
327
336
  }
337
+ const canvas = ImageRenderer.createCanvas(
338
+ this.document, this.dimensions?.css.canvas.width || 0,
339
+ this.dimensions?.css.canvas.height || 0
340
+ );
341
+ canvas.classList.add(`xterm-image-layer-${layer}`);
342
+ const screenElement = this._terminal._core.screenElement;
343
+ // Use isolation to create a stacking context without overriding z-index,
344
+ // which would conflict with integrators (e.g. VS Code) that set their
345
+ // own z-index on the screen element.
346
+ screenElement.style.isolation = 'isolate';
347
+ if (layer === 'bottom') {
348
+ // Use z-index:-1 so it paints behind non-positioned text elements.
349
+ // The screen element needs to be a stacking context (via isolation)
350
+ // to contain the negative z-index, otherwise it would go behind the
351
+ // entire terminal.
352
+ canvas.style.zIndex = '-1';
353
+ screenElement.insertBefore(canvas, screenElement.firstChild);
354
+ } else {
355
+ // Explicit z-index ensures the image canvas reliably stacks above
356
+ // the text layer (DOM renderer rows). z-index: 0 is below the
357
+ // selection overlay (z-index: 1).
358
+ canvas.style.zIndex = '0';
359
+ screenElement.appendChild(canvas);
360
+ }
361
+ const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
362
+ if (!ctx) {
363
+ canvas.remove();
364
+ return;
365
+ }
366
+ this._layers.set(layer, ctx);
367
+ this.clearAll(layer);
328
368
  }
329
369
 
330
- public removeLayerFromDom(): void {
331
- if (this.canvas) {
332
- this._ctx = undefined;
333
- this.canvas.remove();
334
- this.canvas = undefined;
370
+ public removeLayerFromDom(layer: ImageLayer = 'top'): void {
371
+ const ctx = this._layers.get(layer);
372
+ if (ctx) {
373
+ ctx.canvas.remove();
374
+ this._layers.delete(layer);
335
375
  }
336
376
  }
337
377
 
378
+ public hasLayer(layer: ImageLayer): boolean {
379
+ return this._layers.has(layer);
380
+ }
381
+
338
382
  private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
339
383
  this._placeholderBitmap?.close();
340
384
  this._placeholderBitmap = undefined;