Fixes #11193 where terminal surfaces might not appear and focus might be lost when creating multiple nested splits. These bugs are caused by GTK initially allocating a tiny width/height to deeply nested splits. For a split with a tiny size, the split ratio will be set inaccurately e.g. to 1 which means that the right/bottom child of the split is invisible. If that child is the terminal surface that should have the focus, it will lose it. In the current implementation the split ratio can be set at most once, which means the inaccurate ratio never gets corrected and a surface (or an entire sub-tree of the layout) will stay invisible. The following explains the current implementation and bug in more detail, it is a bit long, but I hope it will make it easier to review this PR. ### Current Implementation A split layout is a tree, in code represented by `datastruct/SplitTree`, where inner nodes are splits and leafs are terminal surface. A split can be either horizontal or vertical, and has a ratio that defines how its space should be divided among the 2 children. The counterpart in the GTK UI is the `apprt/gtk/class/SplitTree` widget whose `onRebuild/buildTree` functions build a widget tree that has the same structure as the `datastruct/SplitTree`. The widget tree consists of a `SplitTreeSplit` widget for every split and a `Surface` widget for every terminal surface. A `SplitTreeSplit` widget wraps a `gtk.Paned` widget, which displays its two children with a divider in between, either horizontally or vertically. How much space each child gets is determined by 3 properties. `min_position` is always 0 in our case, `max_position` corresponds to the width/height (for horizontal/vertical splits) of the widget and `position` is where the divider should be. So `position` is equivalent to the width/height of the left/top child and thereby also determines the width/height of the right/bottom child. `SplitTreeSplit` listens for changes in the 3 properties. If there is one, the `propPosition`, `propMinPosition` or `propMaxPosition` function gets triggered and an idle callback for the `onIdle` function is added. We need to make sure that the widget tree and the `datastruct/SplitTree` stay in sync. If we e.g. create a new split or close a surface, the structure of the split tree changes. In that case `gtk/class/SplitTree.onRebuild` will completely rebuild the widget tree (the `Surface` widgets are actually reused) to match the new tree structure. If we resize a split (i.e. change the split ratio) via action/keybind, we also completely rebuild the widget tree. Additionally we need to make sure that for every `SplitTreeSplit/gtk.Paned` the `position` divided by `max_position` matches the ratio of the corresponding split node in our `datastruct/SplitTree`. There are two ways the current implementation keeps these ratios in sync, both are handled by the `SplitTreeSplit.onIdle` function. 1. Initially when the widget tree is built, GTK allocates each widget a size. Specifically it also sets the `position` and `max_position` properties of each `gtk.Paned` widget, which will trigger the `SplitTreeSplit.onIdle` function to run. GTK will not necessarily set position correctly, it is the task of `onIdle` to make sure that the UI matches the layout defined by the `datastruct/SplitTree`. `onIdle` checks if `position/max_position` matches the ratio that the split should have and if not calls `gtk.Paned.setPosition` to update it. This can only happen once for each split since `onIdle` checks if the position was set previously. The idea is that we should only ever need to set the position once, because `gtk.Paned` will automatically keep its current ratio whenever its size/`max-position` changes (if the `setPosition` function has been called before). A size change can happen e.g. because the entire window was resized or because an ancestor split changed its split ratio. 2. The user can manually change the ratio between the two children of a split by dragging the divider between them in the UI. When that happens the `position` property in `gtk.Paned` changes and eventually the `SplitTreeSplit.onIdle` function gets called. Since `setPosition` should have already been called when the widget was initially sized, we should fall through to the second case and write the current ratio back to the `datastructure/SplitTree`. The problem with `SplitTreeSplit.onIdle` is that sometimes the split ratio cannot be set accurately given the current size of the `gtk.Paned` widget. Because `onIdle` can only set the position/ratio once, any previous inaccuracy can never get corrected. For example with many nested vertical splits, GTK might initially allocate a `gtk.Paned` widget a height of 1. It will have `max_position=1` and `position=1`. When `onIdle` runs the current ratio of `position/max_position = 1` is different from the desired ratio of e.g. 0.5. But a ratio of 0.5 cannot be set, the position can only be 0 or 1 corresponding to a ratio of 0 or 1. The position will then get set as 1 and can't be changed later. Even when the split later gets a larger height, it will keep the ratio of 1 and the bottom child will stay invisible. When the surface that should be focused initially becomes invisible it loses focus and the focus will never be restored. That is exactly what happens in the first screencast in the issue description (#11193). Another problem with `onIdle` is that the `setPosition` call in `onIdle` will trigger another idle callback where the position change is sometimes wrongly interpreted as a manual update and written back to the tree. Also sometimes the initial ratio in a `gtk.Paned` can already be correct, in which case position will not get set. The next manual position update is then not detected as a manual update. ### Changes `SplitTreeSplit.onIdle` is now able to set the split position every time the widget is resized, an inaccurate initial ratio will be corrected. To be able to distinguish a widget resize from a manual position update by the user, we keep track of whether `max-position`, `position` or both properties changed. If only `max-position` or both properties changed, then the widget was resized. If just `position` changed it is a manual update. This is kind of hacky but works. To verify I checked the source code for `gtk.Paned`, see the comment in the code on `onIdle`. `SplitTreeSplit` no longer listens to changes in `min-position`, that should always be 0 (because we use the default resize/shrink properties for `gtk.Paned`) and there is already an assert in `onIdle` that checks that. It can still happen that a surface initially gets allocated width/height 0 and loses focus. The only reliable way to detect when we can restore focus, is to listen to the `map/unmap` signals exposed by `gtk.Widget`. The `Surface` widget now listens to these signals on its `GlArea` child (because that is where we want to put focus) and stores the current state in the new `mapped` property. The `SplitTree` widget listens to changes in that property: when surfaces become mapped, an idle callback for the new `onRestoreFocus` function is added, which will check whether the last focused surface is mapped and if so restore focus to it. ### Other possible solutions Alternatively we could try to only set the split ratio once the split has its correct final size, but I think it's hard to detect that reliably. Or we could try to prevent the splits/surfaces from becoming invisible in the first place by e.g. setting a minimal widget size. But then you won't get the exactly correct layout and sometimes you do want a surface to be tiny or invisible e.g. you can drag the divider in a split all the way to one side. The ideal solution would probably be to write a custom version of `gtk.Paned` where you can provide the desired ratio on widget creation. Then everything will be sized correctly from the start, focus will not be lost. In terms of performance it would probably be better as well, because right now there can be multiple rounds of resizes until every split/surface has its correct size. I have played around with this a bit, it is definitely possible. But you would have to implement the divider widget, logic for layouting, handling gestures and co. That is a bigger undertaking. ### Testing Tested by creating/modifying deeply nested layouts, dragging split dividers and resizing splits via keybind. Checked that ratios are maintained when the window is resized and tested that it works with zoom. Tested locally with hyprland and in a VM with KDE. All the bugs described in the issue should be fixed. ### AI Disclosure Discovered the bug and wrote all code/comments by myself. Used AI in researching various internals of GTK.
Fast, native, feature-rich terminal emulator pushing modern features.
A native GUI or embeddable library via libghostty.
About
·
Download
·
Documentation
·
Contributing
·
Developing
About
Ghostty is a terminal emulator that differentiates itself by being fast, feature-rich, and native. While there are many excellent terminal emulators available, they all force you to choose between speed, features, or native UIs. Ghostty provides all three.
libghostty is a cross-platform, zero-dependency C and Zig library
for building terminal emulators or utilizing terminal functionality
(such as style parsing). Anyone can use libghostty to build a terminal
emulator or embed a terminal into their own applications. See
Ghostling for a minimal complete project
example or the examples directory
for smaller examples of using libghostty in C and Zig.
For more details, see About Ghostty.
Download
See the download page on the Ghostty website.
Documentation
See the documentation on the Ghostty website.
Contributing and Developing
If you have any ideas, issues, etc. regarding Ghostty, or would like to contribute to Ghostty through pull requests, please check out our "Contributing to Ghostty" document. Those who would like to get involved with Ghostty's development as well should also read the "Developing Ghostty" document for more technical details.
Roadmap and Status
Ghostty is stable and in use by millions of people and machines daily.
The high-level ambitious plan for the project, in order:
| # | Step | Status |
|---|---|---|
| 1 | Standards-compliant terminal emulation | ✅ |
| 2 | Competitive performance | ✅ |
| 3 | Rich windowing features -- multi-window, tabbing, panes | ✅ |
| 4 | Native Platform Experiences | ✅ |
| 5 | Cross-platform libghostty for Embeddable Terminals |
✅ |
| 6 | Ghostty-only Terminal Control Sequences | ❌ |
Additional details for each step in the big roadmap below:
Standards-Compliant Terminal Emulation
Ghostty implements all of the regularly used control sequences and can run every mainstream terminal program without issue. For legacy sequences, we've done a comprehensive xterm audit comparing Ghostty's behavior to xterm and building a set of conformance test cases.
In addition to legacy sequences (what you'd call real "terminal" emulation), Ghostty also supports more modern sequences than almost any other terminal emulator. These features include things like the Kitty graphics protocol, Kitty image protocol, clipboard sequences, synchronized rendering, light/dark mode notifications, and many, many more.
We believe Ghostty is one of the most compliant and feature-rich terminal emulators available.
Terminal behavior is partially a de jure standard (i.e. ECMA-48) but mostly a de facto standard as defined by popular terminal emulators worldwide. Ghostty takes the approach that our behavior is defined by (1) standards, if available, (2) xterm, if the feature exists, (3) other popular terminals, in that order. This defines what the Ghostty project views as a "standard."
Competitive Performance
Ghostty is generally in the same performance category as the other highest performing terminal emulators.
"The same performance category" means that Ghostty is much faster than traditional or "slow" terminals and is within an unnoticeable margin of the well-known "fast" terminals. For example, Ghostty and Alacritty are usually within a few percentage points of each other on various benchmarks, but are both something like 100x faster than Terminal.app and iTerm. However, Ghostty is much more feature rich than Alacritty and has a much more native app experience.
This performance is achieved through high-level architectural decisions and low-level optimizations. At a high-level, Ghostty has a multi-threaded architecture with a dedicated read thread, write thread, and render thread per terminal. Our renderer uses OpenGL on Linux and Metal on macOS. Our read thread has a heavily optimized terminal parser that leverages CPU-specific SIMD instructions. Etc.
Rich Windowing Features
The Mac and Linux (build with GTK) apps support multi-window, tabbing, and splits with additional features such as tab renaming, coloring, etc. These features allow for a higher degree of organization and customization than single-window terminals.
Native Platform Experiences
Ghostty is a cross-platform terminal emulator but we don't aim for a least-common-denominator experience. There is a large, shared core written in Zig but we do a lot of platform-native things:
- The macOS app is a true SwiftUI-based application with all the things you would expect such as real windowing, menu bars, a settings GUI, etc.
- macOS uses a true Metal renderer with CoreText for font discovery.
- macOS supports AppleScript, Apple Shortcuts (AppIntents), etc.
- The Linux app is built with GTK.
- The Linux app integrates deeply with systemd if available for things like always-on, new windows in a single instance, cgroup isolation, etc.
Our goal with Ghostty is for users of whatever platform they run Ghostty on to think that Ghostty was built for their platform first and maybe even exclusively. We want Ghostty to feel like a native app on every platform, for the best definition of "native" on each platform.
Cross-platform libghostty for Embeddable Terminals
In addition to being a standalone terminal emulator, Ghostty is a
C-compatible library for embedding a fast, feature-rich terminal emulator
in any 3rd party project. This library is called libghostty.
Due to the scope of this project, we're breaking libghostty down into
separate libraries, starting with libghostty-vt. The goal of
this project is to focus on parsing terminal sequences and maintaining
terminal state. This is covered in more detail in this
blog post.
libghostty-vt is already available and usable today for Zig and C and
is compatible for macOS, Linux, Windows, and WebAssembly. The functionality
is extremely stable (since its been proven in Ghostty GUI for a long time),
but the API signatures are still in flux.
libghostty is already heavily in use. See examples
for small examples of using libghostty in C and Zig or the
Ghostling project for a
complete example. See awesome-libghostty
for a list of projects and resources related to libghostty.
We haven't tagged libghostty with a version yet and we're still working on a better docs experience, but our Doxygen website is a good resource for the C API.
Ghostty-only Terminal Control Sequences
We want and believe that terminal applications can and should be able to do so much more. We've worked hard to support a wide variety of modern sequences created by other terminal emulators towards this end, but we also want to fill the gaps by creating our own sequences.
We've been hesitant to do this up until now because we don't want to create more fragmentation in the terminal ecosystem by creating sequences that only work in Ghostty. But, we do want to balance that with the desire to push the terminal forward with stagnant standards and the slow pace of change in the terminal ecosystem.
We haven't done any of this yet.
Crash Reports
Ghostty has a built-in crash reporter that will generate and save crash
reports to disk. The crash reports are saved to the $XDG_STATE_HOME/ghostty/crash
directory. If $XDG_STATE_HOME is not set, the default is ~/.local/state.
Crash reports are not automatically sent anywhere off your machine.
Crash reports are only generated the next time Ghostty is started after a crash. If Ghostty crashes and you want to generate a crash report, you must restart Ghostty at least once. You should see a message in the log that a crash report was generated.
Note
Use the
ghostty +crash-reportCLI command to get a list of available crash reports. A future version of Ghostty will make the contents of the crash reports more easily viewable through the CLI and GUI.
Crash reports end in the .ghosttycrash extension. The crash reports are in
Sentry envelope format. You can
upload these to your own Sentry account to view their contents, but the format
is also publicly documented so any other available tools can also be used.
The ghostty +crash-report CLI command can be used to list any crash reports.
A future version of Ghostty will show you the contents of the crash report
directly in the terminal.
To send the crash report to the Ghostty project, you can use the following CLI command using the Sentry CLI:
SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.sentry.io/4507850923638784 sentry-cli send-envelope --raw <path to ghostty crash>
Warning
The crash report can contain sensitive information. The report doesn't purposely contain sensitive information, but it does contain the full stack memory of each thread at the time of the crash. This information is used to rebuild the stack trace but can also contain sensitive data depending on when the crash occurred.