Support Kitty graphics protocol
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2025, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,47 @@
|
||||
# @xterm/addon-kitty-graphics
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that adds support for the [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install --save @xterm/addon-kitty-graphics @xterm/xterm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { KittyGraphicsAddon } from '@xterm/addon-kitty-graphics';
|
||||
|
||||
const terminal = new Terminal();
|
||||
const kittyGraphicsAddon = new KittyGraphicsAddon();
|
||||
terminal.loadAddon(kittyGraphicsAddon);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
This addon implements the Kitty graphics protocol, allowing applications to display images directly in the terminal using APC (Application Program Command) escape sequences.
|
||||
|
||||
### Supported Features
|
||||
|
||||
- PNG image transmission (f=100)
|
||||
- Direct RGB/RGBA pixel data (f=24, f=32)
|
||||
- Image placement at cursor position
|
||||
- Basic query support (a=q)
|
||||
|
||||
### Protocol Format
|
||||
|
||||
The Kitty graphics protocol uses APC escape sequences:
|
||||
|
||||
```
|
||||
<ESC>_G<key>=<value>,<key>=<value>,...;<base64 data><ESC>\
|
||||
```
|
||||
|
||||
Key parameters:
|
||||
- `a`: Action (t=transmit, T=transmit+display, q=query)
|
||||
- `f`: Format (100=PNG, 24=RGB, 32=RGBA)
|
||||
- `i`: Image ID
|
||||
- `m`: More data follows (1=yes, 0=no)
|
||||
|
||||
See the [Kitty graphics protocol documentation](https://sw.kovidgoyal.net/kitty/graphics-protocol/) for full details.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 123 B |
Binary file not shown.
|
After Width: | Height: | Size: 131 B |
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "@xterm/addon-kitty-graphics",
|
||||
"version": "0.1.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/addon-kitty-graphics.js",
|
||||
"module": "lib/addon-kitty-graphics.mjs",
|
||||
"types": "typings/addon-kitty-graphics.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-kitty-graphics",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js",
|
||||
"kitty",
|
||||
"graphics"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { KittyGraphicsAddon, parseKittyCommand } from './KittyGraphicsAddon';
|
||||
|
||||
/**
|
||||
* Write data to terminal and wait for completion.
|
||||
*/
|
||||
function writeP(terminal: Terminal, data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
}
|
||||
|
||||
// Test image: 1x1 black PNG (captured from `send-png fixture/black-1x1.png`)
|
||||
// Get the below base64-encoded PNG file by: `python3 send-png addons/addon-kitty-graphics/fixture/black-1x1.png`
|
||||
const BLACK_1X1_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAA1BMVEUAAACnej3aAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAAt0RVh0Q29tbWVudAAA1LTqjgAAAApJREFUeJxjYAAAAGQA2AAAAAt0RVh0Q29tbWVudAAA1LTqjg5JREFUAAAAASUVORK5CYII=';
|
||||
|
||||
// Test image: 3x1 RGB PNG (red, green, blue pixels)
|
||||
const RGB_3X1_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAMAAAABCAMAAAAsPuSGAAAACVBMVEX/AAAA/wAAAP8tSs2KAAAADElEQVR4nGNgYGQCAAAIAAQ24LCmAAAAHXRFWHRTb2Z0d2FyZQBAbHVuYXBhaW50L3BuZy1jb2RlY/VDGR4AAAAASUVORK5CYII=';
|
||||
|
||||
// Currently tests the flow: write escape sequence -> addon stores image.
|
||||
// Pixel-level verification of rendered images is done in Playwright tests.
|
||||
describe('KittyGraphicsAddon', () => {
|
||||
let terminal: Terminal;
|
||||
let addon: KittyGraphicsAddon;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new Terminal({ cols: 80, rows: 24, allowProposedApi: true });
|
||||
addon = new KittyGraphicsAddon({ debug: false });
|
||||
terminal.loadAddon(addon);
|
||||
});
|
||||
|
||||
describe('parseKittyCommand', () => {
|
||||
it('should parse control data with action and format', () => {
|
||||
const cmd = parseKittyCommand('a=T,f=100');
|
||||
assert.equal(cmd.action, 'T');
|
||||
assert.equal(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should parse control data with all options', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
|
||||
assert.equal(cmd.action, 't');
|
||||
assert.equal(cmd.format, 32);
|
||||
assert.equal(cmd.id, 5);
|
||||
assert.equal(cmd.width, 10);
|
||||
assert.equal(cmd.height, 20);
|
||||
assert.equal(cmd.columns, 3);
|
||||
assert.equal(cmd.rows, 2);
|
||||
assert.equal(cmd.more, 1);
|
||||
assert.equal(cmd.quiet, 2);
|
||||
});
|
||||
|
||||
it('should handle empty control data', () => {
|
||||
const cmd = parseKittyCommand('');
|
||||
assert.equal(cmd.action, undefined);
|
||||
assert.equal(cmd.format, undefined);
|
||||
});
|
||||
|
||||
it('should parse transmit action', () => {
|
||||
const cmd = parseKittyCommand('a=t,f=100');
|
||||
assert.equal(cmd.action, 't');
|
||||
assert.equal(cmd.format, 100);
|
||||
});
|
||||
|
||||
it('should parse delete action', () => {
|
||||
const cmd = parseKittyCommand('a=d,i=5');
|
||||
assert.equal(cmd.action, 'd');
|
||||
assert.equal(cmd.id, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('APC handler', () => {
|
||||
it('should store image when transmit+display sequence is written', async () => {
|
||||
// Write Kitty graphics sequence: ESC _ G <control>;<payload> ESC \
|
||||
const sequence = `\x1b_Ga=T,f=100;${BLACK_1X1_BASE64}\x1b\\`;
|
||||
await writeP(terminal, sequence);
|
||||
|
||||
// Addon should have stored the image
|
||||
assert.equal(addon.images.size, 1);
|
||||
|
||||
const image = addon.images.get(1)!;
|
||||
assert.exists(image);
|
||||
assert.equal(image.format, 100); // PNG format
|
||||
assert.equal(image.data, BLACK_1X1_BASE64);
|
||||
});
|
||||
|
||||
it('should store RGB image with correct payload', async () => {
|
||||
const sequence = `\x1b_Ga=T,f=100;${RGB_3X1_BASE64}\x1b\\`;
|
||||
await writeP(terminal, sequence);
|
||||
|
||||
assert.equal(addon.images.size, 1);
|
||||
const image = addon.images.get(1)!;
|
||||
assert.equal(image.data, RGB_3X1_BASE64);
|
||||
});
|
||||
|
||||
it('should use explicit image id when provided', async () => {
|
||||
const sequence = `\x1b_Ga=T,f=100,i=42;${BLACK_1X1_BASE64}\x1b\\`;
|
||||
await writeP(terminal, sequence);
|
||||
|
||||
assert.equal(addon.images.size, 1);
|
||||
assert.isTrue(addon.images.has(42));
|
||||
assert.isFalse(addon.images.has(1));
|
||||
});
|
||||
|
||||
it('should handle transmit-only (a=t) without display', async () => {
|
||||
const sequence = `\x1b_Ga=t,f=100;${BLACK_1X1_BASE64}\x1b\\`;
|
||||
await writeP(terminal, sequence);
|
||||
|
||||
// Image should still be stored??
|
||||
assert.equal(addon.images.size, 1);
|
||||
});
|
||||
|
||||
it('should delete image by id', async () => {
|
||||
// First store an image with id=5
|
||||
await writeP(terminal, `\x1b_Ga=T,f=100,i=5;${BLACK_1X1_BASE64}\x1b\\`);
|
||||
assert.equal(addon.images.size, 1);
|
||||
|
||||
// Delete it
|
||||
await writeP(terminal, `\x1b_Ga=d,i=5\x1b\\`);
|
||||
assert.equal(addon.images.size, 0);
|
||||
});
|
||||
|
||||
it('should delete all images when no id specified', async () => {
|
||||
// Store multiple images
|
||||
await writeP(terminal, `\x1b_Ga=T,f=100,i=1;${BLACK_1X1_BASE64}\x1b\\`);
|
||||
await writeP(terminal, `\x1b_Ga=T,f=100,i=2;${RGB_3X1_BASE64}\x1b\\`);
|
||||
assert.equal(addon.images.size, 2);
|
||||
|
||||
// Delete all
|
||||
await writeP(terminal, `\x1b_Ga=d\x1b\\`);
|
||||
assert.equal(addon.images.size, 0);
|
||||
});
|
||||
|
||||
it('should handle chunked transmission (m=1 flag)', async () => {
|
||||
// Split payload into chunks using m=1 (more data coming)
|
||||
const half = Math.floor(BLACK_1X1_BASE64.length / 2);
|
||||
const chunk1 = BLACK_1X1_BASE64.substring(0, half);
|
||||
const chunk2 = BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
// First chunk with m=1
|
||||
await writeP(terminal, `\x1b_Ga=t,f=100,i=10,m=1;${chunk1}\x1b\\`);
|
||||
// Image not complete yet (pending)
|
||||
assert.equal(addon.images.size, 0);
|
||||
|
||||
// Final chunk without m=1
|
||||
await writeP(terminal, `\x1b_Ga=t,f=100,i=10;${chunk2}\x1b\\`);
|
||||
// Now image should be stored
|
||||
assert.equal(addon.images.size, 1);
|
||||
|
||||
const image = addon.images.get(10)!;
|
||||
assert.equal(image.data, BLACK_1X1_BASE64);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||
import type { KittyGraphicsAddon as IKittyGraphicsApi, IKittyGraphicsOptions, IKittyImage } from '@xterm/addon-kitty-graphics';
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol action types.
|
||||
*/
|
||||
const enum KittyAction {
|
||||
TRANSMIT = 't',
|
||||
TRANSMIT_DISPLAY = 'T',
|
||||
QUERY = 'q',
|
||||
PLACEMENT = 'p',
|
||||
DELETE = 'd'
|
||||
}
|
||||
|
||||
/**
|
||||
* Kitty graphics protocol format types.
|
||||
*/
|
||||
const enum KittyFormat {
|
||||
RGB = 24,
|
||||
RGBA = 32,
|
||||
PNG = 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed Kitty graphics command.
|
||||
*/
|
||||
export interface IKittyCommand {
|
||||
action?: string;
|
||||
format?: number;
|
||||
id?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
more?: number;
|
||||
quiet?: number;
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Kitty graphics control data into a command object.
|
||||
* Exported for testing.
|
||||
*/
|
||||
export function parseKittyCommand(data: string): IKittyCommand {
|
||||
const cmd: IKittyCommand = {};
|
||||
const parts = data.split(',');
|
||||
|
||||
for (const part of parts) {
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx === -1) continue;
|
||||
|
||||
const key = part.substring(0, eqIdx);
|
||||
const value = part.substring(eqIdx + 1);
|
||||
|
||||
switch (key) {
|
||||
// Question: How do we know which radix to use?
|
||||
case 'a': cmd.action = value; break;
|
||||
case 'f': cmd.format = parseInt(value); break;
|
||||
case 'i': cmd.id = parseInt(value); break;
|
||||
case 's': cmd.width = parseInt(value); break;
|
||||
case 'v': cmd.height = parseInt(value); break;
|
||||
case 'x': cmd.x = parseInt(value); break;
|
||||
case 'y': cmd.y = parseInt(value); break;
|
||||
case 'c': cmd.columns = parseInt(value); break;
|
||||
case 'r': cmd.rows = parseInt(value); break;
|
||||
case 'm': cmd.more = parseInt(value); break;
|
||||
case 'q': cmd.quiet = parseInt(value); break;
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
export class KittyGraphicsAddon implements ITerminalAddon, IKittyGraphicsApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _apcHandler: IDisposable | undefined;
|
||||
private _images: Map<number, IKittyImage> = new Map();
|
||||
private _pendingData: Map<number, string> = new Map();
|
||||
private _nextImageId = 1;
|
||||
private _debug: boolean;
|
||||
|
||||
constructor(options?: IKittyGraphicsOptions) {
|
||||
this._debug = options?.debug ?? false;
|
||||
}
|
||||
|
||||
public get images(): ReadonlyMap<number, IKittyImage> {
|
||||
return this._images;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
// TODO: Remove console log
|
||||
console.log('[KittyGraphicsAddon] Activated');
|
||||
|
||||
// Register APC handler for 'G' (0x47) - Kitty graphics protocol
|
||||
// APC sequence format: ESC _ G <data> ESC \
|
||||
this._apcHandler = terminal.parser.registerApcHandler(0x47, (data: string) => {
|
||||
return this._handleKittyGraphics(data);
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._apcHandler?.dispose();
|
||||
this._images.clear();
|
||||
this._pendingData.clear();
|
||||
this._terminal = undefined;
|
||||
}
|
||||
|
||||
private _handleKittyGraphics(data: string): boolean {
|
||||
const semiIdx = data.indexOf(';');
|
||||
const controlData = semiIdx === -1 ? data : data.substring(0, semiIdx);
|
||||
const payload = semiIdx === -1 ? '' : data.substring(semiIdx + 1);
|
||||
|
||||
const cmd = parseKittyCommand(controlData);
|
||||
cmd.payload = payload;
|
||||
|
||||
if (this._debug) {
|
||||
console.log('[KittyGraphicsAddon] Received command:', cmd);
|
||||
}
|
||||
|
||||
const action = cmd.action || 't';
|
||||
|
||||
switch (action) {
|
||||
case KittyAction.TRANSMIT:
|
||||
return this._handleTransmit(cmd);
|
||||
case KittyAction.TRANSMIT_DISPLAY:
|
||||
return this._handleTransmitDisplay(cmd);
|
||||
case KittyAction.QUERY:
|
||||
return this._handleQuery(cmd);
|
||||
case KittyAction.DELETE:
|
||||
return this._handleDelete(cmd);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTransmit(cmd: IKittyCommand): boolean {
|
||||
const id = cmd.id || this._nextImageId++;
|
||||
const payload = cmd.payload || '';
|
||||
|
||||
if (cmd.more === 1) {
|
||||
const existing = this._pendingData.get(id) || '';
|
||||
this._pendingData.set(id, existing + payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
let fullPayload = payload;
|
||||
if (this._pendingData.has(id)) {
|
||||
fullPayload = this._pendingData.get(id)! + payload;
|
||||
this._pendingData.delete(id);
|
||||
}
|
||||
|
||||
const image: IKittyImage = {
|
||||
id,
|
||||
data: fullPayload,
|
||||
width: cmd.width || 0,
|
||||
height: cmd.height || 0,
|
||||
format: (cmd.format || KittyFormat.PNG) as 24 | 32 | 100
|
||||
};
|
||||
|
||||
this._images.set(id, image);
|
||||
|
||||
if (this._debug) {
|
||||
console.log(`[KittyGraphicsAddon] Stored image ${id}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleTransmitDisplay(cmd: IKittyCommand): boolean {
|
||||
this._handleTransmit(cmd);
|
||||
// TODO: Display image at cursor position (canvas layer)
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleQuery(cmd: IKittyCommand): boolean {
|
||||
// TODO: Respond with APC sequence indicating graphics support
|
||||
// Protocol: terminal should reply with ESC _ G i=<id>;OK ESC \
|
||||
if (this._debug) {
|
||||
console.log('[KittyGraphicsAddon] Query received');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _handleDelete(cmd: IKittyCommand): boolean {
|
||||
if (cmd.id !== undefined) {
|
||||
this._images.delete(cmd.id);
|
||||
} else {
|
||||
this._images.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"lib": ["dom", "es2015"],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": ["../../../node_modules/@types/mocha"],
|
||||
"paths": {
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
"vs/*": ["../../../src/vs/*"],
|
||||
"@xterm/addon-kitty-graphics": ["../typings/addon-kitty-graphics.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*", "../../../typings/xterm.d.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/vs"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Playwright integration tests for Kitty Graphics Addon.
|
||||
* These test the addon in a real browser with visual verification.
|
||||
*
|
||||
* Unit tests for parsing are in src/KittyGraphicsAddon.test.ts
|
||||
*/
|
||||
|
||||
import test from '@playwright/test';
|
||||
import { deepStrictEqual, strictEqual } from 'assert';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
|
||||
let ctx: ITestContext;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await ctx.page.close();
|
||||
});
|
||||
|
||||
test.describe('KittyGraphicsAddon', () => {
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await ctx.page.evaluate(`
|
||||
window.term.reset();
|
||||
`);
|
||||
});
|
||||
|
||||
test('addon should be loaded and activated', async () => {
|
||||
// Verify the addon is available on the window
|
||||
const hasAddon = await ctx.page.evaluate(`typeof window.KittyGraphicsAddon !== 'undefined'`);
|
||||
strictEqual(hasAddon, true, 'KittyGraphicsAddon should be available');
|
||||
});
|
||||
|
||||
// TODO: WAYYYYYY More tests that would initially fail, but work when addon work is complete. Like implementation of the handler.
|
||||
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
testDir: '.',
|
||||
timeout: 10000,
|
||||
projects: [
|
||||
{
|
||||
name: 'ChromeStable',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'FirefoxStable',
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'WebKit',
|
||||
use: {
|
||||
browserName: 'webkit'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: 'list',
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 3000,
|
||||
timeout: 120000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
}
|
||||
};
|
||||
export default config;
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"lib": ["es2021"],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"types": ["../../../node_modules/@types/node"]
|
||||
},
|
||||
"include": ["./**/*", "../../../typings/xterm.d.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [{ "path": "./src" }, { "path": "./test" }]
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||
|
||||
declare module '@xterm/addon-kitty-graphics' {
|
||||
/**
|
||||
* An xterm.js addon that provides support for the Kitty graphics protocol.
|
||||
* This allows applications to display images in the terminal using APC
|
||||
* escape sequences.
|
||||
*/
|
||||
export class KittyGraphicsAddon implements ITerminalAddon, IDisposable {
|
||||
/**
|
||||
* Creates a new Kitty graphics addon.
|
||||
* @param options Optional configuration for the addon.
|
||||
*/
|
||||
constructor(options?: IKittyGraphicsOptions);
|
||||
|
||||
/**
|
||||
* Activates the addon.
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
|
||||
/**
|
||||
* Gets the current images stored in the addon.
|
||||
* Returns a map of image IDs to their image data.
|
||||
*/
|
||||
public readonly images: ReadonlyMap<number, IKittyImage>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the Kitty graphics addon.
|
||||
*/
|
||||
export interface IKittyGraphicsOptions {
|
||||
/**
|
||||
* Enable debug logging of received graphics commands.
|
||||
* Default: false
|
||||
*/
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a stored Kitty graphics image.
|
||||
*/
|
||||
export interface IKittyImage {
|
||||
/**
|
||||
* The image ID assigned by the terminal or the application.
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* The image data as a base64 string or ImageData object.
|
||||
*/
|
||||
data: string | ImageData;
|
||||
|
||||
/**
|
||||
* Width of the image in pixels.
|
||||
*/
|
||||
width: number;
|
||||
|
||||
/**
|
||||
* Height of the image in pixels.
|
||||
*/
|
||||
height: number;
|
||||
|
||||
/**
|
||||
* Format of the image data.
|
||||
* - 24: RGB (3 bytes per pixel)
|
||||
* - 32: RGBA (4 bytes per pixel)
|
||||
* - 100: PNG
|
||||
*/
|
||||
format: 24 | 32 | 100;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'KittyGraphicsAddon';
|
||||
const mainFile = 'addon-kitty-graphics.js';
|
||||
|
||||
const addon = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser'),
|
||||
vs: path.resolve('../../out/vs')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd',
|
||||
// Force usage of globalThis instead of global / self. (This is cross-env compatible)
|
||||
globalObject: 'globalThis',
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
|
||||
module.exports = [addon];
|
||||
Reference in New Issue
Block a user