mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Support deletion
This commit is contained in:
@@ -428,38 +428,58 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
}
|
||||
|
||||
private _handleDelete(cmd: IKittyCommand): boolean {
|
||||
const id = cmd.id;
|
||||
// Per spec: default delete selector is 'a' (delete all visible placements)
|
||||
const selector = cmd.deleteSelector ?? 'a';
|
||||
|
||||
if (id !== undefined) {
|
||||
// Abort in-flight chunked upload for this specific image
|
||||
const pending = this._pendingTransmissions.get(id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
this._pendingTransmissions.delete(id);
|
||||
}
|
||||
|
||||
this._images.delete(id);
|
||||
const storageId = this._kittyIdToStorageId.get(id);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(id);
|
||||
}
|
||||
} else {
|
||||
// Abort all in-flight chunked uploads
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
// 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':
|
||||
// Delete all — also abort all in-flight uploads
|
||||
for (const pending of this._pendingTransmissions.values()) {
|
||||
pending.decoder.release();
|
||||
}
|
||||
this._pendingTransmissions.clear();
|
||||
this._deleteAll();
|
||||
break;
|
||||
case 'i':
|
||||
case 'I':
|
||||
// Delete by image ID — only abort the targeted upload
|
||||
if (cmd.id !== undefined) {
|
||||
const pending = this._pendingTransmissions.get(cmd.id);
|
||||
if (pending) {
|
||||
pending.decoder.release();
|
||||
this._pendingTransmissions.delete(cmd.id);
|
||||
}
|
||||
this._deleteById(cmd.id);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unsupported selectors (c, n, p, q, r, x, y, z, f) — ignore for now
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _deleteById(id: number): void {
|
||||
this._images.delete(id);
|
||||
const storageId = this._kittyIdToStorageId.get(id);
|
||||
if (storageId !== undefined) {
|
||||
this._storage.deleteImage(storageId);
|
||||
this._kittyIdToStorageId.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
private _deleteAll(): void {
|
||||
this._images.clear();
|
||||
for (const storageId of this._kittyIdToStorageId.values()) {
|
||||
this._storage.deleteImage(storageId);
|
||||
}
|
||||
this._kittyIdToStorageId.clear();
|
||||
}
|
||||
|
||||
private _sendResponse(id: number, message: string, quiet: number): void {
|
||||
const isOk = message === 'OK';
|
||||
if (isOk && quiet === 1) return;
|
||||
|
||||
@@ -106,5 +106,44 @@ describe('KittyGraphicsTypes', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.strictEqual(cmd.zIndex, undefined);
|
||||
});
|
||||
|
||||
it('should parse delete selector key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.action, 'd');
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should parse uppercase delete selector', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=A');
|
||||
assert.strictEqual(cmd.deleteSelector, 'A');
|
||||
});
|
||||
|
||||
it('should parse delete selector d=a (all)', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=a');
|
||||
assert.strictEqual(cmd.deleteSelector, 'a');
|
||||
});
|
||||
|
||||
it('should leave deleteSelector undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.strictEqual(cmd.deleteSelector, undefined);
|
||||
});
|
||||
|
||||
it('should parse placement id key', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5,p=3');
|
||||
assert.strictEqual(cmd.placementId, 3);
|
||||
assert.strictEqual(cmd.deleteSelector, 'i');
|
||||
assert.strictEqual(cmd.id, 5);
|
||||
});
|
||||
|
||||
it('should leave placementId undefined when not specified', () => {
|
||||
const cmd = parseKittyCommand('a=d,d=i,i=5');
|
||||
assert.strictEqual(cmd.placementId, undefined);
|
||||
});
|
||||
|
||||
it('should parse image number key', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100,I=42');
|
||||
assert.strictEqual(cmd.imageNumber, 42);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,11 @@ export const enum KittyKey {
|
||||
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
|
||||
CURSOR_MOVEMENT = 'C',
|
||||
// Z-index for image layering (negative = behind text, 0+ = on top)
|
||||
Z_INDEX = 'z'
|
||||
Z_INDEX = 'z',
|
||||
// Delete selector (a/A=all, i/I=by id, c/C=at cursor, etc.) — only used when a=d
|
||||
DELETE_SELECTOR = 'd',
|
||||
// Placement ID for targeting specific placements
|
||||
PLACEMENT_ID = 'p'
|
||||
}
|
||||
|
||||
// Pixel format constants
|
||||
@@ -98,6 +102,8 @@ export interface IKittyCommand {
|
||||
quiet?: number;
|
||||
cursorMovement?: number;
|
||||
zIndex?: number;
|
||||
deleteSelector?: string;
|
||||
placementId?: number;
|
||||
compression?: string;
|
||||
payload?: string;
|
||||
}
|
||||
@@ -153,6 +159,10 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
||||
cmd.compression = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.DELETE_SELECTOR) {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
@@ -168,6 +178,7 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
||||
case KittyKey.QUIET: cmd.quiet = numValue; break;
|
||||
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
|
||||
case KittyKey.Z_INDEX: cmd.zIndex = numValue; break;
|
||||
case KittyKey.PLACEMENT_ID: cmd.placementId = numValue; break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
// 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) => {
|
||||
// DEBT: This test never worked on webkit
|
||||
@@ -227,12 +228,12 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
});
|
||||
|
||||
test.describe('Delete commands', () => {
|
||||
test('delete command (a=d) removes specific image by id', async () => {
|
||||
test('delete command (a=d,d=i) removes specific image by id', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=10;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,i=10\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=10\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
@@ -256,7 +257,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,i=50\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=50\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 0);
|
||||
});
|
||||
@@ -270,7 +271,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,i=55\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=55\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 1);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.has(56)`), true);
|
||||
@@ -290,6 +291,112 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').pendingTransmissions.size`), 0);
|
||||
});
|
||||
|
||||
test('d=i selector deletes specific image by id', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=80;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=81;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=80\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(81)`), true);
|
||||
});
|
||||
|
||||
test('d=I selector deletes specific image by id (uppercase)', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=82;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=83;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=I,i=82\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(83)`), true);
|
||||
});
|
||||
|
||||
test('d=a selector deletes all images', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=84;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=85;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=a\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
|
||||
test('d=A selector deletes all images (uppercase)', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=86;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=87;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=A\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
|
||||
test('d=a selector also removes displayed images from storage', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=88;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=a\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await getImageStorageLength(), 0);
|
||||
});
|
||||
|
||||
test('d=i selector also removes displayed image from storage', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=89;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=89\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await getImageStorageLength(), 0);
|
||||
});
|
||||
|
||||
test('d=i without id does nothing', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=90;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
});
|
||||
|
||||
test('d=i selector clears pixels from canvas', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=92,q=1;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [0, 0, 0, 255]);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=92\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getPixel(0, 0, 0, 0), null);
|
||||
});
|
||||
|
||||
test('d=a selector clears all pixels from canvas', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=93,q=1;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [0, 0, 0, 255]);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=a\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await getPixel(0, 0, 0, 0), null);
|
||||
});
|
||||
|
||||
test('unsupported delete selector is ignored', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=91;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=c\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
});
|
||||
|
||||
test('chunks sent after delete are not assembled with previous data', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
@@ -865,7 +972,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await timeout(200);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(700)`), true);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,i=700\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=d,d=i,i=700\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(700)`), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user