Add ApcParser

This commit is contained in:
Anthony Kim
2026-01-21 08:31:07 -08:00
parent 14b6a71391
commit 48df6117ed
7 changed files with 754 additions and 28 deletions
+8
View File
@@ -19,6 +19,7 @@ import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseS
import { UnicodeService } from 'common/services/UnicodeService';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { ApcHandler } from 'common/parser/ApcParser';
import { IBuffer } from 'common/buffer/Types';
import { parseColor } from 'common/input/XParseColor';
import { Emitter } from 'vs/base/common/event';
@@ -712,6 +713,13 @@ export class InputHandler extends Disposable implements IInputHandler {
return this._parser.registerOscHandler(ident, new OscHandler(callback));
}
/**
* Forward registerApcHandler from parser.
*/
public registerApcHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerApcHandler(ident, new ApcHandler(callback));
}
/**
* BEL
* Bell (Ctrl-G).
+349
View File
@@ -0,0 +1,349 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { ApcParser, ApcHandler } from 'common/parser/ApcParser';
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { IApcHandler } from 'common/parser/Types';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
class TestHandler implements IApcHandler {
public id: number;
public output: [string, number, string, (boolean | string)?][];
public msg: string;
public returnFalse: boolean;
constructor(
id: number,
output: [string, number, string, (boolean | string)?][],
msg: string,
returnFalse: boolean = false
) {
this.id = id;
this.output = output;
this.msg = msg;
this.returnFalse = returnFalse;
}
public start(): void {
this.output.push([this.msg, this.id, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
public end(success: boolean): boolean {
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
describe('ApcParser', () => {
let parser: ApcParser;
let reports: [number, string, (boolean | string | undefined)?][] = [];
beforeEach(() => {
reports = [];
parser = new ApcParser();
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
reports.push([id, action, data]);
});
});
describe('identifier parsing', () => {
it('single character identifier', () => {
parser.start();
const data = toUtf32('Gf=100,a=T;payload');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[0x47, 'START', undefined], // 0x47 = 'G'
[0x47, 'PUT', 'f=100,a=T;payload'],
[0x47, 'END', true]
]);
});
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 chunked payload', () => {
parser.start();
let data = toUtf32('Gf=100');
parser.put(data, 0, data.length);
data = toUtf32(',a=T');
parser.put(data, 0, data.length);
data = toUtf32(';payload');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[0x47, 'START', undefined],
[0x47, 'PUT', 'f=100'],
[0x47, 'PUT', ',a=T'],
[0x47, 'PUT', ';payload'],
[0x47, 'END', true]
]);
});
it('empty APC sequence', () => {
parser.start();
parser.end(true);
assert.deepEqual(reports, []);
});
});
describe('handler registration', () => {
let handlerReports: [string, number, string, (boolean | string)?][];
beforeEach(() => {
handlerReports = [];
});
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
['kitty', G_CODE, 'START'],
['kitty', G_CODE, 'PUT', 'f=100,a=T;imagedata'],
['kitty', G_CODE, 'END', true]
]);
assert.deepEqual(reports, []);
});
it('unregistered identifier falls back', () => {
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, []);
assert.deepEqual(reports, [
[X_CODE, 'START', undefined],
[X_CODE, 'PUT', 'some data'],
[X_CODE, 'END', true]
]);
});
it('clearHandler removes handler', () => {
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, []);
assert.deepEqual(reports, [
[G_CODE, 'START', undefined],
[G_CODE, 'PUT', 'f=100'],
[G_CODE, 'END', true]
]);
});
it('multiple handlers for same identifier', () => {
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
['handler2', G_CODE, 'START'],
['handler1', G_CODE, 'START'],
['handler2', G_CODE, 'PUT', 'data'],
['handler1', G_CODE, 'PUT', 'data'],
['handler2', G_CODE, 'END', true],
['handler1', G_CODE, 'END', false]
]);
});
it('handler returning false allows fallthrough', () => {
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
['handler2', G_CODE, 'START'],
['handler1', G_CODE, 'START'],
['handler2', G_CODE, 'PUT', 'data'],
['handler1', G_CODE, 'PUT', 'data'],
['handler2', G_CODE, 'END', true],
['handler1', G_CODE, 'END', true]
]);
});
it('dispose removes handler', () => {
const G_CODE = 0x47;
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.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(handlerReports, [
['handler1', G_CODE, 'START'],
['handler1', G_CODE, 'PUT', 'data'],
['handler1', G_CODE, 'END', true]
]);
});
});
describe('ApcHandler convenience class', () => {
it('should be called once on end(true)', () => {
const G_CODE = 0x47;
const results: [number, string][] = [];
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('Gf=100');
parser.put(data, 0, data.length);
data = toUtf32(',a=T;payload');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(results, [[G_CODE, 'f=100,a=T;payload']]);
});
it('should not be called on end(false)', () => {
const G_CODE = 0x47;
const results: [number, string][] = [];
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
const data = toUtf32('Gf=100,a=T;payload');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(results, []);
});
it('should handle payload up to limit', function(): void {
this.timeout(30000);
const G_CODE = 0x47;
const results: [number, string][] = [];
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('G');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
parser.end(true);
assert.deepEqual(results, [[G_CODE, 'A'.repeat(PAYLOAD_LIMIT)]]);
});
it('should abort for payload over limit', function(): void {
this.timeout(30000);
const G_CODE = 0x47;
const results: [number, string][] = [];
parser.registerHandler(G_CODE, new ApcHandler((data: string) => {
results.push([G_CODE, data]);
return true;
}));
parser.start();
let data = toUtf32('G');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(results, []);
});
});
describe('reset behavior', () => {
let handlerReports: [string, number, string, (boolean | string)?][];
beforeEach(() => {
handlerReports = [];
});
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.put(data, 0, data.length);
parser.reset();
assert.deepEqual(handlerReports, [
['kitty', G_CODE, 'START'],
['kitty', G_CODE, 'PUT', 'f=100'],
['kitty', G_CODE, 'END', false]
]);
});
});
});
describe('ApcParser - async tests', () => {
let parser: ApcParser;
let reports: [number, string, (boolean | string | undefined)?][] = [];
beforeEach(() => {
reports = [];
parser = new ApcParser();
parser.setHandlerFallback((id: number, action: 'START' | 'PUT' | 'END', data?: string | boolean) => {
reports.push([id, action, data]);
});
});
async function endP(parser: ApcParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.end(success, prev)) {
prev = await result;
}
}
describe('async ApcHandler', () => {
it('should handle async handler', async () => {
const G_CODE = 0x47;
const results: [number, string][] = [];
parser.registerHandler(G_CODE, new ApcHandler(async (data: string) => {
await new Promise(res => setTimeout(res, 10));
results.push([G_CODE, data]);
return true;
}));
parser.start();
const data = toUtf32('Gf=100,a=T');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(results, [[G_CODE, 'f=100,a=T']]);
});
});
});
+245
View File
@@ -0,0 +1,245 @@
/**
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IApcHandler, IHandlerCollection, ApcFallbackHandlerType, IApcParser, ISubParserStackState } from 'common/parser/Types';
import { ApcState, PAYLOAD_LIMIT } from 'common/parser/Constants';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
const EMPTY_HANDLERS: IApcHandler[] = [];
/**
* APC Parser for handling Application Program Command sequences.
* APC sequences use the format: ESC _ <identifier><data> ESC \
*
* Unlike OSC which uses numeric identifiers (e.g., OSC 1337),
* APC uses the first character as the identifier (e.g., 'G' for Kitty graphics).
* 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 _handlerFb: ApcFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
/**
* Register an APC handler for a specific identifier.
* @param ident The character code of the first byte (e.g., 0x47 for 'G')
* @param handler The handler to register
*/
public registerHandler(ident: number, handler: IApcHandler): IDisposable {
if (this._handlers[ident] === undefined) {
this._handlers[ident] = [];
}
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: ApcFallbackHandlerType): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public reset(): void {
// force cleanup handlers if payload was already sent
if (this._state === ApcState.PAYLOAD) {
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;
}
private _start(): void {
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._id, 'START');
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].start();
}
}
}
private _put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
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._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
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;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as APC handlers.
*/
export class ApcHandler implements IApcHandler {
private _data = '';
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
public start(): void {
this._data = '';
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
this._data += utf32ToString(data, start, end);
if (this._data.length > PAYLOAD_LIMIT) {
this._data = '';
this._hitLimit = true;
}
}
public end(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data);
if (ret instanceof Promise) {
// need to hold data until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._data = '';
this._hitLimit = false;
return res;
});
}
}
this._data = '';
this._hitLimit = false;
return ret;
}
}
+18 -2
View File
@@ -20,7 +20,8 @@ export const enum ParserState {
DCS_PARAM = 10,
DCS_IGNORE = 11,
DCS_INTERMEDIATE = 12,
DCS_PASSTHROUGH = 13
DCS_PASSTHROUGH = 13,
APC_STRING = 14
}
/**
@@ -41,7 +42,10 @@ export const enum ParserAction {
CLEAR = 11,
DCS_HOOK = 12,
DCS_PUT = 13,
DCS_UNHOOK = 14
DCS_UNHOOK = 14,
APC_START = 15,
APC_PUT = 16,
APC_END = 17
}
/**
@@ -54,5 +58,17 @@ export const enum OscState {
ABORT = 3
}
// I actually don't know if below is correct..
// If it is.. it seems like dup of OscState.
/**
* Internal states of ApcParser.
*/
export const enum ApcState {
START = 0,
ID = 1,
PAYLOAD = 2,
ABORT = 3
}
// payload limit for OSC and DCS
export const PAYLOAD_LIMIT = 10000000;
+33 -19
View File
@@ -705,17 +705,17 @@ describe('EscapeSequenceParser', () => {
});
it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', () => {
parser.reset();
// C0
let initializers = ['\x58', '\x5e', '\x5f'];
// C0 (only SOS and PM, APC has separate handling)
let initializers = ['\x58', '\x5e'];
for (let i = 0; i < initializers.length; ++i) {
parse(parser, '\x1b' + initializers[i]);
assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING);
parser.reset();
}
// C1
// C1 (only SOS and PM, APC has separate handling)
for (state in states) {
parser.currentState = state;
initializers = ['\x98', '\x9e', '\x9f'];
initializers = ['\x98', '\x9e'];
for (let i = 0; i < initializers.length; ++i) {
parse(parser, initializers[i]);
assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING);
@@ -723,6 +723,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('state SOS_PM_APC_STRING ignore rules', () => {
parser.reset();
let ignored = r(0x00, 0x18);
@@ -1207,7 +1221,7 @@ describe('EscapeSequenceParser', () => {
clearAccu();
});
it('print handler', () => {
parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void {
parser2.setPrintHandler(function(data: Uint32Array, start: number, end: number): void {
for (let i = start; i < end; ++i) {
print += stringFromCodePoint(data[i]);
}
@@ -1221,11 +1235,11 @@ describe('EscapeSequenceParser', () => {
assert.equal(print, '');
});
it('ESC handler', () => {
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean {
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function(): boolean {
esc.push('%G');
return true;
});
parser2.registerEscHandler({ final: 'E' }, function (): boolean {
parser2.registerEscHandler({ final: 'E' }, function(): boolean {
esc.push('E');
return true;
});
@@ -1293,7 +1307,7 @@ describe('EscapeSequenceParser', () => {
});
});
it('CSI handler', () => {
parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean {
parser2.registerCsiHandler({ final: 'm' }, function(params: IParams): boolean {
csi.push(['m', params.toArray(), '']);
return true;
});
@@ -1373,11 +1387,11 @@ describe('EscapeSequenceParser', () => {
});
});
it('EXECUTE handler', () => {
parser2.setExecuteHandler('\n', function (): boolean {
parser2.setExecuteHandler('\n', function(): boolean {
exe.push('\n');
return true;
});
parser2.setExecuteHandler('\r', function (): boolean {
parser2.setExecuteHandler('\r', function(): boolean {
exe.push('\r');
return true;
});
@@ -1390,7 +1404,7 @@ describe('EscapeSequenceParser', () => {
assert.deepEqual(exe, ['\n']);
});
it('OSC handler', () => {
parser2.registerOscHandler(1, new OscHandler(function (data: string): boolean {
parser2.registerOscHandler(1, new OscHandler(function(data: string): boolean {
osc.push([1, data]);
return true;
}));
@@ -1471,17 +1485,17 @@ describe('EscapeSequenceParser', () => {
});
it('DCS handler', () => {
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, {
hook: function (params: IParams): void {
hook: function(params: IParams): void {
dcs.push(['hook', '', params.toArray(), 0]);
},
put: function (data: Uint32Array, start: number, end: number): void {
put: function(data: Uint32Array, start: number, end: number): void {
let s = '';
for (let i = start; i < end; ++i) {
s += stringFromCodePoint(data[i]);
}
dcs.push(['put', s]);
},
unhook: function (): boolean {
unhook: function(): boolean {
dcs.push(['unhook']);
return true;
}
@@ -1560,7 +1574,7 @@ describe('EscapeSequenceParser', () => {
});
it('ERROR handler', () => {
let errorState: IParsingState | null = null;
parser2.setErrorHandler(function (state: IParsingState): IParsingState {
parser2.setErrorHandler(function(state: IParsingState): IParsingState {
errorState = state;
return state;
});
@@ -1788,7 +1802,7 @@ describe('EscapeSequenceParser - async', () => {
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
parser.registerDcsHandler({ final: 'a' }, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; }));
});
it('sync handlers keep being parsed in sync mode', () => {
@@ -1823,7 +1837,7 @@ describe('EscapeSequenceParser - async', () => {
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true; }));
});
it('sync parse call does not work anymore', () => {
@@ -2156,7 +2170,7 @@ describe('EscapeSequenceParser - async', () => {
});
it('multiple async DCS handlers', async () => {
// register with fallback
const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;}));
const DCS2 = parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
@@ -2187,7 +2201,7 @@ describe('EscapeSequenceParser - async', () => {
clearAccu();
// register without fallback
const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;}));
const DCS22 = parser.registerDcsHandler({ final: 'a' }, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
+63 -5
View File
@@ -3,13 +3,14 @@
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType, IApcHandler, IApcParser, ApcFallbackHandlerType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable } from 'common/Types';
import { Params } from 'common/parser/Params';
import { OscParser } from 'common/parser/OscParser';
import { DcsParser } from 'common/parser/DcsParser';
import { ApcParser } from 'common/parser/ApcParser';
/**
* Table values are generated like this:
@@ -75,7 +76,7 @@ const NON_ASCII_PRINTABLE = 0xA0;
* VT500 compatible transition table.
* Taken from https://vt100.net/emu/dec_ansi_parser.
*/
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
export const VT500_TRANSITION_TABLE = (function(): TransitionTable {
const table: TransitionTable = new TransitionTable(4095);
// range macro for byte
@@ -104,7 +105,8 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany([0x98, 0x9e], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM
table.add(0x9f, state, ParserAction.APC_START, ParserState.APC_STRING); // APC
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
}
@@ -128,8 +130,9 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
// sos/pm/apc does nothing
table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
// sos/pm/apc - sos/pm does nothing, apc gets separate handling
table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // SOS, PM
table.add(0x5f, ParserState.ESCAPE, ParserAction.APC_START, ParserState.APC_STRING); // APC ('_')
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
@@ -187,12 +190,18 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
// apc passthrough - similar to DCS passthrough
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);
// special handling of unicode chars
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
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);
return table;
})();
@@ -243,6 +252,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected readonly _oscParser: IOscParser;
protected readonly _dcsParser: IDcsParser;
protected readonly _apcParser: IApcParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
@@ -290,6 +300,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}));
this._oscParser = this._register(new OscParser());
this._dcsParser = this._register(new DcsParser());
this._apcParser = this._register(new ApcParser());
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
@@ -425,6 +436,16 @@ 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 clearApcHandler(ident: number): void {
this._apcParser.clearHandler(ident);
}
public setApcHandlerFallback(handler: ApcFallbackHandlerType): void {
this._apcParser.setHandlerFallback(handler);
}
public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
this._errorHandler = callback;
}
@@ -445,6 +466,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.currentState = this.initialState;
this._oscParser.reset();
this._dcsParser.reset();
this._apcParser.reset();
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
@@ -606,6 +628,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._params.addParam(0); // ZDM
this._collect = 0;
break;
case ParserStackType.APC:
code = data[this._parseStack.chunkPos];
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
}
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
@@ -785,6 +818,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._collect = 0;
this.precedingJoinState = 0;
break;
case ParserAction.APC_START:
this._apcParser.start();
break;
case ParserAction.APC_PUT:
// 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;
}
}
break;
case ParserAction.APC_END:
handlerResult = this._apcParser.end(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.APC, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingJoinState = 0;
break;
}
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
+38 -2
View File
@@ -36,7 +36,7 @@ export interface IParams {
addSubParam(value: number): void;
hasSubParams(idx: number): boolean;
getSubParams(idx: number): Int32Array | null;
getSubParamsAll(): {[idx: number]: Int32Array};
getSubParamsAll(): { [idx: number]: Int32Array };
}
/**
@@ -134,6 +134,32 @@ export interface IOscHandler {
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* APC handler types.
* APC (Application Program Command) sequences are used by protocols like
* Kitty Graphics Protocol. Format: ESC _ <data> ESC \
*/
export interface IApcHandler {
/**
* Announces start of this APC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
* Note: Data is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of APC command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): boolean | Promise<boolean>;
}
export type ApcFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* PRINT handler types.
*/
@@ -196,6 +222,10 @@ export interface IEscapeSequenceParser extends IDisposable {
clearOscHandler(ident: number): void;
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
registerApcHandler(ident: number, handler: IApcHandler): IDisposable;
clearApcHandler(ident: number): void;
setApcHandlerFallback(handler: ApcFallbackHandlerType): void;
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
@@ -223,6 +253,11 @@ export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerTy
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
export interface IApcParser extends ISubParser<IApcHandler, ApcFallbackHandlerType> {
start(): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
/**
* Interface to denote a specific ESC, CSI or DCS handler slot.
* The values are used to create an integer respresentation during handler
@@ -252,7 +287,8 @@ export const enum ParserStackType {
CSI,
ESC,
OSC,
DCS
DCS,
APC
}
// aggregate of resumable handler lists