@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.
- package/README.md +3 -1
- package/lib/addon-image.js +1 -1
- package/lib/addon-image.js.map +1 -1
- package/lib/addon-image.mjs +1 -22
- package/lib/addon-image.mjs.map +4 -4
- package/package.json +5 -5
- package/src/IIPHandler.ts +32 -21
- package/src/IIPHeaderParser.ts +3 -2
- package/src/IIPImageStorage.ts +26 -0
- package/src/ImageAddon.ts +30 -5
- package/src/ImageRenderer.ts +91 -47
- package/src/ImageStorage.ts +128 -64
- package/src/SixelHandler.ts +3 -3
- package/src/SixelImageStorage.ts +50 -0
- package/src/Types.ts +8 -2
- package/src/kitty/KittyGraphicsHandler.ts +820 -0
- package/src/kitty/KittyGraphicsTypes.ts +193 -0
- package/src/kitty/KittyImageStorage.ts +151 -0
- package/typings/addon-image.d.ts +15 -1
|
@@ -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
|
+
}
|
package/typings/addon-image.d.ts
CHANGED
|
@@ -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
|
*/
|