mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Dont send false query
This commit is contained in:
@@ -309,6 +309,14 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
|
||||
return true;
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=p (placement): place a previously transmitted image
|
||||
// - 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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -319,8 +327,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
switch (action) {
|
||||
case KittyAction.TRANSMIT: {
|
||||
const result = this._handleTransmit(cmd, bytes, decodeError);
|
||||
if (cmd.id !== undefined && !decodeError && bytes.length > 0) {
|
||||
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
|
||||
if (cmd.id !== undefined) {
|
||||
if (decodeError) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
} else if (bytes.length > 0) {
|
||||
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -329,6 +341,14 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
case KittyAction.QUERY:
|
||||
return this._handleQuery(cmd, bytes, decodeError);
|
||||
default:
|
||||
// TODO: Implement remaining actions when needed:
|
||||
// - a=p (placement): place a previously transmitted image
|
||||
// - 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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -338,11 +358,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
// 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.
|
||||
// - 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.
|
||||
|
||||
if (decodeError || bytes.length === 0) return true;
|
||||
|
||||
@@ -360,7 +381,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
|
||||
if (decodeError) return true;
|
||||
if (decodeError) {
|
||||
if (cmd.id !== undefined) {
|
||||
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const pendingKey = cmd.id ?? 0;
|
||||
|
||||
@@ -375,12 +401,12 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
if (image) {
|
||||
const result = this._displayImage(image, cmd);
|
||||
if (cmd.id !== undefined) {
|
||||
return (result as Promise<boolean>).then(r => {
|
||||
this._sendResponse(id, 'OK', cmd.quiet ?? 0);
|
||||
return r;
|
||||
return result.then(success => {
|
||||
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return result;
|
||||
return result.then(() => true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -389,6 +415,15 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
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.
|
||||
const transmission = cmd.transmission ?? 'd';
|
||||
if (transmission !== 'd') {
|
||||
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check decode error first (invalid base64)
|
||||
if (decodeError) {
|
||||
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
|
||||
@@ -491,10 +526,10 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
|
||||
// Image display
|
||||
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): boolean | Promise<boolean> {
|
||||
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): Promise<boolean> {
|
||||
return this._decodeAndDisplay(image, cmd)
|
||||
.then(() => true)
|
||||
.catch(() => true);
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
|
||||
@@ -516,7 +551,9 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
h = Math.round(imgRows * ch);
|
||||
}
|
||||
|
||||
if (w * h > this._opts.pixelLimit) return;
|
||||
if (w * h > this._opts.pixelLimit) {
|
||||
throw new Error('image exceeds pixel limit');
|
||||
}
|
||||
|
||||
// Save cursor position before addImage modifies it
|
||||
const buffer = this._coreTerminal._core.buffer;
|
||||
|
||||
@@ -73,6 +73,8 @@ export const enum KittyKey {
|
||||
CURSOR_MOVEMENT = 'C',
|
||||
// Z-index for image layering (negative = behind text, 0+ = on top)
|
||||
Z_INDEX = 'z',
|
||||
// Transmission medium (d=direct, f=file, t=temp file, s=shared memory)
|
||||
TRANSMISSION = 't',
|
||||
// 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
|
||||
@@ -102,6 +104,7 @@ export interface IKittyCommand {
|
||||
quiet?: number;
|
||||
cursorMovement?: number;
|
||||
zIndex?: number;
|
||||
transmission?: string;
|
||||
deleteSelector?: string;
|
||||
placementId?: number;
|
||||
compression?: string;
|
||||
@@ -159,6 +162,10 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
||||
cmd.compression = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.TRANSMISSION) {
|
||||
cmd.transmission = value;
|
||||
continue;
|
||||
}
|
||||
if (key === KittyKey.DELETE_SELECTOR) {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
|
||||
@@ -544,6 +544,239 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Error responses for transmit and display', () => {
|
||||
test('a=t sends EINVAL on decode error when id is specified', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=110,a=t,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=110;EINVAL:invalid base64 data\x1b\\');
|
||||
});
|
||||
|
||||
test('a=t sends no response on decode error without id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Ga=t,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T sends EINVAL on decode error when id is specified', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=120,a=T,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=120;EINVAL:invalid base64 data\x1b\\');
|
||||
});
|
||||
|
||||
test('a=T sends no response on decode error without id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Ga=T,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T sends EINVAL when raw pixel render fails (missing dimensions)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=130,a=T,f=24;${RAW_RGB_1X1_BLACK}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=130;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('a=T sends OK on successful render with id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=140,a=T,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=140;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('a=t sends OK on successful transmit with id', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=150,a=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=150;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('a=t EINVAL suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=160,a=t,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T EINVAL suppressed by q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=170,a=T,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=t OK suppressed by q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=180,a=t,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('a=T OK suppressed by q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=190,a=T,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Transmission medium rejection', () => {
|
||||
test('query rejects t=f (file transmission)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=200,a=q,t=f,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=200;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('query rejects t=s (shared memory)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=201,a=q,t=s,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=201;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('query rejects t=t (temp file)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=202,a=q,t=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=202;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('query accepts t=d (direct transmission)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=203,a=q,t=d,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=203;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('query without t key defaults to direct (OK)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=204,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=204;OK\x1b\\');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Unimplemented action responses', () => {
|
||||
test('a=p with id responds EINVAL', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=210,a=p\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=210;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('a=p without id sends no response', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=p\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user