@xterm/addon-image 0.10.0-beta.28 → 0.10.0-beta.281

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.28",
3
+ "version": "0.10.0-beta.281",
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.4.1"
29
29
  },
30
- "commit": "d4f725c6499ec3efb723c27adce3762746d0d5d6",
30
+ "commit": "2fe3fd13a164f956d0c6a2365fa89feaf4366074",
31
31
  "peerDependencies": {
32
- "@xterm/xterm": "^6.1.0-beta.28"
32
+ "@xterm/xterm": "^6.1.0-beta.281"
33
33
  }
34
34
  }
package/src/IIPHandler.ts CHANGED
@@ -4,20 +4,26 @@
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
- import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
10
+ import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
11
+ import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
10
12
  import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
11
13
 
12
-
13
- // eslint-disable-next-line
14
- declare const Buffer: any;
15
-
16
- // limit hold memory in base64 decoder
17
- const KEEP_DATA = 4194304;
14
+ // Local const enum mirror - esbuild can't inline const enums from external packages
15
+ const enum DecoderConst {
16
+ // Limit held memory in base64 decoder (encoded bytes).
17
+ KEEP_DATA = 4194304,
18
+ // Initial buffer allocation for the decoder.
19
+ INITIAL_DATA = 1048576,
20
+ // Local mirror of const enum (esbuild can't inline const enums from external packages)
21
+ OK = 0
22
+ }
18
23
 
19
24
  // default IIP header values
20
25
  const DEFAULT_HEADER: IHeaderFields = {
26
+ type: SequenceType.INVALID,
21
27
  name: 'Unnamed file',
22
28
  size: 0,
23
29
  width: 'auto',
@@ -31,21 +37,32 @@ export class IIPHandler implements IOscHandler, IResetHandler {
31
37
  private _aborted = false;
32
38
  private _hp = new HeaderParser();
33
39
  private _header: IHeaderFields = DEFAULT_HEADER;
34
- private _dec = new Base64Decoder(KEEP_DATA);
40
+ private _dec: Base64Decoder;
41
+ private _qoiDec: QoiDecoder;
35
42
  private _metrics = UNSUPPORTED_TYPE;
43
+ private _isMultipart = false;
44
+ private _abortMulti = false;
36
45
 
37
46
  constructor(
38
47
  private readonly _opts: IImageAddonOptions,
39
48
  private readonly _renderer: ImageRenderer,
40
- private readonly _storage: ImageStorage,
49
+ private readonly _storage: IIPImageStorage,
41
50
  private readonly _coreTerminal: ITerminalExt
42
- ) {}
51
+ ) {
52
+ const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
53
+ const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
54
+ this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
55
+ this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
56
+ }
43
57
 
44
- public reset(): void {}
58
+ public reset(): void {
59
+ this._hp.reset();
60
+ this._dec.release();
61
+ this._qoiDec.release();
62
+ }
45
63
 
46
64
  public start(): void {
47
65
  this._aborted = false;
48
- this._header = DEFAULT_HEADER;
49
66
  this._metrics = UNSUPPORTED_TYPE;
50
67
  this._hp.reset();
51
68
  }
@@ -54,7 +71,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
54
71
  if (this._aborted) return;
55
72
 
56
73
  if (this._hp.state === HeaderState.END) {
57
- if (this._dec.put(data, start, end)) {
74
+ if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
58
75
  this._dec.release();
59
76
  this._aborted = true;
60
77
  }
@@ -65,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
65
82
  return;
66
83
  }
67
84
  if (dataPos > 0) {
68
- this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
69
- if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
85
+ const seqType = this._hp.fields.type;
86
+ if (seqType === SequenceType.FILE) {
87
+ if (this._isMultipart) {
88
+ this._isMultipart = false;
89
+ this._abortMulti = false;
90
+ this._dec.release();
91
+ }
92
+ this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
93
+ if (!this._header.inline) {
94
+ this._aborted = true;
95
+ return;
96
+ }
97
+ this._dec.init();
98
+ } else if (this._abortMulti) {
70
99
  this._aborted = true;
71
100
  return;
72
101
  }
73
- this._dec.init(this._header.size);
74
- if (this._dec.put(data, dataPos, end)) {
102
+ if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
75
103
  this._dec.release();
76
104
  this._aborted = true;
105
+ if (this._isMultipart) this._abortMulti = true;
77
106
  }
78
107
  }
79
108
  }
