10781 Commits

Author SHA1 Message Date
Mitchell Hashimoto
efc1ceab5d macOS: New value-based split tree implementation, move split logic out of SwiftUI into AppKit (#7523)
This is a major rework of how we represent, handle, and render splits in
the macOS app.

This new PR moves the split structure into a dedicated, generic
(non-Ghostty-specific) value-type called `SplitTree<V>`. All logic
associated with splits (new split, close split, move split, etc.) is now
handled by notifications on `BaseTerminalController`. The view hierarchy
is still SwiftUI but it has no logic associated with it anymore and
purely renders a static tree of splits.

Previously, the split hierarchy was owned by AppKit in a type called
`SplitNode` (a recursive class that contained the tree structure). All
logic around creating, zooming, etc. splits was handled by notification
listeners directly within the SwiftUI hierarchy. SwiftUI managed a
significant amount of state and we heavily used bindings, publishers,
and more. The reasoning for this is mostly historical: splits date back
to when Ghostty tried to go all-in on SwiftUI. Since then, we've taken a
more balanced approach of SwiftUI for views and AppKit for data and
business logic, and this has proven a lot more maintainable.

## Spatial Navigation

Previously, focus moving was handled by traversing the tree structure.
This led to some awkward behaviors. See:
https://github.com/ghostty-org/ghostty/issues/524#issuecomment-2668396095

In this PR, we now handle focus moving spatially. This means that move
"left" means moving to the visually left split (from the top-left
corner, a future improvement would be to do it from the cursor
position).

Concretely, given the following split structure:

```
+----------+-----+
|          |  b  |
|          |     |
|   a      +-----+
|          |     |
|          |     |
|          |     |
|          |     |
|----------|  d  |
|   c      |     |
|          |     |
+----------+-----+
```

Moving "right" from `c` now moves to `d`. Previously, it would go to
`b`. On Linux, it still goes to `b`.

## Value Types

One of the major architectural shifts is moving **purely to immutable
value types.** Whenever a split property changes such as a new split,
the ratio between splits, zoomed state, etc. we _create an entirely new
`SplitTree` value_ and replace it along the entire view hierarchy. This
is in some ways wasteful, but split hierarchies are relatively small
(even the largest I've seen in practical use are dozens of splits, which
is small for a computer). And using value types lets us get rid of a ton
of change notification soup around the SwiftUI hierarchy. We can rely on
reference counting to properly clean up our closed views.

> [!NOTE]
> 
> As an aside, I think value types are going to make it a lot easier in
the future to implement features like "undo close." We can just keep a
trailing list of surface tree states and just restore them. This PR
doesn't do anything like that, but it's now possible.

## SwiftUI Simplicity

Our SwiftUI view hierarchy is dramatically simplified. See the
difference in `TerminalSplitTreeView` (new) vs `TerminalSplit` (old).
There's so much less logic in our new views (almost none!). All of it is
in the AppKit layer which is just way nicer.

## AI Notes

This PR was heavily written by AI. I reviewed every line of code that
was rewritten, and I did manually rewrite at every step of the way in
minor ways. But it was very much written in concert. Each commit usually
started as an AI agent writing the whole commit, then nudging to get
cleaned up in the right way.

One thing I found in this task was that until the last commit, I kept
the entire previous implementation around and compiling. The agent
having access to a previous working version of code during a refactor
made the code it produced as follow up in the new architecture
significantly better, despite the new architecture having major
fundamental differences in how it works!
2025-06-05 12:59:43 -07:00
Mitchell Hashimoto
a2a3863ad2 macOS: Add option to hide window buttons (#7504)
Conversion of #7497 to a PR. This implements a feature requested in
#7331: an option to hide the default window buttons on macOS for a
cleaner aesthetic.

~~Builds on #7502 as it requires the same change to avoid the main
toolbar title showing on top of the tab bar.~~ EDIT: rebased on main now
that #7502 was merged.

I aligned the scope of the new option with `macos-titlebar-style`, since
they both customize titlebar elements. This means it has the same edge
case quirks: For example, if you change the setting, reload the config,
and then open a new tab, the appearance of the current window will
depend on which tab is in the foreground. I did it this way because
`macos-titlebar-style` provided an easy template for which derived
configs and functions to modify. Let me know if you want me to try
adjusting this so that a change in the setting also takes effect for
current windows/tabs, which I _think_ should be possible.

Screenshots:
* `macos-titlebar-style = transparent` (default)
![Screenshot 2025-06-01 at 18 04
56](https://github.com/user-attachments/assets/01fa3953-d2ef-4c39-a6e3-f236488dd841)
![Screenshot 2025-06-01 at 18 07
24](https://github.com/user-attachments/assets/cd463ded-a0b2-4f69-9abe-384e7eecaa27)
* `macos-titlebar-style = tabs`
![Screenshot 2025-06-01 at 17 56
35](https://github.com/user-attachments/assets/bf99d046-cdbb-4e5d-b1c5-d51bbba79007)
![Screenshot 2025-06-01 at 17 56
48](https://github.com/user-attachments/assets/098164b8-bf97-4df1-9dff-c1c17e12665d)
2025-06-05 07:46:57 -07:00
Mitchell Hashimoto
6db455eee5 fix: exit non-native fullscreen on close (#7525)
Fixes https://github.com/ghostty-org/ghostty/discussions/7159.
2025-06-05 07:36:50 -07:00
Mitchell Hashimoto
5edf0dffda gtk/TabView: do not closeTab within close-page signal handler (#7515)
`TabView` assumes to be the sole owner of all `Tab`s within a Window. As
such, it could close the managed `Window` once all tabs are removed from
its widget.

However, during `AdwTabView::close-page` signal triggered by libadwaita,
the `Tab` to be closed will gain an another reference for the duration
of the signal, breaking `TabView.closeTab` (called via
`Tab.closeWithConfirmation`) assumptions that having no tabs meant they
are all destroyed.

This commit solves the issue by scheduling `Tab.closeWithConfirmation`
to be run after `AdwTabView::close-page` signal has finished processing.

Fixes #7426
2025-06-05 07:36:10 -07:00
Mitchell Hashimoto
d4249679e3 macos: simplify some ServiceProvider code (#7508)
First, remove the always-inlined openTerminalFromPasteboard code and
combine it with openTerminal. Now that we're doing a bit of work inside
openTerminal, there's little better to having an intermediate, inlined
function.

Second, combine some type-casting operations (saving a .map() call).

Lastly, adjust some variable names because a generic `objs` or `urls`
was a little ambiguous now that we're all in one function scope.
2025-06-05 07:30:03 -07:00
Mitchell Hashimoto
2e0a23aa77 gtk: make requesting attention configurable (#7521)
Partially fixes #7520
2025-06-05 07:29:17 -07:00
Francisco Giordano
9008e21637 fix: exit non-native fullscreen on close 2025-06-05 07:25:53 -07:00
Mitchell Hashimoto
c40ac6b785 input: add focus split directional commands to command palette 2025-06-05 07:11:18 -07:00
Mitchell Hashimoto
1966dfdef7 macos: moving some files around 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
f8e3539b7d macos: remove the unused resizeEvent code from SplitView 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
01fa87f2ab macos: fix iOS builds 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
9474092f77 macos: remove the old split implementation 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
69c3c359cb macos: resize split keybind handling 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
5299f10e13 macos: unzoom on new split and focus change 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
19a9156ae1 macos: address remaining todos 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
6c97e4a59a macos: fix focus after closing splits 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
77458ef308 macos: rename surfaceTree2 to surfaceTree 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
f1ed07caf4 macos: Remove the legacy SurfaceTree 2025-06-05 07:05:13 -07:00
Mitchell Hashimoto
22819f8a29 macos: transfer doesBorderTop 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
8b979d6dce macos: handle surfaceTreeDidChange 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
ea1ff438f8 macos: handle split zooming 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
b7c01b5b4a macos: spatial focus navigation 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
ec7fd94d0f macos: equalize splits works with new tree 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
a389926ca7 macos: use surfaceTree2 needsConfirmQuit 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
aef61661a0 macos: fix up command palette, focusing 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
7dcfebcd5d macos: isSplit guarding on focus split directions works 2025-06-05 07:05:12 -07:00
Mitchell Hashimoto
0fb58298a7 macos: focus split previous/next 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
b84b715ddb macos: unify confirm close in our terminal controllers 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
d1dce1e372 macos: restoration for new split tree 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
33d94521ea macos: setup sequence for SplitTree 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
672d276276 macos: confirm close on split close 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
e3bc3422dc macos: handle split resizing 2025-06-05 07:05:11 -07:00
Mitchell Hashimoto
1707159441 new SplitTree 2025-06-05 07:05:11 -07:00
Leah Amelia Chen
77479feee6 gtk: make requesting attention configurable 2025-06-05 00:29:49 +02:00
Mitchell Hashimoto
722629f9fa build(deps): bump namespacelabs/nscloud-cache-action from 1.2.7 to 1.2.8 (#7517)
Bumps
[namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action)
from 1.2.7 to 1.2.8.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="449c929cd5"><code>449c929</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/20">#20</a>
from namespacelabs/niklas-timeout</li>
<li><a
href="a63596ed5b"><code>a63596e</code></a>
Wrap action with a timeout.</li>
<li>See full diff in <a
href="https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.7...v1.2.8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=namespacelabs/nscloud-cache-action&package-manager=github_actions&previous-version=1.2.7&new-version=1.2.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-06-04 09:07:32 -07:00
Mitchell Hashimoto
f383d7b550 core: document keybind actions better (#7522)
The current documentation for actions are very sparse and would leave
someone (even including contributors) as to what exactly they do. On top
of that there are many stylistic and grammatical problems that are
simply no longer in line with our current standards, and certainly not
on par with our configuration options reference.

Hence, I've taken it upon myself to add, clarify, supplement, edit and
even rewrite the documentation for most of these actions, in a wider
effort of trying to offer better, clearer documentation for our users.
2025-06-04 09:04:16 -07:00
Leah Amelia Chen
2c8d6ba944 core: document keybind actions better
The current documentation for actions are very sparse and would leave
someone (even including contributors) as to what exactly they do.
On top of that there are many stylistic and grammatical problems that are
simply no longer in line with our current standards, and certainly not
on par with our configuration options reference.

Hence, I've taken it upon myself to add, clarify, supplement, edit and
even rewrite the documentation for most of these actions, in a wider
effort of trying to offer better, clearer documentation for our users.
2025-06-04 17:04:52 +02:00
dependabot[bot]
037d4732a6 build(deps): bump namespacelabs/nscloud-cache-action from 1.2.7 to 1.2.8
Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.7 to 1.2.8.
- [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases)
- [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.7...v1.2.8)

---
updated-dependencies:
- dependency-name: namespacelabs/nscloud-cache-action
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-04 00:46:01 +00:00
Leorize
4e39144d39 gtk/TabView: do not closeTab within close-page signal handler
`TabView` assumes to be the sole owner of all `Tab`s within a Window.
As such, it could close the managed `Window` once all tabs are removed
from its widget.

However, during `AdwTabView::close-page` signal triggered by libadwaita,
the `Tab` to be closed will gain an another reference for the duration
of the signal, breaking `TabView.closeTab` (called via
`Tab.closeWithConfirmation`) assumptions that having no tabs meant they
are all destroyed.

This commit solves the issue by scheduling `Tab.closeWithConfirmation`
to be run after `AdwTabView::close-page` signal has finished processing.
2025-06-03 03:19:13 -05:00
Leah Amelia Chen
108fab11a5 gtk/GlobalShortcuts: don't request session with no shortcuts (#7510) 2025-06-03 09:22:20 +02:00
Mitchell Hashimoto
d993588263 flatpak: rename .Devel variant to .ghostty-debug (#7511)
This is done to match against the default application id when Ghostty is
built using debug configuration, preparing the Flatpak version for D-Bus
activation support (#7433).
2025-06-02 20:09:16 -07:00
Leorize
1183ac8972 flatpak: rename .Devel variant to .ghostty-debug
This is done to match against the default application id when Ghostty is
built using debug configuration, done to prepare the Flatpak version for
D-Bus activation support.
2025-06-02 21:11:58 -05:00
Leorize
58cece07f0 gtk/GlobalShortcuts: don't request session with no shortcuts
There aren't any reason to pay the D-Bus tax if you don't use global
shortcuts.
2025-06-02 20:55:04 -05:00
Jon Parise
652f551bec macos: simplify some ServiceProvider code
First, remove the always-inlined openTerminalFromPasteboard code and
combine it with openTerminal. Now that we're doing a bit of work inside
openTerminal, there's little better to having an intermediate, inlined
function.

Second, combine some type-casting operations (saving a .map() call).

Lastly, adjust some variable names because a generic `objs` or `urls`
was a little ambiguous now that we're all in one function scope.
2025-06-02 20:11:18 -04:00
Mitchell Hashimoto
aa6c349545 macos: fix small memory leak in surface tree when closing splits (#7507)
This fixes a small memory leak I found where the `SplitNode.Leaf` was
not being deinitialized properly when closing a split. It would get
deinitialized the next time a split was made or the window was closed,
so the leak wasn't big. The surface view underneath the split was also
properly deinitialized because we forced it, so again, the leak was
quite small.

But conceptually this is a big problem, because when we change the
surface tree we expect the deinit chain to propagate properly through
the whole thing, _including_ to the SurfaceView.

This fixes that by removing the `id(node)` call. I don't find this to be
necessary anymore. I don't know when that happened but we've changed
quite a lot in our split system since it was introduced. I'm also not
100% sure why the `id(node)` was causing a strong reference to begin
with... which bothers me a bit.

AI note: While I manually hunted this down, I started up Claude Code and
Codex in separate tabs to also hunt for the memory leak. They both
failed to find it and offered solutions that didn't work.
2025-06-02 14:50:02 -07:00
Mitchell Hashimoto
d1f1be8833 macos: fix small memory leak in surface tree when closing splits
This fixes a small memory leak I found where the `SplitNode.Leaf` was
not being deinitialized properly when closing a split. It would get
deinitialized the next time a split was made or the window was closed,
so the leak wasn't big. The surface view underneath the split was also
properly deinitialized because we forced it, so again, the leak was
quite small.

But conceptually this is a big problem, because when we change the
surface tree we expect the deinit chain to propagate properly through
the whole thing, _including_ to the SurfaceView.

This fixes that by removing the `id(node)` call. I don't find this to be
necessary anymore. I don't know when that happened but we've changed
quite a lot in our split system since it was introduced. I'm also not
100% sure why the `id(node)` was causing a strong reference to begin
with... which bothers me a bit.

AI note: While I manually hunted this down, I started up Claude Code and
Codex in separate tabs to also hunt for the memory leak. They both
failed to find it and offered solutions that didn't work.
2025-06-02 14:12:26 -07:00
Mitchell Hashimoto
957ddd00dd Follow-up to #7462: var -> let (#7505)
Just a quick follow-up to #7462: I noticed Swift was admonishing me
about a `var` that could be a `let`, and realized that it was I who had
failed to make that change. Apologies for the noise.
2025-06-02 12:16:41 -07:00
Daniel Wennberg
5244f8d6ac Follow-up to #7462: var -> let 2025-06-02 10:14:52 -07:00
Daniel Wennberg
232a46d2dc Add option to hide macOS traffic lights 2025-06-02 09:22:01 -07:00
Mitchell Hashimoto
3638916819 Enable reset zoom button when macos-titlebar-style = tabs and only one tab (#7502)
This is the conversion of #7496 into a PR. The problem was discussed in
#3288: When `macos-titlebar-style = tabs`, the reset zoom button doesn't
appear with only a single tab, i.e., no tab bar.

This fix was simply to remove an unnecessary branch, as the titlebar
style doesn't make any difference for the required logic. If the tab bar
is visible, you should unconditionally hide the titlebar's button and
rely on the per-tab buttons. If the tab bar is not visible, you should
hide or show the titlebar button depending on `surfaceIsZoomed`.

One wrinkle was that the main toolbar title started showing on top of
the tabs. Turns out it was previously not showing only because it would
always be pushed into the hidden overflow menu. With the reset zoom
button present (either hidden or visible, but always there), this no
longer happens reliably, presumably due to some cascading change in the
overflow calculations. In any case, it seems brittle to rely on this way
of concealing the main title, so I added a few lines of code to properly
hide it when the tab bar is visible.

Screenshots:
![Screenshot 2025-06-01 at 15 42
37](https://github.com/user-attachments/assets/893ba88e-a7e0-4d2f-8d08-45af4a2ba11b)
![Screenshot 2025-06-01 at 15 42
51](https://github.com/user-attachments/assets/f45db556-15a9-4655-96df-c41a34a80ec2)
![Screenshot 2025-06-01 at 15 42
58](https://github.com/user-attachments/assets/645cfe42-014c-4259-afd4-489b8ccdb311)
2025-06-02 09:18:09 -07:00