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:
co-authored by
Daniel Imms
parent
1a67b6f4b3
commit
4ec85e67ba
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]][] = [
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user