@xterm/addon-image 0.10.0-beta.30 → 0.10.0-beta.300

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,11 @@
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 {
9
+ ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
10
+ IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
11
+ UnderlineStyle, IAddImageOpts
12
+ } from './Types';
9
13
 
10
14
 
11
15
  // fallback default cell size
@@ -124,6 +128,8 @@ export class ImageStorage implements IDisposable {
124
128
  private _pixelLimit: number = 2500000;
125
129
 
126
130
  private _viewportMetrics: { cols: number, rows: number };
131
+ public onImageAdded: (() => void) | undefined;
132
+ public onImageDeleted: ((storageId: number) => void) | undefined;
127
133
 
128
134
  constructor(
129
135
  private _terminal: ITerminalExt,
@@ -132,8 +138,10 @@ export class ImageStorage implements IDisposable {
132
138
  ) {
133
139
  try {
134
140
  this.setLimit(this._opts.storageLimit);
135
- } catch (e: any) {
136
- console.error(e.message);
141
+ } catch (e: unknown) {
142
+ if (e instanceof Error) {
143
+ console.error(e.message);
144
+ }
137
145
  console.warn(`storageLimit is set to ${this.getLimit()} MB`);
138
146
  }
139
147
  this._viewportMetrics = {
@@ -187,11 +195,13 @@ export class ImageStorage implements IDisposable {
187
195
 
188
196
  private _delImg(id: number): void {
189
197
  const spec = this._images.get(id);
198
+ if (!spec) return;
190
199
  this._images.delete(id);
191
200
  // FIXME: really ugly workaround to get bitmaps deallocated :(
192
- if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
201
+ if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
193
202
  spec.orig.close();
194
203
  }
204
+ this.onImageDeleted?.(id);
195
205
  }
196
206
 
197
207
  /**
@@ -215,27 +225,29 @@ export class ImageStorage implements IDisposable {
215
225
  }
216
226
 
217
227
  /**
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.
228
+ * Delete an image by its internal storage ID.
229
+ * Used by protocols that support explicit deletion (e.g. Kitty a=d).
221
230
  */
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
- }
231
+ public deleteImage(id: number): void {
232
+ const spec = this._images.get(id);
233
+ if (spec) {
234
+ spec.marker?.dispose();
235
+ this._delImg(id);
232
236
  }
233
237
  }
234
238
 
235
239
  /**
236
240
  * Method to add an image to the storage.
241
+ * @param img - The image to add (canvas or bitmap).
242
+ * @param opts - Options for addImage:
243
+ * - scrolling: When true, cursor advances with the image.
244
+ * When false, image is placed at ORIGIN and cursor does not move.
245
+ * - layer: Which canvas layer to render on ('top' or 'bottom').
246
+ * - zIndex: Z-index for image layering within the same layer.
247
+ * - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
248
+ * @returns The internal image ID assigned to the stored image.
237
249
  */
238
- public addImage(img: HTMLCanvasElement | ImageBitmap): void {
250
+ public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
239
251
  // never allow storage to exceed memory limit
240
252
  this._evictOldest(img.width * img.height);
241
253
 
@@ -257,7 +269,7 @@ export class ImageStorage implements IDisposable {
257
269
  let offset = originX;
258
270
  let tileCount = 0;
259
271
 
260
- if (!this._opts.sixelScrolling) {
272
+ if (!opts.scrolling) {
261
273
  buffer.x = 0;
262
274
  buffer.y = 0;
263
275
  offset = 0;
@@ -271,7 +283,7 @@ export class ImageStorage implements IDisposable {
271
283
  this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
272
284
  tileCount++;
273
285
  }
274
- if (this._opts.sixelScrolling) {
286
+ if (opts.scrolling) {
275
287
  if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
276
288
  } else {
277
289
  if (++buffer.y >= termRows) break;
@@ -281,8 +293,12 @@ export class ImageStorage implements IDisposable {
281
293
  this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
282
294
 
283
295
  // cursor positioning modes
284
- if (this._opts.sixelScrolling) {
285
- buffer.x = offset;
296
+ if (opts.scrolling) {
297
+ if (opts.cursorPos === 'iip') {
298
+ buffer.x = Math.min(offset + cols, termCols);
299
+ } else {
300
+ buffer.x = offset;
301
+ }
286
302
  } else {
287
303
  buffer.x = originX;
288
304
  buffer.y = originY;
@@ -324,11 +340,15 @@ export class ImageStorage implements IDisposable {
324
340
  actualCellSize: { ...cellSize }, // clone needed, since later modified
325
341
  marker: endMarker || undefined,
326
342
  tileCount,
327
- bufferType: this._terminal.buffer.active.type
343
+ bufferType: this._terminal.buffer.active.type,
344
+ layer: opts.layer,
345
+ zIndex: opts.zIndex
328
346
  };
329
347
 
330
348
  // finally add the image
331
349
  this._images.set(imageId, imgSpec);
350
+ this.onImageAdded?.();
351
+ return imageId;
332
352
  }
333
353
 
334
354
 
@@ -338,16 +358,30 @@ export class ImageStorage implements IDisposable {
338
358
  */
339
359
  // TODO: Should we move this to the ImageRenderer?
340
360
  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;
361
+ // Determine which layers have images
362
+ let hasTopImages = false;
363
+ let hasBottomImages = false;
364
+ for (const spec of this._images.values()) {
365
+ if (spec.layer === 'bottom') {
366
+ hasBottomImages = true;
367
+ } else {
368
+ hasTopImages = true;
347
369
  }
370
+ if (hasTopImages && hasBottomImages) break;
371
+ }
372
+
373
+ // Lazily insert layers that are needed
374
+ if (hasTopImages && !this._renderer.hasLayer('top')) {
375
+ this._renderer.insertLayerToDom('top');
376
+ if (!this._renderer.hasLayer('top')) return;
348
377
  }
378
+ if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
379
+ this._renderer.insertLayerToDom('bottom');
380
+ }
381
+
349
382
  // rescale if needed
350
383
  this._renderer.rescaleCanvas();
384
+
351
385
  // exit early if we dont have any images to test for
352
386
  if (!this._images.size) {
353
387
  if (!this._fullyCleared) {
@@ -355,12 +389,25 @@ export class ImageStorage implements IDisposable {
355
389
  this._fullyCleared = true;
356
390
  this._needsFullClear = false;
357
391
  }
358
- if (this._renderer.canvas) {
359
- this._renderer.removeLayerFromDom();
392
+ if (this._renderer.hasLayer('top')) {
393
+ this._renderer.removeLayerFromDom('top');
394
+ }
395
+ if (this._renderer.hasLayer('bottom')) {
396
+ this._renderer.removeLayerFromDom('bottom');
360
397
  }
361
398
  return;
362
399
  }
363
400
 
401
+ // Remove layers no longer needed
402
+ if (!hasTopImages && this._renderer.hasLayer('top')) {
403
+ this._renderer.clearAll('top');
404
+ this._renderer.removeLayerFromDom('top');
405
+ }
406
+ if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
407
+ this._renderer.clearAll('bottom');
408
+ this._renderer.removeLayerFromDom('bottom');
409
+ }
410
+
364
411
  // buffer switches force a full clear
365
412
  if (this._needsFullClear) {
366
413
  this._renderer.clearAll();
@@ -375,13 +422,17 @@ export class ImageStorage implements IDisposable {
375
422
  // clear drawing area
376
423
  this._renderer.clearLines(start, end);
377
424
 
378
- // walk all cells in viewport and draw tiles found
425
+ // Collect draw calls so we can sort by z-index (lower z drawn first).
426
+ const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
427
+ const placeholderCalls: { col: number, row: number, count: number }[] = [];
428
+
429
+ // walk all cells in viewport and collect tiles found
379
430
  for (let row = start; row <= end; ++row) {
380
431
  const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
381
432
  if (!line) return;
382
433
  for (let col = 0; col < cols; ++col) {
383
434
  if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
384
- let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
435
+ let e: IExtendedAttrsImage = line._extendedAttrs[col] ?? EMPTY_ATTRS;
385
436
  const imageId = e.imageId;
386
437
  if (imageId === undefined || imageId === -1) {
387
438
  continue;
@@ -400,7 +451,7 @@ export class ImageStorage implements IDisposable {
400
451
  while (
401
452
  ++col < cols
402
453
  && (line.getBg(col) & BgFlags.HAS_EXTENDED)
403
- && (e = line._extendedAttrs[col] || EMPTY_ATTRS)
454
+ && (e = line._extendedAttrs[col] ?? EMPTY_ATTRS)
404
455
  && (e.imageId === imageId)
405
456
  && (e.tileId === startTile + count)
406
457
  ) {
@@ -409,16 +460,29 @@ export class ImageStorage implements IDisposable {
409
460
  col--;
410
461
  if (imgSpec) {
411
462
  if (imgSpec.actual) {
412
- this._renderer.draw(imgSpec, startTile, startCol, row, count);
463
+ drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
413
464
  }
414
465
  } else if (this._opts.showPlaceholder) {
415
- this._renderer.drawPlaceholder(startCol, row, count);
466
+ placeholderCalls.push({ col: startCol, row, count });
416
467
  }
417
468
  this._fullyCleared = false;
418
469
  }
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({
@@ -91,7 +91,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
91
91
  const height = this._dec.height;
92
92
 
93
93
  // partial fix for https://github.com/jerch/xterm-addon-image/issues/37
94
- if (!width || ! height) {
94
+ if (!width || !height) {
95
95
  if (height) {
96
96
  this._storage.advanceCursor(height);
97
97
  }
@@ -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,52 @@
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, IAddImageOpts } 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
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'vt340' };
19
+ constructor(
20
+ private readonly _storage: ImageStorage,
21
+ private readonly _opts: IImageAddonOptions,
22
+ private readonly _renderer: ImageRenderer,
23
+ private readonly _terminal: ITerminalExt
24
+ ) {}
25
+
26
+ /**
27
+ * Add a sixel image to storage.
28
+ * Cursor behavior depends on the sixelScrolling option (DECSET 80).
29
+ */
30
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
31
+ this._addImageOpts.scrolling = this._opts.sixelScrolling;
32
+ this._storage.addImage(img, this._addImageOpts);
33
+ }
34
+
35
+ /**
36
+ * Only advance text cursor.
37
+ * This is an edge case from empty sixels carrying only a height but no pixels.
38
+ * Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
39
+ */
40
+ public advanceCursor(height: number): void {
41
+ if (this._opts.sixelScrolling) {
42
+ let cellSize = this._renderer.cellSize;
43
+ if (cellSize.width === -1 || cellSize.height === -1) {
44
+ cellSize = CELL_SIZE_DEFAULT;
45
+ }
46
+ const rows = Math.ceil(height / cellSize.height);
47
+ for (let i = 1; i < rows; ++i) {
48
+ this._terminal._core._inputHandler.lineFeed();
49
+ }
50
+ }
51
+ }
52
+ }
package/src/Types.ts CHANGED
@@ -8,8 +8,9 @@ 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';
12
- import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
11
+ import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
12
+ import type { IInputHandler } from 'common/Types';
13
+ import type { IBufferLine, IExtendedAttrs } from 'common/buffer/Types';
13
14
  import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
14
15
  import type { IRenderDimensions } from 'browser/renderer/shared/Types';
15
16
  import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
@@ -22,7 +23,7 @@ export const enum Cell {
22
23
  }
23
24
 
24
25
  // export some privates for local usage
25
- export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
26
+ export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
26
27
 
27
28
  /**
28
29
  * Plugin ctor options.
@@ -38,6 +39,8 @@ export interface IImageAddonOptions {
38
39
  sixelSizeLimit: number;
39
40
  iipSupport: boolean;
40
41
  iipSizeLimit: number;
42
+ kittySupport: boolean;
43
+ kittySizeLimit: number;
41
44
  }
42
45
 
43
46
  export interface IResetHandler {
@@ -97,6 +100,16 @@ export interface ICellSize {
97
100
  height: number;
98
101
  }
99
102
 
103
+ export type ImageLayer = 'top' | 'bottom';
104
+ export type CursorPos = 'vt340' | 'iip';
105
+
106
+ export interface IAddImageOpts {
107
+ scrolling: boolean;
108
+ layer: ImageLayer;
109
+ zIndex: number;
110
+ cursorPos: CursorPos;
111
+ }
112
+
100
113
  export interface IImageSpec {
101
114
  orig: HTMLCanvasElement | ImageBitmap | undefined;
102
115
  origCellSize: ICellSize;
@@ -105,4 +118,6 @@ export interface IImageSpec {
105
118
  marker: IMarker | undefined;
106
119
  tileCount: number;
107
120
  bufferType: 'alternate' | 'normal';
121
+ layer: ImageLayer;
122
+ zIndex: number;
108
123
  }