mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #5617 from xtermjs/anthonykim1/kittyGraphicsProtocol
Add register apc handler
This commit is contained in:
@@ -107,6 +107,9 @@ 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 {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
@@ -242,6 +242,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
return this._inputHandler.registerOscHandler(ident, callback);
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
this._handleWindowsPtyOptionChange();
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -22,6 +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;
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
@@ -475,6 +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;
|
||||
|
||||
/** C0 BEL */ bell(): boolean;
|
||||
/** C0 LF */ lineFeed(): boolean;
|
||||
|
||||
@@ -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']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,14 @@ export const enum ParserState {
|
||||
CSI_PARAM = 4,
|
||||
CSI_INTERMEDIATE = 5,
|
||||
CSI_IGNORE = 6,
|
||||
SOS_PM_APC_STRING = 7,
|
||||
SOS_PM_STRING = 7,
|
||||
OSC_STRING = 8,
|
||||
DCS_ENTRY = 9,
|
||||
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,15 @@ export const enum OscState {
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -156,13 +156,14 @@ const states: number[] = [
|
||||
ParserState.CSI_PARAM,
|
||||
ParserState.CSI_INTERMEDIATE,
|
||||
ParserState.CSI_IGNORE,
|
||||
ParserState.SOS_PM_APC_STRING,
|
||||
ParserState.SOS_PM_STRING,
|
||||
ParserState.OSC_STRING,
|
||||
ParserState.DCS_ENTRY,
|
||||
ParserState.DCS_PARAM,
|
||||
ParserState.DCS_IGNORE,
|
||||
ParserState.DCS_INTERMEDIATE,
|
||||
ParserState.DCS_PASSTHROUGH
|
||||
ParserState.DCS_PASSTHROUGH,
|
||||
ParserState.APC_STRING
|
||||
];
|
||||
let state: any;
|
||||
|
||||
@@ -276,7 +277,8 @@ 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
|
||||
13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] }, // abort DCS_PASSTHROUGH
|
||||
14: { '\x18': [], '\x1a': [] } // abort APC_STRING
|
||||
};
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
@@ -703,36 +705,50 @@ describe('EscapeSequenceParser', () => {
|
||||
testTerminal.clear();
|
||||
}
|
||||
});
|
||||
it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', () => {
|
||||
it('trans ANYWHERE/ESCAPE --> SOS_PM_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);
|
||||
assert.equal(parser.currentState, ParserState.SOS_PM_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);
|
||||
assert.equal(parser.currentState, ParserState.SOS_PM_STRING);
|
||||
parser.reset();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('state SOS_PM_APC_STRING ignore rules', () => {
|
||||
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);
|
||||
ignored = ignored.concat(['\x19']);
|
||||
ignored = ignored.concat(r(0x1c, 0x20));
|
||||
ignored = ignored.concat(r(0x20, 0x80));
|
||||
for (let i = 0; i < ignored.length; ++i) {
|
||||
parser.currentState = ParserState.SOS_PM_APC_STRING;
|
||||
parser.currentState = ParserState.SOS_PM_STRING;
|
||||
parse(parser, ignored[i]);
|
||||
assert.equal(parser.currentState, ParserState.SOS_PM_APC_STRING);
|
||||
assert.equal(parser.currentState, ParserState.SOS_PM_STRING);
|
||||
parser.reset();
|
||||
}
|
||||
});
|
||||
@@ -1788,7 +1804,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 +1839,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 +2172,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 +2203,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];
|
||||
|
||||
@@ -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:
|
||||
@@ -17,8 +18,8 @@ import { DcsParser } from 'common/parser/DcsParser';
|
||||
* value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState
|
||||
*/
|
||||
const enum TableAccess {
|
||||
TRANSITION_ACTION_SHIFT = 4,
|
||||
TRANSITION_STATE_MASK = 15,
|
||||
TRANSITION_ACTION_SHIFT = 8,
|
||||
TRANSITION_STATE_MASK = 255,
|
||||
INDEX_STATE_SHIFT = 8
|
||||
}
|
||||
|
||||
@@ -26,10 +27,10 @@ const enum TableAccess {
|
||||
* Transition table for EscapeSequenceParser.
|
||||
*/
|
||||
export class TransitionTable {
|
||||
public table: Uint8Array;
|
||||
public table: Uint16Array;
|
||||
|
||||
constructor(length: number) {
|
||||
this.table = new Uint8Array(length);
|
||||
this.table = new Uint16Array(length);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +90,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
EXECUTABLES.push(0x19);
|
||||
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
|
||||
|
||||
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
|
||||
const states: number[] = r(ParserState.GROUND, ParserState.APC_STRING + 1);
|
||||
let state: any;
|
||||
|
||||
// set default transition
|
||||
@@ -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_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,12 +130,18 @@ 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);
|
||||
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);
|
||||
table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
// sos/pm
|
||||
table.addMany([0x58, 0x5e], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.SOS_PM_STRING, ParserAction.IGNORE, ParserState.SOS_PM_STRING);
|
||||
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);
|
||||
// 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);
|
||||
@@ -193,6 +201,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);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -134,6 +134,29 @@ export interface IOscHandler {
|
||||
}
|
||||
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
|
||||
|
||||
/**
|
||||
* APC handler types.
|
||||
*/
|
||||
export interface IApcHandler {
|
||||
/**
|
||||
* Announces start of this APC command.
|
||||
* Prepare needed data structures here.
|
||||
*/
|
||||
start(): void;
|
||||
/**
|
||||
* Incoming data chunk.
|
||||
*/
|
||||
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 +219,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 +250,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 +284,8 @@ export const enum ParserStackType {
|
||||
CSI,
|
||||
ESC,
|
||||
OSC,
|
||||
DCS
|
||||
DCS,
|
||||
APC
|
||||
}
|
||||
|
||||
// aggregate of resumable handler lists
|
||||
|
||||
@@ -34,4 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ declare global {
|
||||
customDcsHandlerCallStack?: [string, (number | number[])[], string][];
|
||||
customEscHandlerCallStack?: string[];
|
||||
customOscHandlerCallStack?: string[][];
|
||||
customApcHandlerCallStack?: string[][];
|
||||
disposable?: IDisposable;
|
||||
disposables?: IDisposable[];
|
||||
}
|
||||
@@ -241,4 +242,110 @@ test.describe('Parser Integration Tests', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
test.describe('registerApcHandler', () => {
|
||||
// TODO: Remove when Kitty Graphics tests added (real-world usage replaces this)
|
||||
test('should call custom APC handler with identifier', async () => {
|
||||
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.customApcHandlerCallStack!.push(['handler', data]);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// APC format: ESC _ <identifier> <data> ESC \
|
||||
await ctx.proxy.write('\x1b_Asome data here\x1b\\');
|
||||
deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [
|
||||
['handler', 'some data here']
|
||||
]);
|
||||
});
|
||||
// TODO: Remove when Kitty Graphics tests added
|
||||
test('should handle short data', async () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposable = term.parser.registerApcHandler(0x42, data => {
|
||||
window.customApcHandlerCallStack!.push(['handler', data]);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
// Short APC with minimal data
|
||||
await ctx.proxy.write('\x1b_Bhi\x1b\\');
|
||||
deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [
|
||||
['handler', 'hi']
|
||||
]);
|
||||
});
|
||||
test('should respect return value', async () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
window.customApcHandlerCallStack!.push(['A', data]);
|
||||
return false;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
window.customApcHandlerCallStack!.push(['B', data]);
|
||||
return true;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x43, data => {
|
||||
window.customApcHandlerCallStack!.push(['C', data]);
|
||||
return false;
|
||||
})
|
||||
];
|
||||
});
|
||||
await ctx.proxy.write('\x1b_Csome data\x1b\\');
|
||||
deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [
|
||||
['C', 'some data'],
|
||||
['B', 'some data']
|
||||
]);
|
||||
});
|
||||
test('async', async () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
window.customApcHandlerCallStack!.push(['A', data]);
|
||||
return false;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
return new Promise(res => setTimeout(res, 50)).then(() => {
|
||||
window.customApcHandlerCallStack!.push(['B', data]);
|
||||
return false;
|
||||
});
|
||||
}),
|
||||
term.parser.registerApcHandler(0x44, data => {
|
||||
window.customApcHandlerCallStack!.push(['C', data]);
|
||||
return false;
|
||||
})
|
||||
];
|
||||
});
|
||||
await ctx.proxy.write('\x1b_Dsome data\x1b\\');
|
||||
deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [
|
||||
['C', 'some data'],
|
||||
['B', 'some data'],
|
||||
['A', 'some data']
|
||||
]);
|
||||
});
|
||||
// TODO: Remove when Kitty Graphics tests added
|
||||
test('should handle different identifiers independently', async () => {
|
||||
await ctx.proxy.evaluate(([term]) => {
|
||||
window.customApcHandlerCallStack = [];
|
||||
window.disposables = [
|
||||
term.parser.registerApcHandler(0x46, data => { // 'F'
|
||||
window.customApcHandlerCallStack!.push(['F', data]);
|
||||
return true;
|
||||
}),
|
||||
term.parser.registerApcHandler(0x58, data => { // 'X'
|
||||
window.customApcHandlerCallStack!.push(['X', data]);
|
||||
return true;
|
||||
})
|
||||
];
|
||||
});
|
||||
await ctx.proxy.write('\x1b_Ffirst data\x1b\\');
|
||||
await ctx.proxy.write('\x1b_Xsecond data\x1b\\');
|
||||
deepStrictEqual(await ctx.page.evaluate(() => window.customApcHandlerCallStack), [
|
||||
['F', 'first data'],
|
||||
['X', 'second data']
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Vendored
+18
@@ -1312,6 +1312,24 @@ declare module '@xterm/headless' {
|
||||
* @returns An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* will be stored up until the sequence is finished. Since APC sequences are
|
||||
* not limited by the amount of data this might impose a problem for big
|
||||
* payloads. Currently xterm.js limits APC payload to 10 MB which should
|
||||
* give enough room for most use cases. The callback is called with APC data
|
||||
* string (excluding the identifier character). Return true if the sequence
|
||||
* was handled; false if we should try a previous 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): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+18
@@ -1932,6 +1932,24 @@ declare module '@xterm/xterm' {
|
||||
* @returns An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* will be stored up until the sequence is finished. Since APC sequences are
|
||||
* not limited by the amount of data this might impose a problem for big
|
||||
* payloads. Currently xterm.js limits APC payload to 10 MB which should
|
||||
* give enough room for most use cases. The callback is called with APC data
|
||||
* string (excluding the identifier character). Return `true` if the
|
||||
* sequence was handled, `false` if the parser should try a previous
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user