multiple changes in APC handling:

- change interface to IFunctionIdentifier (API change)
- extend parser states APC_ENTRY, APC_INTERMEDIATE, APC_PASSTHROUGH
- remove APC_STRING
- change ApcParser.start to retrieve ident
- change actions APC_START and APC_PUT
This commit is contained in:
Jörg Breitbart
2026-04-24 13:53:31 +02:00
parent 9c5420371b
commit 9c8de633e9
16 changed files with 188 additions and 205 deletions
+1 -1
View File
@@ -166,7 +166,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
this._disposeLater(
kittyStorage,
kittyHandler,
terminal._core._inputHandler._parser.registerApcHandler(0x47, kittyHandler)
terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
);
}
}
+11
View File
@@ -245,6 +245,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -558,6 +559,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
},
@@ -599,6 +601,7 @@
"url": "https://opencollective.com/csstools"
}
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -1796,6 +1799,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.1.tgz",
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1",
@@ -2361,6 +2365,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2751,6 +2756,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3746,6 +3752,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7010,6 +7017,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -7850,6 +7858,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -8016,6 +8025,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz",
"integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==",
"dev": true,
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -8064,6 +8074,7 @@
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
"integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==",
"dev": true,
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^1.2.0",
+1 -1
View File
@@ -107,7 +107,7 @@ export class MockTerminal implements ITerminal {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
+2 -2
View File
@@ -243,8 +243,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
}
/** Add handler for APC escape sequence. See xterm.d.ts for details. */
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerApcHandler(ident, callback);
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerApcHandler(id, callback);
}
protected _setup(): void {
+2 -2
View File
@@ -729,8 +729,8 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* Forward registerApcHandler from parser.
*/
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerApcHandler(ident, new ApcHandler(callback));
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerApcHandler(id, new ApcHandler(callback));
}
/**
+2 -2
View File
@@ -22,7 +22,7 @@ export interface ICoreTerminal {
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
}
export interface IDisposable {
@@ -476,7 +476,7 @@ export interface IInputHandler {
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
/** C0 BEL */ bell(): boolean;
/** C0 LF */ lineFeed(): boolean;
+43 -43
View File
@@ -60,8 +60,8 @@ describe('ApcParser', () => {
describe('identifier parsing', () => {
it('single character identifier', () => {
parser.start();
const data = toUtf32('Gf=100,a=T;payload');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100,a=T;payload');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
@@ -71,20 +71,20 @@ describe('ApcParser', () => {
]);
});
it('identifier with no payload', () => {
parser.start();
const data = toUtf32('G');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[0x47, 'START', undefined],
[0x47, 'END', true]
]);
});
//it('identifier with no payload', () => {
// parser.start('G'.charCodeAt(0));
// const data = toUtf32('');
// parser.put(data, 0, data.length);
// parser.end(true);
// assert.deepEqual(reports, [
// [0x47, 'START', undefined],
// [0x47, 'END', true]
// ]);
//});
it('identifier with chunked payload', () => {
parser.start();
let data = toUtf32('Gf=100');
parser.start('G'.charCodeAt(0));
let data = toUtf32('f=100');
parser.put(data, 0, data.length);
data = toUtf32(',a=T');
parser.put(data, 0, data.length);
@@ -100,11 +100,11 @@ describe('ApcParser', () => {
]);
});
it('empty APC sequence', () => {
parser.start();
parser.end(true);
assert.deepEqual(reports, []);
});
//it('empty APC sequence', () => {
// parser.start('G'.charCodeAt(0));
// parser.end(true);
// assert.deepEqual(reports, []);
//});
});
describe('handler registration', () => {
@@ -117,8 +117,8 @@ describe('ApcParser', () => {
it('registerHandler for specific identifier', () => {
const G_CODE = 0x47; // 'G'
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
parser.start();
const data = toUtf32('Gf=100,a=T;imagedata');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100,a=T;imagedata');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
@@ -133,8 +133,8 @@ describe('ApcParser', () => {
const G_CODE = 0x47; // 'G'
const X_CODE = 0x58; // 'X'
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
parser.start();
const data = toUtf32('Xsome data');
parser.start('X'.charCodeAt(0));
const data = toUtf32('some data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, []);
@@ -149,8 +149,8 @@ describe('ApcParser', () => {
const G_CODE = 0x47;
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
parser.clearHandler(G_CODE);
parser.start();
const data = toUtf32('Gf=100');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, []);
@@ -165,8 +165,8 @@ describe('ApcParser', () => {
const G_CODE = 0x47;
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
parser.start();
const data = toUtf32('Gdata');
parser.start('G'.charCodeAt(0));
const data = toUtf32('data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
@@ -183,8 +183,8 @@ describe('ApcParser', () => {
const G_CODE = 0x47;
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2', true));
parser.start();
const data = toUtf32('Gdata');
parser.start('G'.charCodeAt(0));
const data = toUtf32('data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
@@ -202,8 +202,8 @@ describe('ApcParser', () => {
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler1'));
const disposable = parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'handler2'));
disposable.dispose();
parser.start();
const data = toUtf32('Gdata');
parser.start('G'.charCodeAt(0));
const data = toUtf32('data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
@@ -237,8 +237,8 @@ describe('ApcParser', () => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('Gf=100');
parser.start('G'.charCodeAt(0));
let data = toUtf32('f=100');
parser.put(data, 0, data.length);
data = toUtf32(',a=T;payload');
parser.put(data, 0, data.length);
@@ -253,8 +253,8 @@ describe('ApcParser', () => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
const data = toUtf32('Gf=100,a=T;payload');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100,a=T;payload');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(results, []);
@@ -268,8 +268,8 @@ describe('ApcParser', () => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('G');
parser.start('G'.charCodeAt(0));
let data = toUtf32('');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
@@ -287,8 +287,8 @@ describe('ApcParser', () => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('G');
parser.start('G'.charCodeAt(0));
let data = toUtf32('');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(CHUNK_SIZE));
for (let i = 0; i < TEST_PAYLOAD_LIMIT; i += CHUNK_SIZE) {
@@ -311,8 +311,8 @@ describe('ApcParser', () => {
it('reset during payload cleans up handlers', () => {
const G_CODE = 0x47;
parser.registerHandler(G_CODE, new TestHandler(G_CODE, handlerReports, 'kitty'));
parser.start();
const data = toUtf32('Gf=100');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100');
parser.put(data, 0, data.length);
parser.reset();
assert.deepEqual(handlerReports, [
@@ -353,8 +353,8 @@ describe('ApcParser - async tests', () => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
const data = toUtf32('Gf=100,a=T');
parser.start('G'.charCodeAt(0));
const data = toUtf32('f=100,a=T');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(results, [[G_CODE, 'f=100,a=T']]);
+42 -90
View File
@@ -19,10 +19,9 @@ const EMPTY_HANDLERS: IApcHandler[] = [];
* The identifier is the character code of the first byte after ESC _.
*/
export class ApcParser implements IApcParser {
private _state = ApcState.START;
private _active = EMPTY_HANDLERS;
private _id = -1;
private _handlers: IHandlerCollection<IApcHandler> = Object.create(null);
private _active = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: ApcFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
@@ -64,22 +63,24 @@ export class ApcParser implements IApcParser {
}
public reset(): void {
// force cleanup handlers if payload was already sent
if (this._state === ApcState.PAYLOAD) {
// force cleanup handlers
if (this._active.length) {
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = ApcState.START;
this._ident = 0;
}
private _start(): void {
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
public start(ident: number): void {
// always reset leftover handlers
this.reset();
this._ident = ident;
this._active = this._handlers[ident] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._id, 'START');
this._handlerFb(this._ident, 'START');
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].start();
@@ -87,9 +88,9 @@ export class ApcParser implements IApcParser {
}
}
private _put(data: Uint32Array, start: number, end: number): void {
public put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
@@ -97,100 +98,51 @@ export class ApcParser implements IApcParser {
}
}
public start(): void {
// always reset leftover handlers
this.reset();
this._state = ApcState.ID;
}
/**
* Put data to current APC command.
* For APC, the first character is used as the identifier.
* Format: ESC _ <identifier><payload> ESC \
* Example: ESC _ G f=100,a=T;... ESC \ (Kitty graphics, identifier='G')
*/
public put(data: Uint32Array, start: number, end: number): void {
if (this._state === ApcState.ABORT) {
return;
}
if (this._state === ApcState.ID) {
// The first character is the identifier
if (start < end) {
this._id = data[start++];
this._state = ApcState.PAYLOAD;
this._start();
}
}
if (this._state === ApcState.PAYLOAD && end - start > 0) {
this._put(data, start, end);
}
}
/**
* Indicates end of an APC command.
* Whether the APC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (this._state === ApcState.START) {
return;
}
// do nothing if command was faulty
if (this._state !== ApcState.ABORT) {
// if we are still in ID state and get an early end
// means we got an empty APC sequence with no identifier,
// which is invalid - just reset and return
if (this._state === ApcState.ID) {
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = ApcState.START;
return;
if (!this._active.length) {
this._handlerFb(this._ident, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers
// we always have to call .end for proper cleanup,
// here we use `success` to indicate whether a handler should execute
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers (fallThrough for async)
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = ApcState.START;
this._ident = 0;
}
}
+4 -2
View File
@@ -21,9 +21,11 @@ export const enum ParserState {
DCS_IGNORE = 11,
DCS_INTERMEDIATE = 12,
DCS_PASSTHROUGH = 13,
APC_STRING = 14,
APC_ENTRY = 14,
APC_INTERMEDIATE = 15,
APC_PASSTHROUGH = 16,
// Number of states, meaning LAST_STATE + 1.
STATE_LENGTH = 15
STATE_LENGTH = 17
}
/**
+18 -16
View File
@@ -163,7 +163,9 @@ const states: number[] = [
ParserState.DCS_IGNORE,
ParserState.DCS_INTERMEDIATE,
ParserState.DCS_PASSTHROUGH,
ParserState.APC_STRING
ParserState.APC_ENTRY,
ParserState.APC_INTERMEDIATE,
ParserState.APC_PASSTHROUGH
];
let state: any;
@@ -278,7 +280,7 @@ describe('EscapeSequenceParser', () => {
const exceptions: { [key: number]: { [key: string]: any[] } } = {
8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING
13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] }, // abort DCS_PASSTHROUGH
14: { '\x18': [], '\x1a': [] } // abort APC_STRING
16: { '\x18': [], '\x1a': [] } // abort APC_PASSTHROUGH
};
parser.reset();
testTerminal.clear();
@@ -725,20 +727,20 @@ describe('EscapeSequenceParser', () => {
}
}
});
it('trans ANYWHERE/ESCAPE --> APC_STRING', () => {
parser.reset();
// C0 (ESC _)
parse(parser, '\x1b_');
assert.equal(parser.currentState, ParserState.APC_STRING);
parser.reset();
// C1
for (state in states) {
parser.currentState = state;
parse(parser, '\x9f');
assert.equal(parser.currentState, ParserState.APC_STRING);
parser.reset();
}
});
//it('trans ANYWHERE/ESCAPE --> APC_STRING', () => {
// parser.reset();
// // C0 (ESC _)
// parse(parser, '\x1b_');
// assert.equal(parser.currentState, ParserState.APC_STRING);
// parser.reset();
// // C1
// for (state in states) {
// parser.currentState = state;
// parse(parser, '\x9f');
// assert.equal(parser.currentState, ParserState.APC_STRING);
// parser.reset();
// }
//});
it('state SOS_PM_STRING ignore rules', () => {
parser.reset();
let ignored = r(0x00, 0x18);
+37 -21
View File
@@ -77,7 +77,8 @@ const NON_ASCII_PRINTABLE = 0xA0;
* Taken from https://vt100.net/emu/dec_ansi_parser.
*/
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
const table: TransitionTable = new TransitionTable(4095);
// table size: (ParserState.STATE_LENGTH - 1) << TableAccess.INDEX_STATE_SHIFT | NON_ASCII_PRINTABLE + 1
const table: TransitionTable = new TransitionTable(4257);
// range macro for byte
const BYTE_VALUES = 256;
@@ -105,7 +106,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_STRING); // SOS, PM
table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC
table.add(0x9f, state, ParserAction.CLEAR, ParserState.APC_ENTRY); // APC
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
}
@@ -136,11 +137,19 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(0x9c, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x7f, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
// apc
table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING);
table.addMany(PRINTABLES, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
table.addMany(EXECUTABLES, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
table.add(0x7f, ParserState.APC_STRING, ParserAction.IGNORE, ParserState.APC_STRING);
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_STRING, ParserAction.APC_END, ParserState.GROUND);
table.add(0x5f, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.APC_ENTRY);
table.addMany(EXECUTABLES, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
table.add(0x7f, ParserState.APC_ENTRY, ParserAction.IGNORE, ParserState.APC_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.APC_ENTRY, ParserAction.COLLECT, ParserState.APC_INTERMEDIATE);
table.addMany(r(0x30, 0x7f), ParserState.APC_ENTRY, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
table.addMany(r(0x30, 0x7f), ParserState.APC_INTERMEDIATE, ParserAction.APC_START, ParserState.APC_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
table.add(0x7f, ParserState.APC_INTERMEDIATE, ParserAction.IGNORE, ParserState.APC_INTERMEDIATE);
table.addMany(PRINTABLES, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
table.addMany(r(0x08, 0x0e), ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
table.add(0x7f, ParserState.APC_PASSTHROUGH, ParserAction.IGNORE, ParserState.APC_PASSTHROUGH);
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.APC_PASSTHROUGH, ParserAction.APC_END, ParserState.GROUND);
// csi entries
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
@@ -200,7 +209,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(NON_ASCII_PRINTABLE, ParserState.APC_STRING, ParserAction.APC_PUT, ParserState.APC_STRING);
table.add(NON_ASCII_PRINTABLE, ParserState.APC_PASSTHROUGH, ParserAction.APC_PUT, ParserState.APC_PASSTHROUGH);
return table;
})();
@@ -439,11 +448,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._oscParser.setHandlerFallback(handler);
}
public registerApcHandler(ident: number, handler: IApcHandler): IDisposable {
return this._apcParser.registerHandler(ident, handler);
public registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable {
id.prefix = undefined; // APC does not support prefix byte
return this._apcParser.registerHandler(this._identifier(id, [0x30, 0x7e]), handler);
}
public clearApcHandler(ident: number): void {
this._apcParser.clearHandler(ident);
public clearApcHandler(id: IFunctionIdentifier): void {
id.prefix = undefined; // APC does not support prefix byte
this._apcParser.clearHandler(this._identifier(id, [0x30, 0x7e]));
}
public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {
this._apcParser.setHandlerFallback(handler);
@@ -719,7 +730,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
// normal transition & action lookup
transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
transition = this._transitions.table[
this.currentState << TableAccess.INDEX_STATE_SHIFT |
(code < NON_ASCII_PRINTABLE ? code : NON_ASCII_PRINTABLE)
];
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
case ParserAction.PRINT:
// Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
@@ -872,17 +886,19 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.precedingJoinState = 0;
break;
case ParserAction.APC_START:
this._apcParser.start();
this._apcParser.start(this._collect << 8 | code);
break;
case ParserAction.APC_PUT:
// FIXME: this is wrong, must be 00/08 .. 00/13, 02/00 .. 07/14 + NON_ASCII_PRINTABLE
// inner loop - exit APC_PUT: 0x18, 0x1a, 0x1b, 0x9c
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || code === 0x9c || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
this._apcParser.put(data, i, j);
i = j - 1;
break;
}
}
//for (let j = i + 1; ; ++j) {
// if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || code === 0x9c || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
// this._apcParser.put(data, i, j);
// i = j - 1;
// break;
// }
//}
this._apcParser.put(data, i, i+1);
break;
case ParserAction.APC_END:
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);
+3 -3
View File
@@ -220,8 +220,8 @@ export interface IEscapeSequenceParser extends IDisposable {
clearOscHandler(ident: number): void;
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
registerApcHandler(ident: number, handler: IApcHandler): IDisposable;
clearApcHandler(ident: number): void;
registerApcHandler(id: IFunctionIdentifier, handler: IApcHandler): IDisposable;
clearApcHandler(id: IFunctionIdentifier): void;
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
@@ -252,7 +252,7 @@ export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerTy
}
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
start(): void;
start(ident: number): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
+2 -2
View File
@@ -34,7 +34,7 @@ export class ParserApi implements IParser {
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this.registerOscHandler(ident, callback);
}
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._core.registerApcHandler(ident, callback);
public registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._core.registerApcHandler(id, callback);
}
}
+10 -10
View File
@@ -248,7 +248,7 @@ test.describe('Parser Integration Tests', () => {
await ctx.proxy.evaluate(([term]) => {
window.customApcHandlerCallStack = [];
// APC uses first character as identifier (e.g., 0x41 = 'A')
window.disposable = term.parser.registerApcHandler(0x41, data => {
window.disposable = term.parser.registerApcHandler({ final: 'A' }, data => {
window.customApcHandlerCallStack!.push(['handler', data]);
return true;
});
@@ -263,7 +263,7 @@ test.describe('Parser Integration Tests', () => {
test('should handle short data', async () => {
await ctx.proxy.evaluate(([term]) => {
window.customApcHandlerCallStack = [];
window.disposable = term.parser.registerApcHandler(0x42, data => {
window.disposable = term.parser.registerApcHandler({ final: 'B' }, data => {
window.customApcHandlerCallStack!.push(['handler', data]);
return true;
});
@@ -278,15 +278,15 @@ test.describe('Parser Integration Tests', () => {
await ctx.proxy.evaluate(([term]) => {
window.customApcHandlerCallStack = [];
window.disposables = [
term.parser.registerApcHandler(0x43, data => {
term.parser.registerApcHandler({ final: 'C' }, data => {
window.customApcHandlerCallStack!.push(['A', data]);
return false;
}),
term.parser.registerApcHandler(0x43, data => {
term.parser.registerApcHandler({ final: 'C' }, data => {
window.customApcHandlerCallStack!.push(['B', data]);
return true;
}),
term.parser.registerApcHandler(0x43, data => {
term.parser.registerApcHandler({ final: 'C' }, data => {
window.customApcHandlerCallStack!.push(['C', data]);
return false;
})
@@ -302,17 +302,17 @@ test.describe('Parser Integration Tests', () => {
await ctx.proxy.evaluate(([term]) => {
window.customApcHandlerCallStack = [];
window.disposables = [
term.parser.registerApcHandler(0x44, data => {
term.parser.registerApcHandler({ final: 'D' }, data => {
window.customApcHandlerCallStack!.push(['A', data]);
return false;
}),
term.parser.registerApcHandler(0x44, data => {
term.parser.registerApcHandler({ final: 'D' }, data => {
return new Promise(res => setTimeout(res, 50)).then(() => {
window.customApcHandlerCallStack!.push(['B', data]);
return false;
});
}),
term.parser.registerApcHandler(0x44, data => {
term.parser.registerApcHandler({ final: 'D' }, data => {
window.customApcHandlerCallStack!.push(['C', data]);
return false;
})
@@ -330,11 +330,11 @@ test.describe('Parser Integration Tests', () => {
await ctx.proxy.evaluate(([term]) => {
window.customApcHandlerCallStack = [];
window.disposables = [
term.parser.registerApcHandler(0x46, data => { // 'F'
term.parser.registerApcHandler({ final: 'F' }, data => {
window.customApcHandlerCallStack!.push(['F', data]);
return true;
}),
term.parser.registerApcHandler(0x58, data => { // 'X'
term.parser.registerApcHandler({ final: 'X' }, data => {
window.customApcHandlerCallStack!.push(['X', data]);
return true;
})
+5 -5
View File
@@ -1274,12 +1274,12 @@ declare module '@xterm/headless' {
prefix?: string;
/**
* Optional intermediate bytes, must be in range \x20 .. \x2f.
* Usable in CSI, DCS and ESC.
* Usable in CSI, DCS, ESC and APC.
*/
intermediates?: string;
/**
* Final byte, must be in range \x40 .. \x7e for CSI and DCS,
* \x30 .. \x7e for ESC.
* \x30 .. \x7e for ESC and APC.
*/
final: string;
}
@@ -1354,8 +1354,8 @@ declare module '@xterm/headless' {
/**
* Adds a handler for APC escape sequences.
* @param ident The identifier (first character) of the sequence as a
* character code, e.g. 71 for 'G' (Kitty graphics protocol).
* @param id Specifies the function identifier under which the callback
* gets registered, e.g. {final: 'G'} for Kitty graphics protocol.
* @param callback The function to handle the sequence. Note that the
* function will only be called once if the sequence finished successfully.
* There is currently no way to intercept smaller data chunks, data chunks
@@ -1368,7 +1368,7 @@ declare module '@xterm/headless' {
* added handler is tried first.
* @returns An IDisposable you can call to remove this handler.
*/
registerApcHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean): IDisposable;
}
/**
+5 -5
View File
@@ -1909,12 +1909,12 @@ declare module '@xterm/xterm' {
prefix?: string;
/**
* Optional intermediate bytes, must be in range \x20 .. \x2f.
* Usable in CSI, DCS and ESC.
* Usable in CSI, DCS, ESC and APC.
*/
intermediates?: string;
/**
* Final byte, must be in range \x40 .. \x7e for CSI and DCS,
* \x30 .. \x7e for ESC.
* \x30 .. \x7e for ESC and APC.
*/
final: string;
}
@@ -2002,8 +2002,8 @@ declare module '@xterm/xterm' {
/**
* Adds a handler for APC escape sequences.
* @param ident The identifier (first character) of the sequence as a
* character code, e.g. 71 for 'G' (Kitty graphics protocol).
* @param id Specifies the function identifier under which the callback
* gets registered, e.g. {final: 'G'} for Kitty graphics protocol.
* @param callback The function to handle the sequence. Note that the
* function will only be called once if the sequence finished successfully.
* There is currently no way to intercept smaller data chunks, data chunks
@@ -2016,7 +2016,7 @@ declare module '@xterm/xterm' {
* handler. The most recently added handler is tried first.
* @returns An IDisposable you can call to remove this handler.
*/
registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerApcHandler(id: IFunctionIdentifier, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
}
/**