@@ -82,11 +111,49 @@ export class IIPHandler implements IOscHandler, IResetHandler {
82
111
  public end(success: boolean): boolean | Promise<boolean> {
83
112
  if (this._aborted) return true;
84
113
 
114
+ if (this._hp.state !== HeaderState.END) {
115
+ if (this._hp.end()) return true;
116
+ }
117
+ const seqType = this._hp.fields.type;
118
+
119
+ if (seqType === SequenceType.FILEPART) return true;
120
+
121
+ if (seqType === SequenceType.REPORTCELLSIZE) {
122
+ // OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
123
+ let width = CELL_SIZE_DEFAULT.width;
124
+ let height = CELL_SIZE_DEFAULT.height;
125
+ if (this._renderer.dimensions) {
126
+ width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
127
+ height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
128
+ }
129
+ const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
130
+ const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
131
+ this._coreTerminal.input(report, false);
132
+ return true;
133
+ }
134
+
135
+ if (seqType === SequenceType.MULTIPARTFILE) {
136
+ this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
137
+ this._isMultipart = true;
138
+ this._abortMulti = false;
139
+ this._dec.release();
140
+ this._dec.init();
141
+ return true;
142
+ }
143
+
144
+ if (seqType === SequenceType.FILEEND) {
145
+ if (!this._isMultipart) return true;
146
+ this._isMultipart = false;
147
+ if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
148
+ }
149
+
150
+ // fallthrough for SequenceType.FILE & SequenceType.FILEEND
151
+
85
152
  let w = 0;
86
153
  let h = 0;
87
154
 
88
155
  // early exit condition chain
89
- let cond: number | boolean = true;
156
+ let cond: number | boolean;
90
157
  if (cond = success) {
91
158
  if (cond = !this._dec.end()) {
92
159
  this._metrics = imageType(this._dec.data8);
@@ -105,26 +172,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
105
172
  return true;
106
173
  }
107
174
 
108
- const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
109
- this._dec.release();
110
-
111
- if (!window.createImageBitmap) {
112
- const url = URL.createObjectURL(blob);
113
- const img = new Image();
114
- return new Promise<boolean>(r => {
115
- img.addEventListener('load', () => {
116
- URL.revokeObjectURL(url);
117
- const canvas = ImageRenderer.createCanvas(window.document, w, h);
118
- canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
119
- this._storage.addImage(canvas);
120
- r(true);
121
- });
122
- img.src = url;
123
- // sanity measure to avoid terminal blocking from dangling promise
124
- // happens from corrupt data (onload never gets fired)
125
- setTimeout(() => r(true), 1000);
126
- });
175
+ let blob: Blob | ImageData;
176
+ if (this._metrics.mime === 'image/qoi') {
177
+ const data = this._qoiDec.decode(this._dec.data8);
178
+ blob = new ImageData(
179
+ new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
180
+ this._qoiDec.width,
181
+ this._qoiDec.height
182
+ );
183
+ this._qoiDec.release();
184
+ if (w === this._qoiDec.width && h === this._qoiDec.height) {
185
+ // use fast-path if we don't need to rescale
186
+ this._dec.release();
187
+ const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
188
+ canvas.getContext('2d')?.putImageData(blob, 0, 0);
189
+ this._storage.addImage(canvas);
190
+ return true;
191
+ }
192
+ } else {
193
+ blob = new Blob([this._dec.data8], { type: this._metrics.mime });
127
194
  }
195
+ this._dec.release();
128
196
  return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
129
197
  .then(bm => {
130
198
  this._storage.addImage(bm);
@@ -154,8 +222,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
154
222
 
155
223
  private _dim(s: string, total: number, cdim: number): number {
156
224
  if (s === 'auto') return 0;
157
- if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
158
- if (s.endsWith('px')) return parseInt(s.slice(0, -2));
159
- return parseInt(s) * cdim;
225
+ if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
226
+ if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
227
+ return parseInt(s, 10) * cdim;
160
228
  }
161
229
  }
@@ -6,8 +6,27 @@
6
6
  // eslint-disable-next-line
7
7
  declare const Buffer: any;
8
8
 
9
+ export const enum HeaderState {
10
+ START = 0,
11
+ ABORT = 1,
12
+ KEY = 2,
13
+ VALUE = 3,
14
+ END = 4
15
+ }
16
+
17
+ export const enum SequenceType {
18
+ INVALID = 0,
19
+ FILE = 1,
20
+ MULTIPARTFILE = 2,
21
+ FILEPART = 3,
22
+ FILEEND = 4,
23
+ REPORTCELLSIZE = 5
24
+ }
9
25
 
10
26
  export interface IHeaderFields {
27
+ [key: string]: number | string | Uint32Array | null | undefined;
28
+ // sequence type
29
+ type: SequenceType;
11
30
  // base-64 encoded filename. Defaults to "Unnamed file".
12
31
  name: string;
13
32
  // File size in bytes. The file transfer will be canceled if this size is exceeded.
@@ -28,14 +47,6 @@ export interface IHeaderFields {
28
47
  inline?: number;
29
48
  }
30
49
 
31
- export const enum HeaderState {
32
- START = 0,
33
- ABORT = 1,
34
- KEY = 2,
35
- VALUE = 3,
36
- END = 4
37
- }
38
-
39
50
  // field value decoders
40
51
 
41
52
  // ASCII bytes to string
@@ -81,7 +92,7 @@ function toName(data: Uint32Array): string {
81
92
  return new TextDecoder().decode(b);
82
93
  }
83
94
 
84
- const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
95
+ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
85
96
  inline: toInt,
86
97
  size: toInt,
87
98
  name: toName,
@@ -91,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
91
102
  };
92
103
 
93
104
 
105
+ // sequence type markers
106
+ // File
94
107
  const FILE_MARKER = [70, 105, 108, 101];
108
+ // MultipartFile
109
+ const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
110
+ // FilePart
111
+ const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
112
+ // FileEnd
113
+ const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
114
+ // ReportCellSize
115
+ const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
116
+
117
+ // max allowed chars for sequence header
95
118
  const MAX_FIELDCHARS = 1024;
96
119
 
97
120
 
@@ -100,7 +123,7 @@ export class HeaderParser {
100
123
  private _buffer = new Uint32Array(MAX_FIELDCHARS);
101
124
  private _position = 0;
102
125
  private _key = '';
103
- public fields: {[key: string]: any} = {};
126
+ public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
104
127
 
105
128
  public reset(): void {
106
129
  this._buffer.fill(0);
@@ -110,12 +133,43 @@ export class HeaderParser {
110
133
  this._key = '';
111
134
  }
112
135
 
136
+ public end(): number {
137
+ if (this.state === HeaderState.START) {
138
+ if (this._position === FILEEND_MARKER.length) {
139
+ for (let k = 0; k < FILEEND_MARKER.length; ++k) {
140
+ if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
141
+ }
142
+ this.fields['type'] = SequenceType.FILEEND;
143
+ this.state = HeaderState.END;
144
+ return 0;
145
+ }
146
+ if (this._position === REPORTCELLSIZE_MARKER.length) {
147
+ for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
148
+ if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
149
+ }
150
+ this.fields['type'] = SequenceType.REPORTCELLSIZE;
151
+ this.state = HeaderState.END;
152
+ return 0;
153
+ }
154
+ return this._a();
155
+ }
156
+ if (this.state === HeaderState.END) return 0;
157
+ if (this.state === HeaderState.VALUE
158
+ && this.fields.type === SequenceType.MULTIPARTFILE
159
+ ) {
160
+ if (!this._storeValue(this._position)) return this._a();
161
+ this.state = HeaderState.END;
162
+ return 0;
163
+ }
164
+ return this._a();
165
+ }
166
+
113
167
  public parse(data: Uint32Array, start: number, end: number): number {
114
168
  let state = this.state;
115
169
  let pos = this._position;
116
170
  const buffer = this._buffer;
117
171
  if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
118
- if (state === HeaderState.START && pos > 6) return -1;
172
+ if (state === HeaderState.START && pos > 14) return -1;
119
173
  for (let i = start; i < end; ++i) {
120
174
  const c = data[i];
121
175
  switch (c) {
@@ -126,8 +180,29 @@ export class HeaderParser {
126
180
  break;
127
181
  case 61: // =
128
182
  if (state === HeaderState.START) {
129
- for (let k = 0; k < FILE_MARKER.length; ++k) {
130
- if (buffer[k] !== FILE_MARKER[k]) return this._a();
183
+ if (buffer[0] === 70) {
184
+ // 'File' or 'FilePart'
185
+ let k = 0;
186
+ for (; k < FILE_MARKER.length; ++k) {
187
+ if (buffer[k] !== FILE_MARKER[k]) return this._a();
188
+ }
189
+ this.fields['type'] = SequenceType.FILE;
190
+ if (pos === FILEPART_MARKER.length) {
191
+ for (; k < FILEPART_MARKER.length; ++k) {
192
+ if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
193
+ }
194
+ this.fields['type'] = SequenceType.FILEPART;
195
+ this.state = HeaderState.END;
196
+ return i + 1;
197
+ }
198
+ } else if (buffer[0] === 77) {
199
+ // 'MultipartFile'
200
+ for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
201
+ if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
202
+ }
203
+ this.fields['type'] = SequenceType.MULTIPARTFILE;
204
+ } else {
205
+ return this._a();
131
206
  }
132
207
  state = HeaderState.KEY;
133
208
  pos = 0;
@@ -157,6 +232,7 @@ export class HeaderParser {
157
232
  }
158
233
 
159
234
  private _a(): number {
235
+ this.fields.type = SequenceType.INVALID;
160
236
  this.state = HeaderState.ABORT;
161
237
  return -1;
162
238
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Copyright (c) 2023 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { IAddImageOpts } from './Types';
7
+ import { ImageStorage } from './ImageStorage';
8
+
9
+ /**
10
+ * IIP (iTerm Image Protocol) specific image storage controller.
11
+ *
12
+ * Wraps the shared ImageStorage with IIP protocol semantics:
13
+ * - Always uses scrolling mode (cursor advances with image)
14
+ */
15
+ export class IIPImageStorage {
16
+ private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
17
+ constructor(
18
+ private readonly _storage: ImageStorage
19
+ ) {}
20
+
21
+ /**
22
+ * Add an IIP image to storage.
23
+ * Always uses scrolling mode — cursor advances past the image.
24
+ */
25
+ public addImage(img: HTMLCanvasElement | ImageBitmap): void {
26
+ this._storage.addImage(img, this._addImageOpts);
27
+ }
28
+ }
package/src/IIPMetrics.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
 
7
- export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
7
+ export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
8
8
 
9
9
  export interface IMetrics {
10
10
  mime: ImageType;
@@ -45,6 +45,14 @@ export function imageType(d: Uint8Array): IMetrics {
45
45
  height: d[9] << 8 | d[8]
46
46
  };
47
47
  }
48
+ // QOI: qoif
49
+ if (d32[0] === 0x66696F71) {
50
+ return {
51
+ mime: 'image/qoi',
52
+ width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
53
+ height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
54
+ };
55
+ }
48
56
  return UNSUPPORTED_TYPE;
49
57
  }
50
58
 
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 {