@xterm/addon-image 0.10.0-beta.21 → 0.10.0-beta.211

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.
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { IDisposable } from '@xterm/xterm';
7
7
  import { ImageRenderer } from './ImageRenderer';
8
- import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
8
+ import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
9
9
 
10
10
 
11
11
  // fallback default cell size
@@ -124,6 +124,8 @@ export class ImageStorage implements IDisposable {
124
124
  private _pixelLimit: number = 2500000;
125
125
 
126
126
  private _viewportMetrics: { cols: number, rows: number };
127
+ public onImageAdded: (() => void) | undefined;
128
+ public onImageDeleted: ((storageId: number) => void) | undefined;
127
129
 
128
130
  constructor(
129
131
  private _terminal: ITerminalExt,
@@ -132,8 +134,10 @@ export class ImageStorage implements IDisposable {
132
134
  ) {
133
135
  try {
134
136
  this.setLimit(this._opts.storageLimit);
135
- } catch (e: any) {
136
- console.error(e.message);
137
+ } catch (e: unknown) {
138
+ if (e instanceof Error) {
139
+ console.error(e.message);
140
+ }
137
141
  console.warn(`storageLimit is set to ${this.getLimit()} MB`);
138
142
  }
139
143
  this._viewportMetrics = {
@@ -187,11 +191,13 @@ export class ImageStorage implements IDisposable {
187
191
 
188
192
  private _delImg(id: number): void {
189
193
  const spec = this._images.get(id);
194
+ if (!spec) return;
190
195
  this._images.delete(id);
191
196
  // FIXME: really ugly workaround to get bitmaps deallocated :(
192
- if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
197
+ if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
193
198
  spec.orig.close();
194
199
  }
200
+ this.onImageDeleted?.(id);
195
201
  }
196
202
 
197
203
  /**
@@ -215,27 +221,27 @@ export class ImageStorage implements IDisposable {
215
221
  }
216
222
 
217
223
  /**
218
- * Only advance text cursor.
219
- * This is an edge case from empty sixels carrying only a height but no pixels.
220
- * Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
224
+ * Delete an image by its internal storage ID.
225
+ * Used by protocols that support explicit deletion (e.g. Kitty a=d).
221
226
  */
222
- public advanceCursor(height: number): void {
223
- if (this._opts.sixelScrolling) {
224
- let cellSize = this._renderer.cellSize;
225
- if (cellSize.width === -1 || cellSize.height === -1) {
226
- cellSize = CELL_SIZE_DEFAULT;
227
- }
228
- const rows = Math.ceil(height / cellSize.height);
229
- for (let i = 1; i < rows; ++i) {
230
- this._terminal._core._inputHandler.lineFeed();
231
- }
227
+ public deleteImage(id: number): void {
228
+ const spec = this._images.get(id);
229
+ if (spec) {
230
+ spec.marker?.dispose();
231
+ this._delImg(id);
232
232
  }
233
233
  }
234
234
 
235
235
  /**
236
236
  * Method to add an image to the storage.
237
+ * @param img - The image to add (canvas or bitmap).
238
+ * @param scrolling - When true, cursor advances with the image (lineFeed per row).
239
+ * When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
240
+ * @param layer - Which canvas layer to render on ('top' or 'bottom').
241
+ * @param zIndex - Z-index for image layering within the same layer.
242
+ * @returns The internal image ID assigned to the stored image.
237
243
  */
238
- public addImage(img: HTMLCanvasElement | ImageBitmap): void {
244
+ public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
239
245
  // never allow storage to exceed memory limit
240
246
  this._evictOldest(img.width * img.height);
241
247
 
@@ -257,7 +263,7 @@ export class ImageStorage implements IDisposable {
257
263
  let offset = originX;
258
264
  let tileCount = 0;
259
265
 
260
- if (!this._opts.sixelScrolling) {
266
+ if (!scrolling) {
261
267
  buffer.x = 0;
262
268
  buffer.y = 0;
263
269
  offset = 0;
@@ -271,7 +277,7 @@ export class ImageStorage implements IDisposable {
271
277
  this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
272
278
  tileCount++;
273
279
  }
274
- if (this._opts.sixelScrolling) {
280
+ if (scrolling) {
275
281
  if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
276
282
  } else {
277
283
  if (++buffer.y >= termRows) break;
@@ -281,7 +287,7 @@ export class ImageStorage implements IDisposable {
281
287
  this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
282
288
 
283
289
  // cursor positioning modes
284
- if (this._opts.sixelScrolling) {
290
+ if (scrolling) {
285
291
  buffer.x = offset;
286
292
  } else {
287
293
  buffer.x = originX;
@@ -324,11 +330,15 @@ export class ImageStorage implements IDisposable {
324
330
  actualCellSize: { ...cellSize }, // clone needed, since later modified
325
331
  marker: endMarker || undefined,
326
332
  tileCount,
327
- bufferType: this._terminal.buffer.active.type
333
+ bufferType: this._terminal.buffer.active.type,
334
+ layer,
335
+ zIndex
328
336
  };
329
337
 
330
338
  // finally add the image
331
339
  this._images.set(imageId, imgSpec);
340
+ this.onImageAdded?.();
341
+ return imageId;
332
342
  }
333
343
 
334
344
 
@@ -338,16 +348,30 @@ export class ImageStorage implements IDisposable {
338
348
  */
339
349
  // TODO: Should we move this to the ImageRenderer?
340
350
  public render(range: { start: number, end: number }): void {
341
- // setup image canvas in case we have none yet, but have images in store
342
- if (!this._renderer.canvas && this._images.size) {
343
- this._renderer.insertLayerToDom();
344
- // safety measure - in case we cannot spawn a canvas at all, just exit
345
- if (!this._renderer.canvas) {
346
- return;
351
+ // Determine which layers have images
352
+ let hasTopImages = false;
353
+ let hasBottomImages = false;
354
+ for (const spec of this._images.values()) {
355
+ if (spec.layer === 'bottom') {
356
+ hasBottomImages = true;
357
+ } else {
358
+ hasTopImages = true;
347
359
  }
360
+ if (hasTopImages && hasBottomImages) break;
361
+ }
362
+
363
+ // Lazily insert layers that are needed
364
+ if (hasTopImages && !this._renderer.hasLayer('top')) {
365
+ this._renderer.insertLayerToDom('top');
366
+ if (!this._renderer.hasLayer('top')) return;
367
+ }
368
+ if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
369
+ this._renderer.insertLayerToDom('bottom');
348
370
  }
371
+
349
372
  // rescale if needed
350
373
  this._renderer.rescaleCanvas();
374
+
351
375
  // exit early if we dont have any images to test for
352
376
  if (!this._images.size) {
353
377
  if (!this._fullyCleared) {
@@ -355,12 +379,25 @@ export class ImageStorage implements IDisposable {
355
379
  this._fullyCleared = true;
356
380
  this._needsFullClear = false;
357
381
  }
358
- if (this._renderer.canvas) {
359
- this._renderer.removeLayerFromDom();
382
+ if (this._renderer.hasLayer('top')) {
383
+ this._renderer.removeLayerFromDom('top');
384
+ }
385
+ if (this._renderer.hasLayer('bottom')) {
386
+ this._renderer.removeLayerFromDom('bottom');
360
387
  }
361
388
  return;
362
389
  }
363
390
 
391
+ // Remove layers no longer needed
392
+ if (!hasTopImages && this._renderer.hasLayer('top')) {
393
+ this._renderer.clearAll('top');
394
+ this._renderer.removeLayerFromDom('top');
395
+ }
396
+ if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
397
+ this._renderer.clearAll('bottom');
398
+ this._renderer.removeLayerFromDom('bottom');
399
+ }
400
+
364
401
  // buffer switches force a full clear
365
402
  if (this._needsFullClear) {
366
403
  this._renderer.clearAll();
@@ -375,50 +412,77 @@ export class ImageStorage implements IDisposable {
375
412
  // clear drawing area
376
413
  this._renderer.clearLines(start, end);
377
414
 
378
- // walk all cells in viewport and draw tiles found
415
+ // Collect draw calls so we can sort by z-index (lower z drawn first).
416
+ const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
417
+ const placeholderCalls: { col: number, row: number, count: number }[] = [];
418
+
419
+ // walk all cells in viewport and collect tiles found
420
+ // Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
421
+ // because text writes clear the BG flag but leave image tile data intact.
422
+ // This lets top-layer images survive text overwrites (kitty C=1 behavior).
379
423
  for (let row = start; row <= end; ++row) {
380
424
  const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
381
425
  if (!line) return;
382
426
  for (let col = 0; col < cols; ++col) {
427
+ let e: IExtendedAttrsImage;
383
428
  if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
384
- let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
385
- const imageId = e.imageId;
386
- if (imageId === undefined || imageId === -1) {
429
+ e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
430
+ } else {
431
+ const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
432
+ if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
387
433
  continue;
388
434
  }
389
- const imgSpec = this._images.get(imageId);
390
- if (e.tileId !== -1) {
391
- const startTile = e.tileId;
392
- const startCol = col;
393
- let count = 1;
394
- /**
395
- * merge tiles to the right into a single draw call, if:
396
- * - not at end of line
397
- * - cell has same image id
398
- * - cell has consecutive tile id
399
- */
400
- while (
401
- ++col < cols
402
- && (line.getBg(col) & BgFlags.HAS_EXTENDED)
403
- && (e = line._extendedAttrs[col] || EMPTY_ATTRS)
404
- && (e.imageId === imageId)
405
- && (e.tileId === startTile + count)
406
- ) {
407
- count++;
435
+ e = maybeImg;
436
+ }
437
+ const imageId = e.imageId;
438
+ if (imageId === undefined || imageId === -1) {
439
+ continue;
440
+ }
441
+ const imgSpec = this._images.get(imageId);
442
+ if (e.tileId !== -1) {
443
+ const startTile = e.tileId;
444
+ const startCol = col;
445
+ let count = 1;
446
+ /**
447
+ * merge tiles to the right into a single draw call, if:
448
+ * - not at end of line
449
+ * - cell has same image id
450
+ * - cell has consecutive tile id
451
+ * Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
452
+ */
453
+ while (++col < cols) {
454
+ const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
455
+ if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
456
+ break;
408
457
  }
409
- col--;
410
- if (imgSpec) {
411
- if (imgSpec.actual) {
412
- this._renderer.draw(imgSpec, startTile, startCol, row, count);
413
- }
414
- } else if (this._opts.showPlaceholder) {
415
- this._renderer.drawPlaceholder(startCol, row, count);
458
+ e = nextE;
459
+ count++;
460
+ }
461
+ col--;
462
+ if (imgSpec) {
463
+ if (imgSpec.actual) {
464
+ drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
416
465
  }
417
- this._fullyCleared = false;
466
+ } else if (this._opts.showPlaceholder) {
467
+ placeholderCalls.push({ col: startCol, row, count });
418
468
  }
469
+ this._fullyCleared = false;
419
470
  }
420
471
  }
421
472
  }
473
+
474
+ // Sort by z-index so lower z draws first (higher z renders on top)
475
+ drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
476
+
477
+ // Draw placeholders first (lowest priority)
478
+ for (const call of placeholderCalls) {
479
+ this._renderer.drawPlaceholder(call.col, call.row, call.count);
480
+ }
481
+
482
+ // Draw images in z-index order
483
+ for (const call of drawCalls) {
484
+ this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
485
+ }
422
486
  }
423
487
 
424
488
  public viewportResize(metrics: { cols: number, rows: number }): void {
@@ -442,7 +506,7 @@ export class ImageStorage implements IDisposable {
442
506
  for (let row = 0; row < rows; ++row) {
443
507
  const line = buffer.lines.get(row) as IBufferLineExt;
444
508
  if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
445
- const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
509
+ const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
446
510
  const imageId = e.imageId;
447
511
  if (imageId === undefined || imageId === -1) {
448
512
  continue;
@@ -487,7 +551,7 @@ export class ImageStorage implements IDisposable {
487
551
  const buffer = this._terminal._core.buffer;
488
552
  const line = buffer.lines.get(y) as IBufferLineExt;
489
553
  if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
490
- const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
554
+ const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
491
555
  if (e.imageId && e.imageId !== -1) {
492
556
  const orig = this._images.get(e.imageId)?.orig;
493
557
  if (window.ImageBitmap && orig instanceof ImageBitmap) {
@@ -507,7 +571,7 @@ export class ImageStorage implements IDisposable {
507
571
  const buffer = this._terminal._core.buffer;
508
572
  const line = buffer.lines.get(y) as IBufferLineExt;
509
573
  if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
510
- const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
574
+ const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
511
575
  if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
512
576
  const spec = this._images.get(e.imageId);
513
577
  if (spec) {
@@ -3,7 +3,7 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { ImageStorage } from './ImageStorage';
6
+ import { SixelImageStorage } from './SixelImageStorage';
7
7
  import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
8
8
  import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
9
9
  import { RGBA8888 } from 'sixel/lib/Types';
@@ -26,7 +26,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
26
26
 
27
27
  constructor(
28
28
  private readonly _opts: IImageAddonOptions,
29
- private readonly _storage: ImageStorage,
29
+ private readonly _storage: SixelImageStorage,
30
30
  private readonly _coreTerminal: ITerminalExt
31
31
  ) {
32
32
  DecoderAsync({
@@ -99,7 +99,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
99
99
  }
100
100
 
101
101
  const canvas = ImageRenderer.createCanvas(undefined, width, height);
102
- canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
102
+ canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
103
103
  if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
104
104
  this._dec.release();
105
105
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Copyright (c) 2020 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
7
+ import { IImageAddonOptions, ITerminalExt } from './Types';
8
+ import { ImageRenderer } from './ImageRenderer';
9
+
10
+ /**
11
+ * Sixel-specific image storage controller.
12
+ *
13
+ * Wraps the shared ImageStorage with sixel protocol semantics:
14
+ * - Cursor behavior governed by DECSET 80 (sixelScrolling option)
15
+ * - advanceCursor for empty sixels carrying only height
16
+ */
17
+ export class SixelImageStorage {
18
+ constructor(
19
+ private readonly _storage: ImageStorage,
20
+ private readonly _opts: IImageAddonOptions,
21
+ private readonly _renderer: ImageRenderer,
22
+ private readonly _terminal: ITerminalExt
23
+ ) {}
24
+
25
+ /**
26
+ * Add a sixel image to storage.
27
+ * Cursor behavior depends on the sixelScrolling option (DECSET 80).
28
+ */
29
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
30
+ this._storage.addImage(img, this._opts.sixelScrolling);
31
+ }
32
+
33
+ /**
34
+ * Only advance text cursor.
35
+ * This is an edge case from empty sixels carrying only a height but no pixels.
36
+ * Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
37
+ */
38
+ public advanceCursor(height: number): void {
39
+ if (this._opts.sixelScrolling) {
40
+ let cellSize = this._renderer.cellSize;
41
+ if (cellSize.width === -1 || cellSize.height === -1) {
42
+ cellSize = CELL_SIZE_DEFAULT;
43
+ }
44
+ const rows = Math.ceil(height / cellSize.height);
45
+ for (let i = 1; i < rows; ++i) {
46
+ this._terminal._core._inputHandler.lineFeed();
47
+ }
48
+ }
49
+ }
50
+ }
package/src/Types.ts CHANGED
@@ -8,7 +8,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
8
8
  // private imports from base repo we build against
9
9
  import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
10
10
  import type { AttributeData } from 'common/buffer/AttributeData';
11
- import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
11
+ import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
12
12
  import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
13
13
  import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
14
14
  import type { IRenderDimensions } from 'browser/renderer/shared/Types';
@@ -22,7 +22,7 @@ export const enum Cell {
22
22
  }
23
23
 
24
24
  // export some privates for local usage
25
- export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
25
+ export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
26
26
 
27
27
  /**
28
28
  * Plugin ctor options.
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
38
38
  sixelSizeLimit: number;
39
39
  iipSupport: boolean;
40
40
  iipSizeLimit: number;
41
+ kittySupport: boolean;
42
+ kittySizeLimit: number;
41
43
  }
42
44
 
43
45
  export interface IResetHandler {
@@ -97,6 +99,8 @@ export interface ICellSize {
97
99
  height: number;
98
100
  }
99
101
 
102
+ export type ImageLayer = 'top' | 'bottom';
103
+
100
104
  export interface IImageSpec {
101
105
  orig: HTMLCanvasElement | ImageBitmap | undefined;
102
106
  origCellSize: ICellSize;
@@ -105,4 +109,6 @@ export interface IImageSpec {
105
109
  marker: IMarker | undefined;
106
110
  tileCount: number;
107
111
  bufferType: 'alternate' | 'normal';
112
+ layer: ImageLayer;
113
+ zIndex: number;
108
114
  }