mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
fix OSC parsing
This commit is contained in:
+8
-6
@@ -21,6 +21,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IAttributeData, IDisposable } from 'common/Types';
|
||||
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services';
|
||||
import { ISelectionService } from 'browser/services/Services';
|
||||
import { OscHandlerFactory } from 'common/parser/OscParser';
|
||||
|
||||
/**
|
||||
* Map collect to glevel. Used in `selectCharset`.
|
||||
@@ -152,9 +153,10 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._parser.setExecuteHandlerFallback((code: number) => {
|
||||
this._logService.debug('Unknown EXECUTE code: ', { code });
|
||||
});
|
||||
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||
this._logService.debug('Unknown OSC code: ', { identifier, data });
|
||||
});
|
||||
this._parser.setOscHandlerFallback((identifier, action, data) => {
|
||||
this._logService.debug('Unknown OSC code: ', { identifier, action, data });
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* print handler
|
||||
@@ -224,10 +226,10 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* OSC handler
|
||||
*/
|
||||
// 0 - icon name + title
|
||||
this._parser.setOscHandler(0, (data) => this.setTitle(data));
|
||||
this._parser.setOscHandler(0, new OscHandlerFactory((data: string) => this.setTitle(data)));
|
||||
// 1 - icon name
|
||||
// 2 - title
|
||||
this._parser.setOscHandler(2, (data) => this.setTitle(data));
|
||||
this._parser.setOscHandler(2, new OscHandlerFactory((data: string) => this.setTitle(data)));
|
||||
// 3 - set property X in the form "prop=value"
|
||||
// 4 - Change Color Number
|
||||
// 5 - Change Special Color Number
|
||||
@@ -485,7 +487,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Forward addOscHandler from parser.
|
||||
*/
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._parser.addOscHandler(ident, callback);
|
||||
return this._parser.addOscHandler(ident, new OscHandlerFactory(callback));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,3 +43,13 @@ export const enum ParserAction {
|
||||
DCS_PUT = 13,
|
||||
DCS_UNHOOK = 14
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal states of OscParser.
|
||||
*/
|
||||
export const enum OscState {
|
||||
START = 0,
|
||||
ID = 1,
|
||||
PAYLOAD = 2,
|
||||
ABORT = 3
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types';
|
||||
import { IDcsHandler, IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandler } from 'common/parser/Types';
|
||||
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
|
||||
import * as chai from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { ParserState } from 'common/parser/Constants';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { OscHandlerFactory } from 'common/parser/OscParser';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
|
||||
function r(a: number, b: number): string[] {
|
||||
@@ -20,13 +22,45 @@ function r(a: number, b: number): string[] {
|
||||
return arr;
|
||||
}
|
||||
|
||||
class MockOscPutParser implements IOscParser {
|
||||
private _fallback: OscFallbackHandler = () => {};
|
||||
public data = '';
|
||||
public reset(): void {
|
||||
this.data = '';
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.data += utf32ToString(data, start, end);
|
||||
}
|
||||
public dispose(): void { }
|
||||
public start(): void { }
|
||||
public end(): void {
|
||||
const id = parseInt(this.data.slice(0, this.data.indexOf(';')));
|
||||
if (!isNaN(id)) {
|
||||
this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1));
|
||||
}
|
||||
}
|
||||
addOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
setOscHandler(ident: number, handler: IOscHandler): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
setOscHandlerFallback(handler: OscFallbackHandler): void {
|
||||
this._fallback = handler;
|
||||
}
|
||||
}
|
||||
const oscPutParser = new MockOscPutParser();
|
||||
|
||||
// derived parser with access to internal states
|
||||
class TestEscapeSequenceParser extends EscapeSequenceParser {
|
||||
public get osc(): string {
|
||||
return this._osc;
|
||||
return (this._oscParser as MockOscPutParser).data;
|
||||
}
|
||||
public set osc(value: string) {
|
||||
this._osc = value;
|
||||
(this._oscParser as MockOscPutParser).data = value;
|
||||
}
|
||||
public get params(): ParamsArray {
|
||||
return this._params.toArray();
|
||||
@@ -46,6 +80,9 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
|
||||
public mockActiveDcsHandler(): void {
|
||||
this._activeDcsHandler = this._dcsHandlerFb;
|
||||
}
|
||||
public mockOscParser(): void {
|
||||
this._oscParser = oscPutParser;
|
||||
}
|
||||
}
|
||||
|
||||
// test object to collect parser actions and compare them with expected values
|
||||
@@ -124,6 +161,7 @@ let state: any;
|
||||
|
||||
// parser with Uint8Array based transition table
|
||||
const testParser = new TestEscapeSequenceParser();
|
||||
testParser.mockOscParser();
|
||||
testParser.setPrintHandler(testTerminal.print.bind(testTerminal));
|
||||
testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
|
||||
testTerminal.actionCSI(collect, params, String.fromCharCode(flag));
|
||||
@@ -134,9 +172,9 @@ testParser.setEscHandlerFallback((collect: string, flag: number) => {
|
||||
testParser.setExecuteHandlerFallback((code: number) => {
|
||||
testTerminal.actionExecute(String.fromCharCode(code));
|
||||
});
|
||||
testParser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||
testParser.setOscHandlerFallback((identifier, action, data) => {
|
||||
if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently
|
||||
else testTerminal.actionOSC('' + identifier + ';' + data);
|
||||
else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END
|
||||
});
|
||||
testParser.setDcsHandlerFallback(new DcsTest());
|
||||
|
||||
@@ -1026,9 +1064,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
], null);
|
||||
});
|
||||
it('print + OSC(C1) + print', function (): void {
|
||||
test('abc\x9d123tzf\x9cdefg', [
|
||||
test('abc\x9d123;tzf\x9cdefg', [
|
||||
['print', 'abc'],
|
||||
['osc', '123tzf'],
|
||||
['osc', '123;tzf'],
|
||||
['print', 'defg']
|
||||
], null);
|
||||
});
|
||||
@@ -1039,9 +1077,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
], null);
|
||||
});
|
||||
it('7bit ST should be swallowed', function (): void {
|
||||
test('abc\x9d123tzf\x1b\\defg', [
|
||||
test('abc\x9d123;tzf\x1b\\defg', [
|
||||
['print', 'abc'],
|
||||
['osc', '123tzf'],
|
||||
['osc', '123;tzf'],
|
||||
['print', 'defg']
|
||||
], null);
|
||||
});
|
||||
@@ -1256,9 +1294,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
chai.expect(exe).eql(['\n']);
|
||||
});
|
||||
it('OSC handler', function (): void {
|
||||
parser2.setOscHandler(1, function (data: string): void {
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(function (data: string): void {
|
||||
osc.push([1, data]);
|
||||
});
|
||||
}));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']]);
|
||||
parser2.clearOscHandler(1);
|
||||
@@ -1270,16 +1308,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
describe('OSC custom handlers', () => {
|
||||
it('Prevent fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return false; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
@@ -1287,9 +1325,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
const oscCustom2: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return false; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
@@ -1298,9 +1336,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
const oscCustom2: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom2.push([1, data]); return true; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([], 'Should not fallback once');
|
||||
@@ -1308,16 +1346,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.setOscHandler(1, () => order.push(1));
|
||||
parser2.addOscHandler(1, () => { order.push(2); return false; });
|
||||
parser2.addOscHandler(1, () => { order.push(3); return false; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(() => order.push(1)));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(2); return false; }));
|
||||
parser2.addOscHandler(1, new OscHandlerFactory(() => { order.push(3); return false; }));
|
||||
parse(parser2, '\x1b]1;foo=bar\x1b\\');
|
||||
chai.expect(order).eql([3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']]);
|
||||
@@ -1325,8 +1363,8 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandlerFactory(data => osc.push([1, data])));
|
||||
const customHandler = parser2.addOscHandler(1, new OscHandlerFactory(data => { oscCustom.push([1, data]); return true; }));
|
||||
customHandler.dispose();
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
|
||||
@@ -3,20 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandler, OscFallbackHandler, IOscParser } from 'common/parser/Types';
|
||||
import { ParserState, ParserAction } from 'common/parser/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { Params } from 'common/parser/Params';
|
||||
|
||||
interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
type CsiHandler = (params: IParams, collect: string) => boolean | void;
|
||||
type OscHandler = (data: string) => boolean | void;
|
||||
import { OscParser } from 'common/parser/OscParser';
|
||||
|
||||
/**
|
||||
* Table values are generated like this:
|
||||
@@ -240,7 +233,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
public precedingCodepoint: number;
|
||||
|
||||
// buffers over several parse calls
|
||||
protected _osc: string;
|
||||
protected _params: Params;
|
||||
protected _collect: string;
|
||||
|
||||
@@ -249,7 +241,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
protected _executeHandlers: any;
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandler>;
|
||||
protected _escHandlers: any;
|
||||
protected _oscHandlers: IHandlerCollection<OscHandler>;
|
||||
protected _oscParser: IOscParser;
|
||||
protected _dcsHandlers: any;
|
||||
protected _activeDcsHandler: IDcsHandler;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
@@ -259,7 +251,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
protected _executeHandlerFb: (code: number) => void;
|
||||
protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void;
|
||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||
protected _oscHandlerFb: (identifier: number, data: string) => void;
|
||||
protected _dcsHandlerFb: IDcsHandler;
|
||||
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
|
||||
|
||||
@@ -268,7 +259,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
this.initialState = ParserState.GROUND;
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._params = new Params(); // defaults to 32 storable params/subparams
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = '';
|
||||
@@ -279,14 +269,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._executeHandlerFb = (code: number): void => { };
|
||||
this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { };
|
||||
this._escHandlerFb = (collect: string, flag: number): void => { };
|
||||
this._oscHandlerFb = (identifier: number, data: string): void => { };
|
||||
this._dcsHandlerFb = new DcsDummy();
|
||||
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
|
||||
this._printHandler = this._printHandlerFb;
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._oscHandlers = Object.create(null);
|
||||
this._oscParser = new OscParser();
|
||||
this._dcsHandlers = Object.create(null);
|
||||
this._activeDcsHandler = this._dcsHandlerFb;
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
@@ -300,6 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._escHandlers = null;
|
||||
this._dcsHandlers = null;
|
||||
this._activeDcsHandler = new DcsDummy();
|
||||
this._oscParser.dispose();
|
||||
}
|
||||
|
||||
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
|
||||
@@ -355,29 +345,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._escHandlerFb = callback;
|
||||
}
|
||||
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
if (this._oscHandlers[ident] === undefined) {
|
||||
this._oscHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._oscHandlers[ident];
|
||||
handlerList.push(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(callback);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
addOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
return this._oscParser.addOscHandler(ident, handler);
|
||||
}
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void {
|
||||
this._oscHandlers[ident] = [callback];
|
||||
setOscHandler(ident: number, handler: IOscHandler): void {
|
||||
this._oscParser.setOscHandler(ident, handler);
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||
this._oscParser.clearOscHandler(ident);
|
||||
}
|
||||
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
|
||||
this._oscHandlerFb = callback;
|
||||
setOscHandlerFallback(handler: OscFallbackHandler): void {
|
||||
this._oscParser.setOscHandlerFallback(handler);
|
||||
}
|
||||
|
||||
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
|
||||
@@ -404,7 +382,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
reset(): void {
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._oscParser.reset();
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = '';
|
||||
@@ -430,7 +408,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let currentState = this.currentState;
|
||||
let osc = this._osc;
|
||||
const osc = this._oscParser;
|
||||
let collect = this._collect;
|
||||
const params = this._params;
|
||||
const table: Uint8Array = this.TRANSITIONS.table;
|
||||
@@ -484,7 +462,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
position: i,
|
||||
code,
|
||||
currentState,
|
||||
osc,
|
||||
osc: '', // FIXME: what to send here?
|
||||
collect,
|
||||
params,
|
||||
abort: false
|
||||
@@ -537,7 +515,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
osc = '';
|
||||
osc.reset();
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
@@ -562,54 +540,29 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
dcsHandler.unhook();
|
||||
dcsHandler = this._dcsHandlerFb;
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
osc.reset();
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.OSC_START:
|
||||
osc = '';
|
||||
osc.start();
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) {
|
||||
osc += utf32ToString(data, i, j);
|
||||
osc.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||
// NOTE: OSC subparsing is not part of the original parser
|
||||
// we do basic identifier parsing here to offer a jump table for OSC as well
|
||||
const idx = osc.indexOf(';');
|
||||
if (idx === -1) {
|
||||
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
|
||||
} else {
|
||||
// Note: NaN is not handled here
|
||||
// either catch it with the fallback handler
|
||||
// or with an explicit NaN OSC handler
|
||||
const identifier = parseInt(osc.substring(0, idx));
|
||||
const content = osc.substring(idx + 1);
|
||||
// Trigger OSC Handler
|
||||
const handlers = this._oscHandlers[identifier];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](content) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
osc.end(code !== 0x18 && code !== 0x1a);
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
osc.reset();
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
@@ -620,7 +573,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
|
||||
// save non pushable buffers
|
||||
this._osc = osc;
|
||||
this._collect = collect;
|
||||
this._params = params;
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { OscParser, OscHandlerFactory } from 'common/parser/OscParser';
|
||||
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IOscHandler } from 'common/parser/Types';
|
||||
|
||||
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 IOscHandler {
|
||||
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
}
|
||||
put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
end(success: boolean): void | boolean {
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('OscParser', () => {
|
||||
let parser: OscParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new OscParser();
|
||||
parser.setOscHandlerFallback((id, action, data) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('identifier parsing', () => {
|
||||
it('no report for illegal ids', () => {
|
||||
const data = toUtf32('hello world!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('no payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('12');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('34');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
|
||||
});
|
||||
it('with payload', () => {
|
||||
parser.start();
|
||||
let data = toUtf32('12');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('34');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(';h');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('ello');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
[1234, 'START', undefined],
|
||||
[1234, 'PUT', 'h'],
|
||||
[1234, 'PUT', 'ello'],
|
||||
[1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
it('setOscHandler', () => {
|
||||
parser.setOscHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'Here comes'],
|
||||
['th', 1234, 'PUT', 'the mouse!'],
|
||||
['th', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
it('clearOscHandler', () => {
|
||||
parser.setOscHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.clearOscHandler(1234);
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from fallback handler
|
||||
[1234, 'START', undefined],
|
||||
[1234, 'PUT', 'Here comes'],
|
||||
[1234, 'PUT', 'the mouse!'],
|
||||
[1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
it('addOscHandler', () => {
|
||||
parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 1234, 'START'],
|
||||
['th1', 1234, 'START'],
|
||||
['th2', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th2', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th2', 1234, 'END', true],
|
||||
['th1', 1234, 'END', false] // false due being already handled by th2!
|
||||
]);
|
||||
});
|
||||
it('addOscHandler with return false', () => {
|
||||
parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th2', 1234, 'START'],
|
||||
['th1', 1234, 'START'],
|
||||
['th2', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th2', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th2', 1234, 'END', true],
|
||||
['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
|
||||
]);
|
||||
});
|
||||
it('dispose handlers', () => {
|
||||
parser.setOscHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
const dispo = parser.addOscHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th1', 1234, 'START'],
|
||||
['th1', 1234, 'PUT', 'Here comes'],
|
||||
['th1', 1234, 'PUT', 'the mouse!'],
|
||||
['th1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data])));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push([1234, data])));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data])));
|
||||
const dispo = parser.addOscHandler(1234, new OscHandlerFactory(data => reports.push(['two', data])));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
data = toUtf32('1234;some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.setOscHandler(1234, new OscHandlerFactory(data => reports.push(['one', data])));
|
||||
parser.addOscHandler(1234, new OscHandlerFactory(data => { reports.push(['two', data]); return false; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IOscHandler, IHandlerCollection, OscFallbackHandler } from 'common/parser/Types';
|
||||
import { OscState } from 'common/parser/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
|
||||
export class OscParser extends Disposable {
|
||||
private _state = OscState.START;
|
||||
private _id = -1;
|
||||
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
|
||||
private _handlerFb: OscFallbackHandler = () => { };
|
||||
|
||||
addOscHandler(ident: number, handler: IOscHandler): 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
setOscHandler(ident: number, handler: IOscHandler): void {
|
||||
this._handlers[ident] = [handler];
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
setOscHandlerFallback(handler: OscFallbackHandler): void {
|
||||
this._handlerFb = handler;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = {};
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// cleanup handlers if payload was already sent
|
||||
if (this._state === OscState.PAYLOAD) {
|
||||
this.end(false);
|
||||
}
|
||||
this._id = -1;
|
||||
this._state = OscState.START;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
this._handlerFb(this._id, 'START');
|
||||
} else {
|
||||
for (let j = handlers.length - 1; j >= 0; j--) {
|
||||
handlers[j].start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _put(data: Uint32Array, start: number, end: number): void {
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
|
||||
} else {
|
||||
for (let j = handlers.length - 1; j >= 0; j--) {
|
||||
handlers[j].put(data, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _end(success: boolean): void {
|
||||
// other than the old code we always have to call .end
|
||||
// to keep the bubbling we use `success` to indicate
|
||||
// whether a handler should execute
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
this._handlerFb(this._id, 'END', success);
|
||||
} else {
|
||||
let j = handlers.length - 1;
|
||||
for (; j >= 0; j--) {
|
||||
if (handlers[j].end(success) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
// cleanup left over handlers
|
||||
for (; j >= 0; j--) {
|
||||
handlers[j].end(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this._id = -1;
|
||||
this._state = OscState.ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put data to current OSC command.
|
||||
* Expects the identifier of the OSC command in the form
|
||||
* OSC id ; payload ST/BEL
|
||||
* Payload chunks are not further processed and get
|
||||
* directly passed to the handlers.
|
||||
*/
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._state === OscState.ABORT) {
|
||||
return;
|
||||
}
|
||||
if (this._state === OscState.ID) {
|
||||
while (start < end) {
|
||||
const code = data[start++];
|
||||
if (code === 0x3b) {
|
||||
this._state = OscState.PAYLOAD;
|
||||
this._start();
|
||||
break;
|
||||
}
|
||||
if (code < 0x30 || 0x39 < code) {
|
||||
this._state = OscState.ABORT;
|
||||
return;
|
||||
}
|
||||
if (this._id === -1) {
|
||||
this._id = 0;
|
||||
}
|
||||
this._id = this._id * 10 + code - 48;
|
||||
}
|
||||
}
|
||||
if (this._state === OscState.PAYLOAD && end - start > 0) {
|
||||
this._put(data, start, end);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates end of an OSC command.
|
||||
* Whether the OSC got aborted or finished normally
|
||||
* is indicated by `success`.
|
||||
*/
|
||||
public end(success: boolean): void {
|
||||
if (this._state === OscState.START) {
|
||||
return;
|
||||
}
|
||||
// do nothing if command was faulty
|
||||
if (this._state !== OscState.ABORT) {
|
||||
// if we are still in ID state and get an early end
|
||||
// means that the command has no payload thus we still have
|
||||
// to announce START and send END right after
|
||||
if (this._state === OscState.ID) {
|
||||
this._start();
|
||||
}
|
||||
this._end(success);
|
||||
}
|
||||
this._id = -1;
|
||||
this._state = OscState.START;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convenient class to allow attaching string based handler functions
|
||||
* as OSC handlers.
|
||||
*/
|
||||
export class OscHandlerFactory implements IOscHandler {
|
||||
private _data = '';
|
||||
constructor(private _handler: (data: string) => any) {}
|
||||
public start(): void {
|
||||
this._data = '';
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this._data += utf32ToString(data, start, end);
|
||||
}
|
||||
public end(success: boolean): any {
|
||||
let ret;
|
||||
if (success) {
|
||||
ret = this._handler(this._data);
|
||||
}
|
||||
this._data = '';
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
Vendored
+43
-3
@@ -63,6 +63,12 @@ export interface IParsingState {
|
||||
abort: boolean;
|
||||
}
|
||||
|
||||
export interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
export type CsiHandler = (params: IParams, collect: string) => boolean | void;
|
||||
|
||||
/**
|
||||
* DCS handler signature for EscapeSequenceParser.
|
||||
* EscapeSequenceParser handles DCS commands via separate
|
||||
@@ -90,6 +96,29 @@ export interface IDcsHandler {
|
||||
unhook(): void;
|
||||
}
|
||||
|
||||
export type OscFallbackHandler = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
|
||||
|
||||
export interface IOscHandler {
|
||||
/**
|
||||
* Announces start of this OSC command.
|
||||
* Prepare needed data structures here.
|
||||
*/
|
||||
start(): void;
|
||||
|
||||
/**
|
||||
* Incoming data chunk.
|
||||
*/
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
|
||||
/**
|
||||
* End of OSC command. `success` indicates whether the
|
||||
* command finished normally or got aborted, thus execution
|
||||
* of the command should depend on `success`.
|
||||
* To save memory cleanup data structures in `.end`.
|
||||
*/
|
||||
end(success: boolean): void | boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser interface.
|
||||
*/
|
||||
@@ -123,15 +152,15 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
clearCsiHandler(flag: string): void;
|
||||
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void;
|
||||
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
|
||||
|
||||
setEscHandler(collectAndFlag: string, callback: () => void): void;
|
||||
clearEscHandler(collectAndFlag: string): void;
|
||||
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
|
||||
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void;
|
||||
setOscHandler(ident: number, handler: IOscHandler): void;
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandler): void;
|
||||
|
||||
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
|
||||
clearDcsHandler(collectAndFlag: string): void;
|
||||
@@ -140,3 +169,14 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
||||
clearErrorHandler(): void;
|
||||
}
|
||||
|
||||
export interface IOscParser extends IDisposable {
|
||||
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
|
||||
setOscHandler(ident: number, handler: IOscHandler): void;
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandler): void;
|
||||
reset(): void;
|
||||
start(): void;
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
end(success: boolean): void;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be
|
||||
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
|
||||
import { C0, C1 } from 'common/data/EscapeSequences';
|
||||
import { IDcsHandler, IParams } from 'common/parser/Types';
|
||||
import { OscHandlerFactory } from 'common/parser/OscParser';
|
||||
|
||||
|
||||
function toUtf32(s: string): Uint32Array {
|
||||
@@ -79,8 +80,8 @@ perfContext('Parser throughput - 50MB data', () => {
|
||||
parser.setExecuteHandler(C1.IND, () => {});
|
||||
parser.setExecuteHandler(C1.NEL, () => {});
|
||||
parser.setExecuteHandler(C1.HTS, () => {});
|
||||
parser.setOscHandler(0, (data) => {});
|
||||
parser.setOscHandler(2, (data) => {});
|
||||
parser.setOscHandler(0, new OscHandlerFactory((data) => {}));
|
||||
parser.setOscHandler(2, new OscHandlerFactory((data) => {}));
|
||||
parser.setEscHandler('7', () => {});
|
||||
parser.setEscHandler('8', () => {});
|
||||
parser.setEscHandler('D', () => {});
|
||||
|
||||
Vendored
+1
-1
@@ -516,7 +516,7 @@ declare module 'xterm' {
|
||||
addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.
|
||||
* Adds a handler for OSC escape sequences.
|
||||
* @param ident The number (first parameter) of the sequence.
|
||||
* @param callback The function to handle the escape sequence. The callback
|
||||
* is called with OSC data string. Return true if the sequence was handled;
|
||||
|
||||
Reference in New Issue
Block a user