-
Notifications
You must be signed in to change notification settings - Fork 62
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
Add support for custom parsing of APC, SOS and PM sequences. #115
base: master
Are you sure you want to change the base?
Conversation
04fbc6e
to
255c2fe
Compare
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'll give a detailed review later today/tomorrow, but assuming that this was benchmarked it seems like a reasonable approach.
Generally I think the VTE crate needs a little rewrite, but I don't want to hold up other changes for that since it will likely take a long time before I get around to that.
I ran I don't expect any performance impact since I've added no new state and I've only added tailed branches to an already-long match expression, but you never know until you test... |
You can open a PR against alacritty with your changes, so we can use our bot for it. But generally you just build alacritty with your changes and run the |
Thanks! Here are the results: Master Branch:
This PR:
The |
@boazy just open a PR against alacritty's repo, it should better for us to review that way, since we can run the bot. |
@kchibisov I've added a pull request. I'm not sure how to trigger the benchmarks bot. |
You can't. I did. |
@boazy This has stalled out for a while now, are you still interested in working on this? I'm slightly concerned because the longer we wait, the higher the chance that a vte rewrite will make all your work worthless. |
I didn't have time for a while, but I've had some time again today and I've checked your suggestion. Unfortunately, I don't think it would be possible without completely re-architecting the way state works. As I've mentioned before, we only have 4 bits for actions that change the state, but we also only have 4 bits for the state itself, and here all possible values (0-15) are used. If we want to support three sets of functions, we would need to track three separate states for each type of string (SOS, PM, APC). This is not possible without either:
The only thing I can do without any major change is to have 3 different functions instead of |
Just to recap because this is quickly fading into obscurity and I don't want to forget about it entirely: Splitting But couldn't you just store that on I believe that's kinda your first suggestion? Why do you think this would impact performance? |
I honestly don't know. I assumed the size is important for performance since we want to be able to have all state transitions in one table (which only has 256 entries?). If we go back to the start, I created this PR trying to answer the comments to the original PR that tried to add APC support (#110), particularly this comment:
I realize I can add additional state that doesn't affect OSC sequences, but seeing this comment I was a bit worried. Do you believe adding an additional field to |
Yeah that's fair. This would make this a slow sloppy implementation and it might not be worth adding it for that reason. But that applies to your current solution too and the only alternative is a bigger rework that integrates this as a core part of our parser.
I think we should either do this, or go for something that properly integrates everything into our standard processing pipeline, which of course is difficult without affecting performance. I'm very hesitant to accept this sort of "second rate escape sequence", but I would prefer that over the current implementation. |
Ok, I think I've understood you now. I'll modify the pull request along these lines. |
cbbdde3
to
45061fd
Compare
d511cdd
to
4839d4d
Compare
Going it tenatively approve the current revision, I don't think there's any other changes necessary from your end. While I'm not entirely convinced I'll give this a closer look and if there are no performance regressions it should be fine. |
We've reworked vte a bit, so probably would be easier to add things without hitting perf, since all the code that caused problem here is gone now. |
Hello! Just wanted to re-ping this thread to see what progress was being made here. We'd really like to support Kitty Image protocol in the Warp terminal, and this PR would help us get there |
This PR was languishing for a while now and now, and now that vte is ready, I probably need to rewrite it I guess, but I didn't have time to revisit this recently. I'll try to find time to do it. The good news is that it should impact performance. I hope. |
Hello again, sorry to be a bother, just wanted to check in on the status of this. We’re kicking off the full-time effort to support kitty image protocol starting this week, and just want to get an understanding how much of a risk this PR is to our project. Thank you! |
a816971
to
dc153f4
Compare
@chrisduerr , @kchibisov I've finally got time to rebase the PR to work with the new version. I haven't written any unit tests yet, since I want to see if you are ok with the direction. Some points to take note of:
There are two approaches I can see for keeping track of additional state (particularly important for item #6):
I believe the first approach should be better. @heysweet Sorry for the time it took me to rebase this PR, I basically had to rewrite it from scratch. |
Note that most terminals only accept |
dc153f4
to
6421e17
Compare
6421e17
to
c515809
Compare
One more point of concern: |
Assuming apps actually follow the spec, then kitty image sequences should never be more than 4K ("the pixel data must first be base64 encoded then chunked up into chunks no larger than 4096 bytes"). But otherwise you're right - in theory these string sequences can be infinitely large. |
Makes sense. I wasn't aware of the actual Kitty Image Protocol spec so I've missed that. |
The perf looks pretty much the same alacritty/alacritty#8507 Can not really answer wrt design now questions you've raised. |
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.
Honestly seems fine to me. Just some minor nits.
src/lib.rs
Outdated
// ESC-ST (ESC-\) and C1-ST (0x9C), but kitty (and probably some other | ||
// terminals) also support bell-terminated strings. Some |
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 comment is a bit silly because "some other terminals" includes Alacritty. This is mostly an extension to how other sequences like OSCs are handled, which also support the bell terminator and do so already in Alacritty. So this might need some rephrasing to not sound strange.
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 was referring to SOS/PM/APC sequences, which are not yet supported by alacritty. The current behavior (treating them as the anywhere state) doesn't seem to support termination with BEL, so this would be a behavior change for alacrity, wouldn't it?
But since as you've said, this is how OSC sequences already behave, so I think we should change this behavior to be more consistent.
As stated above, I added these comments only to explain my reasoning, I didn't intend them to be included in the code in the final version of the PR.
src/lib.rs
Outdated
// XTerm terminates SOS/APC/PM strings on C1 CAN (^X) and SUB (^Z). This is also | ||
// the same behavior we implement for OSC strings. |
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.
0x18/0x1A is just a general-purpose reset into ground from anywhere, the transition is unrelated to its origin state. There's not really any reason for this comment to exist.
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 was a bit confused, since not all terminals seem to do that (at least when handling SOS/PM/APC), but I'll remove the comment.
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.
Yeah I'll refer to https://vt100.net/emu/dec_ansi_parser for "general" guidance on Alacritty's parser here (even though it doesn't handle opaque strings). It's not really a SOS/PM/APC, but more of an "anywhere" thing. As such a SOS/PM/APC-specific comment is unnecessary.
src/lib.rs
Outdated
// Any escape code ends the SOS/APC/PM string. This is not standard behavior, | ||
// but avoids having to keep additional state. |
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 comment seems misleading. Escape resetting ongoing escape sequences from anywhere is a de-facto standard implemented by many terminal emulators. It might not be explicitly called for in older specifications, but is not incorrect behavior either, it's mostly an implementation detail really.
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'm sorry if I got my intentions wrong. The comments are not meant to be here to stay. They are mostly documenting my thoughts while looking for feedback to this pull request. I have little experience with de-facto terminal implementations, so I can only refer to the specifications (which are a bit vague).
If escape sequences resetting the state is common de-facto behavior, then I would be happy to keep it this way, since it keeps the implementation clean and simple.
src/lib.rs
Outdated
// Only dispatch valid characters. | ||
dispatcher.opaque_put(byte) |
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.
Why is this comment on a method call that dispatches bytes indiscriminately? It might be more appropriate on the match arm instead, but even then I question whether the 0x80
to 0xFF
range can be considered "valid characters", considering they're not printable characters (so effectively the same as any other byte).
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.
Sorry for that. This is a leftover comment from a previous implementation attempt that I gave up on and forgot to remove.
src/lib.rs
Outdated
// Only dispatch valid characters. | ||
dispatcher.opaque_put(byte) | ||
}, | ||
// Ignore all other control codes |
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.
// Ignore all other control codes | |
// Ignore all other control bytes. |
src/lib.rs
Outdated
/// Invoked for every valid character (0x20-0xFF) in a SOS (Start of String) | ||
/// sequence. |
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.
/// Invoked for every valid character (0x20-0xFF) in a SOS (Start of String) | |
/// sequence. | |
/// Invoked for every byte (0x20-0xFF) in a SOS (Start of String) sequence. |
Same comment applies for the other functions. Calling these "valid characters" could be misleading to consumers.
src/lib.rs
Outdated
/// Invoked when the beginning of a new SOS (Start of String) sequence is | ||
/// encountered. | ||
fn sos_start(&mut self) {} | ||
|
||
/// Invoked when the beginning of a new APC (Application Program Command) | ||
/// sequence is encountered. | ||
fn apc_start(&mut self) {} | ||
|
||
/// Invoked when the beginning of a new PM (Privacy Message) sequence is | ||
/// encountered. | ||
fn pm_start(&mut self) {} |
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.
Rather than sorting these by start/put/end, I'd prefer grouping them by sos/apc/pm. I think it makes more sense since the grouping into start/put/end is somewhat arbitrary considering they aren't really interconnected.
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.
Makes sense. I'll do it.
src/lib.rs
Outdated
fn sos_dispatch(&mut self, _byte: u8) {} | ||
|
||
/// Invoked for every valid character (0x20-0xFF) in an APC (Application | ||
/// Program Command) sequence. | ||
fn apc_dispatch(&mut self, _byte: u8) {} | ||
|
||
/// Invoked for every valid character (0x20-0xFF) in a PM (Privacy Message) | ||
/// sequence. | ||
fn pm_dispatch(&mut self, _byte: u8) {} |
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'm not sure the naming _dispatch
is consistent with the rest of our methods. The existing _dispatch
functions generally represent the dispatch of an entire escape sequence in full, while this function just represents the dispatch of a single byte in the sequence.
I think this is much closer to the way esc works in VTE, where it is split in hook
, put
, and unhook
.
Other parts of this patch already make use of the _put
nomenclature, so I think it's better to be consistent and rename these functions to sos/pm/apc_put
.
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.
You're absolutely right, considering the method on the internal OpaqueDispatch
trait is called opaque_put
and then it ends up calling sos/apc/pm_dispatch()
...
trait OpaqueDispatch { | ||
fn execute(&mut self, byte: u8); | ||
fn opaque_put(&mut self, byte: u8); | ||
fn opaque_end(&mut self); | ||
} |
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.
Honestly this trait and its implementations is the main issue I have with this patch. It should work fine since it all just gets inlined and optimized out essentially, but it's still a whole lot of boilerplate.
I'm not sure I have any better ideas for now, but at the very least this trait needs a comment explaining that it's just a helper for dispatching over the opaque string escapes and in practice is inlined everywhere to function as static dispatch without conditional indirection.
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 trait was the best solution I could think of until now, which means it is the least terrible one. I'll add a comment.
Just to be clear on this:
I personally don't mind this "simplistic" approach because I have very little interest in sos/pm/apc escape sequences, so this way there is only a minimal amount of impact on Alacritty while VTE is kept simple. That's also why I don't care if these escapes are dispatched particularly efficiently or not, since ideally nobody every uses them. That said, the collection into an (Array)Vec in VTE is essentially a solved issue. We have this for OSCs already where things are somewhat dynamically limited and having a matching/similar approach probably wouldn't be a huge deal for these escapes either. Ideally to ensure parser state doesn't get too big one would likely reuse the same byte buffers for both, since it's not possible to have two different escape sequences at the same time anyway. But going for something like that is likely to slow down the implementation process, since it doesn't sound like that's something you're particularly familiar with. So I don't mind just going with the status quo in this patch. |
Familiarity is not my issue here. I do understand how to collect bytes into a Vec (I hope). I thought of reusing
I don't think these two issues are big in practice. Programs that send Kitty Image Protocol sequences to a terminal that doesn't support that are already misbehaving, and 4096 or 8192 bytes of extra memory per terminal are not a big thing. I also don't think cache locality would be impacted, since the shorter OSC sequences would keep using just the beginning of the buffer. I will create another branch and a draft PR with a buffered version for comparison. |
On a second thought, I'm not sure if buffering opaque sequences will be such a great boon for clients of VTE anyway. Just like VTE itself is inlining methods, clients can just choose to inline their |
If the goal was to write the ideal implementation as far as performance goes, we'd probably special-case the OSC state to look for its termination sequence using |
input: &[u8], | ||
kind: OpaqueSequenceKind, | ||
expected_payload: &[u8], | ||
expected_trailer: &[Sequence], |
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 can just be a boolean to simplify this function and remove the necessity for a constant. st_terminated: bool
or something like that.
expected_payload: &[u8], | ||
expected_trailer: &[Sequence], | ||
) { | ||
let mut expected_dispatched: Vec<Sequence> = vec![Sequence::OpaqueStart(kind)]; |
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 mut expected_dispatched: Vec<Sequence> = vec![Sequence::OpaqueStart(kind)]; | |
let mut expected_dispatched = vec![Sequence::OpaqueStart(kind)]; |
Is this really necessary? I'd be extremely surprised if Rust wasn't able to infer it here.
Added some small comments, but didn't want to actually do a review yet. Please just re-request one once you think you're done with your changes. |
Fixes #109.
This attempts the same goal at John-Toohey's PR #110, but since this PR was was not accepted, I wanted to try another approach and see whether it is acceptable.
Rationale for support
APC sequences are rarely supported by general-purpose terminals (if we put aside Kermit clients, tmux and screen), but there is one exception: The Kitty Terminal Graphics Protocol. While the Kitty Image Protocol is not as ubiquitous as Sixel, it is more robust, accurate and powerful and it's implemented by several prominent terminal emulators:
It's also supported by a large number of programs and libraries, some of them are mentioned here.
Design
Design Goals
Design Choices
Reorder actions into packable and non-packable actions
Currently, resulting state and actions are packed into an 8-bit value in the state change table (with a 4-bit nibble allocated for each). All values from 0 to 15 for both state and actions are used, so I could not add new states and action that would be packable. Unfortunately, I needed to support a state change with the
OpaquePut
action. The best way I've found is to separate the integer values of actions that do not need to be packed into the state change table (such asHook,
Unhookand
Clear`) into a value higher than 15 and reserve lower values for actions that need to be packedI'm not sure if my current solution is acceptable or not but I have no other idea on how to resolve this situation without repurposing the
OscString
state and actions and re-introducing an embedded parser.Action matching order
This should have minimal impact, but in order to avoid any performance impact for programs which do not use APC sequences, I made sure that when matching APC/SOS/PM-specific actions, they are always matched last.
Streaming
Unlike #110, this PR does not collect the sequence payload inside an array. Instead, the sequence payload is streamed directly to the
Perform
trait, one byte at a time. I believe this approach is more flexible and would lead to better performance overall. The key gains here are:osc_raw
and in any case we would have to increase its default size to 4096 to be compatible with the max payload size).Names
I've changed
SosPmApcString
toOpaqueString
, since I wanted to have a general name for all these types of sequences where the VTE parser has no understanding of the internal structure of the sequence payload. This is difference from OSC and CSI sequences where the VTE parser is aware of the high-level structure of the sequence (if not the semantics).