-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
hooks for custom control sequences #1813
Conversation
This fixes (at least partially) issue xtermjs#1176 "Add a way to plugin a custom control sequence handler".
@PerBothner Thx alot for this PR. I have severals remarks on this before we can go further down this rabbit hole:
@Tyriar, @mofux, @bgw: Should we refactor the public API to contain secondary entries from |
I think multiple * are probably the way to go for most of the APIs, right now they're a bit of a mess as they were developed without much thinking about the future:
We should probably come to a consensus on the ideal way to name things in the API. I would probably like the above to be close to what @PerBothner is proposing in order to align with the web platform (add/removeEventListener): type ICustomKeyEventHandlerId = number;
addCustomKeyEventHandler(...): ICustomKeyEventHandlerId
removeCustomKeyEventHandler(id: ICustomKeyEventHandlerId): void;
type ILinkMatcherId = number;
addLinkMatcher(...): ILinkMatcherId
removeLinkMatcher(id: ILinkMatcherId): void;
type ICharacterJoinerId = number;
addCharacterJoiner(...): ICharacterJoinerId
removeCharacterJoiner(id: ICharacterJoinerId): void;
createMarker(cursorYOffset: number): IMarker;
// setOption unchanged The rules being:
Related:
This is correct, any API also needs to be includes in typings/xterm.d.ts which is self contained and we don't want to expose internals here. They also need some examples added to
I'm not sure we want to introduce another API, we kind of already have this with things marked "experimental" either in the name or comments and I think it's a bit of a mess. I'm fine with us committing to a way to set CSI/OSC handlers, provided we get the naming right and the API itself is future proof (I'm thinking mainly of @jerch's buffer/parser perf work here). The calls should be plumbed through the layers like this though to protect internal code and align with the future direction (#1507): xterm.js/src/public/Terminal.ts Lines 62 to 64 in b5f1c33
|
I'd prefer to have most of these methods return a
The only drawback I see with disposables is if you have to create lots (thousands) of them - in that case the creation of the disposable objects will put a penalty on the runtime. |
Certainly slims things down: addCustomKeyEventHandler(...): IDisposable;
addLinkMatcher(...): IDisposable
addCharacterJoiner(...): IDisposable;
addMarker(...): IMarker; The scale problem doesn't apply to any of these APIs so far. |
👍 on using If we really plan to solve this in a once and forever way I think we also have to discuss and decide about the general capabilities of an "event system" and/or/vs. a "callback invocation system".
About the naming I am fine with whatever you all find appropiate. We should keep this simple, maybe use |
Lots of good comments. Besides the vague goal of wanting addons to work as orthogonally as possible, I have some concrete examples in my DomTerm-using-xterm.js prototype: DomTerm has a number of custom escape sequences of the form CSI+params+"u"; xterm.js has a builtin handler ( Order of execution for multiple callbacks That seems backwards. I think we want last added, first tried. How else could one override a builtin? most systems I know from other languages have some way to tell the event system, that a particular handler finally handled it and can stop propagation The pull request has the callback return a boolean ( _Returning an custom handler access to internals Perhaps - but let's fix one problem at a time. Earlier I gave examples where addon handlers are useful, even without expanded access to Terminal internals. API in general I could change Performance |
Here is an alternative implement of
The builtin bindings will continue to be set using ([This link]{https://spin.atomicobject.com/2017/04/11/typescript-represent-function-properties/} helped with the idea.) (This has only been minimally tested, with no testing of |
@PerBothner I think we need to find a consensus how to do things here. About my perf concerns:
Thats the reason why I raise an eyebrow on the idea to enhance the parser "events" with a more capable event handling system. Both additions - the capability to handle more handlers (looping) and the fact to rely on the return value for early stop propagation (flipping results) will lead to many more deopts. I am not against this, but it needs to be tested. I think a small penalty is acceptable, but we should not sacrifice input runtime just for "being able to do niftier things" with the parser events, and noone really uses this afterwards. Again - I think this does not necessarily apply to any other terminal parts. |
Worst is OSC which does a char by char string concat. This seems a worthwhile optimization:
|
This is not how I originally interpreted #1176, I believe the original request was all about filling in functionality not overriding. Was this the one you want to implement or something custom?
|
The original #1176 request was partly about adding custom non-standard control sequences (see the 2nd paragraph). I'm not looking to override functionality, but I do want to to both add functionality to existing control sequences, and add extra behaviors to existing control sequences (such as those for I'm not interested in "Set margin-bell volume" but I have a lot of other custom escape sequences. |
@PerBothner oh I forgot to mention, all setTitle does is expose a hook for embedders to listening to the change: Line 420 in 76e7057
|
Yes something like this should do the trick for OSC (did not yet optimize this either, we currently support only 2 OSC calls that occur once in a while). The inner loop though has to cover all OSC conditions, for example EXECUTES are a NOOP in this state (means neither executed nor added to the string, see https://vt100.net/emu/dec_ansi_parser). Well I think its abit offtopic here... |
Yes something like this should do the trick for OSC See pull request #1822. The inner loop though has to cover all OSC conditions, for example EXECUTES are a NOOP in this state (means neither executed nor added to the string The pull request is conservative - it stops as soon as its sees a character that is is not OSC_PUT. |
I updated and cleaned my patch to have addCsiHandler/addOscHandler both return IDisposable. If these new functions are not used, there is no difference in the logic, so there should be no performance impact. Awkwardly, the pull request shows 8 commits, but some of them a reverts and some touch on the OSC_STRING pull request. The actual cleaned up patch is just a simple commit: 8ceea11 . I don't know how to update the pull request to reflect this (except by starting over with a new pull request). (I can't say I've masted GitHub's "pull request" work flow, and I have very mixed feelings about it.) I've run the xterm-benchmark. I don't see any consistent difference. Which is as expected, since there should be no change to execution flow as long as addCsiHandler/addOscHandler are not used. These changes are used by the xdomterm prototype. |
Generally on GitHub you should just keep committing and merging like you have done, the maintainers can squash at the time of merge if they want. If you squash yourself it makes it hard to review just the new changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this is getting close 😃
src/Types.ts
Outdated
@@ -492,6 +500,8 @@ export interface IEscapeSequenceParser extends IDisposable { | |||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; | |||
clearCsiHandler(flag: string): void; | |||
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; | |||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jerch are the string
s here future proof with your upcoming changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tyriar, @PerBothner: Not future proof, its still a subject to change, but imho good to go for now. I will address this with one of the typed array transitions to come.
Not sure about including this in public API yet, might be better to go unofficial until the transition is done (will not before 3.11 though due to the JS Array buffer).
src/public/Terminal.ts
Outdated
@@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { | |||
this._core = new TerminalCore(options); | |||
} | |||
|
|||
public get inputHandler(): IInputHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change this to expose the following interface (pending @jerch's comments on whether the types are future proof):
class Terminal {
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
}
You'll then call through to _core
, see addDisposableListener
for what I mean here.
You should also merge IVtInputHandler
back into IInputHandler
after doing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, they will most likely change type to a borrowed typed array, but not until 3.11 since I have to wait for the JS Array buffer to be gone. So not sure if we should include it yet in the public API, well marking them "experimental" or "unstable" would work for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simplified things as suggested (as I understand it).
src/public/Terminal.ts
Outdated
@@ -15,6 +15,9 @@ export class Terminal implements ITerminalApi { | |||
this._core = new TerminalCore(options); | |||
} | |||
|
|||
public get inputHandler(): IInputHandler { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this becomes API we'll need an entry also in xterm.d.ts as well as a test in fixtures/typings-test/typings-test.ts
. The .d.ts file is a TypeScript declaration file and that defines the entire API we want to expose, the test file just makes sure we don't regress the declaration file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in commit 6b65ebd.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made a few comments, overall it looks good 👍
src/EscapeSequenceParser.ts
Outdated
@@ -303,6 +304,33 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP | |||
this._executeHandlerFb = callback; | |||
} | |||
|
|||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yupp this will work without penalty if no other handler is attached 👍
One thing though: can we make this management code more general usable, like in a private method called for CSI, OSC etc. when needed. This way the code is cleaner without duplication and feels less monkey patching, and can also be used for ESC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See commit 8a5a032 "New method _linkHandler used by both addCsiHandler and addOscHandler." I had some problems with the type-checking, and the resulting code is a bit ugly, but not too bad, I think.
src/EscapeSequenceParser.ts
Outdated
const index = flag.charCodeAt(0); | ||
const oldHead = this._csiHandlers[index]; | ||
const parser = this; | ||
const newHead = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to be documented: Inserting the additional handlers at top. (since it is somewhat surprising compared to many other event systems)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in commit 6b65ebd.
(I added detailed doc comments in xterm.d.ts, with just a "link" in Terminal.ts.)
src/EscapeSequenceParser.ts
Outdated
newHead.nextHandler = oldHead; | ||
newHead.dispose = function (): void { | ||
let previous = null; let cur = parser._csiHandlers[index]; | ||
for (; cur && cur.nextHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this still work with cycles, like someone added a handler twice by accident? Also an cycle might prevent the autoclean up by the .dispose
method, imho the dispose chain of additional handlers should be called there, too.
Ah well it creates always a new function object, so cycles should not be an issue (I think).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it should be ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yupp found no issues with it (handler is recreated anyway).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner with this management method. 👍
There are still tests to be added (in EscapeSequenceParser.test.ts), but otherwise I hope the latest commit is good. |
Any feedback on the latest version? I'm hoping to not drag this on too much longer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PerBothner Code looks good (just a few comments). Just one thing - please move the changes into its own branch so we can pull lastest master changes and merge later on.
src/EscapeSequenceParser.ts
Outdated
newHead.nextHandler = oldHead; | ||
newHead.dispose = function (): void { | ||
let previous = null; let cur = parser._csiHandlers[index]; | ||
for (; cur && cur.nextHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yupp found no issues with it (handler is recreated anyway).
src/EscapeSequenceParser.ts
Outdated
newHead.nextHandler = oldHead; | ||
newHead.dispose = function (): void { | ||
let previous = null; let cur = parser._csiHandlers[index]; | ||
for (; cur && cur.nextHandler; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much cleaner with this management method. 👍
src/EscapeSequenceParser.ts
Outdated
return newHead; | ||
} | ||
|
||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have now a somewhat weird API here:
- set...: sets a handler, would remove any previously set/added handlers
- add...: sets (first) or adds a handler
- clear...: clears all handler(s)
Since set.../clear...
are only internally used I dont care much, but once we want this go public we have to refactor it (or at least document the weird behavior).
typings/xterm.d.ts
Outdated
@@ -481,6 +481,31 @@ declare module 'xterm' { | |||
*/ | |||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; | |||
|
|||
/** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likely to change with the typed array in the parser (around v3.11): a single char string arg will turn into a number, the string arg is likely to be a typed array itself with start/end offsets (not clear yet whether UTF32 or UTF16 based).
"please move the changes into its own branch" EDIT: There doesn't seem to be a way to change the pull request to use the new branch. I can create a new pull request if you want. |
@PerBothner Not sure if changing the PR branch is possible at all. Seems creating a new PR is the only option. |
Created pull request #1853 . |
@PerBothner Thank you for the branch split. Closing this PR. |
This fixes (at least partially) issue #1176
"Add a way to plugin a custom control sequence handler".
I have use of these new methods in an experimental option to build DomTerm using xterm.js: https://github.com/PerBothner/DomTerm/blob/master/README-xtermjs.md
Specifically, I use these hooks for escape sequences to create new sub-windows, and to set windows title, session name, and relayed. (The DomTerm-using-xterm.js option is rough, and missing many of the features of DomTerm, but the basic functionality works.)