@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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ *
5
+ * Kitty graphics protocol types, constants, and parsing utilities.
6
+ */
7
+
8
+ import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
9
+
10
+ // Kitty graphics protocol action types.
11
+ // See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
12
+ export const enum KittyAction {
13
+ TRANSMIT = 't',
14
+ TRANSMIT_DISPLAY = 'T',
15
+ QUERY = 'q',
16
+ PLACEMENT = 'p',
17
+ DELETE = 'd'
18
+ }
19
+
20
+ // Kitty graphics protocol format types.
21
+ // See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
22
+ export const enum KittyFormat {
23
+ RGB = 24,
24
+ RGBA = 32,
25
+ PNG = 100
26
+ }
27
+
28
+ // Kitty graphics protocol compression types.
29
+ // See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
30
+ export const enum KittyCompression {
31
+ NONE = '',
32
+ ZLIB = 'z'
33
+ }
34
+
35
+ // Kitty graphics protocol control data keys.
36
+ // See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
37
+ export const enum KittyKey {
38
+ // Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
39
+ ACTION = 'a',
40
+ // Image format (24=RGB, 32=RGBA, 100=PNG)
41
+ FORMAT = 'f',
42
+ // Image ID for referencing stored images
43
+ ID = 'i',
44
+ // Image number (alternative to ID, terminal assigns ID)
45
+ IMAGE_NUMBER = 'I',
46
+ // Source image width in pixels
47
+ WIDTH = 's',
48
+ // Source image height in pixels
49
+ HEIGHT = 'v',
50
+ // The left edge (in pixels) of the image area to display
51
+ X_OFFSET = 'x',
52
+ // The top edge (in pixels) of the image area to display
53
+ Y_OFFSET = 'y',
54
+ // Width (in pixels) of the source rectangle to display
55
+ SOURCE_WIDTH = 'w',
56
+ // Height (in pixels) of the source rectangle to display
57
+ SOURCE_HEIGHT = 'h',
58
+ // Horizontal offset (in pixels) within the first cell
59
+ X_PLACEMENT_OFFSET = 'X',
60
+ // Vertical offset (in pixels) within the first cell
61
+ Y_PLACEMENT_OFFSET = 'Y',
62
+ // Number of terminal columns to display the image over
63
+ COLUMNS = 'c',
64
+ // Number of terminal rows to display the image over
65
+ ROWS = 'r',
66
+ // More data flag (1=more chunks coming, 0=final chunk)
67
+ MORE = 'm',
68
+ // Compression type (z=zlib). This is essential for chunking larger images.
69
+ COMPRESSION = 'o',
70
+ // Quiet mode (1=suppress OK responses, 2=suppress error responses)
71
+ QUIET = 'q',
72
+ // Cursor movement policy (0=move cursor after image, 1=don't move cursor)
73
+ CURSOR_MOVEMENT = 'C',
74
+ // Z-index for image layering (negative = behind text, 0+ = on top)
75
+ Z_INDEX = 'z',
76
+ // Transmission medium (d=direct, f=file, t=temp file, s=shared memory)
77
+ TRANSMISSION = 't',
78
+ // Delete selector (a/A=all, i/I=by id, c/C=at cursor, etc.) — only used when a=d
79
+ DELETE_SELECTOR = 'd',
80
+ // Placement ID for targeting specific placements
81
+ PLACEMENT_ID = 'p'
82
+ }
83
+
84
+ // Pixel format constants
85
+ export const BYTES_PER_PIXEL_RGB = 3;
86
+ export const BYTES_PER_PIXEL_RGBA = 4;
87
+ export const ALPHA_OPAQUE = 255;
88
+
89
+ // Parsed Kitty graphics command.
90
+ export interface IKittyCommand {
91
+ action?: string;
92
+ format?: number;
93
+ id?: number;
94
+ imageNumber?: number;
95
+ width?: number;
96
+ height?: number;
97
+ x?: number;
98
+ y?: number;
99
+ sourceWidth?: number;
100
+ sourceHeight?: number;
101
+ xOffset?: number;
102
+ yOffset?: number;
103
+ columns?: number;
104
+ rows?: number;
105
+ more?: number;
106
+ quiet?: number;
107
+ cursorMovement?: number;
108
+ zIndex?: number;
109
+ transmission?: string;
110
+ deleteSelector?: string;
111
+ placementId?: number;
112
+ compression?: string;
113
+ payload?: string;
114
+ }
115
+
116
+ // Pending chunked transmission state.
117
+ // Stores metadata from the first chunk while accumulating decoded payload data.
118
+ export interface IPendingTransmission {
119
+ // The parsed command from the first chunk (contains action, format, dimensions, etc.)
120
+ cmd: IKittyCommand;
121
+ // Decoder used across chunked payloads
122
+ decoder: Base64Decoder;
123
+ // Total encoded (base64) bytes received across all chunks - for size limit enforcement
124
+ totalEncodedSize: number;
125
+ // Whether any chunk has failed to decode
126
+ decodeError: boolean;
127
+ }
128
+
129
+ // Stored Kitty image data.
130
+ export interface IKittyImageData {
131
+ id: number;
132
+ // Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit
133
+ data: Blob;
134
+ width: number;
135
+ height: number;
136
+ format: 24 | 32 | 100;
137
+ compression?: string;
138
+ }
139
+
140
+ // Parses Kitty graphics control data into a command object.
141
+ export function parseKittyCommand(data: string): IKittyCommand {
142
+ const cmd: IKittyCommand = {};
143
+ const parts = data.split(',');
144
+
145
+ for (const part of parts) {
146
+ const eqIdx = part.indexOf('=');
147
+ if (eqIdx === -1) continue;
148
+
149
+ const key = part.substring(0, eqIdx);
150
+ const value = part.substring(eqIdx + 1);
151
+
152
+ // Handle string keys first
153
+ if (key === KittyKey.ACTION) {
154
+ cmd.action = value;
155
+ continue;
156
+ }
157
+ if (key === KittyKey.COMPRESSION) {
158
+ cmd.compression = value;
159
+ continue;
160
+ }
161
+ if (key === KittyKey.TRANSMISSION) {
162
+ cmd.transmission = value;
163
+ continue;
164
+ }
165
+ if (key === KittyKey.DELETE_SELECTOR) {
166
+ cmd.deleteSelector = value;
167
+ continue;
168
+ }
169
+ const numValue = parseInt(value);
170
+ switch (key) {
171
+ case KittyKey.FORMAT: cmd.format = numValue; break;
172
+ case KittyKey.ID: cmd.id = numValue; break;
173
+ case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
174
+ case KittyKey.WIDTH: cmd.width = numValue; break;
175
+ case KittyKey.HEIGHT: cmd.height = numValue; break;
176
+ case KittyKey.X_OFFSET: cmd.x = numValue; break;
177
+ case KittyKey.Y_OFFSET: cmd.y = numValue; break;
178
+ case KittyKey.SOURCE_WIDTH: cmd.sourceWidth = numValue; break;
179
+ case KittyKey.SOURCE_HEIGHT: cmd.sourceHeight = numValue; break;
180
+ case KittyKey.X_PLACEMENT_OFFSET: cmd.xOffset = numValue; break;
181
+ case KittyKey.Y_PLACEMENT_OFFSET: cmd.yOffset = numValue; break;
182
+ case KittyKey.COLUMNS: cmd.columns = numValue; break;
183
+ case KittyKey.ROWS: cmd.rows = numValue; break;
184
+ case KittyKey.MORE: cmd.more = numValue; break;
185
+ case KittyKey.QUIET: cmd.quiet = numValue; break;
186
+ case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
187
+ case KittyKey.Z_INDEX: cmd.zIndex = numValue; break;
188
+ case KittyKey.PLACEMENT_ID: cmd.placementId = numValue; break;
189
+ }
190
+ }
191
+
192
+ return cmd;
193
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Copyright (c) 2026 The xterm.js authors. All rights reserved.
3
+ * @license MIT
4
+ */
5
+
6
+ import { IDisposable } from '@xterm/xterm';
7
+ import { ImageStorage } from '../ImageStorage';
8
+ import { ImageLayer } from '../Types';
9
+ import { IKittyImageData } from './KittyGraphicsTypes';
10
+
11
+ // Kitty-specific image storage controller.
12
+ //
13
+ // Wraps shared ImageStorage with kitty protocol semantics:
14
+ // - tracks transmitted image payloads by kitty image id
15
+ // - tracks kitty image id -> shared ImageStorage id mapping for displayed images
16
+ // - mirrors shared-storage evictions into kitty maps
17
+ // - applies protocol-level undisplayed-image eviction policy
18
+ export class KittyImageStorage implements IDisposable {
19
+ private static readonly _maxStoredImages = 256;
20
+
21
+ private _nextImageId = 1;
22
+ private readonly _images: Map<number, IKittyImageData> = new Map();
23
+ // TODO: Support multiple placements per image. The kitty spec identifies
24
+ // placements by an (image id, placement id) pair — same i + different p
25
+ // values should coexist, and same i + same p should replace the prior
26
+ // placement. Currently we track only one storage entry per kitty image id,
27
+ // so multiple placements of the same image overwrite each other. Fixing
28
+ // this requires changing these maps to Map<number, Map<number, number>>
29
+ // (kittyId → placementId → storageId) and updating addImage/deleteById
30
+ // accordingly. The underlying shared ImageStorage would also need to
31
+ // support multiple entries per logical image.
32
+ private readonly _kittyIdToStorageId: Map<number, number> = new Map();
33
+ private readonly _storageIdToKittyId: Map<number, number> = new Map();
34
+
35
+ private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined;
36
+ private readonly _wrappedOnImageDeleted: (storageId: number) => void;
37
+ private readonly _handleStorageImageDeleted = (storageId: number): void => {
38
+ const kittyId = this._storageIdToKittyId.get(storageId);
39
+ if (kittyId !== undefined) {
40
+ this._kittyIdToStorageId.delete(kittyId);
41
+ this._storageIdToKittyId.delete(storageId);
42
+ this._images.delete(kittyId);
43
+ }
44
+ };
45
+
46
+ constructor(
47
+ private readonly _storage: ImageStorage
48
+ ) {
49
+ this._previousOnImageDeleted = this._storage.onImageDeleted;
50
+ this._wrappedOnImageDeleted = (storageId: number) => {
51
+ this._previousOnImageDeleted?.(storageId);
52
+ this._handleStorageImageDeleted(storageId);
53
+ };
54
+ this._storage.onImageDeleted = this._wrappedOnImageDeleted;
55
+ }
56
+
57
+ public reset(): void {
58
+ this._nextImageId = 1;
59
+ this._images.clear();
60
+ this._kittyIdToStorageId.clear();
61
+ this._storageIdToKittyId.clear();
62
+ }
63
+
64
+ public dispose(): void {
65
+ this.reset();
66
+ if (this._storage.onImageDeleted === this._wrappedOnImageDeleted) {
67
+ this._storage.onImageDeleted = this._previousOnImageDeleted;
68
+ }
69
+ }
70
+
71
+ public storeImage(id: number | undefined, imageData: Omit<IKittyImageData, 'id'>): number {
72
+ const imageId = id ?? this._nextImageId++;
73
+
74
+ const oldStorageId = this._kittyIdToStorageId.get(imageId);
75
+ if (oldStorageId !== undefined) {
76
+ this._storage.deleteImage(oldStorageId);
77
+ this._kittyIdToStorageId.delete(imageId);
78
+ this._storageIdToKittyId.delete(oldStorageId);
79
+ }
80
+
81
+ if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) {
82
+ this._evictUndisplayedImages();
83
+ }
84
+
85
+ this._images.set(imageId, {
86
+ ...imageData,
87
+ id: imageId
88
+ });
89
+ return imageId;
90
+ }
91
+
92
+ public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
93
+ // Clean up stale reverse-mapping from a previous placement of the same
94
+ // kitty image. The old shared-storage entry is kept (it may still be
95
+ // visible on screen) but its reverse mapping is removed so that eviction
96
+ // of the old entry won't incorrectly delete the kitty image data.
97
+ const oldStorageId = this._kittyIdToStorageId.get(kittyId);
98
+ if (oldStorageId !== undefined) {
99
+ this._storageIdToKittyId.delete(oldStorageId);
100
+ }
101
+ const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
102
+ this._kittyIdToStorageId.set(kittyId, storageId);
103
+ this._storageIdToKittyId.set(storageId, kittyId);
104
+ }
105
+
106
+ public getImage(kittyId: number): IKittyImageData | undefined {
107
+ return this._images.get(kittyId);
108
+ }
109
+
110
+ public deleteById(kittyId: number): void {
111
+ this._images.delete(kittyId);
112
+ const storageId = this._kittyIdToStorageId.get(kittyId);
113
+ if (storageId !== undefined) {
114
+ this._storage.deleteImage(storageId);
115
+ this._kittyIdToStorageId.delete(kittyId);
116
+ this._storageIdToKittyId.delete(storageId);
117
+ }
118
+ }
119
+
120
+ public deleteAll(): void {
121
+ this._images.clear();
122
+ for (const storageId of this._kittyIdToStorageId.values()) {
123
+ this._storage.deleteImage(storageId);
124
+ }
125
+ this._kittyIdToStorageId.clear();
126
+ this._storageIdToKittyId.clear();
127
+ }
128
+
129
+ public get images(): ReadonlyMap<number, IKittyImageData> {
130
+ return this._images;
131
+ }
132
+
133
+ public get kittyIdToStorageId(): ReadonlyMap<number, number> {
134
+ return this._kittyIdToStorageId;
135
+ }
136
+
137
+ public get lastImageId(): number {
138
+ return this._nextImageId - 1;
139
+ }
140
+
141
+ private _evictUndisplayedImages(): void {
142
+ for (const [kittyId] of this._images) {
143
+ if (this._images.size <= KittyImageStorage._maxStoredImages / 2) {
144
+ break;
145
+ }
146
+ if (!this._kittyIdToStorageId.has(kittyId)) {
147
+ this._images.delete(kittyId);
148
+ }
149
+ }
150
+ }
151
+ }
@@ -3,7 +3,7 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { Terminal, ITerminalAddon } from '@xterm/xterm';
6
+ import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm';
7
7
 
8
8
  declare module '@xterm/addon-image' {
9
9
  export interface IImageAddonOptions {
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
76
76
  iipSupport?: boolean;
77
77
  /** IIP sequence size limit (default 20000000 bytes). */
78
78
  iipSizeLimit?: number;
79
+
80
+ /**
81
+ * Kitty graphics protocol settings
82
+ */
83
+
84
+ /** Whether Kitty graphics protocol is enabled (default is true). */
85
+ kittySupport?: boolean;
86
+ /** Kitty image size limit in bytes (default 20000000 bytes). */
87
+ kittySizeLimit?: number;
79
88
  }
80
89
 
81
90
  export class ImageAddon implements ITerminalAddon {
@@ -107,6 +116,11 @@ declare module '@xterm/addon-image' {
107
116
  */
108
117
  public showPlaceholder: boolean;
109
118
 
119
+ /**
120
+ * Event fired whenever a new image is added to storage.
121
+ */
122
+ public readonly onImageAdded: IEvent<void>;
123
+
110
124
  /**
111
125
  * Get original image canvas at buffer position.
112
126
  */