@xterm/addon-image 0.10.0-beta.3 → 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.
package/src/ImageAddon.ts CHANGED
@@ -5,24 +5,72 @@
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
 
19
+
20
+ /**
21
+ * Document VT features provided by this addon.
22
+ *
23
+ * @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
24
+ *
25
+ * Sixel support is provided by the addon @xterm/addon-image with these limitations:
26
+ * - immediate coloring (no shared palette, allows high color settings of `img2sixel`)
27
+ * - max. palette size of 4096 colors
28
+ * - max. pixel width of 16K
29
+ * - max. 25 MB per sixel sequence
30
+ * - VT340 cursor positioning (begin of last sixel data row)
31
+ *
32
+ * See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image) for more details.
33
+ *
34
+ *
35
+ * @vt: #E[Supported via @xterm/addon-image.] OSC 1337 "iTerm2 Commands" "OSC 1337 ; Pt BEL" "Custom iTerm2 commands."
36
+ *
37
+ * Only the inline image protocol (IIP) is supported by the addon @xterm/addon-image with
38
+ * the following limitations:
39
+ * - sequence:
40
+ * - format: `OSC 1337 ; File=inline=1 ; size=<unencoded size> ; ... : <base64 payload> BEL`
41
+ * - size param must be set and payload may not exceed CEIL(size * 4 / 3)
42
+ * - strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding,
43
+ * no separator bytes allowed)
44
+ * - supported params: size, name, width, height, preserveAspectRatio
45
+ * - image formats: PNG, JPEG and GIF
46
+ * - no animation support (renders first image of a GIF)
47
+ * - no multipart support
48
+ * - VT340 cursor positioning (begin of last sixel data row)
49
+ *
50
+ * See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image)
51
+ * and [iTerm2 IIP docs](https://iterm2.com/documentation-images.html) for more details.
52
+ *
53
+ *
54
+ * @vt: #E[Supported via @xterm/addon-image.] APC KITTY_GRAPHICS "Kitty Graphics" "APC G Pt ST" "Kitty Graphics Protocol."
55
+ *
56
+ * Kitty graphics support is provided by the addon @xterm/addon-image.
57
+ * Note that while basic image output already works, this is still work in progress.
58
+ */
59
+
14
60
  // default values of addon ctor options
15
61
  const DEFAULT_OPTIONS: IImageAddonOptions = {
16
62
  enableSizeReports: true,
17
63
  pixelLimit: 16777216, // limit to 4096 * 4096 pixels
18
64
  sixelSupport: true,
19
65
  sixelScrolling: true,
20
- sixelPaletteLimit: 256,
21
- sixelSizeLimit: 25000000,
66
+ sixelPaletteLimit: 4096,
67
+ sixelSizeLimit: 33554432,
22
68
  storageLimit: 128,
23
69
  showPlaceholder: true,
24
70
  iipSupport: true,
25
- iipSizeLimit: 20000000
71
+ iipSizeLimit: 33554432,
72
+ kittySupport: true,
73
+ kittySizeLimit: 33554432
26
74
  };
27
75
 
28
76
  // max palette size supported by the sixel lib (compile time setting)
@@ -48,7 +96,7 @@ const enum GaStatus {
48
96
  }
49
97
 
50
98
 
