Add multiline-comment-style lint rule and fix violations

Enable multiline-comment-style with starred-block to require JSDoc-style
block comments with content on separate lines from the opening delimiter.
Auto-fix existing violations across the codebase.

Fixes #5899

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-05-26 20:15:55 +00:00
co-authored by Daniel Imms
parent 1a67b6f4b3
commit 4ec85e67ba
138 changed files with 3040 additions and 1837 deletions
+4 -2
View File
@@ -24,8 +24,10 @@ export interface IHeaderFields {
height?: string;
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
preserveAspectRatio?: number;
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
// (download not supported).
/*
* Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
* (download not supported).
*/
inline?: number;
}
+4 -2
View File
@@ -23,8 +23,10 @@ export function imageType(d: Uint8Array): IMetrics {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
/*
* PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
* + first chunk must be IHDR
*/
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
+18 -12
View File
@@ -137,11 +137,13 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
// enable size reports
if (this._opts.enableSizeReports) {
// const windowOptions = terminal.getOption('windowOptions');
// windowOptions.getWinSizePixels = true;
// windowOptions.getCellSizePixels = true;
// windowOptions.getWinSizeChars = true;
// terminal.setOption('windowOptions', windowOptions);
/*
* const windowOptions = terminal.getOption('windowOptions');
* windowOptions.getWinSizePixels = true;
* windowOptions.getCellSizePixels = true;
* windowOptions.getWinSizeChars = true;
* terminal.setOption('windowOptions', windowOptions);
*/
const windowOps = terminal.options.windowOptions ?? {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
@@ -290,11 +292,13 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
if (params[0]) {
return true;
}
// reported features:
// 62 - VT220
// 4 - SIXEL support
// 9 - charsets
// 22 - ANSI colors
/*
* reported features:
* 62 - VT220
* 4 - SIXEL support
* 9 - charsets
* 22 - ANSI colors
*/
if (this._opts.sixelSupport) {
this._report(`\x1b[?62;4;9;22c`);
return true;
@@ -352,8 +356,10 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
let width = this._renderer?.dimensions?.css.canvas.width;
let height = this._renderer?.dimensions?.css.canvas.height;
if (!width || !height) {
// for some reason we have no working image renderer
// --> fallback to default cell size
/*
* for some reason we have no working image renderer
* --> fallback to default cell size
*/
const cellSize = CELL_SIZE_DEFAULT;
width = (this._terminal?.cols || 80) * cellSize.width;
height = (this._terminal?.rows || 24) * cellSize.height;
+22 -14
View File
@@ -197,10 +197,12 @@ export class ImageRenderer extends Disposable implements IDisposable {
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
// Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
/*
* Floor all pixel offsets to get stable tile mapping without any overflows.
* Note: For not pixel perfect aligned cells like in the DOM renderer
* this will move a tile slightly to the top/left (subpixel range, thus ignore it).
* FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
*/
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
@@ -342,21 +344,27 @@ export class ImageRenderer extends Disposable implements IDisposable {
);
canvas.classList.add(`xterm-image-layer-${layer}`);
const screenElement = this._terminal._core.screenElement;
// Use isolation to create a stacking context without overriding z-index,
// which would conflict with integrators (e.g. VS Code) that set their
// own z-index on the screen element.
/*
* Use isolation to create a stacking context without overriding z-index,
* which would conflict with integrators (e.g. VS Code) that set their
* own z-index on the screen element.
*/
screenElement.style.isolation = 'isolate';
if (layer === 'bottom') {
// Use z-index:-1 so it paints behind non-positioned text elements.
// The screen element needs to be a stacking context (via isolation)
// to contain the negative z-index, otherwise it would go behind the
// entire terminal.
/*
* Use z-index:-1 so it paints behind non-positioned text elements.
* The screen element needs to be a stacking context (via isolation)
* to contain the negative z-index, otherwise it would go behind the
* entire terminal.
*/
canvas.style.zIndex = '-1';
screenElement.insertBefore(canvas, screenElement.firstChild);
} else {
// Explicit z-index ensures the image canvas reliably stacks above
// the text layer (DOM renderer rows). z-index: 0 is below the
// selection overlay (z-index: 1).
/*
* Explicit z-index ensures the image canvas reliably stacks above
* the text layer (DOM renderer rows). z-index: 0 is below the
* selection overlay (z-index: 1).
*/
canvas.style.zIndex = '0';
screenElement.appendChild(canvas);
}
+31 -17
View File
@@ -154,8 +154,10 @@ export class ImageStorage implements IDisposable {
for (const spec of this._images.values()) {
spec.marker?.dispose();
}
// NOTE: marker.dispose above already calls ImageBitmap.close
// therefore we can just wipe the map here
/*
* NOTE: marker.dispose above already calls ImageBitmap.close
* therefore we can just wipe the map here
*/
this._images.clear();
this._renderer.clearAll();
}
@@ -306,8 +308,10 @@ export class ImageStorage implements IDisposable {
this._delImg(id);
}
// eviction marker:
// delete the image when the marker gets disposed
/*
* eviction marker:
* delete the image when the marker gets disposed
*/
const endMarker = this._terminal.registerMarker(0);
endMarker?.onDispose(() => {
const spec = this._images.get(imageId);
@@ -316,8 +320,10 @@ export class ImageStorage implements IDisposable {
}
});
// since markers do not work on alternate for some reason,
// we evict images here manually
/*
* since markers do not work on alternate for some reason,
* we evict images here manually
*/
if (this._terminal.buffer.active.type === 'alternate') {
this._evictOnAlternate();
}
@@ -416,10 +422,12 @@ export class ImageStorage implements IDisposable {
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
const placeholderCalls: { col: number, row: number, count: number }[] = [];
// walk all cells in viewport and collect tiles found
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
// because text writes clear the BG flag but leave image tile data intact.
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
/*
* walk all cells in viewport and collect tiles found
* Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
* because text writes clear the BG flag but leave image tile data intact.
* This lets top-layer images survive text overwrites (kitty C=1 behavior).
*/
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
if (!line) return;
@@ -492,8 +500,10 @@ export class ImageStorage implements IDisposable {
return;
}
// handle only viewport width enlargements, exit all other cases
// TODO: needs patch for tile counter
/*
* handle only viewport width enlargements, exit all other cases
* TODO: needs patch for tile counter
*/
if (this._viewportMetrics.cols >= metrics.cols) {
this._viewportMetrics = metrics;
return;
@@ -581,8 +591,10 @@ export class ImageStorage implements IDisposable {
}
}
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
/*
* TODO: Do we need some blob offloading tricks here to avoid early eviction?
* also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
*/
private _evictOldest(room: number): number {
const used = this._getStoredPixels();
let current = used;
@@ -605,9 +617,11 @@ export class ImageStorage implements IDisposable {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
/*
* found an old ExtendedAttrsImage, since we know that
* they are always isolated instances (single cell usage),
* we can re-use it and just update their id entries
*/
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
+8 -4
View File
@@ -113,13 +113,17 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
* Some helpers to extract current terminal colors.
*/
// get currently active background color from terminal
// also respect INVERSE setting
/*
* get currently active background color from terminal
* also respect INVERSE setting
*/
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet | undefined): RGBA8888 {
let bg = 0;
if (!colors) {
// FIXME: theme service is prolly not available yet,
// happens if .open() was not called yet (bug in core?)
/*
* FIXME: theme service is prolly not available yet,
* happens if .open() was not called yet (bug in core?)
*/
return bg;
}
if (attr.isInverse()) {
@@ -62,9 +62,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Storage related states
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
// Tracks the pending key of the most recently started chunked upload.
// Per spec, subsequent chunks only need m= (and optionally q=), without i=.
// When a chunk arrives with no i=, this key is used to find the pending upload.
/*
* Tracks the pending key of the most recently started chunked upload.
* Per spec, subsequent chunks only need m= (and optionally q=), without i=.
* When a chunk arrives with no i=, this key is used to find the pending upload.
*/
private _lastPendingKey: number | undefined;
constructor(
@@ -173,9 +175,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
private _streamPayload(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
// Check size limit (compare encoded bytes against pre-calculated limit)
// Include cumulative size from pending transmission for multi-chunk images.
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
/*
* Check size limit (compare encoded bytes against pre-calculated limit)
* Include cumulative size from pending transmission for multi-chunk images.
* Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
*/
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
const pending = this._pendingTransmissions.get(pendingKey);
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
@@ -285,9 +289,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
}
this._activeDecoder = null;
// Handle command first — handlers create Blob/ImageData from imageBytes,
// which copies the data. Only then is it safe to release the decoder's
// wasm memory that imageBytes points into.
/*
* Handle command first — handlers create Blob/ImageData from imageBytes,
* which copies the data. Only then is it safe to release the decoder's
* wasm memory that imageBytes points into.
*/
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
if (decoder) {
decoder.release();
@@ -325,10 +331,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
case KittyAction.PLACEMENT:
return this._handlePlacement(cmd);
default:
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
/*
* TODO: Implement remaining actions when needed:
* - a=f (frame): animation frame operations
* - a=a (animation): animation control
* - a=c (compose): compose images
*/
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
@@ -342,8 +350,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
switch (action) {
case KittyAction.TRANSMIT: {
const result = this._handleTransmit(cmd, bytes, decodeError);
// Only send response when _handleTransmit didn't already respond
// (it handles unsupported transmission medium responses internally)
/*
* Only send response when _handleTransmit didn't already respond
* (it handles unsupported transmission medium responses internally)
*/
if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) {
if (decodeError) {
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
@@ -361,10 +371,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// a=p ignores any payload — image data was already transmitted
return this._handlePlacement(cmd);
default:
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
/*
* TODO: Implement remaining actions when needed:
* - a=f (frame): animation frame operations
* - a=a (animation): animation control
* - a=c (compose): compose images
*/
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
@@ -390,16 +402,18 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
}
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
// Currently only supports direct transmission (t=d, the default).
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
// - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
// 1. Check cmd.transmission (t key) before treating bytes as image data
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
// 3. For t=d: treat bytes as image data (current behavior)
// When implementing, also update _handleQuery to accept these transmission mediums.
/*
* TODO: Support file-based transmission modes (t=f, t=t, t=s)
* Currently only supports direct transmission (t=d, the default).
* - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
* - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
* - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
* These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
* 1. Check cmd.transmission (t key) before treating bytes as image data
* 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
* 3. For t=d: treat bytes as image data (current behavior)
* When implementing, also update _handleQuery to accept these transmission mediums.
*/
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
if (cmd.id !== undefined) {
@@ -449,9 +463,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const id = cmd.id ?? 0;
const quiet = cmd.quiet ?? 0;
// Per spec: reject unsupported transmission mediums (only t=d is supported atm)
// TODO: When filesystem support is added (Node.js/Electron), update this to accept
// t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
/*
* Per spec: reject unsupported transmission mediums (only t=d is supported atm)
* TODO: When filesystem support is added (Node.js/Electron), update this to accept
* t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
*/
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
@@ -500,9 +516,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Per spec: default delete selector is 'a' (delete all visible placements)
const selector = cmd.deleteSelector ?? 'a';
// TODO: Distinguish lowercase (delete placements only) from uppercase
// (delete placements + free stored image data). Currently both variants
// free everything since we don't separate stored data from placements.
/*
* TODO: Distinguish lowercase (delete placements only) from uppercase
* (delete placements + free stored image data). Currently both variants
* free everything since we don't separate stored data from placements.
*/
switch (selector) {
case 'a':
case 'A':
@@ -511,10 +529,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
break;
case 'i':
case 'I':
// TODO: When placement id tracking is implemented (see TODO in
// KittyImageStorage), d=i with p=<pid> should delete only that
// specific placement, while d=i without p should delete all
// placements for the image.
/*
* TODO: When placement id tracking is implemented (see TODO in
* KittyImageStorage), d=i with p=<pid> should delete only that
* specific placement, while d=i without p should delete all
* placements for the image.
*/
if (cmd.id !== undefined) {
const pending = this._pendingTransmissions.get(cmd.id);
if (pending) {
@@ -576,8 +596,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
// Per spec: c/r default to image's natural cell dimensions.
// If only one of c/r is specified, compute the other from image aspect ratio.
/*
* Per spec: c/r default to image's natural cell dimensions.
* If only one of c/r is specified, compute the other from image aspect ratio.
*/
let imgCols: number;
let imgRows: number;
if (cmd.columns !== undefined && cmd.rows !== undefined) {
@@ -613,10 +635,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const savedY = buffer.y;
const savedYbase = buffer.ybase;
// Determine layer based on z-index: negative = behind text, 0+ = on top.
// When z<0 we always use the bottom layer even without allowTransparency —
// the image will simply be hidden behind the opaque text background, which
// is the correct behavior (client asked for "behind text").
/*
* Determine layer based on z-index: negative = behind text, 0+ = on top.
* When z<0 we always use the bottom layer even without allowTransparency —
* the image will simply be hidden behind the opaque text background, which
* is the correct behavior (client asked for "behind text").
*/
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
@@ -630,10 +654,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
if (xOffset !== 0 || yOffset !== 0) {
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
// total placement area remains c*cw × r*ch pixels and the offset image
// is clipped to fit. When c/r are unset, the padded canvas determines
// the natural cell dimensions.
/*
* Per spec: X/Y is not added to c/r area. When c/r are explicit, the
* total placement area remains c*cw × r*ch pixels and the offset image
* is clipped to fit. When c/r are unset, the padded canvas determines
* the natural cell dimensions.
*/
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
@@ -664,9 +690,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
bitmap = undefined; // ownership transferred to storage
// Kitty cursor movement
// Per spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
/*
* Kitty cursor movement
* Per spec: cursor placed at first column after last image column,
* on the last row of the image. C=1 means don't move cursor.
*/
if (cmd.cursorMovement === 1) {
// C=1: restore cursor to position before image was placed
const scrolled = buffer.ybase - savedYbase;
@@ -674,8 +702,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
// Can't restore cursor to scrollback?
buffer.y = Math.max(savedY - scrolled, 0);
} else {
// Default (C=0): advance cursor horizontally past the image
// addImage already positioned cursor on the last row via lineFeeds
/*
* Default (C=0): advance cursor horizontally past the image
* addImage already positioned cursor on the last row via lineFeeds
*/
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
}
} catch (e) {
@@ -736,9 +766,11 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA), width, height));
}
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
// Assumes little-endian (all modern browsers/Node.js).
/*
* RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
* 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
* Assumes little-endian (all modern browsers/Node.js).
*/
const data = new Uint8ClampedArray(pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA);
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
const dst32 = new Uint32Array(data.buffer);
@@ -7,8 +7,10 @@
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
// Kitty graphics protocol action types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
/*
* Kitty graphics protocol action types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
*/
export const enum KittyAction {
TRANSMIT = 't',
TRANSMIT_DISPLAY = 'T',
@@ -17,23 +19,29 @@ export const enum KittyAction {
DELETE = 'd'
}
// Kitty graphics protocol format types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
/*
* Kitty graphics protocol format types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
export const enum KittyFormat {
RGB = 24,
RGBA = 32,
PNG = 100
}
// Kitty graphics protocol compression types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
/*
* Kitty graphics protocol compression types.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
*/
export const enum KittyCompression {
NONE = '',
ZLIB = 'z'
}
// Kitty graphics protocol control data keys.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
/*
* Kitty graphics protocol control data keys.
* See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
*/
export const enum KittyKey {
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
ACTION = 'a',
@@ -115,8 +123,10 @@ export interface IKittyCommand {
payload?: string;
}
// Pending chunked transmission state.
// Stores metadata from the first chunk while accumulating decoded payload data.
/*
* Pending chunked transmission state.
* Stores metadata from the first chunk while accumulating decoded payload data.
*/
export interface IPendingTransmission {
// The parsed command from the first chunk (contains action, format, dimensions, etc.)
cmd: IKittyCommand;
@@ -8,27 +8,31 @@ import { ImageStorage } from '../ImageStorage';
import { ImageLayer } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes';
// Kitty-specific image storage controller.
//
// Wraps shared ImageStorage with kitty protocol semantics:
// - tracks transmitted image payloads by kitty image id
// - tracks kitty image id -> shared ImageStorage id mapping for displayed images
// - mirrors shared-storage evictions into kitty maps
// - applies protocol-level undisplayed-image eviction policy
/*
* Kitty-specific image storage controller.
*
* Wraps shared ImageStorage with kitty protocol semantics:
* - tracks transmitted image payloads by kitty image id
* - tracks kitty image id -> shared ImageStorage id mapping for displayed images
* - mirrors shared-storage evictions into kitty maps
* - applies protocol-level undisplayed-image eviction policy
*/
export class KittyImageStorage implements IDisposable {
private static readonly _maxStoredImages = 256;
private _nextImageId = 1;
private readonly _images: Map<number, IKittyImageData> = new Map();
// TODO: Support multiple placements per image. The kitty spec identifies
// placements by an (image id, placement id) pair — same i + different p
// values should coexist, and same i + same p should replace the prior
// placement. Currently we track only one storage entry per kitty image id,
// so multiple placements of the same image overwrite each other. Fixing
// this requires changing these maps to Map<number, Map<number, number>>
// (kittyId → placementId → storageId) and updating addImage/deleteById
// accordingly. The underlying shared ImageStorage would also need to
// support multiple entries per logical image.
/*
* TODO: Support multiple placements per image. The kitty spec identifies
* placements by an (image id, placement id) pair — same i + different p
* values should coexist, and same i + same p should replace the prior
* placement. Currently we track only one storage entry per kitty image id,
* so multiple placements of the same image overwrite each other. Fixing
* this requires changing these maps to Map<number, Map<number, number>>
* (kittyId → placementId → storageId) and updating addImage/deleteById
* accordingly. The underlying shared ImageStorage would also need to
* support multiple entries per logical image.
*/
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
private readonly _storageIdToKittyId: Map<number, number> = new Map();
@@ -90,10 +94,12 @@ export class KittyImageStorage implements IDisposable {
}
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
// Clean up stale reverse-mapping from a previous placement of the same
// kitty image. The old shared-storage entry is kept (it may still be
// visible on screen) but its reverse mapping is removed so that eviction
// of the old entry won't incorrectly delete the kitty image data.
/*
* Clean up stale reverse-mapping from a previous placement of the same
* kitty image. The old shared-storage entry is kept (it may still be
* visible on screen) but its reverse mapping is removed so that eviction
* of the old entry won't incorrectly delete the kitty image data.
*/
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
if (oldStorageId !== undefined) {
this._storageIdToKittyId.delete(oldStorageId);
+4 -2
View File
@@ -64,8 +64,10 @@ const TESTDATA: ITestData = (() => {
};
})();
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
/*
* const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
* const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
*/
// NOTE: the data is loaded as string for easier transport through playwright
const TESTDATA_IIP: [string, [number, number]][] = [
+57 -33
View File
@@ -101,12 +101,14 @@ test.beforeAll(async ({ browser }) => {
test.afterAll(async () => await ctx.page.close());
test.describe('Kitty Graphics Protocol', () => {
// TODO: Add tests for larger images with various dimensions
// TODO: Add tests for virtual placement (U=1)
// TODO: Add tests for animation frames
// TODO: Add performance tests for streaming large images
// TODO: Implement cursor movement per Kitty spec - cursor should move by cols/rows after placement (unless C=1)
// TODO: Distinguish lowercase delete selectors (placement only) from uppercase (placement + free data)
/*
* TODO: Add tests for larger images with various dimensions
* TODO: Add tests for virtual placement (U=1)
* TODO: Add tests for animation frames
* TODO: Add performance tests for streaming large images
* TODO: Implement cursor movement per Kitty spec - cursor should move by cols/rows after placement (unless C=1)
* TODO: Distinguish lowercase delete selectors (placement only) from uppercase (placement + free data)
*/
test.beforeEach(async ({}, testInfo) => {
await ctx.page.evaluate(`
@@ -209,8 +211,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('enforces size limit across chunked transmissions', async () => {
// Create a custom addon with very small size limit (100 bytes)
// The 1x1 PNG is ~164 bytes base64, so 2 chunks should exceed 100
/*
* Create a custom addon with very small size limit (100 bytes)
* The 1x1 PNG is ~164 bytes base64, so 2 chunks should exceed 100
*/
await ctx.page.evaluate(() => {
(window as any).smallLimitAddon = new ImageAddon({
kittySupport: true,
@@ -1279,8 +1283,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('only c specified computes r from aspect ratio', async () => {
// 200x100 image (2:1 aspect) with c=10.
// Per spec: r = ceil((h/w) * c * cw / ch) = ceil(0.5 * 10 * cw / ch)
/*
* 200x100 image (2:1 aspect) with c=10.
* Per spec: r = ceil((h/w) * c * cw / ch) = ceil(0.5 * 10 * cw / ch)
*/
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=225;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(100);
@@ -1297,8 +1303,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('only r specified computes c from aspect ratio', async () => {
// 200x100 image (2:1 aspect) with r=5.
// Per spec: c = ceil((w/h) * r * ch / cw) = ceil(2 * 5 * ch / cw)
/*
* 200x100 image (2:1 aspect) with r=5.
* Per spec: c = ceil((w/h) * r * ch / cw) = ceil(2 * 5 * ch / cw)
*/
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=226;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(100);
@@ -1316,9 +1324,11 @@ test.describe('Kitty Graphics Protocol', () => {
});
test.describe('Cursor positioning', () => {
// NOTE: Current tests document ACTUAL behavior (MVP - cursor doesn't move)
// Per Kitty spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
/*
* NOTE: Current tests document ACTUAL behavior (MVP - cursor doesn't move)
* Per Kitty spec: cursor placed at first column after last image column,
* on the last row of the image. C=1 means don't move cursor.
*/
test('cursor advances past 1x1 image', async () => {
const cursorBefore = await getCursor();
@@ -1376,8 +1386,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('cursor should move right by cols when c specified', async () => {
// c=5, r computed from 1x1 aspect ratio: r = ceil((1/1) * (5*cw) / ch)
// With 1:1 aspect, image height in pixels = 5*cw, so r = ceil(5*cw/ch)
/*
* c=5, r computed from 1x1 aspect ratio: r = ceil((1/1) * (5*cw) / ch)
* With 1:1 aspect, image height in pixels = 5*cw, so r = ceil(5*cw/ch)
*/
await ctx.proxy.write(`\x1b_Ga=T,f=100,c=5;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
await timeout(100);
@@ -1634,10 +1646,12 @@ test.describe('Kitty Graphics Protocol', () => {
});
test.describe('Pixel verification', () => {
// The 200x100 image has 20 colored rectangles in a 10x2 grid.
// Each rectangle is 20px wide x 50px tall.
// Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
// Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
/*
* The 200x100 image has 20 colored rectangles in a 10x2 grid.
* Each rectangle is 20px wide x 50px tall.
* Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
* Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
*/
test('renders red rectangle at top-left origin (0,0)', async () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
@@ -1650,9 +1664,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200);
// Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
// All within the first cell row, so we read from the canvas at cell (0,0)
// Red at x=10
/*
* Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
* All within the first cell row, so we read from the canvas at cell (0,0)
* Red at x=10
*/
deepStrictEqual(await getPixel(0, 0, 10, 25), [255, 0, 0, 255]);
// Orange at x=30
deepStrictEqual(await getPixel(0, 0, 30, 25), [255, 128, 0, 255]);
@@ -1668,8 +1684,10 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
await timeout(200);
// Bottom row starts at y=50. Center at y=75.
// Pink at x=10
/*
* Bottom row starts at y=50. Center at y=75.
* Pink at x=10
*/
deepStrictEqual(await getPixel(0, 0, 10, 75), [255, 192, 203, 255]);
// Brown at x=30
deepStrictEqual(await getPixel(0, 0, 30, 75), [165, 42, 42, 255]);
@@ -1725,8 +1743,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('scales cropped source region to c/r placement rectangle', async () => {
// Firefox's createImageBitmap uses different resize sampling, producing
// slightly off pixel values compared to Chromium, so skip on Firefox.
/*
* Firefox's createImageBitmap uses different resize sampling, producing
* slightly off pixel values compared to Chromium, so skip on Firefox.
*/
if (ctx.browser.browserType().name() === 'firefox') {
test.skip();
}
@@ -2137,8 +2157,10 @@ test.describe('Kitty Graphics Protocol', () => {
});
test('memory limit eviction cleans Kitty handler maps', async () => {
// Resize terminal to fit 7 non-overlapping 200x100 images without scrolling.
// Each image ≈ 29 cols × 8 rows at default cell size.
/*
* Resize terminal to fit 7 non-overlapping 200x100 images without scrolling.
* Each image ≈ 29 cols × 8 rows at default cell size.
*/
await ctx.page.evaluate(`
window.term.reset();
window.imageAddon?.dispose();
@@ -2147,9 +2169,11 @@ test.describe('Kitty Graphics Protocol', () => {
window.term.loadAddon(window.imageAddon);
`);
// storageLimit 0.5 MB = 125,000 pixels. Each 200x100 image = 20,000 pixels.
// 6 images = 120K pixels (under limit). 7th triggers eviction (140K > 125K).
// Place non-overlapping so tile-count eviction doesn't interfere.
/*
* storageLimit 0.5 MB = 125,000 pixels. Each 200x100 image = 20,000 pixels.
* 6 images = 120K pixels (under limit). 7th triggers eviction (140K > 125K).
* Place non-overlapping so tile-count eviction doesn't interfere.
*/
const positions = [[1, 1], [30, 1], [1, 9], [30, 9], [1, 17], [30, 17]];
for (let n = 0; n < 6; n++) {
const [c, r] = positions[n];