-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Switch terminal backend from Crossterm to Termina #13307
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
base: master
Are you sure you want to change the base?
Conversation
51008f7
to
8e2764b
Compare
This comment was marked as resolved.
This comment was marked as resolved.
I see you linked crossterm-rs/crossterm#879, would the performance gains seen there benefit Windows this time now that the terminal is "spoken" to the same way on all platforms? |
I was able to reproduce this with the same setup. I changed the OSC52 code a bit to do some explicit locking and flushing and I haven't been able to reproduce it so far. That part is a bit hacky - we basically I also noticed that true-color detection was broken for me in that workflow (i.e. SSH with Ghostty. Probably I would need to install the Ghostty terminfo on the NixOS side). I've updated the true-color detection to use a query following https://github.com/termstandard/colors#querying-the-terminal so it works out of the box in this case.
Crossterm does actually "speak VT," meaning that it emits escape sequences - like those used to set foreground/background colors - when it detects that ConPTY is available. The issue with its Windows handling is that it doesn't "hear VT": it enables |
On windows 11 it hangs during startup on wezterm, but launches correctly on windows terminal. The logs don't show anything. |
This comment was marked as resolved.
This comment was marked as resolved.
The unicode stuff should be fixed in the latest push - I was accidentally resetting the input/output "code pages" (encodings) after querying for features. That should cover the weird glyphs for non-ascii characters and input problems like |
This comment was marked as resolved.
This comment was marked as resolved.
What does your WezTerm config look like? |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This is all with wezterm: The text issue was fixed, but now no capabilities seem to have been detected
With For me, capslock works as expected. ctrl and alt also seem to be fine. Colors are fine. |
latest commit looks more promising with the capability check, though I wonder why true color and underlines are false?
|
For better compatibility I switched to the foreground/background sequences suggested in the microsoft docs: https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#extended-colors. (As I understand it it's not spec-wise correct but is an idiom used in the wild more often than the correct syntax.) That should fix the colors on conhost and stable WezTerm (which is quite old, probably it bundles an old conhost). It's the same format as crossterm uses so it should be the same compatibility-wise. That last push should fix the detection: out of the box WezTerm should be |
Is that with the Kitty keyboard protocol enabled? I've seen that behavior with WezTerm as well when it is enabled - termina doesn't seem to get anything from the input handle until another button is pressed - but I haven't debugged why yet. I don't see the same in Alacritty notably and I believe it has at least some support for the Kitty keyboard protocol. |
That is correct, the behavior shown is with these set: config.enable_kitty_keyboard = true
config.allow_win32_input_mode = true |
It looks like this is a bug in the Kitty keyboard encoding function in WezTerm. I sent a patch wezterm/wezterm#6872. I'm not sure why the old encoding causes the key to 'queue up' and wait for a non-escape input but using the proper representation seems to fix it. |
Ok, added those wezterm patches and now running them.
All capabilities seem to be detected. The keys and mouse seem to work correctly. I will run this more and get back if anything is up. Thanks for getting those upstream fixes in, the time and effort you spent is greatly appreciated. |
This seems to be resolved on the current tip. |
That underline behavior may be a bug with WezTerm, I haven't been able to reproduce on Kitty, Alacritty or Foot yet. I'll take a look at how WezTerm is parsing those underline commands |
I remember we were dealing with a wezterm bug like that before, I think it depended on the order of the style data |
Previously seen here: #4061 (comment) |
I believe that was a different issue with the crossterm backend at the time since I could reproduce it then on different terminals. I added a commit to work around the problem. It seems like WezTerm doesn't like having the underline color included in the big SGR update that updates all attributes that changed between two cells. We can work around it for now by emitting a separate SGR update whenever the underline color changes. |
This is likely because of a parameter limit. WezTerm has a limit of 16 (at least last I tested), and attributes with subparameters count as multiple parameters. For example an RGB color sequence like 16 is actually a fairly common limit, and it's not even the lowest! I believe MLTerm has a limit of 10. That's just enough for two RGB colors, but if you're trying to change other attributes at the same time they're liable to get dropped or corrupted. So you need to be very careful how much you change in one sequence. |
Aha, that makes sense, thanks! I'll update this escape to "chunk" the updates so they stay under that conservative limit. If we find lower limits we can tune that further. |
Looks like WezTerm just bumped its CSI param limit from 32 to 256: wezterm/wezterm#6194. It's still probably better to keep the number of parameters low for wider compatibility though |
This implements only the bare minimum to allow the multi-client support to be tested. To test it open three terminal windows. - In window 1 run: ./s - In window 2 run: ./c - In window 3 run: ./c You will see the UI appear in windows 2 and 3. The UI will be usable in those windows and buffers will be shared between them. TODO: - Support more than two clients. - Support dynamically attaching and detaching clients. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
This implements only the bare minimum to allow the multi-client support to be tested. To test it open three terminal windows. - In window 1 run: ./s - In window 2 run: ./c - In window 3 run: ./c You will see the UI appear in windows 2 and 3. You can open the same file in both windows and observe changes in one window appear in the other. TODO: - Support more than two clients. - Support dynamically attaching and detaching clients. - Support sending command line, current directory, stdin, signals from the client to the server. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
This implements only the bare minimum to allow the multi-client support to be tested. To test it open three terminal windows. - In window 1 run: ./s - In window 2 run: ./c - In window 3 run: ./c You will see the UI appear in windows 2 and 3. You can open the same file in both windows and observe changes in one window appear in the other. TODO: - Support more than two clients. - Support dynamically attaching and detaching clients. - Support sending current directory, stdin, signals from the client to the server. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
This implements only the bare minimum to allow the multi-client support to be tested. To test it open three terminal windows. - In window 1 run: ./s - In window 2 run: ./c - In window 3 run: ./c You will see the UI appear in windows 2 and 3. You can open the same file in both windows and observe changes in one window appear in the other. TODO: - Support more than two clients. - Support dynamically attaching and detaching clients. - Prevent reading from terminal while client is suspended. - Support sending current directory, stdin from the client to the server. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
Run the ./s command to start the server, and run ./c to connect to it. You can open the same file in multiple windows and observe changes in one window appear in the others. TODO: - Support sending current directory, stdin from the client to the server. - Figure out why it always opens a split with a scratch view after the first client. - Implement automatically starting the server. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
Run the ./s command to start the server, and run ./c to connect to it. You can open the same file in multiple windows and observe changes in one window appear in the others. TODO: - Support sending current directory, stdin from the client to the server. - Implement automatically starting the server. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
With this change, helix becomes a multi-client application. `hx` will automatically start the server process if necessary, and connect to it. Buffers and language servers run as subprocesses of the server and are used by all clients. The client/server protocol is very simple. The server listens on a Unix socket. The client opens the socket and sends a serialized ClientInfo to the server, which contains the command line arguments, working directory and some other information. The terminal file descriptor (and the stdin file descriptor, if stdin is not a terminal) are then sent to the server using SCM_RIGHTS. The server uses the tty file descriptor to render the UI and receive user input exactly as it does today afer opening /dev/tty, and uses the stdin file descriptor to create a scratch buffer. The client listens for relevant signals such as SIGWINCH, SIGTSTP and SIGCONT. When the client receives a signal, it sends a single byte signal number to the server. The server responds to receiving a byte on the socket similarly to how Helix handles signals today. One exception is that in response to a SIGTSTP, the server sends SIGSTOP to the client's process group instead of its own process group. This way, the server keeps running while the client process (and the other processes in its process group) is suspended. When a client terminates (e.g. after :q), the server sends a single byte exit code to the client, and the client exits using the exit code. TODO: - Unbreak the integration tests. - Unbreak the Windows build. For the time being, the native Windows build will not support multi-client. Windows users who want multi-client can use WSL. - Unbreak per-workspace .helix/config.toml support. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
The `Config` can be passed when creating the backend (for example `CrosstermBackend::new`) and is already updated in the `Backend::reconfigure` callback. Recreating the tui `Config` during `claim` and `restore` is unnecessary and causes a clone of the editor's Config which is a fairly large type. This change drops the `Config` parameter from those callbacks and updates the callers. Instead it is passed to `CrosstermBackend` which then owns it. I've also moved the override from the `editor.undercurl` key onto the tui `Config` type - I believe it was just an oversight that this was not done originally. And I've updated the `From<EditorConfig> for Config` to take a reference to the editor's `Config` to avoid the unnecessary clone during `CrosstermBackend::new` and `Backend::reconfigure`.
That should be fixed now after helix-editor/termina@bd391b1. Also added the "chunking" for SGR attributes |
This change switches out the terminal manipulation library to one I've been working on: Termina. It's somewhat similar to Crossterm API-wise but is a bit lower-level. It's also influenced a lot by TermWiz - the terminal manipulation library in WezTerm which we've considered switching to a few times. Termina is more verbose than Crossterm as it has a lower level interface that exposes escape sequences and pushes handling to the application. API-wise the important piece is that the equivalents of Crossterm's `poll_internal` / `read_internal` are exposed. This is used for reading the cursor position in both Crossterm and Termina, for example, but also now can be used to detect features like the Kitty keyboard protocol and synchronized output sequences simultaneously.
It is unused and cannot be used on some terminal hosts like `conhost` that do not respond to VT queries. This doesn't have any affect on behavior - I'm removing it so that we don't rely on it in the future.
My cursor is disappearing when I move from right to left over a closing bracket via the This only happens with Ghostty+Tmux (not standalone Ghostty). Can't reproduce on (helix) main branch. Screen.Recording.2025-05-19.at.22.14.22.mov |
For those building from source (and need to resolve Helix itself does not use them, as stated here helix-editor/termina#6 (review) |
I think this has something to do with chunking in termina. Decreasing the |
With this change, helix becomes a multi-client application. `hx` will automatically start the server process if necessary, and connect to it. Buffers and language servers run as subprocesses of the server and are used by all clients. The client/server protocol is very simple. The server listens on a Unix socket. The client opens the socket and sends a serialized ClientInfo to the server, which contains the command line arguments, working directory and some other information. The terminal file descriptor (and the stdin file descriptor, if stdin is not a terminal) are then sent to the server using SCM_RIGHTS. The server uses the tty file descriptor to render the UI and receive user input exactly as it does today afer opening /dev/tty, and uses the stdin file descriptor to create a scratch buffer. The client listens for relevant signals such as SIGWINCH, SIGTSTP and SIGCONT. When the client receives a signal, it sends a single byte signal number to the server. The server responds to receiving a byte on the socket similarly to how Helix handles signals today. One exception is that in response to a SIGTSTP, the server sends SIGSTOP to the client's process group instead of its own process group. This way, the server keeps running while the client process (and the other processes in its process group) is suspended. When a client terminates (e.g. after :q), the server sends a single byte exit code to the client, and the client exits using the exit code. TODO: - Unbreak the integration tests. - Unbreak the Windows build. For the time being, the native Windows build will not support multi-client. Windows users who want multi-client can use WSL. - Unbreak per-workspace .helix/config.toml support. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
With this change, helix becomes a multi-client application. `hx` will automatically start the server process if necessary, and connect to it. Buffers and language servers run as subprocesses of the server and are used by all clients. The client/server protocol is very simple. The server listens on a Unix socket. The client opens the socket and sends a serialized ClientInfo to the server, which contains the command line arguments, working directory and some other information. The terminal file descriptor (and the stdin file descriptor, if stdin is not a terminal) are then sent to the server using SCM_RIGHTS. The server uses the tty file descriptor to render the UI and receive user input exactly as it does today afer opening /dev/tty, and uses the stdin file descriptor to create a scratch buffer. The client listens for relevant signals such as SIGWINCH, SIGTSTP and SIGCONT. When the client receives a signal, it sends a single byte signal number to the server. The server responds to receiving a byte on the socket similarly to how Helix handles signals today. One exception is that in response to a SIGTSTP, the server sends SIGSTOP to the client's process group instead of its own process group. This way, the server keeps running while the client process (and the other processes in its process group) is suspended. When a client terminates (e.g. after :q), the server sends a single byte exit code to the client, and the client exits using the exit code. TODO: - Unbreak the integration tests. - Unbreak the Windows build. For the time being, the native Windows build will not support multi-client. Windows users who want multi-client can use WSL. - Unbreak per-workspace .helix/config.toml support. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
Termina is a new terminal manipulation crate I've written that has a similar interface as Crossterm. The interface exposes lower-level details than Crossterm's, exposing escape sequences and pushing all of that handling to the application.
Termina has a minimal Windows integration that works because of ConPTY - we tell terminals on Windows to "speak VT" like a *NIX PTY would. This drops a lot of Windows-specific code for the old Console API and allows us to support VT features consistently on all platforms. In particular OSC52 bracketed paste should work on Windows now. The downside is that we will require Windows 10 like WezTerm: version 10.0.17763 or later. This is a fairly old version though, published ~Fall 2018. The Kitty keyboard protocol should work on WezTerm (when enabled in config) now,
though it seems a bit buggy in my testing - needs some debugging.Termina also has an interface for polling and reading for arbitrary events and escape sequences. (This exists in Crossterm too but is private.) This allows us to detect VT extensions like synchronized rendering and true color support while setting up the terminal.
Rendering should be smoother on this branch since synchronized output sequences are enabled when we detect support for them (fixes #731). Termina also has better "compression" for SGR sequences (things like foreground/background/underline colors, bold, italic, etc.) similar to crossterm-rs/crossterm#879 but covering all SGR attributes in addition to foreground and background - this should give the terminal less work to do when it parses the escapes we emit during rendering.
This needs some testing. I did some limited manual testing on Kitty and Ghostty on macOS, Kitty, Foot and WezTerm on Linux and cmd.exe, PowerShell and WezTerm on Windows.
Also fixes #12232 (the same way as #13223 did)