51
- export class ImageAddon implements ITerminalAddon , IImageApi {
99
+ export class ImageAddon implements ITerminalAddon, IImageApi {
52
100
  private _opts: IImageAddonOptions;
53
101
  private _defaultOpts: IImageAddonOptions;
54
102
  private _storage: ImageStorage | undefined;
@@ -56,6 +104,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
56
104
  private _disposables: IDisposable[] = [];
57
105
  private _terminal: ITerminalExt | undefined;
58
106
  private _handlers: Map<String, IResetHandler> = new Map();
107
+ private readonly _onImageAdded = new Emitter<void>();
108
+ public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
59
109
 
60
110
  constructor(opts?: Partial<IImageAddonOptions>) {
61
111
  this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
@@ -68,6 +118,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
68
118
  }
69
119
  this._disposables.length = 0;
70
120
  this._handlers.clear();
121
+ this._onImageAdded.dispose();
71
122
  }
72
123
 
73
124
  private _disposeLater(...args: IDisposable[]): void {
@@ -82,15 +133,11 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
82
133
  // internal data structures
83
134
  this._renderer = new ImageRenderer(terminal);
84
135
  this._storage = new ImageStorage(terminal, this._renderer, this._opts);
136
+ this._storage.onImageAdded = () => this._onImageAdded.fire();
85
137
 
86
138
  // enable size reports
87
139
  if (this._opts.enableSizeReports) {
88
- // const windowOptions = terminal.getOption('windowOptions');
89
- // windowOptions.getWinSizePixels = true;
90
- // windowOptions.getCellSizePixels = true;
91
- // windowOptions.getWinSizeChars = true;
92
- // terminal.setOption('windowOptions', windowOptions);
93
- const windowOps = terminal.options.windowOptions || {};
140
+ const windowOps = terminal.options.windowOptions ?? {};
94
141
  windowOps.getWinSizePixels = true;
95
142
  windowOps.getCellSizePixels = true;
96
143
  windowOps.getWinSizeChars = true;
@@ -129,7 +176,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
129
176
 
130
177
  // SIXEL handler
131
178
  if (this._opts.sixelSupport) {
132
- const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
179
+ const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
180
+ const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
133
181
  this._handlers.set('sixel', sixelHandler);
134
182
  this._disposeLater(
135
183
  terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
@@ -138,12 +186,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
138
186
 
139
187
  // iTerm IIP handler
140
188
  if (this._opts.iipSupport) {
141
- const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
189
+ const iipStorage = new IIPImageStorage(this._storage!);
190
+ const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
142
191
  this._handlers.set('iip', iipHandler);
143
192
  this._disposeLater(
144
193
  terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
145
194
  );
146
195
  }
196
+
197
+ // Kitty graphics handler
198
+ if (this._opts.kittySupport) {
199
+ const kittyStorage = new KittyImageStorage(this._storage!);
200
+ const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
201
+ this._handlers.set('kitty', kittyHandler);
202
+ this._disposeLater(
203
+ kittyStorage,
204
+ kittyHandler,
205
+ terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
206
+ );
207
+ }
147
208
  }
148
209
 
149
210
  // Note: storageLimit is skipped here to not intoduce a surprising side effect.
@@ -194,7 +255,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
194
255
  }
195
256
 
196
257
  private _report(s: string): void {
197
- this._terminal?._core.coreService.triggerDataEvent(s);
258
+ this._terminal?._core.input(s, false);
198
259
  }
199
260
 
200
261
  private _decset(params: (number | number[])[]): boolean {
@@ -5,11 +5,13 @@
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
- const PLACEHOLDER_LENGTH = 4096;
12
- const PLACEHOLDER_HEIGHT = 24;
11
+ const enum Constants {
12
+ PLACEHOLDER_LENGTH = 4096,
13
+ PLACEHOLDER_HEIGHT = 24
14
+ }
13
15
 
14
16
  /**
15
17
  * ImageRenderer - terminal frontend extension:
@@ -18,8 +20,9 @@ const PLACEHOLDER_HEIGHT = 24;
18
20
  * - draw image tiles onRender
19
21
  */
20
22
  export class ImageRenderer extends Disposable implements IDisposable {
21
- public canvas: HTMLCanvasElement | undefined;
22
- private _ctx: CanvasRenderingContext2D | null | undefined;
23
+ /** @deprecated Kept for backward compat — points to top layer canvas. */
24
+ public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
25
+ private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
23
26
  private _placeholder: HTMLCanvasElement | undefined;
24
27
  private _placeholderBitmap: ImageBitmap | undefined;
25
28
  private _optionsRefresh = this._register(new MutableDisposable());
@@ -38,7 +41,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
38
41
  * Only the DOM output canvas should be on the terminal's document,
39
42
  * which gets explicitly checked in `insertLayerToDom`.
40
43
  */
41
- const canvas = (localDocument || document).createElement('canvas');
44
+ const canvas = (localDocument ?? document).createElement('canvas');
42
45
  canvas.width = width | 0;
43
46
  canvas.height = height | 0;
44
47
  return canvas;
@@ -86,6 +89,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
86
89
  });
87
90
  this._register(toDisposable(() => {
88
91
  this.removeLayerFromDom();
92
+ this.removeLayerFromDom('bottom');
89
93
  if (this._terminal._core && this._oldOpen) {
90
94
  this._terminal._core.open = this._oldOpen;
91
95
  this._oldOpen = undefined;
@@ -95,8 +99,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
95
99
  this._oldSetRenderer = undefined;
96
100
  }
97
101
  this._renderService = undefined;
98
- this.canvas = undefined;
99
- this._ctx = undefined;
102
+ this._layers.clear();
100
103
  this._placeholderBitmap?.close();
101
104
  this._placeholderBitmap = undefined;
102
105
  this._placeholder = undefined;
@@ -109,7 +112,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
109
112
  public showPlaceholder(value: boolean): void {
110
113
  if (value) {
111
114
  if (!this._placeholder && this.cellSize.height !== -1) {
112
- this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
115
+ this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
113
116
  }
114
117
  } else {
115
118
  this._placeholderBitmap?.close();
@@ -124,7 +127,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
124
127
  * Forwarded from internal render service.
125
128
  */
126
129
  public get dimensions(): IRenderDimensions | undefined {
127
- return this._renderService?.dimensions;
130
+ return this._terminal.dimensions;
128
131
  }
129
132
 
130
133
  /**
@@ -140,27 +143,38 @@ export class ImageRenderer extends Disposable implements IDisposable {
140
143
  /**
141
144
  * Clear a region of the image layer canvas.
142
145
  */
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
- );
146
+ public clearLines(start: number, end: number, layer?: ImageLayer): void {
147
+ const y = start * (this.dimensions?.css.cell.height || 0);
148
+ const w = this.dimensions?.css.canvas.width || 0;
149
+ const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
150
+ if (!layer || layer === 'top') {
151
+ this._layers.get('top')?.clearRect(0, y, w, h);
152
+ }
153
+ if (!layer || layer === 'bottom') {
154
+ this._layers.get('bottom')?.clearRect(0, y, w, h);
155
+ }
150
156
  }
151
157
 
152
158
  /**
153
159
  * Clear whole image canvas.
154
160
  */
155
- public clearAll(): void {
156
- this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
161
+ public clearAll(layer?: ImageLayer): void {
162
+ if (!layer || layer === 'top') {
163
+ const ctx = this._layers.get('top');
164
+ ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
165
+ }
166
+ if (!layer || layer === 'bottom') {
167
+ const ctx = this._layers.get('bottom');
168
+ ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
169
+ }
157
170
  }
158
171
 
159
172
  /**
160
173
  * Draw neighboring tiles on the image layer canvas.
161
174
  */
162
175
  public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
163
- if (!this._ctx) {
176
+ const ctx = this._layers.get(imgSpec.layer);
177
+ if (!ctx) {
164
178
  return;
165
179
  }
166
180
  const { width, height } = this.cellSize;
@@ -187,7 +201,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
187
201
  // Note: For not pixel perfect aligned cells like in the DOM renderer
188
202
  // this will move a tile slightly to the top/left (subpixel range, thus ignore it).
189
203
  // FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
190
- this._ctx.drawImage(
204
+ ctx.drawImage(
191
205
  img,
192
206
  Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
193
207
  Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
@@ -227,7 +241,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
227
241
  * Draw a line with placeholder on the image layer canvas.
228
242
  */
229
243
  public drawPlaceholder(col: number, row: number, count: number = 1): void {
230
- if (this._ctx) {
244
+ const ctx = this._layers.get('top');
245
+ if (ctx) {
231
246
  const { width, height } = this.cellSize;
232
247
 
233
248
  // Don't try to draw anything, if we cannot get valid renderer metrics.
@@ -236,13 +251,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
236
251
  }
237
252
 
238
253
  if (!this._placeholder) {
239
- this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
254
+ this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
240
255
  } else if (height >= this._placeholder!.height) {
241
256
  this._createPlaceHolder(height + 1);
242
257
  }
243
258
  if (!this._placeholder) return;
244
- this._ctx.drawImage(
245
- this._placeholderBitmap || this._placeholder!,
259
+ ctx.drawImage(
260
+ this._placeholderBitmap ?? this._placeholder!,
246
261
  col * width,
247
262
  (row * height) % 2 ? 0 : 1, // needs %2 offset correction
248
263
  width * count,
@@ -260,12 +275,13 @@ export class ImageRenderer extends Disposable implements IDisposable {
260
275
  * Checked once from `ImageStorage.render`.
261
276
  */
262
277
  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;
278
+ const w = this.dimensions?.css.canvas.width || 0;
279
+ const h = this.dimensions?.css.canvas.height || 0;
280
+ for (const ctx of this._layers.values()) {
281
+ if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
282
+ ctx.canvas.width = w;
283
+ ctx.canvas.height = h;
284
+ }
269
285
  }
270
286
  }
271
287
 
@@ -304,38 +320,68 @@ export class ImageRenderer extends Disposable implements IDisposable {
304
320
  this._renderService = this._terminal._core._renderService;
305
321
  this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
306
322
  this._renderService.setRenderer = (renderer: any) => {
307
- this.removeLayerFromDom();
323
+ for (const key of [...this._layers.keys()]) {
324
+ this.removeLayerFromDom(key);
325
+ }
308
326
  this._oldSetRenderer?.call(this._renderService, renderer);
309
327
  };
310
328
  }
311
329
 
312
- public insertLayerToDom(): void {
330
+ public insertLayerToDom(layer: ImageLayer = 'top'): void {
313
331
  // 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 {
332
+ if (!this.document || !this._terminal._core.screenElement) {
326
333
  console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
334
+ return;
335
+ }
336
+ if (this._layers.has(layer)) {
337
+ return;
327
338
  }
339
+ const canvas = ImageRenderer.createCanvas(
340
+ this.document, this.dimensions?.css.canvas.width || 0,
341
+ this.dimensions?.css.canvas.height || 0
342
+ );
343
+ canvas.classList.add(`xterm-image-layer-${layer}`);
344
+ const screenElement = this._terminal._core.screenElement;
345
+ // Use isolation to create a stacking context without overriding z-index,
346
+ // which would conflict with integrators (e.g. VS Code) that set their
347
+ // own z-index on the screen element.
348
+ screenElement.style.isolation = 'isolate';
349
+ if (layer === 'bottom') {
350
+ // Use z-index:-1 so it paints behind non-positioned text elements.
351
+ // The screen element needs to be a stacking context (via isolation)
352
+ // to contain the negative z-index, otherwise it would go behind the
353
+ // entire terminal.
354
+ canvas.style.zIndex = '-1';
355
+ screenElement.insertBefore(canvas, screenElement.firstChild);
356
+ } else {
357
+ // Explicit z-index ensures the image canvas reliably stacks above
358
+ // the text layer (DOM renderer rows). z-index: 0 is below the
359
+ // selection overlay (z-index: 1).
360
+ canvas.style.zIndex = '0';
361
+ screenElement.appendChild(canvas);
362
+ }
363
+ const ctx = canvas.getContext('2d', { alpha: true });
364
+ if (!ctx) {
365
+ canvas.remove();
366
+ return;
367
+ }
368
+ this._layers.set(layer, ctx);
369
+ this.clearAll(layer);
328
370
  }
329
371
 
330
- public removeLayerFromDom(): void {
331
- if (this.canvas) {
332
- this._ctx = undefined;
333
- this.canvas.remove();
334
- this.canvas = undefined;
372
+ public removeLayerFromDom(layer: ImageLayer = 'top'): void {
373
+ const ctx = this._layers.get(layer);
374
+ if (ctx) {
375
+ ctx.canvas.remove();
376
+ this._layers.delete(layer);
335
377
  }
336
378
  }
337
379
 
338
- private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
380
+ public hasLayer(layer: ImageLayer): boolean {
381
+ return this._layers.has(layer);
382
+ }
383
+
384
+ private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
339
385
  this._placeholderBitmap?.close();
340
386
  this._placeholderBitmap = undefined;
341
387
 
@@ -359,7 +405,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
359
405
  ctx.putImageData(imgData, 0, 0);
360
406
 
361
407
  // create placeholder line, width aligned to blueprint width
362
- const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
408
+ const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
363
409
  this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
364
410
  const ctx2 = this._placeholder.getContext('2d', { alpha: false });
365
411
  if (!ctx2) {