From ca700b2d7b8d9e9e9ed46df2e1bb49a13b2fe606 Mon Sep 17 00:00:00 2001 From: phush0 Date: Wed, 18 Feb 2026 16:27:03 +0200 Subject: [PATCH 01/51] additional strings for 3.0 --- po/bg_BG.UTF-8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 3f7d55913..ac8bf362f 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -20,7 +20,7 @@ msgstr "" #: dist/linux/ghostty_nautilus.py:53 msgid "Open in Ghostty" -msgstr "" +msgstr "Отвори в Гоусти" #: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 #: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 @@ -179,7 +179,7 @@ msgstr "Раздел" #: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 #: src/apprt/gtk/ui/1.5/window.blp:320 msgid "Change Tab Title…" -msgstr "" +msgstr "Смени името на таба…" #: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 #: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 @@ -336,7 +336,7 @@ msgstr "Промяна на заглавието на терминала" #: src/apprt/gtk/class/title_dialog.zig:226 msgid "Change Tab Title" -msgstr "" +msgstr "Смени името на таба" #: src/apprt/gtk/class/window.zig:1007 msgid "Reloaded the configuration" From d1cb225895f6f3b195a5477d41d89b69216d5fc6 Mon Sep 17 00:00:00 2001 From: phush0 Date: Wed, 18 Feb 2026 17:23:51 +0200 Subject: [PATCH 02/51] Fix translation for 'Open in Ghostty' --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index ac8bf362f..ac4e8c24c 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -20,7 +20,7 @@ msgstr "" #: dist/linux/ghostty_nautilus.py:53 msgid "Open in Ghostty" -msgstr "Отвори в Гоусти" +msgstr "Отвори в Ghostty" #: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 #: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 From 16b320ca8aafc81bd977a2c74d2cffa46340e637 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:14:31 +0000 Subject: [PATCH 03/51] build(deps): bump namespacelabs/nscloud-cache-action from 1.4.1 to 1.4.2 Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.4.1 to 1.4.2. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/4d61c33d0b4333a518e975a0c4de7633d28713bb...a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.4.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/snap.yml | 2 +- .github/workflows/test.yml | 48 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index f12ba2211..1897fa4a1 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -41,7 +41,7 @@ jobs: - name: Checkout code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index a24e5a389..d9f73197d 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 2227ae09c..3eb4296f7 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -165,7 +165,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/snap.yml b/.github/workflows/snap.yml index 68ec9cacd..b16f5b209 100644 --- a/.github/workflows/snap.yml +++ b/.github/workflows/snap.yml @@ -38,7 +38,7 @@ jobs: tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e07ffe859..4ca34cefd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,7 +78,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -121,7 +121,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -154,7 +154,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -188,7 +188,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -232,7 +232,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -296,7 +296,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -325,7 +325,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -358,7 +358,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -404,7 +404,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -633,7 +633,7 @@ jobs: echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -675,7 +675,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -723,7 +723,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -758,7 +758,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -822,7 +822,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -851,7 +851,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -881,7 +881,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -910,7 +910,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -937,7 +937,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -964,7 +964,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -991,7 +991,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -1023,7 +1023,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -1050,7 +1050,7 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -1085,7 +1085,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix @@ -1147,7 +1147,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 026b1e9df..ac92c0608 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1 + uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2 with: path: | /nix From 9f2d66c9c9a90d15b6d7ad01af5eef25d4cfd539 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 18 Feb 2026 21:32:16 -0600 Subject: [PATCH 04/51] toggle command palette works on gtk --- src/apprt/action.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 1d9ef633c..06634856e 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -109,7 +109,7 @@ pub const Action = union(Key) { /// Toggle the quick terminal in or out. toggle_quick_terminal, - /// Toggle the command palette. This currently only works on macOS. + /// Toggle the command palette. toggle_command_palette, /// Toggle the visibility of all Ghostty terminal windows. From 2ac3c1f1da69a7b3474fb1adcc8c2280001cf23d Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:38:07 +0000 Subject: [PATCH 05/51] Update VOUCHED list (#10862) Triggered by [comment](https://github.com/ghostty-org/ghostty/issues/10861#issuecomment-3928416623) from @trag1c. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 36b38ccc1..3c67b603f 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -18,6 +18,7 @@ # Maintainers can vouch for new contributors by commenting "!vouch" on a # discussion by the author. Maintainers can denounce users by commenting # "!denounce" or "!denounce [username]" on a discussion. +alanmoyano bennettp123 bernsno bitigchi From 21ea94610abedabb37bbcbbd82429e5d2a274847 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 10:55:43 -0500 Subject: [PATCH 06/51] macos: lint Swift files using SwiftLint SwiftLint is both a linter and formatting. It's a popular way to spot issues and enforce a consistent style. Our SwiftLint configuration lives in macos/.swiftlint.yml, where is is automatically discovered. It's very configurable, and I made an initial pass as some basic, weakly-opinionated rules. The "TODO" section lists rules that currently have violations but can be easily (auto)fixed in follow-up commits. Our integration is CLI-based. Similar to our other support tools, we expect developers to install `swiftlint` via nix or e.g. Homebrew. This is documented in HACKING.md. We also have an optional Xcode integration, for in-editor feedback. When `swiftlint` is available, it's run as a script-based Build Phase. SwiftLint supports an auto-fix mode (--fix). Agents are aware of this via AGENTS.md. The rules are enforced using a (nix-based) CI job. --- .github/workflows/test.yml | 22 +++++++++ AGENTS.md | 1 + HACKING.md | 25 ++++++++++ macos/.swiftlint.yml | 64 +++++++++++++++++++++++++ macos/Ghostty.xcodeproj/project.pbxproj | 24 ++++++++++ nix/devShell.nix | 4 ++ 6 files changed, 140 insertions(+) create mode 100644 macos/.swiftlint.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ca34cefd..bcc028dd4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,7 @@ jobs: - test-macos - pinact - prettier + - swiftlint - alejandra - typos - shellcheck @@ -927,6 +928,27 @@ jobs: - name: prettier check run: nix develop -c prettier --check . + swiftlint: + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-macos-tahoe + timeout-minutes: 60 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + skipPush: true + useDaemon: false # sometimes fails on short jobs + + - name: swiftlint check + run: nix develop -c swiftlint lint --strict macos + alejandra: if: github.repository == 'ghostty-org/ghostty' runs-on: namespace-profile-ghostty-xsm diff --git a/AGENTS.md b/AGENTS.md index 04d3570a7..949bf588e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ A file for [guiding coding agents](https://agents.md/). - **Test (Zig):** `zig build test` - **Test filter (Zig)**: `zig build test -Dtest-filter=` - **Formatting (Zig)**: `zig fmt .` +- **Formatting (Swift)**: `swiftlint lint --fix macos` - **Formatting (other)**: `prettier -w .` ## Directory Structure diff --git a/HACKING.md b/HACKING.md index 921ed71ff..23657cea5 100644 --- a/HACKING.md +++ b/HACKING.md @@ -186,6 +186,31 @@ shellcheck \ $(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort) ``` +### SwiftLint + +Swift code is linted using [SwiftLint](https://github.com/realm/SwiftLint). A +SwiftLint CI check will fail builds with improper formatting. Therefore, if you +are modifying Swift code, you may want to install it locally and run this from +the repo root before you commit: + +``` +swiftlint lint --fix macos +``` + +Make sure your SwiftLint version matches the version in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). + +Nix users can use the following command to format with SwiftLint: + +``` +nix develop -c swiftlint lint --fix macos +``` + +To check for violations without auto-fixing: + +``` +nix develop -c swiftlint lint --strict macos +``` + ### Updating the Zig Cache Fixed-Output Derivation Hash The Nix package depends on a [fixed-output diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml new file mode 100644 index 000000000..e9767a43f --- /dev/null +++ b/macos/.swiftlint.yml @@ -0,0 +1,64 @@ +# SwiftLint +# +check_for_updates: false + +disabled_rules: + - cyclomatic_complexity + - file_length + - function_body_length + - nesting + - todo + - trailing_comma + - trailing_newline + - type_body_length + + # TODO + - colon + - comma + - comment_spacing + - control_statement + - deployment_target + - empty_enum_arguments + - empty_parentheses_with_trailing_closure + - for_where + - force_cast + - implicit_getter + - implicit_optional_initialization + - legacy_constant + - legacy_constructor + - line_length + - mark + - multiple_closures_with_trailing_closure + - no_fallthrough_only + - opening_brace + - orphaned_doc_comment + - private_over_fileprivate + - shorthand_operator + - switch_case_alignment + - syntactic_sugar + - trailing_semicolon + - trailing_whitespace + - unneeded_synthesized_initializer + - unused_closure_parameter + - unused_enumerated + - unused_optional_binding + - vertical_parameter_alignment + - vertical_whitespace + +identifier_name: + min_length: 1 + allowed_symbols: ["_"] + excluded: + - Core.* + +type_name: + min_length: 2 + allowed_symbols: ["_"] + excluded: + - iOS_.* + +function_parameter_count: + warning: 6 + +large_tuple: + warning: 3 diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index ab6dde118..e69331367 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -355,6 +355,7 @@ isa = PBXNativeTarget; buildConfigurationList = A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */; buildPhases = ( + FC501E0B2F46B410007AE49D /* Run SwiftLint */, A5B3052D299BEAAA0047F10C /* Sources */, A5B3052E299BEAAA0047F10C /* Frameworks */, A5B3052F299BEAAA0047F10C /* Resources */, @@ -490,6 +491,29 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + FC501E0B2F46B410007AE49D /* Run SwiftLint */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Run SwiftLint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "[[ -z \"$GITHUB_ACTIONS\" ]] || exit 0;\n\nSWIFTLINT=\"\"\nif command -v swiftlint >/dev/null 2>&1; then\n SWIFTLINT=\"$(command -v swiftlint)\"\nelif [[ -f \"/opt/homebrew/bin/swiftlint\" ]]; then\n SWIFTLINT=\"/opt/homebrew/bin/swiftlint\"\nfi\n\nif [[ -n \"$SWIFTLINT\" ]]; then\n \"$SWIFTLINT\" lint --quiet \"$SRCROOT\"\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 810ACC9B2E9D3301004F8F92 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/nix/devShell.nix b/nix/devShell.nix index 90059a730..c78c9081b 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -66,6 +66,7 @@ poop, typos, shellcheck, + swiftlint, uv, wayland, wayland-scanner, @@ -198,6 +199,9 @@ in # for benchmarking poop + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + swiftlint ]; # This should be set onto the rpath of the ghostty binary if you want From a5dd7a750b2986da303f60fa4a4e4d24516b884d Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 11:26:45 -0500 Subject: [PATCH 07/51] ci: only run 'swiftlint' when macos/ changes This saves us some work on the majority of our commits. I think we'd only miss commits to .github/ and the nix environment with this filter, but we can expand the filter's scope as needed. --- .github/workflows/test.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bcc028dd4..4d7b1292b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,10 +11,25 @@ concurrency: cancel-in-progress: true jobs: + paths: + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-xsm + outputs: + macos: ${{ steps.filter.outputs.macos }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + macos: + - 'macos/**' + required: name: "Required Checks: Test" runs-on: namespace-profile-ghostty-xsm needs: + - paths - build-bench - build-dist - build-examples @@ -929,8 +944,9 @@ jobs: run: nix develop -c prettier --check . swiftlint: - if: github.repository == 'ghostty-org/ghostty' + if: github.repository == 'ghostty-org/ghostty' && needs.paths.outputs.macos == 'true' runs-on: namespace-profile-ghostty-macos-tahoe + needs: paths timeout-minutes: 60 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From c5488afc75a9f9bd2e4c6b718e7828925b276f4a Mon Sep 17 00:00:00 2001 From: Ben Kircher Date: Mon, 16 Feb 2026 07:05:12 +0100 Subject: [PATCH 08/51] url: improve space in path handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The space-segment patterns in the path regex (dotted_path_space_segments and any_path_space_segments) greedily consume text after a space even when we know that the text is the start of a new independent path (e.g., `/tmp/bar` in `/tmp/foo /tmp/bar`). Fix: Add two negative lookaheads after the space in both patterns: - `(?!\.{0,2}\/)` → don't match if the next segment starts with `/`, `./`, or `../` - `(?!~\/)` → don't match if the next segment starts with `~/` --- src/config/url.zig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/config/url.zig b/src/config/url.zig index da0892a91..359724501 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -65,11 +65,11 @@ const non_dotted_path_lookahead = ; const dotted_path_space_segments = - \\(?:(? Date: Thu, 19 Feb 2026 21:01:24 +0100 Subject: [PATCH 09/51] url: fix regression with unified diff lines Bare relative paths don't need space-continuation semantics. Fixes #10773 --- src/config/url.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/url.zig b/src/config/url.zig index 359724501..e7cf8603c 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -110,7 +110,6 @@ const bare_relative_path_branch = dotted_path_lookahead ++ bare_relative_path_prefix ++ path_chars ++ "+" ++ - dotted_path_space_segments ++ no_trailing_colon ++ trailing_spaces_at_eol; @@ -363,6 +362,11 @@ test "url regex" { .input = "/tmp/test folder/file.txt", .expect = "/tmp/test", }, + // unified diff lines + .{ + .input = "diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig", + .expect = "a/src/font/shaper/harfbuzz.zig", + }, // Two space-separated absolute paths should match only the first .{ .input = "/tmp/foo /tmp/bar", From 7928883f73dfc432ce3723305774f594ca75a80e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 10:24:26 -0800 Subject: [PATCH 10/51] ci: add sync-codeowners action so that codeowners auto-vouch --- .github/workflows/vouch-check-issue.yml | 2 +- .github/workflows/vouch-check-pr.yml | 2 +- .../workflows/vouch-manage-by-discussion.yml | 2 +- .github/workflows/vouch-manage-by-issue.yml | 2 +- .github/workflows/vouch-sync-codeowners.yml | 32 +++++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/vouch-sync-codeowners.yml diff --git a/.github/workflows/vouch-check-issue.yml b/.github/workflows/vouch-check-issue.yml index e0933aaf1..60c56fe8f 100644 --- a/.github/workflows/vouch-check-issue.yml +++ b/.github/workflows/vouch-check-issue.yml @@ -14,7 +14,7 @@ jobs: app-id: ${{ secrets.VOUCH_APP_ID }} private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} - - uses: mitchellh/vouch/action/check-issue@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/check-issue@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0 with: issue-number: ${{ github.event.issue.number }} auto-close: true diff --git a/.github/workflows/vouch-check-pr.yml b/.github/workflows/vouch-check-pr.yml index eb5a7e6fb..aaf9176b3 100644 --- a/.github/workflows/vouch-check-pr.yml +++ b/.github/workflows/vouch-check-pr.yml @@ -14,7 +14,7 @@ jobs: app-id: ${{ secrets.VOUCH_APP_ID }} private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} - - uses: mitchellh/vouch/action/check-pr@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/check-pr@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0 with: pr-number: ${{ github.event.pull_request.number }} auto-close: true diff --git a/.github/workflows/vouch-manage-by-discussion.yml b/.github/workflows/vouch-manage-by-discussion.yml index 50e2a23f3..93e7a1343 100644 --- a/.github/workflows/vouch-manage-by-discussion.yml +++ b/.github/workflows/vouch-manage-by-discussion.yml @@ -22,7 +22,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - - uses: mitchellh/vouch/action/manage-by-discussion@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/manage-by-discussion@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0 with: discussion-number: ${{ github.event.discussion.number }} comment-node-id: ${{ github.event.comment.node_id }} diff --git a/.github/workflows/vouch-manage-by-issue.yml b/.github/workflows/vouch-manage-by-issue.yml index f00270a0d..acea8f4fd 100644 --- a/.github/workflows/vouch-manage-by-issue.yml +++ b/.github/workflows/vouch-manage-by-issue.yml @@ -22,7 +22,7 @@ jobs: with: token: ${{ steps.app-token.outputs.token }} - - uses: mitchellh/vouch/action/manage-by-issue@6803dde571265e13489c3f118200f60b6ab59e4d # v1.3.1 + - uses: mitchellh/vouch/action/manage-by-issue@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0 with: repo: ${{ github.repository }} issue-id: ${{ github.event.issue.number }} diff --git a/.github/workflows/vouch-sync-codeowners.yml b/.github/workflows/vouch-sync-codeowners.yml new file mode 100644 index 000000000..fe1977a66 --- /dev/null +++ b/.github/workflows/vouch-sync-codeowners.yml @@ -0,0 +1,32 @@ +on: + schedule: + - cron: "0 0 * * 1" # Every Monday at midnight UTC + workflow_dispatch: + +name: "Vouch - Sync CODEOWNERS" + +concurrency: + group: vouch-manage + cancel-in-progress: false + +jobs: + sync: + runs-on: namespace-profile-ghostty-xsm + steps: + - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + id: app-token + with: + app-id: ${{ secrets.VOUCH_APP_ID }} + private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + token: ${{ steps.app-token.outputs.token }} + + - uses: mitchellh/vouch/action/sync-codeowners@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0 + with: + repo: ${{ github.repository }} + pull-request: "true" + merge-immediately: "true" + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From df7cd0fb37714f8bd13310635c39c829ff983079 Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:28:47 +0000 Subject: [PATCH 11/51] Sync CODEOWNERS vouch list (#10872) Sync CODEOWNERS owners with vouch list. ## Added Users - @00-kat - @abudvytis - @aindriu80 - @andrejdaskalov - @balazs-szucs - @Beryesa - @bo2themax - @charliie-dev - @CraziestOwl - @d-Dudas - @damyanbogoev - @danulqua - @flou - @francescarpi - @gagbo - @ghokun - @gmile - @gordonbondon - @gpanders - @guilhermetk - @halosatrio - @johnslavik - @jparise - @kenvandine - @Kirwiisp - @kjvdven - @kloneets - @kristina8888 - @liby - @lonsagisawa - @marijagjorgjieva - @MatkoTiric - @Misairuzame - @mtak - @OshDubh - @piedrahitac - @reo101 - @RMEngelbrecht - @rockorager - @rpfaeffle - @silveirapf - @tdslot - @TicClick - @tnagatomi - @trag1c - @tristan957 - @uhojin - @zenyr - @zeshi09 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 49 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 3c67b603f..8f0d5b380 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -18,46 +18,95 @@ # Maintainers can vouch for new contributors by commenting "!vouch" on a # discussion by the author. Maintainers can denounce users by commenting # "!denounce" or "!denounce [username]" on a discussion. +00-kat +abudvytis +aindriu80 alanmoyano +andrejdaskalov +balazs-szucs bennettp123 bernsno +beryesa bitigchi bkircher +bo2themax +charliie-dev chernetskyi +craziestowl +d-dudas daiimus +damyanbogoev +danulqua doprz elias8 ephemera eriksremess filip7 +flou +francescarpi +gagbo +ghokun +gmile +gordonbondon +gpanders +guilhermetk hakonhagland +halosatrio hqnna jake-stewart jcollie +johnslavik +jparise juniqlim kawarimidoll +kenvandine khipp +kirwiisp +kjvdven +kloneets +kristina8888 kristofersoler +liby +lonsagisawa mahnokropotkinvich +marijagjorgjieva marrocco-simone +matkotiric miguelelgallo mikailmm +misairuzame mitchellh +mtak nwehg +oshdubh pan93412 pangoraw peilingjiang peterdavehello phush0 +piedrahitac pluiedev pouwerkerk priyans-hu prsweet qwerasd205 +reo101 +rmengelbrecht rmunn +rockorager +rpfaeffle secrus +silveirapf slsrepo +tdslot +ticclick +tnagatomi +trag1c +tristan957 tweedbeetle +uhojin uzaaft vlsi yamshta +zenyr +zeshi09 From 81f537e9927c94c84c5356424d6baabfabaee1b6 Mon Sep 17 00:00:00 2001 From: benodiwal Date: Wed, 17 Dec 2025 16:45:18 +0530 Subject: [PATCH 12/51] feat: implement scroll-to-bottom on output option Implements the `output` option for the `scroll-to-bottom` configuration, which scrolls the viewport to the bottom when new lines are printed. Co-Authored-By: Sachin --- src/config/Config.zig | 2 +- src/termio/Termio.zig | 3 +++ src/termio/stream_handler.zig | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index bb86b6bd5..d409fb6fa 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -906,7 +906,7 @@ palette: Palette = .{}, /// anything but modifiers or keybinds that are processed by Ghostty). /// /// - `output` If set, scroll the surface to the bottom if there is new data -/// to display. (Currently unimplemented.) +/// to display (e.g., when new lines are printed to the terminal). /// /// The default is `keystroke, no-output`. @"scroll-to-bottom": ScrollToBottom = .default, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index dee58dc22..a5b3cbed3 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -166,6 +166,7 @@ pub const DerivedConfig = struct { clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, conditional_state: configpkg.ConditionalState, + scroll_to_bottom: configpkg.Config.ScrollToBottom, pub fn init( alloc_gpa: Allocator, @@ -207,6 +208,7 @@ pub const DerivedConfig = struct { .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), .conditional_state = config._conditional_state, + .scroll_to_bottom = config.@"scroll-to-bottom", // This has to be last so that we copy AFTER the arena allocations // above happen (Zig assigns in order). @@ -301,6 +303,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .enquiry_response = opts.config.enquiry_response, .default_cursor_style = opts.config.cursor_style, .default_cursor_blink = opts.config.cursor_blink, + .scroll_to_bottom_on_output = opts.config.scroll_to_bottom.output, }; const thread_enter_state = try ThreadEnterState.create( diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index bc3edd185..76de83c08 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -56,6 +56,9 @@ pub const StreamHandler = struct { /// The clipboard write access configuration. clipboard_write: configpkg.ClipboardAccess, + /// The scroll-to-bottom behavior configuration. + scroll_to_bottom_on_output: bool, + //--------------------------------------------------------------- // Internal state @@ -112,6 +115,7 @@ pub const StreamHandler = struct { self.enquiry_response = config.enquiry_response; self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; + self.scroll_to_bottom_on_output = config.scroll_to_bottom.output; // If our cursor is the default, then we update it immediately. if (self.default_cursor) self.setCursorStyle(.default) catch |err| { @@ -576,6 +580,11 @@ pub const StreamHandler = struct { // Small optimization: call index instead of linefeed because they're // identical and this avoids one layer of function call overhead. try self.terminal.index(); + + // If configured, scroll to bottom on output so the user sees new content + if (self.scroll_to_bottom_on_output) { + try self.terminal.scrollViewport(.bottom); + } } pub inline fn reverseIndex(self: *StreamHandler) !void { From e197b95c32f12abe97fbc1b0ca1f0514367ef702 Mon Sep 17 00:00:00 2001 From: benodiwal Date: Thu, 18 Dec 2025 05:44:46 +0530 Subject: [PATCH 13/51] feat: scroll-to-bottom on output via renderer pin tracking Co-Authored-By: Sachin --- src/Surface.zig | 8 ++++++++ src/apprt/surface.zig | 4 ++++ src/renderer/generic.zig | 29 +++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 588d52968..fea5b2a87 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1070,6 +1070,14 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), + .scroll_to_bottom => { + self.queueIo(.{ + .scroll_viewport = .{ .bottom = {} }, + }, .unlocked); + }, + + .report_color_scheme => |force| self.reportColorScheme(force), + .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 5c25281c8..159e8db16 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -108,6 +108,10 @@ pub const Message = union(enum) { /// Selected search index change search_selected: ?usize, + /// Scroll the viewport to the bottom. This is triggered by the renderer + /// when new output is detected and scroll-to-bottom on output is enabled. + scroll_to_bottom, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 19c7b3375..4d986336b 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -125,6 +125,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type { scrollbar: terminal.Scrollbar, scrollbar_dirty: bool, + /// Tracks the last bottom-right pin of the screen to detect new output. + /// When the final line changes (node or y differs), new content was added. + /// Used for scroll-to-bottom on output feature. + last_bottom_node: ?*terminal.PageList.List.Node, + last_bottom_y: terminal.size.CellCountInt, + /// The most recent viewport matches so that we can render search /// matches in the visible frame. This is provided asynchronously /// from the search thread so we have the dirty flag to also note @@ -563,6 +569,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { colorspace: configpkg.Config.WindowColorspace, blending: configpkg.Config.AlphaBlending, background_blur: configpkg.Config.BackgroundBlur, + scroll_to_bottom_on_output: bool, pub fn init( alloc_gpa: Allocator, @@ -636,6 +643,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .colorspace = config.@"window-colorspace", .blending = config.@"alpha-blending", .background_blur = config.@"background-blur", + .scroll_to_bottom_on_output = config.@"scroll-to-bottom".output, .arena = arena, }; } @@ -699,6 +707,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .focused = true, .scrollbar = .zero, .scrollbar_dirty = false, + .last_bottom_node = null, + .last_bottom_y = 0, .search_matches = null, .search_selected_match = null, .search_matches_dirty = false, @@ -1182,6 +1192,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // cross-thread mailbox message within the IO path. const scrollbar = state.terminal.screens.active.pages.scrollbar(); + // If scroll-to-bottom on output is enabled, check if the final line + // changed by comparing the bottom-right pin. If the node pointer or + // y offset changed, new content was added to the screen. + if (self.config.scroll_to_bottom_on_output) { + const bottom_right = state.terminal.screens.active.pages.getBottomRight(.screen); + if (bottom_right) |br| { + const pin_changed = (self.last_bottom_node != br.node) or + (self.last_bottom_y != br.y); + + if (pin_changed and !state.terminal.screens.active.viewportIsBottom()) { + _ = self.surface_mailbox.push(.scroll_to_bottom, .instant); + } + + // Update tracked pin state for next frame + self.last_bottom_node = br.node; + self.last_bottom_y = br.y; + } + } + // Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { const p = state.preedit orelse break :preedit null; From 263469755c56fa79474be2a56f2c85478cb1e25e Mon Sep 17 00:00:00 2001 From: benodiwal Date: Thu, 18 Dec 2025 05:50:47 +0530 Subject: [PATCH 14/51] refactor: remove unused stream handler scroll-to-bottom code The renderer approach doesn't need termio changes. Co-Authored-By: Sachin --- src/Surface.zig | 2 -- src/termio/Termio.zig | 3 --- src/termio/stream_handler.zig | 9 --------- 3 files changed, 14 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index fea5b2a87..6e01daba2 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1076,8 +1076,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .unlocked); }, - .report_color_scheme => |force| self.reportColorScheme(force), - .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index a5b3cbed3..dee58dc22 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -166,7 +166,6 @@ pub const DerivedConfig = struct { clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, conditional_state: configpkg.ConditionalState, - scroll_to_bottom: configpkg.Config.ScrollToBottom, pub fn init( alloc_gpa: Allocator, @@ -208,7 +207,6 @@ pub const DerivedConfig = struct { .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), .conditional_state = config._conditional_state, - .scroll_to_bottom = config.@"scroll-to-bottom", // This has to be last so that we copy AFTER the arena allocations // above happen (Zig assigns in order). @@ -303,7 +301,6 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .enquiry_response = opts.config.enquiry_response, .default_cursor_style = opts.config.cursor_style, .default_cursor_blink = opts.config.cursor_blink, - .scroll_to_bottom_on_output = opts.config.scroll_to_bottom.output, }; const thread_enter_state = try ThreadEnterState.create( diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 76de83c08..bc3edd185 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -56,9 +56,6 @@ pub const StreamHandler = struct { /// The clipboard write access configuration. clipboard_write: configpkg.ClipboardAccess, - /// The scroll-to-bottom behavior configuration. - scroll_to_bottom_on_output: bool, - //--------------------------------------------------------------- // Internal state @@ -115,7 +112,6 @@ pub const StreamHandler = struct { self.enquiry_response = config.enquiry_response; self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; - self.scroll_to_bottom_on_output = config.scroll_to_bottom.output; // If our cursor is the default, then we update it immediately. if (self.default_cursor) self.setCursorStyle(.default) catch |err| { @@ -580,11 +576,6 @@ pub const StreamHandler = struct { // Small optimization: call index instead of linefeed because they're // identical and this avoids one layer of function call overhead. try self.terminal.index(); - - // If configured, scroll to bottom on output so the user sees new content - if (self.scroll_to_bottom_on_output) { - try self.terminal.scrollViewport(.bottom); - } } pub inline fn reverseIndex(self: *StreamHandler) !void { From eb335fb8ddc8cc088c2c96924aaec979e1fea39f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 14:06:52 -0800 Subject: [PATCH 15/51] cleanup by just scrolling in the renderer --- src/Surface.zig | 14 ++++-------- src/apprt/surface.zig | 4 ---- src/renderer/generic.zig | 41 ++++++++++++++++++----------------- src/terminal/Terminal.zig | 2 +- src/termio/Termio.zig | 7 ++++-- src/termio/Thread.zig | 2 +- src/termio/stream_handler.zig | 2 +- 7 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6e01daba2..fbb4d9119 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1070,12 +1070,6 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .scrollbar => |scrollbar| self.updateScrollbar(scrollbar), - .scroll_to_bottom => { - self.queueIo(.{ - .scroll_viewport = .{ .bottom = {} }, - }, .unlocked); - }, - .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), @@ -1180,7 +1174,7 @@ fn selectionScrollTick(self: *Surface) !void { } // Scroll the viewport as required - try t.scrollViewport(.{ .delta = delta }); + t.scrollViewport(.{ .delta = delta }); // Next, trigger our drag behavior const pin = t.screens.active.pages.pin(.{ @@ -2785,7 +2779,7 @@ pub fn keyCallback( try self.setSelection(null); } - if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom); + if (self.config.scroll_to_bottom.keystroke) self.io.terminal.scrollViewport(.bottom); try self.queueRender(); } @@ -3538,7 +3532,7 @@ pub fn scrollCallback( // Modify our viewport, this requires a lock since it affects // rendering. We have to switch signs here because our delta // is negative down but our viewport is positive down. - try self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 }); + self.io.terminal.scrollViewport(.{ .delta = y.delta * -1 }); } } @@ -5069,7 +5063,7 @@ pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordin /// /// Precondition: the render_state mutex must be held. fn scrollToBottom(self: *Surface) !void { - try self.io.terminal.scrollViewport(.{ .bottom = {} }); + self.io.terminal.scrollViewport(.{ .bottom = {} }); try self.queueRender(); } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 159e8db16..5c25281c8 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -108,10 +108,6 @@ pub const Message = union(enum) { /// Selected search index change search_selected: ?usize, - /// Scroll the viewport to the bottom. This is triggered by the renderer - /// when new output is detected and scroll-to-bottom on output is enabled. - scroll_to_bottom, - pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 4d986336b..83417429e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -128,7 +128,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Tracks the last bottom-right pin of the screen to detect new output. /// When the final line changes (node or y differs), new content was added. /// Used for scroll-to-bottom on output feature. - last_bottom_node: ?*terminal.PageList.List.Node, + last_bottom_node: ?usize, last_bottom_y: terminal.size.CellCountInt, /// The most recent viewport matches so that we can render search @@ -1176,6 +1176,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } + // If scroll-to-bottom on output is enabled, check if the final line + // changed by comparing the bottom-right pin. If the node pointer or + // y offset changed, new content was added to the screen. + // Update this BEFORE we update our render state so we can + // draw the new scrolled data immediately. + if (self.config.scroll_to_bottom_on_output) scroll: { + const br = state.terminal.screens.active.pages.getBottomRight(.screen) orelse break :scroll; + + // If the pin hasn't changed, then don't scroll. + if (self.last_bottom_node == @intFromPtr(br.node) and + self.last_bottom_y == br.y) break :scroll; + + // Update tracked pin state for next frame + self.last_bottom_node = @intFromPtr(br.node); + self.last_bottom_y = br.y; + + // Scroll + state.terminal.scrollViewport(.bottom); + } + // Update our terminal state try self.terminal_state.update(self.alloc, state.terminal); @@ -1192,25 +1212,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // cross-thread mailbox message within the IO path. const scrollbar = state.terminal.screens.active.pages.scrollbar(); - // If scroll-to-bottom on output is enabled, check if the final line - // changed by comparing the bottom-right pin. If the node pointer or - // y offset changed, new content was added to the screen. - if (self.config.scroll_to_bottom_on_output) { - const bottom_right = state.terminal.screens.active.pages.getBottomRight(.screen); - if (bottom_right) |br| { - const pin_changed = (self.last_bottom_node != br.node) or - (self.last_bottom_y != br.y); - - if (pin_changed and !state.terminal.screens.active.viewportIsBottom()) { - _ = self.surface_mailbox.push(.scroll_to_bottom, .instant); - } - - // Update tracked pin state for next frame - self.last_bottom_node = br.node; - self.last_bottom_y = br.y; - } - } - // Get our preedit state const preedit: ?renderer.State.Preedit = preedit: { const p = state.preedit orelse break :preedit null; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 81c21d3b0..248a2c512 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1625,7 +1625,7 @@ pub const ScrollViewport = union(enum) { }; /// Scroll the viewport of the terminal grid. -pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) !void { +pub fn scrollViewport(self: *Terminal, behavior: ScrollViewport) void { self.screens.active.scroll(switch (behavior) { .top => .{ .top = {} }, .bottom => .{ .active = {} }, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index dee58dc22..55621854c 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -641,10 +641,13 @@ pub fn clearScreen(self: *Termio, td: *ThreadData, history: bool) !void { } /// Scroll the viewport -pub fn scrollViewport(self: *Termio, scroll: terminalpkg.Terminal.ScrollViewport) !void { +pub fn scrollViewport( + self: *Termio, + scroll: terminalpkg.Terminal.ScrollViewport, +) void { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - try self.terminal.scrollViewport(scroll); + self.terminal.scrollViewport(scroll); } /// Jump the viewport to the prompt. diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 6aa5e1c26..ce4c1f4af 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -321,7 +321,7 @@ fn drainMailbox( .resize => |v| self.handleResize(cb, v), .size_report => |v| try io.sizeReport(data, v), .clear_screen => |v| try io.clearScreen(data, v.history), - .scroll_viewport => |v| try io.scrollViewport(v), + .scroll_viewport => |v| io.scrollViewport(v), .selection_scroll => |v| { if (v) { self.startScrollTimer(cb); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index bc3edd185..8c1b5b8ab 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -232,7 +232,7 @@ pub const StreamHandler = struct { .erase_display_below => self.terminal.eraseDisplay(.below, value), .erase_display_above => self.terminal.eraseDisplay(.above, value), .erase_display_complete => { - try self.terminal.scrollViewport(.{ .bottom = {} }); + self.terminal.scrollViewport(.{ .bottom = {} }); self.terminal.eraseDisplay(.complete, value); }, .erase_display_scrollback => self.terminal.eraseDisplay(.scrollback, value), From 2a62f21bf079f253245e0b9f752dbcdbb49eb95a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 14:10:32 -0800 Subject: [PATCH 16/51] fix tests --- src/renderer/cursor.zig | 2 +- src/terminal/render.zig | 2 +- src/terminal/search/viewport.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/cursor.zig b/src/renderer/cursor.zig index bfa92f31d..cddda9871 100644 --- a/src/renderer/cursor.zig +++ b/src/renderer/cursor.zig @@ -143,7 +143,7 @@ test "cursor: always block with preedit" { // If we're scrolled though, then we don't show the cursor. for (0..100) |_| try term.index(); - try term.scrollViewport(.{ .top = {} }); + term.scrollViewport(.{ .top = {} }); try state.update(alloc, &term); // In any bool state diff --git a/src/terminal/render.zig b/src/terminal/render.zig index 9d75fe4b7..2332866ac 100644 --- a/src/terminal/render.zig +++ b/src/terminal/render.zig @@ -1092,7 +1092,7 @@ test "cursor state out of viewport" { try testing.expectEqual(1, state.cursor.viewport.?.y); // Scroll the viewport - try t.scrollViewport(.top); + t.scrollViewport(.top); try state.update(alloc, &t); // Set a style on the cursor diff --git a/src/terminal/search/viewport.zig b/src/terminal/search/viewport.zig index 76deebcec..f5e6c8601 100644 --- a/src/terminal/search/viewport.zig +++ b/src/terminal/search/viewport.zig @@ -358,7 +358,7 @@ test "history search, no active area" { try testing.expect(t.screens.active.pages.pages.first != t.screens.active.pages.pages.last); try s.nextSlice("Buzz\r\nFizz"); - try t.scrollViewport(.top); + t.scrollViewport(.top); var search: ViewportSearch = try .init(alloc, "Fizz"); defer search.deinit(); From 70cc121736c55415c4f97f75294f6f29b1b65698 Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:24:03 +0000 Subject: [PATCH 17/51] Update VOUCHED list (#10873) Triggered by [discussion comment](https://github.com/ghostty-org/ghostty/discussions/10789) from @mitchellh. Vouch: @MiUPa Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 8f0d5b380..363e9aee9 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -76,6 +76,7 @@ miguelelgallo mikailmm misairuzame mitchellh +miupa mtak nwehg oshdubh From faead2d559b3cc90760bd17fdf902c69b2f40814 Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:26:55 +0000 Subject: [PATCH 18/51] Update VOUCHED list (#10874) Triggered by [discussion comment](https://github.com/ghostty-org/ghostty/discussions/10807) from @mitchellh. Vouch: @nicosuave Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 363e9aee9..6df2a7c31 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -78,6 +78,7 @@ misairuzame mitchellh miupa mtak +nicosuave nwehg oshdubh pan93412 From cc17e968955b57d770f7bf01891b520c35ed41ce Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:32:04 +0000 Subject: [PATCH 19/51] Update VOUCHED list (#10875) Triggered by [comment](https://github.com/ghostty-org/ghostty/issues/9876#issuecomment-3930522015) from @mitchellh. Vouch: @benodiwal Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 6df2a7c31..356731c56 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -25,6 +25,7 @@ alanmoyano andrejdaskalov balazs-szucs bennettp123 +benodiwal bernsno beryesa bitigchi From 3d0da44e15cc8f4506acd17ad599229bc493f1c6 Mon Sep 17 00:00:00 2001 From: benodiwal Date: Fri, 12 Dec 2025 04:00:09 +0530 Subject: [PATCH 20/51] feat(config): allow fullscreen config to specify non-native mode directly Co-Authored-By: Sachin --- include/ghostty.h | 9 ++++ .../Terminal/TerminalController.swift | 36 ++++++++-------- macos/Sources/Ghostty/Ghostty.Config.swift | 42 ++++++++++++++++--- src/Surface.zig | 2 + src/apprt/gtk/class/window.zig | 2 +- src/config.zig | 1 + src/config/Config.zig | 36 ++++++++++++++-- 7 files changed, 99 insertions(+), 29 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index b32cc9856..ae41429de 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -509,6 +509,15 @@ typedef struct { ghostty_quick_terminal_size_s secondary; } ghostty_config_quick_terminal_size_s; +// config.Fullscreen +typedef enum { + GHOSTTY_CONFIG_FULLSCREEN_FALSE, + GHOSTTY_CONFIG_FULLSCREEN_TRUE, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, + GHOSTTY_CONFIG_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, +} ghostty_config_fullscreen_e; + // apprt.Target.Key typedef enum { GHOSTTY_TARGET_APP, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c7f9fe086..fc23cc28f 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -224,27 +224,25 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // otherwise the focused terminal, otherwise an arbitrary one. let parent: NSWindow? = explicitParent ?? preferredParent?.window - if let parent { - if parent.styleMask.contains(.fullScreen) { - // If our previous window was fullscreen then we want our new window to - // be fullscreen. This behavior actually doesn't match the native tabbing - // behavior of macOS apps where new windows create tabs when in native - // fullscreen but this is how we've always done it. This matches iTerm2 - // behavior. + if let parent, parent.styleMask.contains(.fullScreen) { + // If our previous window was fullscreen then we want our new window to + // be fullscreen. This behavior actually doesn't match the native tabbing + // behavior of macOS apps where new windows create tabs when in native + // fullscreen but this is how we've always done it. This matches iTerm2 + // behavior. + c.toggleFullscreen(mode: .native) + } else if let fullscreenMode = ghostty.config.windowFullscreen { + switch fullscreenMode { + case .native: + // Native has to be done immediately so that our stylemask contains + // fullscreen for the logic later in this method. c.toggleFullscreen(mode: .native) - } else if ghostty.config.windowFullscreen { - switch (ghostty.config.windowFullscreenMode) { - case .native: - // Native has to be done immediately so that our stylemask contains - // fullscreen for the logic later in this method. - c.toggleFullscreen(mode: .native) - case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: - // If we're non-native then we have to do it on a later loop - // so that the content view is setup. - DispatchQueue.main.async { - c.toggleFullscreen(mode: ghostty.config.windowFullscreenMode) - } + case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: + // If we're non-native then we have to do it on a later loop + // so that the content view is setup. + DispatchQueue.main.async { + c.toggleFullscreen(mode: fullscreenMode) } } } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index c64646e25..55e289a3c 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -227,14 +227,46 @@ extension Ghostty { return v } - var windowFullscreen: Bool { - guard let config = self.config else { return true } - var v = false + /// Returns the fullscreen mode if fullscreen is enabled, or nil if disabled. + /// This parses the `fullscreen` enum config which supports both + /// native and non-native fullscreen modes. + #if canImport(AppKit) + var windowFullscreen: FullscreenMode? { + guard let config = self.config else { return nil } + var v: UnsafePointer? = nil let key = "fullscreen" - _ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) - return v + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } + guard let ptr = v else { return nil } + let str = String(cString: ptr) + return switch str { + case "false": + nil + case "true": + .native + case "non-native": + .nonNative + case "non-native-visible-menu": + .nonNativeVisibleMenu + case "non-native-padded-notch": + .nonNativePaddedNotch + default: + nil + } } + #else + var windowFullscreen: Bool { + guard let config = self.config else { return false } + var v: UnsafePointer? = nil + let key = "fullscreen" + guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false } + guard let ptr = v else { return false } + let str = String(cString: ptr) + return str != "false" + } + #endif + /// Returns the fullscreen mode for toggle actions (keybindings). + /// This is controlled by `macos-non-native-fullscreen` config. #if canImport(AppKit) var windowFullscreenMode: FullscreenMode { let defaultValue: FullscreenMode = .native diff --git a/src/Surface.zig b/src/Surface.zig index fbb4d9119..b9dbefa1b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -312,6 +312,7 @@ const DerivedConfig = struct { mouse_reporting: bool, mouse_scroll_multiplier: configpkg.MouseScrollMultiplier, mouse_shift_capture: configpkg.MouseShiftCapture, + fullscreen: configpkg.Fullscreen, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?input.OptionAsAlt, selection_clear_on_copy: bool, @@ -389,6 +390,7 @@ const DerivedConfig = struct { .mouse_reporting = config.@"mouse-reporting", .mouse_scroll_multiplier = config.@"mouse-scroll-multiplier", .mouse_shift_capture = config.@"mouse-shift-capture", + .fullscreen = config.fullscreen, .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_copy = config.@"selection-clear-on-copy", diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index f96bccd64..543080394 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -308,7 +308,7 @@ pub const Window = extern struct { if (priv.config) |config_obj| { const config = config_obj.get(); if (config.maximize) self.as(gtk.Window).maximize(); - if (config.fullscreen) self.as(gtk.Window).fullscreen(); + if (config.fullscreen != .false) self.as(gtk.Window).fullscreen(); // If we have an explicit title set, we set that immediately // so that any applications inspecting the window states see diff --git a/src/config.zig b/src/config.zig index 4abd319a6..0bf61a47f 100644 --- a/src/config.zig +++ b/src/config.zig @@ -31,6 +31,7 @@ pub const Keybinds = Config.Keybinds; pub const MouseShiftCapture = Config.MouseShiftCapture; pub const MouseScrollMultiplier = Config.MouseScrollMultiplier; pub const NonNativeFullscreen = Config.NonNativeFullscreen; +pub const Fullscreen = Config.Fullscreen; pub const RepeatableCodepointMap = Config.RepeatableCodepointMap; pub const RepeatableFontVariation = Config.RepeatableFontVariation; pub const RepeatableString = Config.RepeatableString; diff --git a/src/config/Config.zig b/src/config/Config.zig index d409fb6fa..8a68bce4a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1428,10 +1428,27 @@ maximize: bool = false, /// does not apply to tabs, splits, etc. However, this setting will apply to all /// new windows, not just the first one. /// -/// On macOS, this setting does not work if window-decoration is set to -/// "none", because native fullscreen on macOS requires window decorations -/// to be set. -fullscreen: bool = false, +/// Allowable values are: +/// +/// * `false` - Don't start in fullscreen (default) +/// * `true` - Start in native fullscreen +/// * `non-native` - (macOS only) Start in non-native fullscreen, hiding the +/// menu bar. This is faster than native fullscreen since it doesn't use +/// animations. On non-macOS platforms, this behaves the same as `true`. +/// * `non-native-visible-menu` - (macOS only) Start in non-native fullscreen, +/// keeping the menu bar visible. On non-macOS platforms, behaves like `true`. +/// * `non-native-padded-notch` - (macOS only) Start in non-native fullscreen, +/// hiding the menu bar but padding for the notch on applicable devices. +/// On non-macOS platforms, behaves like `true`. +/// +/// Important: tabs DO NOT WORK with non-native fullscreen modes. Non-native +/// fullscreen removes the titlebar and macOS native tabs require the titlebar. +/// If you use tabs, use `true` (native) instead. +/// +/// On macOS, `true` (native fullscreen) does not work if `window-decoration` +/// is set to `false`, because native fullscreen on macOS requires window +/// decorations. +fullscreen: Fullscreen = .false, /// The title Ghostty will use for the window. This will force the title of the /// window to be this title at all times and Ghostty will ignore any set title @@ -5136,6 +5153,17 @@ pub const NonNativeFullscreen = enum(c_int) { @"padded-notch", }; +/// Valid values for fullscreen config option +/// c_int because it needs to be extern compatible +/// If this is changed, you must also update ghostty.h +pub const Fullscreen = enum(c_int) { + false, + true, + @"non-native", + @"non-native-visible-menu", + @"non-native-padded-notch", +}; + pub const WindowPaddingColor = enum { background, extend, From 786bad9774cfa4753a11f8bb46b47aedc974bf3e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:32:44 -0500 Subject: [PATCH 21/51] macos: swiftlint 'colon' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Features/App Intents/InputIntent.swift | 2 +- .../ClipboardConfirmationView.swift | 2 +- .../QuickTerminal/QuickTerminalPosition.swift | 4 ++-- .../Sources/Features/Secure Input/SecureInput.swift | 2 +- .../Terminal/Window Styles/TerminalWindow.swift | 2 +- .../TitlebarTabsVenturaTerminalWindow.swift | 2 +- macos/Sources/Ghostty/Ghostty.Config.swift | 10 +++++----- macos/Sources/Ghostty/Ghostty.Input.swift | 12 ++++++------ macos/Sources/Ghostty/Package.swift | 2 +- .../Ghostty/Surface View/SurfaceView_AppKit.swift | 4 ++-- 11 files changed, 21 insertions(+), 22 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index e9767a43f..9447ab577 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -13,7 +13,6 @@ disabled_rules: - type_body_length # TODO - - colon - comma - comment_spacing - control_statement diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index d169b3a8c..e03eaf640 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -309,7 +309,7 @@ enum KeyEventMods: String, AppEnum, CaseIterable { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key") - static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [KeyEventMods: DisplayRepresentation] = [ .shift: "Shift", .control: "Control", .option: "Option", diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index 6423e3cf6..e4827e7fe 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -7,7 +7,7 @@ protocol ClipboardConfirmationViewDelegate: AnyObject { /// The SwiftUI view for showing a clipboard confirmation dialog. struct ClipboardConfirmationView: View { - enum Action : String { + enum Action: String { case cancel case confirm diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index d7660f77a..f0b031a1c 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -1,6 +1,6 @@ import Cocoa -enum QuickTerminalPosition : String { +enum QuickTerminalPosition: String { case top case bottom case left @@ -86,7 +86,7 @@ enum QuickTerminalPosition : String { y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: - return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) } } diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index f999ce5ca..305510956 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -12,7 +12,7 @@ import OSLog // it. You have to yield secure input on application deactivation (because // it'll affect other apps) and reacquire on reactivation, and every enable // needs to be balanced with a disable. -class SecureInput : ObservableObject { +class SecureInput: ObservableObject { static let shared = SecureInput() private static let logger = Logger( diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 501ac0e67..5775e569e 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -347,7 +347,7 @@ class TerminalWindow: NSWindow { button.toolTip = "Reset Zoom" button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 39db13c6d..c47c4cb8b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -251,7 +251,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { button.toolTip = "Reset Zoom" button.contentTintColor = .controlAccentColor button.state = .on - button.image = NSImage(named:"ResetZoom") + button.image = NSImage(named: "ResetZoom") button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.translatesAutoresizingMaskIntoConstraints = false button.widthAnchor.constraint(equalToConstant: 20).isActive = true diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 55e289a3c..88ad9fb07 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -416,7 +416,7 @@ extension Ghostty { return MacHidden(rawValue: str) ?? .never } - var focusFollowsMouse : Bool { + var focusFollowsMouse: Bool { guard let config = self.config else { return false } var v = false; let key = "focus-follows-mouse" @@ -680,7 +680,7 @@ extension Ghostty { // MARK: Configuration Enums extension Ghostty.Config { - enum AutoUpdate : String { + enum AutoUpdate: String { case off case check case download @@ -769,7 +769,7 @@ extension Ghostty.Config { case new_window = "new-window" } - enum MacHidden : String { + enum MacHidden: String { case never case always } @@ -785,13 +785,13 @@ extension Ghostty.Config { case never } - enum ResizeOverlay : String { + enum ResizeOverlay: String { case always case never case after_first = "after-first" } - enum ResizeOverlayPosition : String { + enum ResizeOverlayPosition: String { case center case top_left = "top-left" case top_center = "top-center" diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 7b2905abb..04d7560fd 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -81,7 +81,7 @@ extension Ghostty { /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. Note that /// not all ghostty key enum values are represented here because not all of them can be /// mapped to a KeyEquivalent. - static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [ + static let keyToEquivalent: [ghostty_input_key_e: KeyEquivalent] = [ // Function keys GHOSTTY_KEY_ARROW_UP: .upArrow, GHOSTTY_KEY_ARROW_DOWN: .downArrow, @@ -243,7 +243,7 @@ extension Ghostty.Input { extension Ghostty.Input.Action: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key Action") - static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Action: DisplayRepresentation] = [ .release: "Release", .press: "Press", .repeat: "Repeat" @@ -355,7 +355,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseState: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State") - static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseState: DisplayRepresentation] = [ .release: "Release", .press: "Press" ] @@ -420,7 +420,7 @@ extension Ghostty.Input { extension Ghostty.Input.MouseButton: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button") - static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.MouseButton: DisplayRepresentation] = [ .unknown: "Unknown", .left: "Left", .right: "Right", @@ -504,7 +504,7 @@ extension Ghostty.Input { extension Ghostty.Input.Momentum: AppEnum { static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") - static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Momentum: DisplayRepresentation] = [ .none: "None", .began: "Began", .stationary: "Stationary", @@ -1223,7 +1223,7 @@ extension Ghostty.Input.Key: AppEnum { ] } - static var caseDisplayRepresentations: [Ghostty.Input.Key : DisplayRepresentation] = [ + static var caseDisplayRepresentations: [Ghostty.Input.Key: DisplayRepresentation] = [ // Letters (A-Z) .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 15cb3a51e..15b0bd9b0 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -498,4 +498,4 @@ extension Ghostty.Notification { } // Make the input enum hashable. -extension ghostty_input_key_e : @retroactive Hashable {} +extension ghostty_input_key_e: @retroactive Hashable {} diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index c856b0163..85dc1d612 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1389,7 +1389,7 @@ extension Ghostty { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:]; if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1831,7 +1831,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key : Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:]; if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the From 6ade5df7990d8f2fbd7dce5c05f43ba43bd510e5 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:33:34 -0500 Subject: [PATCH 22/51] macos: swiftlint 'comma' rule --- macos/.swiftlint.yml | 1 - .../Features/Terminal/Window Styles/TerminalWindow.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 9447ab577..af5fec161 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -13,7 +13,6 @@ disabled_rules: - type_body_length # TODO - - comma - comment_spacing - control_statement - deployment_target diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 5775e569e..686d561f5 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -459,7 +459,7 @@ class TerminalWindow: NSWindow { backgroundColor = .white.withAlphaComponent(0.001) // We don't need to set blur when using glass - if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { + if !surfaceConfig.backgroundBlur.isGlassStyle, let appDelegate = NSApp.delegate as? AppDelegate { ghostty_set_window_background_blur( appDelegate.ghostty.app, Unmanaged.passUnretained(self).toOpaque()) From a8903c1bb199aa52b63d20593361672f27a4d4ba Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:34:02 -0500 Subject: [PATCH 23/51] macos: swiftlint 'comment_spacing' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 18 +++++++++--------- .../Features/About/AboutController.swift | 2 +- .../ClipboardConfirmationController.swift | 2 +- .../ConfigurationErrorsController.swift | 2 +- .../Features/Terminal/TerminalController.swift | 8 ++++---- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index af5fec161..f73ccb791 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -13,7 +13,6 @@ disabled_rules: - type_body_length # TODO - - comment_spacing - control_statement - deployment_target - empty_enum_arguments diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2ca7d4813..d4deac49f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -166,7 +166,7 @@ class AppDelegate: NSObject, ghostty.delegate = self } - //MARK: - NSApplicationDelegate + // MARK: - NSApplicationDelegate func applicationWillFinishLaunching(_ notification: Notification) { UserDefaults.standard.register(defaults: [ @@ -887,7 +887,7 @@ class AppDelegate: NSObject, Note: When `auto-update = download`, you may need to `Clean Build Folder` if a background install has already begun. */ - //updateController.updater.checkForUpdatesInBackground() + // updateController.updater.checkForUpdatesInBackground() } // Config could change keybindings, so update everything that depends on that @@ -1022,7 +1022,7 @@ class AppDelegate: NSObject, UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild") } - //MARK: - Restorable State + // MARK: - Restorable State /// We support NSSecureCoding for restorable state. Required as of macOS Sonoma (14) but a good idea anyways. func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { @@ -1058,7 +1058,7 @@ class AppDelegate: NSObject, } } - //MARK: - UNUserNotificationCenterDelegate + // MARK: - UNUserNotificationCenterDelegate func userNotificationCenter( _ center: UNUserNotificationCenter, @@ -1079,7 +1079,7 @@ class AppDelegate: NSObject, withCompletionHandler(options) } - //MARK: - GhosttyAppDelegate + // MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in TerminalController.all { @@ -1093,7 +1093,7 @@ class AppDelegate: NSObject, return nil } - //MARK: - Dock Menu + // MARK: - Dock Menu private func reloadDockMenu() { let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "") @@ -1104,7 +1104,7 @@ class AppDelegate: NSObject, dockMenu.addItem(newTab) } - //MARK: - Global State + // MARK: - Global State func setSecureInput(_ mode: Ghostty.SetSecureInput) { let input = SecureInput.shared @@ -1122,7 +1122,7 @@ class AppDelegate: NSObject, UserDefaults.standard.set(input.global, forKey: "SecureInput") } - //MARK: - IB Actions + // MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { Ghostty.App.openConfig() @@ -1134,7 +1134,7 @@ class AppDelegate: NSObject, @IBAction func checkForUpdates(_ sender: Any?) { updateController.checkForUpdates() - //UpdateSimulator.happyPath.simulate(with: updateViewModel) + // UpdateSimulator.happyPath.simulate(with: updateViewModel) } @IBAction func newWindow(_ sender: Any?) { diff --git a/macos/Sources/Features/About/AboutController.swift b/macos/Sources/Features/About/AboutController.swift index efd7a515a..2f494f12c 100644 --- a/macos/Sources/Features/About/AboutController.swift +++ b/macos/Sources/Features/About/AboutController.swift @@ -24,7 +24,7 @@ class AboutController: NSWindowController, NSWindowDelegate { window?.close() } - //MARK: - First Responder + // MARK: - First Responder @IBAction func close(_ sender: Any) { self.window?.performClose(sender) diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index 2040dcfae..cb4500af7 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -28,7 +28,7 @@ class ClipboardConfirmationController: NSWindowController { fatalError("init(coder:) is not supported for this view") } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowDidLoad() { guard let window = window else { return } diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift index b2550b94e..531730f1b 100644 --- a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift +++ b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift @@ -18,7 +18,7 @@ class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, Confi } } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { shouldCascadeWindows = false diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index fc23cc28f..317031f5b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -482,7 +482,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - //MARK: - Methods + // MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { // Get our managed configuration object out @@ -994,7 +994,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabColor: (window as? TerminalWindow)?.tabColor ?? .none) } - //MARK: - NSWindowController + // MARK: - NSWindowController override func windowWillLoad() { // We do NOT want to cascade because we handle this manually from the manager. @@ -1315,7 +1315,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ghostty.toggleTerminalInspector(surface: surface) } - //MARK: - TerminalViewDelegate + // MARK: - TerminalViewDelegate override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) @@ -1347,7 +1347,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } - //MARK: - Notifications + // MARK: - Notifications @objc private func onMoveTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } From 56d67ce88f05f9cf499ae483f34212722475ebf4 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:34:37 -0500 Subject: [PATCH 24/51] macos: swiftlint 'control_statement' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 46 +++---- .../App Intents/IntentPermission.swift | 2 +- .../ClipboardConfirmationController.swift | 2 +- .../ClipboardConfirmationView.swift | 2 +- .../ColorizedGhosttyIcon.swift | 2 +- .../Command Palette/CommandPalette.swift | 2 +- .../Global Keybinds/GlobalEventTap.swift | 6 +- .../QuickTerminalController.swift | 8 +- .../QuickTerminal/QuickTerminalPosition.swift | 6 +- .../QuickTerminal/QuickTerminalScreen.swift | 4 +- .../QuickTerminalSpaceBehavior.swift | 4 +- .../Features/Secure Input/SecureInput.swift | 8 +- .../Features/Services/ServiceProvider.swift | 2 +- .../ConfigurationErrorsController.swift | 2 +- macos/Sources/Features/Splits/SplitTree.swift | 4 +- .../Features/Splits/SplitView.Divider.swift | 14 +- macos/Sources/Features/Splits/SplitView.swift | 8 +- .../Splits/TerminalSplitTreeView.swift | 4 +- .../Terminal/BaseTerminalController.swift | 12 +- .../Terminal/TerminalController.swift | 32 ++--- .../Terminal/TerminalRestorable.swift | 8 +- .../Features/Terminal/TerminalView.swift | 2 +- .../Window Styles/TerminalWindow.swift | 4 +- .../TitlebarTabsTahoeTerminalWindow.swift | 2 +- .../TitlebarTabsVenturaTerminalWindow.swift | 12 +- .../Features/Update/UpdateDelegate.swift | 2 +- .../Features/Update/UpdateViewModel.swift | 4 +- macos/Sources/Ghostty/Ghostty.Action.swift | 2 +- macos/Sources/Ghostty/Ghostty.App.swift | 128 +++++++++--------- macos/Sources/Ghostty/Ghostty.Config.swift | 14 +- macos/Sources/Ghostty/Ghostty.Input.swift | 28 ++-- macos/Sources/Ghostty/Ghostty.Surface.swift | 4 +- macos/Sources/Ghostty/Package.swift | 16 +-- .../Ghostty/Surface View/InspectorView.swift | 20 +-- .../Ghostty/Surface View/SurfaceView.swift | 34 ++--- .../Surface View/SurfaceView_AppKit.swift | 94 ++++++------- .../Surface View/SurfaceView_UIKit.swift | 2 +- .../Extensions/NSAppearance+Extension.swift | 2 +- .../Extensions/NSApplication+Extension.swift | 2 +- .../Extensions/NSPasteboard+Extension.swift | 2 +- .../Extensions/NSScreen+Extension.swift | 4 +- macos/Sources/Helpers/Fullscreen.swift | 14 +- 43 files changed, 285 insertions(+), 286 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index f73ccb791..a87348f80 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -13,7 +13,6 @@ disabled_rules: - type_body_length # TODO - - control_statement - deployment_target - empty_enum_arguments - empty_parentheses_with_trailing_closure diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index d4deac49f..5f5899654 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -197,7 +197,7 @@ class AppDelegate: NSObject, applicationLaunchTime = ProcessInfo.processInfo.systemUptime // Check if secure input was enabled when we last quit. - if (UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled) { + if UserDefaults.standard.bool(forKey: "SecureInput") != SecureInput.shared.enabled { toggleSecureInput(self) } @@ -280,7 +280,7 @@ class AppDelegate: NSObject, guard let appearance = change.newValue else { return } guard let app = self.ghostty.app else { return } let scheme: ghostty_color_scheme_e - if (appearance.isDark) { + if appearance.isDark { scheme = GHOSTTY_COLOR_SCHEME_DARK } else { scheme = GHOSTTY_COLOR_SCHEME_LIGHT @@ -332,7 +332,7 @@ class AppDelegate: NSObject, self.setDockBadge(nil) // First launch stuff - if (!applicationHasBecomeActive) { + if !applicationHasBecomeActive { applicationHasBecomeActive = true // Let's launch our first window. We only do this if we have no other windows. It @@ -353,7 +353,7 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows - if (windows.isEmpty) { return .terminateNow } + if windows.isEmpty { return .terminateNow } // If we've already accepted to install an update, then we don't need to // confirm quit. The user is already expecting the update to happen. @@ -380,7 +380,7 @@ class AppDelegate: NSObject, guard let keyword = AEKeyword("why?") else { break why } if let why = event.attributeDescriptor(forKeyword: keyword) { - switch (why.typeCodeValue) { + switch why.typeCodeValue { case kAEShutDown: fallthrough @@ -397,7 +397,7 @@ class AppDelegate: NSObject, } // If our app says we don't need to confirm, we can exit now. - if (!ghostty.needsConfirmQuit) { return .terminateNow } + if !ghostty.needsConfirmQuit { return .terminateNow } // We have some visible window. Show an app-wide modal to confirm quitting. let alert = NSAlert() @@ -406,7 +406,7 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Close Ghostty") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: return .terminateNow @@ -460,7 +460,7 @@ class AppDelegate: NSObject, // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - if (isDirectory.boolValue) { + if isDirectory.boolValue { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. config.workingDirectory = filename @@ -498,7 +498,7 @@ class AppDelegate: NSObject, alert.addButton(withTitle: "Allow") alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning - switch (alert.runModal()) { + switch alert.runModal() { case .alertFirstButtonReturn: break @@ -746,7 +746,7 @@ class AppDelegate: NSObject, guard let ghostty = self.ghostty.app else { return event } // Build our event input and call ghostty - if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) { + if ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) { // The key was used so we want to stop it from going to our Mac app Ghostty.logger.debug("local key event handled event=\(event)") return nil @@ -761,7 +761,7 @@ class AppDelegate: NSObject, @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } - self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } + self.menuQuickTerminal?.state = if quickController.visible { .on } else { .off } } @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -777,11 +777,11 @@ class AppDelegate: NSObject, } @objc private func ghosttyBellDidRing(_ notification: Notification) { - if (ghostty.config.bellFeatures.contains(.system)) { + if ghostty.config.bellFeatures.contains(.system) { NSSound.beep() } - if (ghostty.config.bellFeatures.contains(.attention)) { + if ghostty.config.bellFeatures.contains(.attention) { // Bounce the dock icon if we're not focused. NSApp.requestUserAttention(.informationalRequest) @@ -861,7 +861,7 @@ class AppDelegate: NSObject, // Depending on the "window-save-state" setting we have to set the NSQuitAlwaysKeepsWindows // configuration. This is the only way to carefully control whether macOS invokes the // state restoration system. - switch (config.windowSaveState) { + switch config.windowSaveState { case "never": UserDefaults.standard.setValue(false, forKey: "NSQuitAlwaysKeepsWindows") case "always": UserDefaults.standard.setValue(true, forKey: "NSQuitAlwaysKeepsWindows") case "default": fallthrough @@ -900,7 +900,7 @@ class AppDelegate: NSObject, DispatchQueue.main.async { self.syncAppearance(config: config) } // Decide whether to hide/unhide app from dock and app switcher - switch (config.macosHidden) { + switch config.macosHidden { case .never: NSApp.setActivationPolicy(.regular) @@ -911,16 +911,16 @@ class AppDelegate: NSObject, // If we have configuration errors, we need to show them. let c = ConfigurationErrorsController.sharedInstance c.errors = config.errors - if (c.errors.count > 0) { - if (c.window == nil || !c.window!.isVisible) { + if c.errors.count > 0 { + if c.window == nil || !c.window!.isVisible { c.showWindow(self) } } // We need to handle our global event tap depending on if there are global // events that we care about in Ghostty. - if (ghostty_app_has_global_keybinds(ghostty.app!)) { - if (timeSinceLaunch > 5) { + if ghostty_app_has_global_keybinds(ghostty.app!) { + if timeSinceLaunch > 5 { // If the process has been running for awhile we enable right away // because no windows are likely to pop up. GlobalEventTap.shared.enable() @@ -952,7 +952,7 @@ class AppDelegate: NSObject, var appIcon: NSImage? var appIconName: String? = config.macosIcon.rawValue - switch (config.macosIcon) { + switch config.macosIcon { case let icon where icon.assetName != nil: appIcon = NSImage(named: icon.assetName!)! @@ -1108,7 +1108,7 @@ class AppDelegate: NSObject, func setSecureInput(_ mode: Ghostty.SetSecureInput) { let input = SecureInput.shared - switch (mode) { + switch mode { case .on: input.global = true @@ -1118,7 +1118,7 @@ class AppDelegate: NSObject, case .toggle: input.global.toggle() } - self.menuSecureInput?.state = if (input.global) { .on } else { .off } + self.menuSecureInput?.state = if input.global { .on } else { .off } UserDefaults.standard.set(input.global, forKey: "SecureInput") } @@ -1288,7 +1288,7 @@ extension AppDelegate { @IBAction func useAsDefault(_ sender: NSMenuItem) { let ud = UserDefaults.standard let key = TerminalWindow.defaultLevelKey - if (menuFloatOnTop?.state == .on) { + if menuFloatOnTop?.state == .on { ud.set(NSWindow.Level.floating, forKey: key) } else { ud.removeObject(forKey: key) diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 210d2cb2e..32c0a9b81 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -28,7 +28,7 @@ func requestIntentPermission() async -> Bool { await withCheckedContinuation { continuation in Task { @MainActor in if let delegate = NSApp.delegate as? AppDelegate { - switch (delegate.ghostty.config.macosShortcuts) { + switch delegate.ghostty.config.macosShortcuts { case .allow: continuation.resume(returning: true) return diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index cb4500af7..e8776edee 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -33,7 +33,7 @@ class ClipboardConfirmationController: NSWindowController { override func windowDidLoad() { guard let window = window else { return } - switch (request) { + switch request { case .paste: window.title = "Warning: Potentially Unsafe Paste" case .osc_52_read, .osc_52_write: diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index e4827e7fe..de7052ff9 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -74,7 +74,7 @@ struct ClipboardConfirmationView: View { // If we didn't unhide anything, we just send an unhide to be safe. // I don't think the count can go negative on NSCursor so this handles // scenarios cursor is hidden outside of our own NSCursor usage. - if (cursorHiddenCount == 0) { + if cursorHiddenCount == 0 { _ = Cursor.unhide() } } diff --git a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift index 58de8f771..e58699cff 100644 --- a/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift +++ b/macos/Sources/Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift @@ -19,7 +19,7 @@ struct ColorizedGhosttyIcon { guard let crt = NSImage(named: "CustomIconCRT") else { return nil } guard let gloss = NSImage(named: "CustomIconGloss") else { return nil } - let baseName = switch (frame) { + let baseName = switch frame { case .aluminum: "CustomIconBaseAluminum" case .beige: "CustomIconBaseBeige" case .chrome: "CustomIconBaseChrome" diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 235881dde..eb2d631b6 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -106,7 +106,7 @@ struct CommandPaletteView: View { VStack(alignment: .leading, spacing: 0) { CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in - switch (event) { + switch event { case .exit: isPresented = false diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index ae77535be..e9541f434 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -33,7 +33,7 @@ class GlobalEventTap { // If enabling fails due to permissions, this will start a timer to retry since // accessibility permissions take affect immediately. func enable() { - if (eventTap != nil) { + if eventTap != nil { // Already enabled return } @@ -44,7 +44,7 @@ class GlobalEventTap { } // Try to enable the event tap immediately. If this succeeds then we're done! - if (tryEnable()) { + if tryEnable() { return } @@ -142,7 +142,7 @@ fileprivate func cgEventFlagsChangedHandler( // Build our event input and call ghostty let key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) - if (ghostty_app_key(ghostty, key_ev)) { + if ghostty_app_key(ghostty, key_ev) { GlobalEventTap.logger.info("global key event handled event=\(event)") return nil } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 07c0c4c19..dfcfa1f05 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -316,7 +316,7 @@ class QuickTerminalController: BaseTerminalController { // MARK: Methods func toggle() { - if (visible) { + if visible { animateOut() } else { animateIn() @@ -441,7 +441,7 @@ class QuickTerminalController: BaseTerminalController { // If our dock position would conflict with our target location then // we autohide the dock. if position.conflictsWithDock(on: screen) { - if (hiddenDock == nil) { + if hiddenDock == nil { hiddenDock = .init() } @@ -675,10 +675,10 @@ class QuickTerminalController: BaseTerminalController { // We ignore the configured fullscreen style and always use non-native // because the way the quick terminal works doesn't support native. let mode: FullscreenMode - if (NSApp.isFrontmost) { + if NSApp.isFrontmost { // If we're frontmost and we have a notch then we keep padding // so all lines of the terminal are visible. - if (window?.screen?.hasNotch ?? false) { + if window?.screen?.hasNotch ?? false { mode = .nonNativePaddedNotch } else { mode = .nonNative diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index f0b031a1c..9fd7d83bb 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -64,7 +64,7 @@ enum QuickTerminalPosition: String { /// The initial point origin for this position. func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -92,7 +92,7 @@ enum QuickTerminalPosition: String { /// The final point origin for this position. func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { - switch (self) { + switch self { case .top: return .init( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), @@ -128,7 +128,7 @@ enum QuickTerminalPosition: String { // Depending on the orientation of the dock, we conflict if our quick terminal // would potentially "hit" the dock. In the future we should probably consider // the frame of the quick terminal. - return switch (orientation) { + return switch orientation { case .top: self == .top || self == .left || self == .right case .bottom: self == .bottom || self == .left || self == .right case .left: self == .top || self == .bottom diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift index cd07a6f12..815bda9d6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift @@ -6,7 +6,7 @@ enum QuickTerminalScreen { case menuBar init?(fromGhosttyConfig string: String) { - switch (string) { + switch string { case "main": self = .main @@ -22,7 +22,7 @@ enum QuickTerminalScreen { } var screen: NSScreen? { - switch (self) { + switch self { case .main: return NSScreen.main diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift index 0561aaa18..9f544b7e6 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSpaceBehavior.swift @@ -6,7 +6,7 @@ enum QuickTerminalSpaceBehavior { case move init?(fromGhosttyConfig string: String) { - switch (string) { + switch string { case "move": self = .move @@ -24,7 +24,7 @@ enum QuickTerminalSpaceBehavior { .fullScreenAuxiliary ] - switch (self) { + switch self { case .move: // We want this to move the window to the active space. return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior) diff --git a/macos/Sources/Features/Secure Input/SecureInput.swift b/macos/Sources/Features/Secure Input/SecureInput.swift index 305510956..261a38e5c 100644 --- a/macos/Sources/Features/Secure Input/SecureInput.swift +++ b/macos/Sources/Features/Secure Input/SecureInput.swift @@ -90,12 +90,12 @@ class SecureInput: ObservableObject { guard enabled != desired else { return } let err: OSStatus - if (enabled) { + if enabled { err = DisableSecureEventInput() } else { err = EnableSecureEventInput() } - if (err == noErr) { + if err == noErr { enabled = desired Self.logger.debug("secure input state=\(self.enabled)") return @@ -111,7 +111,7 @@ class SecureInput: ObservableObject { // desire to be enabled. guard !enabled && desired else { return } let err = EnableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = true Self.logger.debug("secure input enabled on activation") return @@ -124,7 +124,7 @@ class SecureInput: ObservableObject { // We only want to disable if we're enabled. guard enabled else { return } let err = DisableSecureEventInput() - if (err == noErr) { + if err == noErr { enabled = false Self.logger.debug("secure input disabled on deactivation") return diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index f165769a7..9bf46fcf9 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -50,7 +50,7 @@ class ServiceProvider: NSObject { var config = Ghostty.SurfaceConfiguration() config.workingDirectory = url.path(percentEncoded: false) - switch (target) { + switch target { case .window: _ = TerminalController.newWindow(delegate.ghostty, withBaseConfig: config) diff --git a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift index 531730f1b..06fcebda3 100644 --- a/macos/Sources/Features/Settings/ConfigurationErrorsController.swift +++ b/macos/Sources/Features/Settings/ConfigurationErrorsController.swift @@ -12,7 +12,7 @@ class ConfigurationErrorsController: NSWindowController, NSWindowDelegate, Confi /// The data model for this view. Update this directly and the associated view will be updated, too. @Published var errors: [String] = [] { didSet { - if (errors.count == 0) { + if errors.count == 0 { self.window?.performClose(nil) } } diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 2fb83e64c..5b776afbc 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -222,7 +222,7 @@ extension SplitTree { case .split: // If the best candidate is a split node, use its the leaf/rightmost // depending on our spatial direction. - return switch (spatialDirection) { + return switch spatialDirection { case .up, .left: bestNode.node.leftmostLeaf() case .down, .right: bestNode.node.rightmostLeaf() } @@ -422,7 +422,7 @@ extension SplitTree.Node { /// Returns the node in the tree that contains the given view. func node(view: ViewType) -> Node? { - switch (self) { + switch self { case .leaf(view): return self diff --git a/macos/Sources/Features/Splits/SplitView.Divider.swift b/macos/Sources/Features/Splits/SplitView.Divider.swift index a01175dce..59a10ef60 100644 --- a/macos/Sources/Features/Splits/SplitView.Divider.swift +++ b/macos/Sources/Features/Splits/SplitView.Divider.swift @@ -10,7 +10,7 @@ extension SplitView { @Binding var split: CGFloat private var visibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize case .vertical: @@ -19,7 +19,7 @@ extension SplitView { } private var visibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -28,7 +28,7 @@ extension SplitView { } private var invisibleWidth: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return visibleSize + invisibleSize case .vertical: @@ -37,7 +37,7 @@ extension SplitView { } private var invisibleHeight: CGFloat? { - switch (direction) { + switch direction { case .horizontal: return nil case .vertical: @@ -46,7 +46,7 @@ extension SplitView { } private var pointerStyle: BackportPointerStyle { - return switch (direction) { + return switch direction { case .horizontal: .resizeLeftRight case .vertical: .resizeUpDown } @@ -69,8 +69,8 @@ extension SplitView { return } - if (isHovered) { - switch (direction) { + if isHovered { + switch direction { case .horizontal: NSCursor.resizeLeftRight.push() case .vertical: diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index 42de97590..fe455d2df 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -90,7 +90,7 @@ struct SplitView: View { private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { return DragGesture() .onChanged { gesture in - switch (direction) { + switch direction { case .horizontal: let new = min(max(minSize, gesture.location.x), size.width - minSize) split = new / size.width @@ -106,7 +106,7 @@ struct SplitView: View { private func leftRect(for size: CGSize) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: result.size.width = result.size.width * split result.size.width -= splitterVisibleSize / 2 @@ -125,7 +125,7 @@ struct SplitView: View { private func rightRect(for size: CGSize, leftRect: CGRect) -> CGRect { // Initially the rect is the full size var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) - switch (direction) { + switch direction { case .horizontal: // For horizontal layouts we offset the starting X by the left rect // and make the width fit the remaining space. @@ -144,7 +144,7 @@ struct SplitView: View { /// Calculates the point at which the splitter should be rendered. private func splitterPoint(for size: CGSize, leftRect: CGRect) -> CGPoint { - switch (direction) { + switch direction { case .horizontal: return CGPoint(x: leftRect.size.width, y: size.height / 2) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 2a42dc599..635bea980 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -52,12 +52,12 @@ fileprivate struct TerminalSplitSubtreeView: View { let action: (TerminalSplitOperation) -> Void var body: some View { - switch (node) { + switch node { case .leaf(let leafView): TerminalSplitLeaf(surfaceView: leafView, isSplit: !isRoot, action: action) case .split(let split): - let splitViewDirection: SplitViewDirection = switch (split.direction) { + let splitViewDirection: SplitViewDirection = switch split.direction { case .horizontal: .horizontal case .vertical: .vertical } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b739e9ed1..8bb9d9e36 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -281,7 +281,7 @@ class BaseTerminalController: NSWindowController, /// Subclasses should call super first. func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { // If our surface tree becomes empty then we have no focused surface. - if (to.isEmpty) { + if to.isEmpty { focusedSurface = nil } } @@ -596,7 +596,7 @@ class BaseTerminalController: NSWindowController, guard let directionAny = notification.userInfo?["direction"] else { return } guard let direction = directionAny as? ghostty_action_split_direction_e else { return } let splitDirection: SplitTree.NewDirection - switch (direction) { + switch direction { case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .down @@ -820,7 +820,7 @@ class BaseTerminalController: NSWindowController, private func computeTitle(title: String, bell: Bool) -> String { var result = title - if (bell && ghostty.config.bellFeatures.contains(.title)) { + if bell && ghostty.config.bellFeatures.contains(.title) { result = "🔔 \(result)" } @@ -966,7 +966,7 @@ class BaseTerminalController: NSWindowController, func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { guard let surface = surfaceView.surface else { return } let len = action.utf8CString.count - if (len == 0) { return } + if len == 0 { return } _ = action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } @@ -1109,7 +1109,7 @@ class BaseTerminalController: NSWindowController, window?.endSheet(ccWindow) } - switch (request) { + switch request { case let .osc_52_write(pasteboard): guard case .confirm = action else { break } let pb = pasteboard ?? NSPasteboard.general @@ -1117,7 +1117,7 @@ class BaseTerminalController: NSWindowController, pb.setString(cc.contents, forType: .string) case .osc_52_read, .paste: let str: String - switch (action) { + switch action { case .cancel: str = "" diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 317031f5b..c61c7a88d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -160,7 +160,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // If our surface tree is now nil then we close our window. - if (to.isEmpty) { + if to.isEmpty { self.window?.close() } } @@ -253,7 +253,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr DispatchQueue.main.async { // Only cascade if we aren't fullscreen. if let window = c.window { - if (!window.styleMask.contains(.fullScreen)) { + if !window.styleMask.contains(.fullScreen) { Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) } } @@ -390,7 +390,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // If the parent is miniaturized, then macOS exhibits really strange behaviors // so we have to bring it back out. - if (parent.isMiniaturized) { parent.deminiaturize(self) } + if parent.isMiniaturized { parent.deminiaturize(self) } // If our parent tab group already has this window, macOS added it and // we need to remove it so we can set the correct order in the next line. @@ -405,7 +405,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } // If we don't allow tabs then we create a new window instead. - if (window.tabbingMode != .disallowed) { + if window.tabbingMode != .disallowed { // Add the window to the tab group and show it. switch ghostty.config.windowNewTabPosition { case "end": @@ -491,7 +491,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr ] as? Ghostty.Config else { return } // If this is an app-level config update then we update some things. - if (notification.object == nil) { + if notification.object == nil { // Update our derived config self.derivedConfig = DerivedConfig(config) @@ -907,7 +907,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr alert.addButton(withTitle: "Cancel") alert.alertStyle = .warning alert.beginSheetModal(for: confirmWindow, completionHandler: { response in - if (response == .alertFirstButtonReturn) { + if response == .alertFirstButtonReturn { // This is important so that we avoid losing focus when Stage // Manager is used (#8336) alert.window.orderOut(nil) @@ -1013,7 +1013,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Setting all three of these is required for restoration to work. window.isRestorable = restorable - if (restorable) { + if restorable { window.restorationClass = TerminalWindowRestoration.self window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } @@ -1035,7 +1035,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // If we have a default size, we want to apply it. if let defaultSize { - switch (defaultSize) { + switch defaultSize { case .frame: // Frames can be applied immediately defaultSize.apply(to: window) @@ -1071,7 +1071,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // We don't run this logic in fullscreen because in fullscreen this will end up // removing the window and putting it into its own dedicated fullscreen, which is not // the expected or desired behavior of anyone I've found. - if (!window.styleMask.contains(.fullScreen)) { + if !window.styleMask.contains(.fullScreen) { // If we have more than 1 window in our tab group we know we're a new window. // Since Ghostty manages tabbing manually this will never be more than one // at this point in the AppKit lifecycle (we add to the group after this). @@ -1101,7 +1101,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr override func windowShouldClose(_ sender: NSWindow) -> Bool { tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in guard let self else { return } - switch (scope) { + switch scope { case .tab: closeTab(nil) case .window: guard self.window?.isFirstWindowInTabGroup ?? false else { return } @@ -1430,23 +1430,23 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let finalIndex: Int // An index that is invalid is used to signal some special values. - if (tabIndex <= 0) { + if tabIndex <= 0 { guard let selectedWindow = tabGroup.selectedWindow else { return } guard let selectedIndex = tabbedWindows.firstIndex(where: { $0 == selectedWindow }) else { return } - if (tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue) { - if (selectedIndex == 0) { + if tabIndex == GHOSTTY_GOTO_TAB_PREVIOUS.rawValue { + if selectedIndex == 0 { finalIndex = tabbedWindows.count - 1 } else { finalIndex = selectedIndex - 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue) { - if (selectedIndex == tabbedWindows.count - 1) { + } else if tabIndex == GHOSTTY_GOTO_TAB_NEXT.rawValue { + if selectedIndex == tabbedWindows.count - 1 { finalIndex = 0 } else { finalIndex = selectedIndex + 1 } - } else if (tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue) { + } else if tabIndex == GHOSTTY_GOTO_TAB_LAST.rawValue { finalIndex = tabbedWindows.count - 1 } else { return diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index fd0f4eab5..c3cc6961c 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -98,7 +98,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // no matter what. Note its safe to use "ghostty.config" directly here // because window restoration is only ever invoked on app start so we // don't have to deal with config reloads. - if (appDelegate.ghostty.config.windowSaveState == "never") { + if appDelegate.ghostty.config.windowSaveState == "never" { completionHandler(nil, nil) return } @@ -161,9 +161,9 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // For the first attempt, we schedule it immediately. Subsequent events wait a bit // so we don't just spin the CPU at 100%. Give up after some period of time. let after: DispatchTime - if (attempts == 0) { + if attempts == 0 { after = .now() - } else if (attempts > 40) { + } else if attempts > 40 { // 2 seconds, give up return } else { @@ -185,7 +185,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // If the window is main, then we also make sure it comes forward. This // prevents a bug found in #1177 where sometimes on restore the windows // would be behind other applications. - if (viewWindow.isMainWindow) { + if viewWindow.isMainWindow { viewWindow.orderFront(nil) } } diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index e117e0647..914e9119f 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -76,7 +76,7 @@ struct TerminalView: View { VStack(spacing: 0) { // If we're running in debug mode we show a warning so that users // know that performance will be degraded. - if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { + if Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE { DebugBuildWarningView() } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 686d561f5..cab33856a 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -112,7 +112,7 @@ class TerminalWindow: NSWindow { } // If window decorations are disabled, remove our title - if (!config.windowDecorations) { styleMask.remove(.titled) } + if !config.windowDecorations { styleMask.remove(.titled) } // Set our window positioning to coordinates if config value exists, otherwise // fallback to original centering behavior @@ -510,7 +510,7 @@ class TerminalWindow: NSWindow { private func setInitialWindowPosition(x: Int16?, y: Int16?) { // If we don't have an X/Y then we try to use the previously saved window pos. guard x != nil, y != nil else { - if (!LastWindowPosition.shared.restore(self)) { + if !LastWindowPosition.shared.restore(self) { center() } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 918191522..21d5ef0dd 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -199,7 +199,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // The padding for the tab bar. If we're showing window buttons then // we need to offset the window buttons. - let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + let leftPadding: CGFloat = switch self.derivedConfig.macosWindowButtons { case .hidden: 0 case .visible: 70 } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index c47c4cb8b..1fe68cd51 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -159,7 +159,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } - if (isOpaque || themeChanged) { + if isOpaque || themeChanged { // If there is transparency, calling this will make the titlebar opaque // so we only call this if we are opaque. updateTabBar() @@ -359,7 +359,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { let isTabBar = self.titlebarTabs && isTabBar(childViewController) - if (isTabBar) { + if isTabBar { // Ensure it has the right layoutAttribute to force it next to our titlebar childViewController.layoutAttribute = .right @@ -374,7 +374,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { super.addTitlebarAccessoryViewController(childViewController) - if (isTabBar) { + if isTabBar { pushTabsToTitlebar(childViewController) } } @@ -382,7 +382,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { override func removeTitlebarAccessoryViewController(at index: Int) { let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.tabBarIdentifier super.removeTitlebarAccessoryViewController(at: index) - if (isTabBar) { + if isTabBar { resetCustomTabBarViews() } } @@ -403,7 +403,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { // We need a toolbar as a target for our titlebar tabs. - if (toolbar == nil) { + if toolbar == nil { generateToolbar() } @@ -509,7 +509,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { fileprivate class WindowDragView: NSView { override public func mouseDown(with event: NSEvent) { // Drag the window for single left clicks, double clicks should bypass the drag handle. - if (event.type == .leftMouseDown && event.clickCount == 1) { + if event.type == .leftMouseDown && event.clickCount == 1 { window?.performDrag(with: event) NSCursor.closedHand.set() } else { diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index 619540851..cbfbdede0 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -10,7 +10,7 @@ extension UpdateDriver: SPUUpdaterDelegate { // Sparkle supports a native concept of "channels" but it requires that // you share a single appcast file. We don't want to do that so we // do this instead. - switch (appDelegate.ghostty.config.autoUpdateChannel) { + switch appDelegate.ghostty.config.autoUpdateChannel { case .tip: return "https://tip.files.ghostty.org/appcast.xml" case .stable: return "https://release.files.ghostty.org/appcast.xml" } diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index 1f9304616..a3ef765cf 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -192,7 +192,7 @@ enum UpdateState: Equatable { /// This is true if we're in a state that can be force installed. var isInstallable: Bool { - switch (self) { + switch self { case .checking, .updateAvailable, .downloading, @@ -339,7 +339,7 @@ enum UpdateState: Equatable { } var label: String { - switch (self) { + switch self { case .commit: return "View GitHub Commit" case .compareTip: return "Changes Since This Tip Release" case .tagged: return "View Release Notes" diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 91f1491dd..1bd197e7d 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -18,7 +18,7 @@ extension Ghostty.Action { } init(c: ghostty_action_color_change_s) { - switch (c.kind) { + switch c.kind { case GHOSTTY_ACTION_COLOR_KIND_FOREGROUND: self.kind = .foreground case GHOSTTY_ACTION_COLOR_KIND_BACKGROUND: diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e3441257f..ecf3709d6 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -140,7 +140,7 @@ extension Ghostty { guard let app = self.app else { return } // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_app_update_config(app, config.config!) return } @@ -158,7 +158,7 @@ extension Ghostty { func reloadConfig(surface: ghostty_surface_t, soft: Bool = false) { // Soft updates just call with our existing config - if (soft) { + if soft { ghostty_surface_update_config(surface, config.config!) return } @@ -183,14 +183,14 @@ extension Ghostty { func newTab(surface: ghostty_surface_t) { let action = "new_tab" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func newWindow(surface: ghostty_surface_t) { let action = "new_window" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -213,14 +213,14 @@ extension Ghostty { func splitToggleZoom(surface: ghostty_surface_t) { let action = "toggle_split_zoom" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleFullscreen(surface: ghostty_surface_t) { let action = "toggle_fullscreen" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -241,21 +241,21 @@ extension Ghostty { case .reset: action = "reset_font_size" } - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func toggleTerminalInspector(surface: ghostty_surface_t) { let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } func resetTerminal(surface: ghostty_surface_t) { let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { logger.warning("action failed action=\(action)") } } @@ -463,7 +463,7 @@ extension Ghostty { static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { // Make sure it a target we understand so all our action handlers can assert - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE: break @@ -473,7 +473,7 @@ extension Ghostty { } // Action dispatch - switch (action.tag) { + switch action.tag { case GHOSTTY_ACTION_QUIT: quit(app) @@ -722,7 +722,7 @@ extension Ghostty { private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -743,7 +743,7 @@ extension Ghostty { private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: undoManager = (NSApp.delegate as? AppDelegate)?.undoManager @@ -763,7 +763,7 @@ extension Ghostty { } private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewWindow, @@ -789,7 +789,7 @@ extension Ghostty { } private static func newTab(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: NotificationCenter.default.post( name: Notification.ghosttyNewTab, @@ -829,7 +829,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_split_direction_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // New split does nothing with an app target Ghostty.logger.warning("new split does nothing with an app target") @@ -858,7 +858,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: return false @@ -879,7 +879,7 @@ extension Ghostty { } private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close tabs does nothing with an app target") return @@ -888,7 +888,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (mode) { + switch mode { case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: NotificationCenter.default.post( name: .ghosttyCloseTab, @@ -921,7 +921,7 @@ extension Ghostty { } private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("close window does nothing with an app target") return @@ -949,7 +949,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode raw: ghostty_action_fullscreen_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle fullscreen does nothing with an app target") return @@ -978,7 +978,7 @@ extension Ghostty { private static func toggleCommandPalette( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle command palette does nothing with an app target") return @@ -1001,7 +1001,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle maximize does nothing with an app target") return @@ -1031,7 +1031,7 @@ extension Ghostty { private static func ringBell( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Technically we could still request app attention here but there // are no known cases where the bell is rang with an app target so @@ -1056,7 +1056,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_readonly_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set readonly does nothing with an app target") return @@ -1081,7 +1081,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, move: ghostty_action_move_tab_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("move tab does nothing with an app target") return false @@ -1112,7 +1112,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, tab: ghostty_action_goto_tab_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto tab does nothing with an app target") return false @@ -1144,7 +1144,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, direction: ghostty_action_goto_split_e) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("goto split does nothing with an app target") return false @@ -1250,7 +1250,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, resize: ghostty_action_resize_split_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("resize split does nothing with an app target") return false @@ -1283,7 +1283,7 @@ extension Ghostty { private static func equalizeSplits( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("equalize splits does nothing with an app target") return @@ -1305,7 +1305,7 @@ extension Ghostty { private static func toggleSplitZoom( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") return false @@ -1335,7 +1335,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_inspector_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle inspector does nothing with an app target") return @@ -1359,7 +1359,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, n: ghostty_action_desktop_notification_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle split zoom does nothing with an app target") return @@ -1395,7 +1395,7 @@ extension Ghostty { ) { guard let mode = SetFloatWIndow.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle float window does nothing with an app target") return @@ -1405,7 +1405,7 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } guard let window = surfaceView.window as? TerminalWindow else { return } - switch (mode) { + switch mode { case .on: window.level = .floating @@ -1429,7 +1429,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("toggle background opacity does nothing with an app target") return @@ -1453,7 +1453,7 @@ extension Ghostty { ) { guard let mode = SetSecureInput.from(mode_raw) else { return } - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } appDelegate.setSecureInput(mode) @@ -1464,7 +1464,7 @@ extension Ghostty { guard let appState = self.appState(fromView: surfaceView) else { return } guard appState.config.autoSecureInput else { return } - switch (mode) { + switch mode { case .on: surfaceView.passwordInput = true @@ -1492,7 +1492,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_set_title_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title does nothing with an app target") return @@ -1511,7 +1511,7 @@ extension Ghostty { private static func copyTitleToClipboard( _ app: ghostty_app_t, target: ghostty_target_s) -> Bool { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return false } guard let surfaceView = self.surfaceView(from: surface) else { return false } @@ -1534,7 +1534,7 @@ extension Ghostty { let promptTitle = Action.PromptTitle(v) switch promptTitle { case .surface: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set title prompt does nothing with an app target") return false @@ -1551,7 +1551,7 @@ extension Ghostty { } case .tab: - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: guard let window = NSApp.mainWindow ?? NSApp.keyWindow, let controller = window.windowController as? BaseTerminalController @@ -1579,7 +1579,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_pwd_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("pwd change does nothing with an app target") return @@ -1599,7 +1599,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, shape: ghostty_action_mouse_shape_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1619,7 +1619,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_visibility_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("set mouse shapes nothing with an app target") return @@ -1627,7 +1627,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - switch (v) { + switch v { case GHOSTTY_MOUSE_VISIBLE: surfaceView.setCursorVisibility(true) @@ -1648,7 +1648,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_mouse_over_link_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1674,7 +1674,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_initial_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("initial size does nothing with an app target") return @@ -1693,7 +1693,7 @@ extension Ghostty { private static func resetWindowSize( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("reset window size does nothing with an app target") return @@ -1716,7 +1716,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_cell_size_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1738,7 +1738,7 @@ extension Ghostty { private static func renderInspector( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1760,7 +1760,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_renderer_health_e) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("mouse over link does nothing with an app target") return @@ -1785,7 +1785,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_sequence_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key sequence does nothing with an app target") return @@ -1817,7 +1817,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_key_table_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("key table does nothing with an app target") return @@ -1842,7 +1842,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_progress_report_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("progress report does nothing with an app target") return @@ -1869,7 +1869,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_scrollbar_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("scrollbar does nothing with an app target") return @@ -1896,7 +1896,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_start_search_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("start_search does nothing with an app target") return @@ -1926,7 +1926,7 @@ extension Ghostty { private static func endSearch( _ app: ghostty_app_t, target: ghostty_target_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("end_search does nothing with an app target") return @@ -1948,7 +1948,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_total_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_total does nothing with an app target") return @@ -1971,7 +1971,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, v: ghostty_action_search_selected_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("search_selected does nothing with an app target") return @@ -2000,7 +2000,7 @@ extension Ghostty { guard let app_ud = ghostty_app_userdata(app) else { return } let ghostty = Unmanaged.fromOpaque(app_ud).takeUnretainedValue() - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: ghostty.reloadConfig(soft: v.soft) return @@ -2026,7 +2026,7 @@ extension Ghostty { // something so apprt's do not have to do this. let config = Config(clone: v.config) - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: // Notify the world that the app config changed NotificationCenter.default.post( @@ -2066,7 +2066,7 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s, change: ghostty_action_color_change_s) { - switch (target.tag) { + switch target.tag { case GHOSTTY_TARGET_APP: Ghostty.logger.warning("color change does nothing with an app target") return @@ -2097,7 +2097,7 @@ extension Ghostty { let uuid = UUID(uuidString: uuidString), let surface = delegate?.findSurface(forUUID: uuid) else { return } - switch (response.actionIdentifier) { + switch response.actionIdentifier { case UNNotificationDefaultActionIdentifier, Ghostty.userNotificationActionShow: // The user clicked on a notification surface.handleUserNotification(notification: response.notification, focus: true) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 88ad9fb07..555b93029 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -427,7 +427,7 @@ extension Ghostty { var backgroundColor: Color { var color: ghostty_config_color_s = .init(); let bg_key = "background" - if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))) { #if os(macOS) return Color(NSColor.windowBackgroundColor) #elseif os(iOS) @@ -473,7 +473,7 @@ extension Ghostty { var color: ghostty_config_color_s = .init(); let key = "unfocused-split-fill" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { let bg_key = "background" _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8))); } @@ -494,7 +494,7 @@ extension Ghostty { var color: ghostty_config_color_s = .init(); let key = "split-divider-color" - if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) { + if !ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8))) { return Color(newColor) } @@ -801,28 +801,28 @@ extension Ghostty.Config { case bottom_right = "bottom-right" func top() -> Bool { - switch (self) { + switch self { case .top_left, .top_center, .top_right: return true; default: return false; } } func bottom() -> Bool { - switch (self) { + switch self { case .bottom_left, .bottom_center, .bottom_right: return true; default: return false; } } func left() -> Bool { - switch (self) { + switch self { case .top_left, .bottom_left: return true; default: return false; } } func right() -> Bool { - switch (self) { + switch self { case .top_right, .bottom_right: return true; default: return false; } diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 04d7560fd..563e4a794 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -18,7 +18,7 @@ extension Ghostty { /// be used for things like NSMenu that only support keyboard shortcuts anyways. static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? { let key: KeyEquivalent - switch (trigger.tag) { + switch trigger.tag { case GHOSTTY_TRIGGER_PHYSICAL: // Only functional keys can be converted to a KeyboardShortcut. Other physical // mappings cannot because KeyboardShortcut in Swift is inherently layout-dependent. @@ -50,10 +50,10 @@ extension Ghostty { /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { var flags = NSEvent.ModifierFlags(rawValue: 0); - if (mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0) { flags.insert(.shift) } - if (mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0) { flags.insert(.control) } - if (mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0) { flags.insert(.option) } - if (mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0) { flags.insert(.command) } + if mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0 { flags.insert(.shift) } + if mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0 { flags.insert(.control) } + if mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0 { flags.insert(.option) } + if mods.rawValue & GHOSTTY_MODS_SUPER.rawValue != 0 { flags.insert(.command) } return flags } @@ -61,19 +61,19 @@ extension Ghostty { static func ghosttyMods(_ flags: NSEvent.ModifierFlags) -> ghostty_input_mods_e { var mods: UInt32 = GHOSTTY_MODS_NONE.rawValue - if (flags.contains(.shift)) { mods |= GHOSTTY_MODS_SHIFT.rawValue } - if (flags.contains(.control)) { mods |= GHOSTTY_MODS_CTRL.rawValue } - if (flags.contains(.option)) { mods |= GHOSTTY_MODS_ALT.rawValue } - if (flags.contains(.command)) { mods |= GHOSTTY_MODS_SUPER.rawValue } - if (flags.contains(.capsLock)) { mods |= GHOSTTY_MODS_CAPS.rawValue } + if flags.contains(.shift) { mods |= GHOSTTY_MODS_SHIFT.rawValue } + if flags.contains(.control) { mods |= GHOSTTY_MODS_CTRL.rawValue } + if flags.contains(.option) { mods |= GHOSTTY_MODS_ALT.rawValue } + if flags.contains(.command) { mods |= GHOSTTY_MODS_SUPER.rawValue } + if flags.contains(.capsLock) { mods |= GHOSTTY_MODS_CAPS.rawValue } // Handle sided input. We can't tell that both are pressed in the // Ghostty structure but that's okay -- we don't use that information. let rawFlags = flags.rawValue - if (rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0) { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0) { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0) { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } - if (rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0) { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERSHIFTKEYMASK) != 0 { mods |= GHOSTTY_MODS_SHIFT_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERCTLKEYMASK) != 0 { mods |= GHOSTTY_MODS_CTRL_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERALTKEYMASK) != 0 { mods |= GHOSTTY_MODS_ALT_RIGHT.rawValue } + if rawFlags & UInt(NX_DEVICERCMDKEYMASK) != 0 { mods |= GHOSTTY_MODS_SUPER_RIGHT.rawValue } return ghostty_input_mods_e(mods) } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 7cb32ed71..b072db15e 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -40,7 +40,7 @@ extension Ghostty { @MainActor func sendText(_ text: String) { let len = text.utf8CString.count - if (len == 0) { return } + if len == 0 { return } text.withCString { ptr in // len includes the null terminator so we do len - 1 @@ -149,7 +149,7 @@ extension Ghostty { @MainActor func perform(action: String) -> Bool { let len = action.utf8CString.count - if (len == 0) { return false } + if len == 0 { return false } return action.withCString { cString in ghostty_surface_binding_action(surface, cString, UInt(len - 1)) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 15b0bd9b0..41dcfad28 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -100,7 +100,7 @@ extension Ghostty { case toggle static func from(_ c: ghostty_action_float_window_e) -> Self? { - switch (c) { + switch c { case GHOSTTY_FLOAT_WINDOW_ON: return .on @@ -122,7 +122,7 @@ extension Ghostty { case toggle static func from(_ c: ghostty_action_secure_input_e) -> Self? { - switch (c) { + switch c { case GHOSTTY_SECURE_INPUT_ON: return .on @@ -144,7 +144,7 @@ extension Ghostty { /// Initialize from a Ghostty API enum. static func from(direction: ghostty_action_goto_split_e) -> Self? { - switch (direction) { + switch direction { case GHOSTTY_GOTO_SPLIT_PREVIOUS: return .previous @@ -169,7 +169,7 @@ extension Ghostty { } func toNative() -> ghostty_action_goto_split_e { - switch (self) { + switch self { case .previous: return GHOSTTY_GOTO_SPLIT_PREVIOUS @@ -196,7 +196,7 @@ extension Ghostty { case up, down, left, right static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { - switch (direction) { + switch direction { case GHOSTTY_RESIZE_SPLIT_UP: return .up; case GHOSTTY_RESIZE_SPLIT_DOWN: @@ -211,7 +211,7 @@ extension Ghostty { } func toNative() -> ghostty_action_resize_split_direction_e { - switch (self) { + switch self { case .up: return GHOSTTY_RESIZE_SPLIT_UP; case .down: @@ -268,7 +268,7 @@ extension Ghostty { /// The text to show in the clipboard confirmation prompt for a given request type func text() -> String { - switch (self) { + switch self { case .paste: return """ Pasting this text to the terminal may be dangerous as it looks like some commands may be executed. @@ -287,7 +287,7 @@ extension Ghostty { } static func from(request: ghostty_clipboard_request_e) -> ClipboardRequest? { - switch (request) { + switch request { case GHOSTTY_CLIPBOARD_REQUEST_PASTE: return .paste case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ: diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 03be794e9..23fbc854b 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -23,7 +23,7 @@ extension Ghostty { let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView) ZStack { - if (!surfaceView.inspectorVisible) { + if !surfaceView.inspectorVisible { SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit) } else { SplitView(.vertical, $split, dividerColor: ghostty.config.splitDividerColor, left: { @@ -42,7 +42,7 @@ extension Ghostty { .onChange(of: surfaceView.inspectorVisible) { inspectorVisible in // When we show the inspector, we want to focus on the inspector. // When we hide the inspector, we want to move focus back to the surface. - if (inspectorVisible) { + if inspectorVisible { // We need to delay this until SwiftUI shows the inspector. DispatchQueue.main.async { _ = surfaceView.resignFirstResponder() @@ -59,7 +59,7 @@ extension Ghostty { guard let modeAny = notification.userInfo?["mode"] else { return } guard let mode = modeAny as? ghostty_action_inspector_e else { return } - switch (mode) { + switch mode { case GHOSTTY_INSPECTOR_TOGGLE: surfaceView.inspectorVisible = !surfaceView.inspectorVisible @@ -180,7 +180,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(true) } @@ -190,7 +190,7 @@ extension Ghostty { override func resignFirstResponder() -> Bool { let result = super.resignFirstResponder() - if (result) { + if result { if let inspector = self.inspector { inspector.setFocus(false) } @@ -275,7 +275,7 @@ extension Ghostty { // Determine our momentum value var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE - switch (event.momentumPhase) { + switch event.momentumPhase { case .began: momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN case .stationary: @@ -310,7 +310,7 @@ extension Ghostty { override func flagsChanged(with event: NSEvent) { let mod: UInt32; - switch (event.keyCode) { + switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue @@ -325,7 +325,7 @@ extension Ghostty { // If the key that pressed this is active, its a press, else release var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { action = GHOSTTY_ACTION_PRESS } + if mods.rawValue & mod != 0 { action = GHOSTTY_ACTION_PRESS } keyAction(action, event: event) } @@ -392,7 +392,7 @@ extension Ghostty { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -402,7 +402,7 @@ extension Ghostty { } let len = chars.utf8CString.count - if (len == 0) { return } + if len == 0 { return } inspector.text(chars) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index c5c2ee97c..d72fa742c 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -84,7 +84,7 @@ extension Ghostty { .onReceive(pubResign) { notification in guard let window = notification.object as? NSWindow else { return } guard let surfaceWindow = surfaceView.window else { return } - if (surfaceWindow == window) { + if surfaceWindow == window { windowFocus = false } } @@ -177,10 +177,10 @@ extension Ghostty { #if canImport(AppKit) // If we have secure input enabled and we're the focused surface and window // then we want to show the secure input overlay. - if (ghostty.config.secureInputIndication && + if ghostty.config.secureInputIndication && secureInput.enabled && surfaceFocus && - windowFocus) { + windowFocus { SecureInputOverlay() } #endif @@ -200,7 +200,7 @@ extension Ghostty { } // Show bell border if enabled - if (ghostty.config.bellFeatures.contains(.border)) { + if ghostty.config.bellFeatures.contains(.border) { BellBorderOverlay(bell: surfaceView.bell) } @@ -208,10 +208,10 @@ extension Ghostty { HighlightOverlay(highlighted: surfaceView.highlighted) // If our surface is not healthy, then we render an error view over it. - if (!surfaceView.healthy) { + if !surfaceView.healthy { Rectangle().fill(ghostty.config.backgroundColor) SurfaceRendererUnhealthyView() - } else if (surfaceView.error != nil) { + } else if surfaceView.error != nil { Rectangle().fill(ghostty.config.backgroundColor) SurfaceErrorView() } @@ -220,9 +220,9 @@ extension Ghostty { // rectangle above our view to make it look unfocused. We use "surfaceFocus" // because we want to keep our focused surface dark even if we don't have window // focus. - if (isSplit && !surfaceFocus) { + if isSplit && !surfaceFocus { let overlayOpacity = ghostty.config.unfocusedSplitOpacity; - if (overlayOpacity > 0) { + if overlayOpacity > 0 { Rectangle() .fill(ghostty.config.unfocusedSplitFill) .allowsHitTesting(false) @@ -312,16 +312,16 @@ extension Ghostty { // This computed boolean is set to true when the overlay should be hidden. private var hidden: Bool { // If we aren't ready yet then we wait... - if (!ready) { return true; } + if !ready { return true; } // Hidden if we already processed this size. - if (lastSize == geoSize) { return true; } + if lastSize == geoSize { return true; } // If we were focused recently we hide it as well. This avoids showing // the resize overlay when SwiftUI is lazily resizing. if let instant = focusInstant { let d = instant.duration(to: ContinuousClock.now) - if (d < .milliseconds(500)) { + if d < .milliseconds(500) { // Avoid this size completely. We can't set values during // view updates so we have to defer this to another tick. DispatchQueue.main.async { @@ -333,7 +333,7 @@ extension Ghostty { } // Hidden depending on overlay config - switch (overlay) { + switch overlay { case .never: return true; case .always: return false; case .after_first: return lastSize == nil; @@ -342,12 +342,12 @@ extension Ghostty { var body: some View { VStack { - if (!position.top()) { + if !position.top() { Spacer() } HStack { - if (!position.left()) { + if !position.left() { Spacer() } @@ -361,12 +361,12 @@ extension Ghostty { .lineLimit(1) .truncationMode(.tail) - if (!position.right()) { + if !position.right() { Spacer() } } - if (!position.bottom()) { + if !position.bottom() { Spacer() } } @@ -386,7 +386,7 @@ extension Ghostty { // We only sleep if we're ready. If we're not ready then we want to set // our last size right away to avoid a flash. - if (ready) { + if ready { try? await Task.sleep(nanoseconds: UInt64(duration) * 1_000_000) } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 85dc1d612..6f4f3c497 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -151,7 +151,7 @@ extension Ghostty { // We need to update our state within the SecureInput manager. let input = SecureInput.shared let id = ObjectIdentifier(self) - if (passwordInput) { + if passwordInput { input.setScoped(id, focused: focused) } else { input.removeScoped(id) @@ -183,7 +183,7 @@ extension Ghostty { // True if the inspector should be visible @Published var inspectorVisible: Bool = false { didSet { - if (oldValue && !inspectorVisible) { + if oldValue && !inspectorVisible { guard let surface = self.surface else { return } ghostty_inspector_free(surface) } @@ -431,11 +431,11 @@ extension Ghostty { ghostty_surface_set_focus(surface, focused) // Update our secure input state if we are a password input - if (passwordInput) { + if passwordInput { SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused) } - if (focused) { + if focused { // On macOS 13+ we can store our continuous clock... focusInstant = ContinuousClock.now @@ -480,7 +480,7 @@ extension Ghostty { } func setCursorShape(_ shape: ghostty_action_mouse_shape_e) { - switch (shape) { + switch shape { case GHOSTTY_MOUSE_SHAPE_DEFAULT: pointerStyle = .default @@ -656,7 +656,7 @@ extension Ghostty { private func localEventKeyUp(_ event: NSEvent) -> NSEvent? { // We only care about events with "command" because all others will // trigger the normal responder chain. - if (!event.modifierFlags.contains(.command)) { return event } + if !event.modifierFlags.contains(.command) { return event } // Command keyUp events are never sent to the normal responder chain // so we send them here. @@ -722,7 +722,7 @@ extension Ghostty { SwiftUI.Notification.Name.GhosttyColorChangeKey ] as? Ghostty.Action.ColorChange else { return } - switch (change.kind) { + switch change.kind { case .background: DispatchQueue.main.async { [weak self] in self?.backgroundColor = change.color @@ -767,7 +767,7 @@ extension Ghostty { override func becomeFirstResponder() -> Bool { let result = super.becomeFirstResponder() - if (result) { focusDidChange(true) } + if result { focusDidChange(true) } return result } @@ -776,7 +776,7 @@ extension Ghostty { // We sometimes call this manually (see SplitView) as a way to force us to // yield our focus state. - if (result) { focusDidChange(false) } + if result { focusDidChange(false) } return result } @@ -878,12 +878,12 @@ extension Ghostty { guard let surface = self.surface else { return super.rightMouseDown(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_PRESS, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Consumed return } @@ -896,12 +896,12 @@ extension Ghostty { guard let surface = self.surface else { return super.rightMouseUp(with: event) } let mods = Ghostty.ghosttyMods(event.modifierFlags) - if (ghostty_surface_mouse_button( + if ghostty_surface_mouse_button( surface, GHOSTTY_MOUSE_RELEASE, GHOSTTY_MOUSE_RIGHT, mods - )) { + ) { // Handled return } @@ -963,9 +963,9 @@ extension Ghostty { if let window, let controller = window.windowController as? BaseTerminalController, !controller.commandPaletteIsShowing, - (window.isKeyWindow && + window.isKeyWindow && !self.focused && - controller.focusFollowsMouse) + controller.focusFollowsMouse { Ghostty.moveFocus(to: self) } @@ -1048,7 +1048,7 @@ extension Ghostty { // for exact states and set them. var translationMods = event.modifierFlags for flag in [NSEvent.ModifierFlags.shift, .control, .option, .command] { - if (translationModsGhostty.contains(flag)) { + if translationModsGhostty.contains(flag) { translationMods.insert(flag) } else { translationMods.remove(flag) @@ -1061,7 +1061,7 @@ extension Ghostty { // this keeps things like Korean input working. There must be some object // equality happening in AppKit somewhere because this is required. let translationEvent: NSEvent - if (translationMods == event.modifierFlags) { + if translationMods == event.modifierFlags { translationEvent = event } else { translationEvent = NSEvent.keyEvent( @@ -1093,7 +1093,7 @@ extension Ghostty { // We need to know the keyboard layout before below because some keyboard // input events will change our keyboard layout and we don't want those // going to the terminal. - let keyboardIdBefore: String? = if (!markedTextBefore) { + let keyboardIdBefore: String? = if !markedTextBefore { KeyboardLayout.id } else { nil @@ -1108,7 +1108,7 @@ extension Ghostty { // If our keyboard changed from this we just assume an input method // grabbed it and do nothing. - if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) { + if !markedTextBefore && keyboardIdBefore != KeyboardLayout.id { return } @@ -1192,7 +1192,7 @@ extension Ghostty { // Besides C-/, its important we don't process key equivalents if unfocused // because there are other event listeners for that (i.e. AppDelegate's // local event handler). - if (!focused) { + if !focused { return false } @@ -1227,11 +1227,11 @@ extension Ghostty { } let equivalent: String - switch (event.charactersIgnoringModifiers) { + switch event.charactersIgnoringModifiers { case "\r": // Pass C- through verbatim // (prevent the default context menu equivalent) - if (!event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.control) { return false } @@ -1240,8 +1240,8 @@ extension Ghostty { case "/": // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep // sound and we don't like the beep sound. - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { + if !event.modifierFlags.contains(.control) || + !event.modifierFlags.isDisjoint(with: [.shift, .command, .option]) { return false } @@ -1265,8 +1265,8 @@ extension Ghostty { // Ignore all other non-command events. This lets the event continue // through the AppKit event systems. - if (!event.modifierFlags.contains(.command) && - !event.modifierFlags.contains(.control)) { + if !event.modifierFlags.contains(.command) && + !event.modifierFlags.contains(.control) { // Reset since we got a non-command event. lastPerformKeyEvent = nil return false @@ -1305,7 +1305,7 @@ extension Ghostty { override func flagsChanged(with event: NSEvent) { let mod: UInt32; - switch (event.keyCode) { + switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue @@ -1323,13 +1323,13 @@ extension Ghostty { // If the key that pressed this is active, its a press, else release. var action = GHOSTTY_ACTION_RELEASE - if (mods.rawValue & mod != 0) { + if mods.rawValue & mod != 0 { // If the key is pressed, its slightly more complicated, because we // want to check if the pressed modifier is the correct side. If the // correct side is pressed then its a press event otherwise its a release // event with the opposite modifier still held. let sidePressed: Bool - switch (event.keyCode) { + switch event.keyCode { case 0x3C: sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0; case 0x3E: @@ -1342,7 +1342,7 @@ extension Ghostty { sidePressed = true } - if (sidePressed) { + if sidePressed { action = GHOSTTY_ACTION_PRESS } } @@ -1483,7 +1483,7 @@ extension Ghostty { @IBAction func copy(_ sender: Any?) { guard let surface = self.surface else { return } let action = "copy_to_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1491,7 +1491,7 @@ extension Ghostty { @IBAction func paste(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1500,7 +1500,7 @@ extension Ghostty { @IBAction func pasteAsPlainText(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1508,7 +1508,7 @@ extension Ghostty { @IBAction func pasteSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1516,7 +1516,7 @@ extension Ghostty { @IBAction override func selectAll(_ sender: Any?) { guard let surface = self.surface else { return } let action = "select_all" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1524,7 +1524,7 @@ extension Ghostty { @IBAction func find(_ sender: Any?) { guard let surface = self.surface else { return } let action = "start_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1532,7 +1532,7 @@ extension Ghostty { @IBAction func selectionForFind(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1540,7 +1540,7 @@ extension Ghostty { @IBAction func scrollToSelection(_ sender: Any?) { guard let surface = self.surface else { return } let action = "scroll_to_selection" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1548,7 +1548,7 @@ extension Ghostty { @IBAction func findNext(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:next" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1556,7 +1556,7 @@ extension Ghostty { @IBAction func findPrevious(_ sender: Any?) { guard let surface = self.surface else { return } let action = "search:previous" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1564,7 +1564,7 @@ extension Ghostty { @IBAction func findHide(_ sender: Any?) { guard let surface = self.surface else { return } let action = "end_search" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1572,7 +1572,7 @@ extension Ghostty { @IBAction func toggleReadonly(_ sender: Any?) { guard let surface = self.surface else { return } let action = "toggle_readonly" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1608,7 +1608,7 @@ extension Ghostty { @objc func resetTerminal(_ sender: Any) { guard let surface = self.surface else { return } let action = "reset" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1616,7 +1616,7 @@ extension Ghostty { @objc func toggleTerminalInspector(_ sender: Any) { guard let surface = self.surface else { return } let action = "inspector:toggle" - if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) { + if !ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8))) { AppDelegate.logger.warning("action failed action=\(action)") } } @@ -1657,7 +1657,7 @@ extension Ghostty { // If we're focused then we schedule to remove the notification // after a few seconds. If we gain focus we automatically remove it // in focusDidChange. - if (self.focused) { + if self.focused { Task { @MainActor [weak self] in try await Task.sleep(for: .seconds(3)) self?.notificationIdentifiers.remove(uuid) @@ -1913,7 +1913,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // We want the string view of the any value var chars = "" - switch (string) { + switch string { case let v as NSAttributedString: chars = v.string case let v as String: @@ -2052,7 +2052,7 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { guard let str = pboard.getOpinionatedStringContents() else { return false } let len = str.utf8CString.count - if (len == 0) { return true } + if len == 0 { return true } str.withCString { ptr in // len includes the null terminator so we do len - 1 ghostty_surface_text(surface, ptr, UInt(len - 1)) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index f9baf56c9..aafad909a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -98,7 +98,7 @@ extension Ghostty { ghostty_surface_set_focus(surface, focused) // On macOS 13+ we can store our continuous clock... - if (focused) { + if focused { focusInstant = ContinuousClock.now } } diff --git a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift index 28edb1a35..c45f37a62 100644 --- a/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift @@ -9,7 +9,7 @@ extension NSAppearance { /// Initialize a desired NSAppearance for the Ghostty configuration. convenience init?(ghosttyConfig config: Ghostty.Config) { guard let theme = config.windowTheme else { return nil } - switch (theme) { + switch theme { case "dark": self.init(named: .darkAqua) diff --git a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift index 0bc79fb6a..2d3bc2cba 100644 --- a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift @@ -18,7 +18,7 @@ extension NSApplication { func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) { guard let value = Self.presentationOptionCounts[option] else { return } guard value > 0 else { return } - if (value == 1) { + if value == 1 { presentationOptions.remove(option) Self.presentationOptionCounts.removeValue(forKey: option) } else { diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index a036f02b4..11ab2b14a 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -50,7 +50,7 @@ extension NSPasteboard { /// The pasteboard for the Ghostty enum type. static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? { - switch (clipboard) { + switch clipboard { case GHOSTTY_CLIPBOARD_STANDARD: return Self.general diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index a8eb7b876..eea67ee2d 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -19,7 +19,7 @@ extension NSScreen { var hasDock: Bool { // If the dock autohides then we don't have a dock ever. if let dockAutohide = UserDefaults.standard.persistentDomain(forName: "com.apple.dock")?["autohide"] as? Bool { - if (dockAutohide) { return false } + if dockAutohide { return false } } // There is no public API to directly ask about dock visibility, so we have to figure it out @@ -29,7 +29,7 @@ extension NSScreen { // which triggers showing the dock. // If our visible width is less than the frame we assume its the dock. - if (visibleFrame.width < frame.width) { + if visibleFrame.width < frame.width { return true } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 8ab476267..6773b6f0c 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -204,12 +204,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // We must hide the dock FIRST then hide the menu: // If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock. // https://developer.apple.com/documentation/appkit/nsapplication/presentationoptions-swift.struct - if (savedState.dock) { + if savedState.dock { hideDock() } // Hide the menu if requested - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { hideMenu() } @@ -261,7 +261,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if savedState.dock { unhideDock() } - if (properties.hideMenu && savedState.menu) { + if properties.hideMenu && savedState.menu { unhideMenu() } @@ -328,8 +328,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // calculate this ourselves. var frame = screen.frame - if (!NSApp.presentationOptions.contains(.autoHideMenuBar) && - !NSApp.presentationOptions.contains(.hideMenuBar)) { + if !NSApp.presentationOptions.contains(.autoHideMenuBar) && + !NSApp.presentationOptions.contains(.hideMenuBar) { // We need to subtract the menu height since we're still showing it. frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 @@ -339,7 +339,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // put an #available check, but it was in a bug fix release so I think // if a bug is reported to Ghostty we can just advise the user to // update. - } else if (properties.paddedNotch) { + } else if properties.paddedNotch { // We are hiding the menu, we may need to avoid the notch. frame.size.height -= screen.safeAreaInsets.top } @@ -413,7 +413,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.toolbarStyle = window.toolbarStyle self.dock = window.screen?.hasDock ?? false - self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { + self.titlebarAccessoryViewControllers = if window.hasTitleBar { // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. window.titlebarAccessoryViewControllers } else { From 1b827e3e45f93b30767bfc4aa5e2131594dc60d0 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:35:17 -0500 Subject: [PATCH 25/51] macos: swiftlint 'empty_enum_arguments' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Features/Command Palette/CommandPalette.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index a87348f80..946dcc4d8 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -14,7 +14,6 @@ disabled_rules: # TODO - deployment_target - - empty_enum_arguments - empty_parentheses_with_trailing_closure - for_where - force_cast diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index eb2d631b6..38c915a58 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -128,7 +128,7 @@ struct CommandPaletteView: View { ? 0 : current + 1 - case .move(_): + case .move: // Unknown, ignore break } From a83c8f8a9d2b82956a02c24f82f5ab0ddaf9998c Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:35:40 -0500 Subject: [PATCH 26/51] macos: swiftlint 'empty_parentheses_with_trailing_closure' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Ghostty/Ghostty.App.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 946dcc4d8..64e141ea7 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -14,7 +14,6 @@ disabled_rules: # TODO - deployment_target - - empty_parentheses_with_trailing_closure - for_where - force_cast - implicit_getter diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index ecf3709d6..1d6e59577 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1377,7 +1377,7 @@ extension Ghostty { } } - center.getNotificationSettings() { settings in + center.getNotificationSettings { settings in guard settings.authorizationStatus == .authorized else { return } surfaceView.showUserNotification(title: title, body: body) } From 9287a61920da4262cd6c544dbd51c3e918d50174 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:36:38 -0500 Subject: [PATCH 27/51] macos: swiftlint 'implicit_optional_initialization' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 6 +-- .../ClipboardConfirmationController.swift | 2 +- .../ClipboardConfirmationView.swift | 2 +- .../Command Palette/CommandPalette.swift | 2 +- .../Global Keybinds/GlobalEventTap.swift | 4 +- .../QuickTerminalController.swift | 6 +-- .../QuickTerminal/QuickTerminalWindow.swift | 2 +- .../Terminal/BaseTerminalController.swift | 12 ++--- .../Terminal/TerminalController.swift | 4 +- .../Features/Terminal/TerminalView.swift | 2 +- .../Window Styles/TerminalWindow.swift | 4 +- .../TitlebarTabsVenturaTerminalWindow.swift | 6 +-- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- macos/Sources/Ghostty/Ghostty.Config.swift | 54 +++++++++---------- .../Ghostty/Surface View/InspectorView.swift | 2 +- .../Surface View/SurfaceDragSource.swift | 2 +- .../Ghostty/Surface View/SurfaceView.swift | 14 ++--- .../Surface View/SurfaceView_AppKit.swift | 24 ++++----- .../Surface View/SurfaceView_UIKit.swift | 12 ++--- 20 files changed, 81 insertions(+), 82 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 64e141ea7..192521deb 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -17,7 +17,6 @@ disabled_rules: - for_where - force_cast - implicit_getter - - implicit_optional_initialization - legacy_constant - legacy_constructor - line_length diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 5f5899654..54009bf96 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -144,16 +144,16 @@ class AppDelegate: NSObject, } /// Tracks the windows that we hid for toggleVisibility. - private(set) var hiddenState: ToggleVisibilityState? = nil + private(set) var hiddenState: ToggleVisibilityState? /// The observer for the app appearance. - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? /// Signals private var signals: [DispatchSourceSignal] = [] /// The custom app icon image that is currently in use. - @Published private(set) var appIcon: NSImage? = nil + @Published private(set) var appIcon: NSImage? override init() { #if DEBUG diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift index e8776edee..37b20afb0 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationController.swift @@ -13,7 +13,7 @@ class ClipboardConfirmationController: NSWindowController { let contents: String let request: Ghostty.ClipboardRequest let state: UnsafeMutableRawPointer? - weak private var delegate: ClipboardConfirmationViewDelegate? = nil + weak private var delegate: ClipboardConfirmationViewDelegate? init(surface: ghostty_surface_t, contents: String, request: Ghostty.ClipboardRequest, state: UnsafeMutableRawPointer?, delegate: ClipboardConfirmationViewDelegate) { self.surface = surface diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index de7052ff9..839061092 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -32,7 +32,7 @@ struct ClipboardConfirmationView: View { let request: Ghostty.ClipboardRequest /// Optional delegate to get results. If this is nil, then this view will never close on its own. - weak var delegate: ClipboardConfirmationViewDelegate? = nil + weak var delegate: ClipboardConfirmationViewDelegate? /// Used to track if we should rehide on disappear @State private var cursorHiddenCount: UInt = 0 diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 38c915a58..46542a4b5 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -231,7 +231,7 @@ struct CommandPaletteView: View { /// The text field for building the query for the command palette. fileprivate struct CommandPaletteQuery: View { @Binding var query: String - var onEvent: ((KeyboardEvent) -> Void)? = nil + var onEvent: ((KeyboardEvent) -> Void)? @FocusState private var isTextFieldFocused: Bool init(query: Binding, isTextFieldFocused: FocusState, onEvent: ((KeyboardEvent) -> Void)? = nil) { diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index e9541f434..a57895a86 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -16,11 +16,11 @@ class GlobalEventTap { // The event tap used for global event listening. This is non-nil if it is // created. - private var eventTap: CFMachPort? = nil + private var eventTap: CFMachPort? // This is the timer used to retry enabling the global event tap if we // don't have permissions. - private var enableTimer: Timer? = nil + private var enableTimer: Timer? // Private init so it can't be constructed outside of our singleton private init() {} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index dfcfa1f05..820420c77 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -16,16 +16,16 @@ class QuickTerminalController: BaseTerminalController { /// The previously running application when the terminal is shown. This is NEVER Ghostty. /// If this is set then when the quick terminal is animated out then we will restore this /// application to the front. - private var previousApp: NSRunningApplication? = nil + private var previousApp: NSRunningApplication? // The active space when the quick terminal was last shown. - private var previousActiveSpace: CGSSpace? = nil + private var previousActiveSpace: CGSSpace? /// Cache for per-screen window state. let screenStateCache: QuickTerminalScreenStateCache /// Non-nil if we have hidden dock state. - private var hiddenDock: HiddenDock? = nil + private var hiddenDock: HiddenDock? /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 1a4170dbc..680e6008a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -32,7 +32,7 @@ class QuickTerminalWindow: NSPanel { /// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround /// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026 - var initialFrame: NSRect? = nil + var initialFrame: NSRect? override func setFrame(_ frameRect: NSRect, display flag: Bool) { // Upon first adding this Window to its host view, older SwiftUI diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 8bb9d9e36..71be8174b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -37,7 +37,7 @@ class BaseTerminalController: NSWindowController, let ghostty: Ghostty.App /// The currently focused surface. - var focusedSurface: Ghostty.SurfaceView? = nil { + var focusedSurface: Ghostty.SurfaceView? { didSet { syncFocusToSurfaceTree() } } @@ -58,19 +58,19 @@ class BaseTerminalController: NSWindowController, } /// Non-nil when an alert is active so we don't overlap multiple. - private var alert: NSAlert? = nil + private var alert: NSAlert? /// The clipboard confirmation window, if shown. - private var clipboardConfirmation: ClipboardConfirmationController? = nil + private var clipboardConfirmation: ClipboardConfirmationController? /// Fullscreen state management. private(set) var fullscreenStyle: FullscreenStyle? /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? /// The previous frame information from the window - private var savedFrame: SavedFrame? = nil + private var savedFrame: SavedFrame? /// Cache previously applied appearance to avoid unnecessary updates private var appliedColorScheme: ghostty_color_scheme_e? @@ -86,7 +86,7 @@ class BaseTerminalController: NSWindowController, /// An override title for the tab/window set by the user via prompt_tab_title. /// When set, this takes precedence over the computed title from the terminal. - var titleOverride: String? = nil { + var titleOverride: String? { didSet { applyTitleToWindow() } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c61c7a88d..a30e1f1de 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -59,7 +59,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr private var surfaceAppearanceCancellables: Set = [] /// This will be set to the initial frame of the window from the xib on load. - private var initialFrame: NSRect? = nil + private var initialFrame: NSRect? init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, @@ -210,7 +210,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // to find the preferred window to attach new tabs, perform actions, etc. We // always prefer the main window but if there isn't any (because we're triggered // by something like an App Intent) then we prefer the most previous main. - static private(set) weak var lastMain: TerminalController? = nil + static private(set) weak var lastMain: TerminalController? /// The "new window" action. static func newWindow( diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 914e9119f..9f6a81e89 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -45,7 +45,7 @@ struct TerminalView: View { @ObservedObject var viewModel: ViewModel // An optional delegate to receive information about terminal changes. - weak var delegate: (any TerminalViewDelegate)? = nil + weak var delegate: (any TerminalViewDelegate)? // The most recently focused surface, equal to focusedSurface when // it is non-nil. diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index cab33856a..58f5f4eb8 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -35,7 +35,7 @@ class TerminalWindow: NSWindow { private(set) var derivedConfig: DerivedConfig = .init() /// Sets up our tab context menu - private var tabMenuObserver: NSObjectProtocol? = nil + private var tabMenuObserver: NSObjectProtocol? /// Whether this window supports the update accessory. If this is false, then views within this /// window should determine how to show update notifications. @@ -295,7 +295,7 @@ class TerminalWindow: NSWindow { // MARK: Tab Key Equivalents - var keyEquivalent: String? = nil { + var keyEquivalent: String? { didSet { // When our key equivalent is set, we must update the tab label. guard let keyEquivalent else { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 1fe68cd51..0d138c4d9 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -172,7 +172,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { backgroundColor.luminance < 0.05 } - private var newTabButtonImageLayer: VibrantLayer? = nil + private var newTabButtonImageLayer: VibrantLayer? func updateTabBar() { newTabButtonImageLayer = nil @@ -286,9 +286,9 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // MARK: - Titlebar Tabs - private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil + private var windowButtonsBackdrop: WindowButtonsBackdropView? - private var windowDragHandle: WindowDragView? = nil + private var windowDragHandle: WindowDragView? // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 1d6e59577..2f9288b40 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -33,7 +33,7 @@ extension Ghostty { private var configPath: String? /// The ghostty app instance. We only have one of these for the entire app, although I guess /// in theory you can have multiple... I don't know why you would... - @Published var app: ghostty_app_t? = nil { + @Published var app: ghostty_app_t? { didSet { guard let old = oldValue else { return } ghostty_app_free(old) diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 555b93029..7acf44aff 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -7,7 +7,7 @@ extension Ghostty { // The underlying C pointer to the Ghostty config structure. This // should never be accessed directly. Any operations on this should // be called from the functions on this or another class. - private(set) var config: ghostty_config_t? = nil { + private(set) var config: ghostty_config_t? { didSet { // Free the old value whenever we change guard let old = oldValue else { return } @@ -160,7 +160,7 @@ extension Ghostty { var title: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "title" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -169,7 +169,7 @@ extension Ghostty { var windowSaveState: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-save-state" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -192,7 +192,7 @@ extension Ghostty { var windowNewTabPosition: String { guard let config = self.config else { return "" } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-new-tab-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" } guard let ptr = v else { return "" } @@ -202,7 +202,7 @@ extension Ghostty { var windowDecorations: Bool { let defaultValue = true guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-decoration" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -212,7 +212,7 @@ extension Ghostty { var windowTheme: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-theme" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -233,7 +233,7 @@ extension Ghostty { #if canImport(AppKit) var windowFullscreen: FullscreenMode? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -256,7 +256,7 @@ extension Ghostty { #else var windowFullscreen: Bool { guard let config = self.config else { return false } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return false } guard let ptr = v else { return false } @@ -271,7 +271,7 @@ extension Ghostty { var windowFullscreenMode: FullscreenMode { let defaultValue: FullscreenMode = .native guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-non-native-fullscreen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -293,7 +293,7 @@ extension Ghostty { var windowTitleFontFamily: String? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "window-title-font-family" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -303,7 +303,7 @@ extension Ghostty { var macosWindowButtons: MacOSWindowButtons { let defaultValue = MacOSWindowButtons.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-window-buttons" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -314,7 +314,7 @@ extension Ghostty { var macosTitlebarStyle: String { let defaultValue = "transparent" guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-style" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -324,7 +324,7 @@ extension Ghostty { var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon { let defaultValue = MacOSTitlebarProxyIcon.visible guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-titlebar-proxy-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -335,7 +335,7 @@ extension Ghostty { var macosDockDropBehavior: MacDockDropBehavior { let defaultValue = MacDockDropBehavior.new_tab guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-dock-drop-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -354,7 +354,7 @@ extension Ghostty { var macosIcon: MacOSIcon { let defaultValue = MacOSIcon.official guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -366,7 +366,7 @@ extension Ghostty { #if os(macOS) let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-custom-icon" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -380,7 +380,7 @@ extension Ghostty { var macosIconFrame: MacOSIconFrame { let defaultValue = MacOSIconFrame.aluminum guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-icon-frame" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -408,7 +408,7 @@ extension Ghostty { var macosHidden: MacHidden { guard let config = self.config else { return .never } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-hidden" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never } guard let ptr = v else { return .never } @@ -508,7 +508,7 @@ extension Ghostty { #if canImport(AppKit) var quickTerminalPosition: QuickTerminalPosition { guard let config = self.config else { return .top } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .top } guard let ptr = v else { return .top } @@ -518,7 +518,7 @@ extension Ghostty { var quickTerminalScreen: QuickTerminalScreen { guard let config = self.config else { return .main } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-screen" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .main } guard let ptr = v else { return .main } @@ -544,7 +544,7 @@ extension Ghostty { var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior { guard let config = self.config else { return .move } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "quick-terminal-space-behavior" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .move } guard let ptr = v else { return .move } @@ -563,7 +563,7 @@ extension Ghostty { var resizeOverlay: ResizeOverlay { guard let config = self.config else { return .after_first } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .after_first } guard let ptr = v else { return .after_first } @@ -574,7 +574,7 @@ extension Ghostty { var resizeOverlayPosition: ResizeOverlayPosition { let defaultValue = ResizeOverlayPosition.center guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "resize-overlay-position" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -600,7 +600,7 @@ extension Ghostty { var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil } guard let ptr = v else { return nil } @@ -611,7 +611,7 @@ extension Ghostty { var autoUpdateChannel: AutoUpdateChannel { let defaultValue = AutoUpdateChannel.stable guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "auto-update-channel" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -646,7 +646,7 @@ extension Ghostty { var macosShortcuts: MacShortcuts { let defaultValue = MacShortcuts.ask guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "macos-shortcuts" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } @@ -657,7 +657,7 @@ extension Ghostty { var scrollbar: Scrollbar { let defaultValue = Scrollbar.system guard let config = self.config else { return defaultValue } - var v: UnsafePointer? = nil + var v: UnsafePointer? let key = "scrollbar" guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue } guard let ptr = v else { return defaultValue } diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 23fbc854b..246bfc8a6 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -94,7 +94,7 @@ extension Ghostty { class InspectorView: MTKView, NSTextInputClient { let commandQueue: MTLCommandQueue - var surfaceView: SurfaceView? = nil { + var surfaceView: SurfaceView? { didSet { surfaceViewDidChange() } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 37a69852e..8b312f5c0 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -5,7 +5,7 @@ extension Ghostty { /// A preference key that propagates the ID of the SurfaceView currently being dragged, /// or nil if no surface is being dragged. struct DraggingSurfaceKey: PreferenceKey { - static var defaultValue: SurfaceView.ID? = nil + static var defaultValue: SurfaceView.ID? static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { value = nextValue() ?? value diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index d72fa742c..ba4239d12 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -300,7 +300,7 @@ extension Ghostty { // This is the last size that we processed. This is how we handle our // timer state. - @State var lastSize: CGSize? = nil + @State var lastSize: CGSize? // Ready is set to true after a short delay. This avoids some of the // challenges of initial view sizing from SwiftUI. @@ -640,19 +640,19 @@ extension Ghostty { /// libghostty, usually from the Ghostty configuration. struct SurfaceConfiguration { /// Explicit font size to use in points - var fontSize: Float32? = nil + var fontSize: Float32? /// Explicit working directory to set - var workingDirectory: String? = nil + var workingDirectory: String? /// Explicit command to set - var command: String? = nil + var command: String? /// Environment variables to set for the terminal var environmentVariables: [String: String] = [:] /// Extra input to send as stdin - var initialInput: String? = nil + var initialInput: String? /// Wait after the command var waitAfterCommand: Bool = false @@ -1252,8 +1252,8 @@ extension FocusedValues { extension Ghostty.SurfaceView { class SearchState: ObservableObject { @Published var needle: String = "" - @Published var selected: UInt? = nil - @Published var total: UInt? = nil + @Published var selected: UInt? + @Published var total: UInt? init(from startSearch: Ghostty.Action.StartSearch) { self.needle = startSearch.needle ?? "" diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 6f4f3c497..db68ffb62 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -27,7 +27,7 @@ extension Ghostty { // The current pwd of the surface as defined by the pty. This can be // changed with escape codes. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -40,13 +40,13 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL string - @Published var hoverUrl: String? = nil + @Published var hoverUrl: String? // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil { + @Published var progressReport: Action.ProgressReport? { didSet { // Cancel any existing timer progressReportTimer?.invalidate() @@ -69,7 +69,7 @@ extension Ghostty { @Published var keyTables: [String] = [] // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil { + @Published var searchState: SearchState? { didSet { if let searchState { // I'm not a Combine expert so if there is a better way to do this I'm @@ -107,11 +107,11 @@ extension Ghostty { // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. - @Published var surfaceSize: ghostty_surface_size_s? = nil + @Published var surfaceSize: ghostty_surface_size_s? // Whether the pointer should be visible or not @Published private(set) var pointerStyle: CursorStyle = .horizontalText @@ -121,7 +121,7 @@ extension Ghostty { /// The background color within the color palette of the surface. This is only set if it is /// dynamically updated. Otherwise, the background color is the default background color. - @Published private(set) var backgroundColor: Color? = nil + @Published private(set) var backgroundColor: Color? /// True when the bell is active. This is set inactive on focus or event. @Published private(set) var bell: Bool = false @@ -134,7 +134,7 @@ extension Ghostty { // An initial size to request for a window. This will only affect // then the view is moved to a new window. - var initialSize: NSSize? = nil + var initialSize: NSSize? // A content size received through sizeDidChange that may in some cases // be different from the frame size. @@ -210,10 +210,10 @@ extension Ghostty { private var markedText: NSMutableAttributedString private(set) var focused: Bool = true private var prevPressureStage: Int = 0 - private var appearanceObserver: NSKeyValueObservation? = nil + private var appearanceObserver: NSKeyValueObservation? // This is set to non-null during keyDown to accumulate insertText contents - private var keyTextAccumulator: [String]? = nil + private var keyTextAccumulator: [String]? // A small delay that is introduced before a title change to avoid flickers private var titleChangeTimer: Timer? @@ -234,7 +234,7 @@ extension Ghostty { private(set) var cachedVisibleContents: CachedValue /// Event monitor (see individual events for why) - private var eventMonitor: Any? = nil + private var eventMonitor: Any? // We need to support being a first responder so that we can get input events override var acceptsFirstResponder: Bool { return true } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index aafad909a..3b1c63d57 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -15,7 +15,7 @@ extension Ghostty { @Published var title: String = "👻" // The current pwd of the surface. - @Published var pwd: String? = nil + @Published var pwd: String? // The cell size of this surface. This is set by the core when the // surface is first created and any time the cell size changes (i.e. @@ -28,23 +28,23 @@ extension Ghostty { @Published var healthy: Bool = true // Any error while initializing the surface. - @Published var error: Error? = nil + @Published var error: Error? // The hovered URL - @Published var hoverUrl: String? = nil + @Published var hoverUrl: String? // The progress report (if any) - @Published var progressReport: Action.ProgressReport? = nil + @Published var progressReport: Action.ProgressReport? // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. - @Published var focusInstant: ContinuousClock.Instant? = nil + @Published var focusInstant: ContinuousClock.Instant? /// True when the bell is active. This is set inactive on focus or event. @Published var bell: Bool = false // The current search state. When non-nil, the search overlay should be shown. - @Published var searchState: SearchState? = nil + @Published var searchState: SearchState? // The currently active key tables. Empty if no tables are active. @Published var keyTables: [String] = [] From 32e540c248815bed6a09bcf76ea1df536e25bf4f Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:38:32 -0500 Subject: [PATCH 28/51] macos: swiftlint 'legacy_constant' rule --- macos/.swiftlint.yml | 1 - .../Terminal/TerminalController.swift | 66 +++++++++---------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 192521deb..be9a78875 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -17,7 +17,6 @@ disabled_rules: - for_where - force_cast - implicit_getter - - legacy_constant - legacy_constructor - line_length - mark diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a30e1f1de..ad1c68ddc 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -8,16 +8,16 @@ import GhosttyKit class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" - + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config - + // If we have no window decorations, there's no reason to do anything but // the default titlebar (because there will be no titlebar). if !config.windowDecorations { return defaultValue } - + let nib = switch config.macosTitlebarStyle { case "native": "Terminal" case "hidden": "TerminalHiddenTitlebar" @@ -34,33 +34,33 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr #endif default: defaultValue } - + return nib } - + /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail /// early if we don't care. private var tabListenForFrame: Bool = false - + /// This is the hash value of the last tabGroup.windows array. We use this to detect order /// changes in the list. private var tabWindowsHash: Int = 0 - + /// This is set to false by init if the window managed by this controller should not be restorable. /// For example, terminals executing custom scripts are not restorable. private var restorable: Bool = true - + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - - + + /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] - + /// This will be set to the initial frame of the window from the xib on load. private var initialFrame: NSRect? - + init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, withSurfaceTree tree: SplitTree? = nil, @@ -72,12 +72,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // as the script. We may want to revisit this behavior when we have scrollback // restoration. self.restorable = (base?.command ?? "") == "" - + // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - + super.init(ghostty, baseConfig: base, surfaceTree: tree) - + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -134,37 +134,37 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr object: nil ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) is not supported for this view") } - + deinit { // Remove all of our notificationcenter subscriptions let center = NotificationCenter.default center.removeObserver(self) } - + // MARK: Base Controller Overrides - + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - + // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() - + // Update our zoom state if let window = window as? TerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } - + // If our surface tree is now nil then we close our window. if to.isEmpty { self.window?.close() } } - + override func replaceSurfaceTree( _ newTree: SplitTree, moveFocusTo newView: Ghostty.SurfaceView? = nil, @@ -177,7 +177,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeTabImmediately() return } - + super.replaceSurfaceTree( newTree, moveFocusTo: newView, @@ -481,7 +481,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr return controller } - + // MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -562,7 +562,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr tabWindowsHash = v self.relabelTabs() } - + override func syncAppearance() { // When our focus changes, we update our window appearance based on the // currently focused surface. @@ -963,7 +963,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Make it the key window window.makeKeyAndOrderFront(nil) } - + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) { @@ -1131,7 +1131,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // https://github.com/ghostty-org/ghostty/issues/2565 let oldFrame = focusedWindow.frame - Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) + Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: .zero) if focusedWindow.frame != oldFrame { focusedWindow.setFrame(oldFrame, display: true) @@ -1548,24 +1548,24 @@ extension TerminalController { guard let window, let tabGroup = window.tabGroup else { return false } guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false } return tabGroup.windows.enumerated().contains { $0.offset > currentIndex } - + case #selector(returnToDefaultSize): guard let window else { return false } - + // Native fullscreen windows can't revert to default size. if window.styleMask.contains(.fullScreen) { return false } - + // If we're fullscreen at all then we can't change size if fullscreenStyle?.isFullscreen ?? false { return false } - + // If our window is already the default size or we don't have a // default size, then disable. return defaultSize?.isChanged(for: window) ?? false - + default: return super.validateMenuItem(item) } From b10dcc96299aea9e4c276896a7331a3a2ded837f Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:43:22 -0500 Subject: [PATCH 29/51] macos: swiftlint 'legacy_constructor' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Ghostty/Ghostty.App.swift | 2 +- .../Ghostty/Surface View/InspectorView.swift | 2 +- .../Surface View/SurfaceView_AppKit.swift | 18 +++++++++--------- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index be9a78875..f695dae66 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -17,7 +17,6 @@ disabled_rules: - for_where - force_cast - implicit_getter - - legacy_constructor - line_length - mark - multiple_closures_with_trailing_closure diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 2f9288b40..6ed7f83a5 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1682,7 +1682,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - surfaceView.initialSize = NSMakeSize(Double(v.width), Double(v.height)) + surfaceView.initialSize = NSSize(width: Double(v.width), height: Double(v.height)) default: diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 246bfc8a6..076a465f7 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -382,7 +382,7 @@ extension Ghostty { } func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } func insertText(_ string: Any, replacementRange: NSRange) { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index db68ffb62..30862237a 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -259,7 +259,7 @@ extension Ghostty { // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer // can do SOMETHING. - super.init(frame: NSMakeRect(0, 0, 800, 600)) + super.init(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) // Our cache of screen data cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in @@ -1400,7 +1400,7 @@ extension Ghostty { } // Ghostty coordinate system is top-left, convert to bottom-left for AppKit - let pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y) + let pt = NSPoint(x: text.tl_px_x, y: frame.size.height - text.tl_px_y) let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes) self.showDefinition(for: str, at: pt); } @@ -1850,7 +1850,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect { guard let surface = self.surface else { - return NSMakeRect(frame.origin.x, frame.origin.y, 0, 0) + return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0) } // Ghostty will tell us where it thinks an IME keyboard should render. @@ -1892,11 +1892,11 @@ extension Ghostty.SurfaceView: NSTextInputClient { // when there's is no characters selected, // width should be 0 so that dictation indicator // can start in the right place - let viewRect = NSMakeRect( - x, - frame.size.height - y, - width, - max(height, cellSize.height)) + let viewRect = NSRect( + x: x, + y: frame.size.height - y, + width: width, + height: max(height, cellSize.height)) // Convert the point to the window coordinates let winRect = self.convert(viewRect, to: nil) @@ -2134,7 +2134,7 @@ extension Ghostty.SurfaceView { DispatchQueue.main.async { self.insertText( content, - replacementRange: NSMakeRange(0, 0) + replacementRange: NSRange(location: 0, length: 0) ) } return true From c4c87f8c85fb7339c093538196847fc3d0eed3c8 Mon Sep 17 00:00:00 2001 From: Jake Stewart Date: Fri, 20 Feb 2026 07:46:16 +0800 Subject: [PATCH 30/51] make palette inversion opt-in --- src/config/Config.zig | 6 ++++++ src/terminal/color.zig | 29 ++++++++++++++++++++--------- src/termio/Termio.zig | 1 + 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index bb86b6bd5..b56aee004 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -808,6 +808,12 @@ palette: Palette = .{}, /// Available since: 1.3.0 @"palette-generate": bool = true, +/// Whether to invert generated light themes colors (see `palette-generate`). +/// This helps give the 256-color palette more semantic meaning. +/// +/// Available since: 1.3.0 +@"palette-harmonious": bool = false, + /// The color of the cursor. If this is not set, a default will be chosen. /// /// Direct colors can be specified as either hex (`#RRGGBB` or `RRGGBB`) diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 483d65e28..1daa50107 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -1,9 +1,10 @@ -const colorpkg = @This(); - const std = @import("std"); + const assert = @import("../quirks.zig").inlineAssert; const x11_color = @import("x11_color.zig"); +const colorpkg = @This(); + /// The default palette. pub const default: Palette = default: { var result: Palette = undefined; @@ -90,15 +91,25 @@ pub fn generate256Color( skip: PaletteMask, bg: RGB, fg: RGB, + harmonious: bool ) Palette { // Convert the background, foreground, and 8 base theme colors into // CIELAB space so that all interpolation is perceptually uniform. const bg_lab: LAB = .fromRgb(bg); const fg_lab: LAB = .fromRgb(fg); - const base8_lab: [8]LAB = base8: { - var base8: [8]LAB = undefined; - for (0..8) |i| base8[i] = .fromRgb(base[i]); - break :base8 base8; + + const is_light_theme = bg_lab.l > 50; + const invert = is_light_theme and !harmonious; + + const base8_lab: [8]LAB = .{ + if (invert) fg_lab else bg_lab, + LAB.fromRgb(base[1]), + LAB.fromRgb(base[2]), + LAB.fromRgb(base[3]), + LAB.fromRgb(base[4]), + LAB.fromRgb(base[5]), + LAB.fromRgb(base[6]), + if (invert) bg_lab else fg_lab, }; // Start from the base palette so indices 0–15 are preserved as-is. @@ -115,10 +126,10 @@ pub fn generate256Color( for (0..6) |ri| { // R-axis corners: blend base colors along the red dimension. const tr = @as(f32, @floatFromInt(ri)) / 5.0; - const c0: LAB = .lerp(tr, bg_lab, base8_lab[1]); + const c0: LAB = .lerp(tr, base8_lab[0], base8_lab[1]); const c1: LAB = .lerp(tr, base8_lab[2], base8_lab[3]); const c2: LAB = .lerp(tr, base8_lab[4], base8_lab[5]); - const c3: LAB = .lerp(tr, base8_lab[6], fg_lab); + const c3: LAB = .lerp(tr, base8_lab[6], base8_lab[7]); for (0..6) |gi| { // G-axis edges: blend the R-interpolated corners along green. const tg = @as(f32, @floatFromInt(gi)) / 5.0; @@ -147,7 +158,7 @@ pub fn generate256Color( for (0..24) |i| { const t = @as(f32, @floatFromInt(i + 1)) / 25.0; if (!skip.isSet(idx)) { - const c: LAB = .lerp(t, bg_lab, fg_lab); + const c: LAB = .lerp(t, base8_lab[0], base8_lab[7]); result[idx] = c.toRgb(); } idx += 1; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index dee58dc22..80ab4d7c7 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -189,6 +189,7 @@ pub const DerivedConfig = struct { config.palette.mask, config.background.toTerminalRGB(), config.foreground.toTerminalRGB(), + config.@"palette-harmonious" ); } From 6052f664cf83921114d05c6db19bb3ff1fb23063 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:53:13 -0500 Subject: [PATCH 31/51] macos: swiftlint 'opening_brace' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 5 ++--- macos/Sources/Features/About/AboutView.swift | 3 +-- .../Features/QuickTerminal/QuickTerminalController.swift | 3 +-- .../Sources/Features/Terminal/BaseTerminalController.swift | 3 +-- .../Features/Terminal/Window Styles/TerminalWindow.swift | 3 +-- macos/Sources/Ghostty/Ghostty.App.swift | 3 +-- macos/Sources/Ghostty/NSEvent+Extension.swift | 3 +-- macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift | 6 ++---- .../Helpers/Extensions/EventModifiers+Extension.swift | 4 ++++ 10 files changed, 14 insertions(+), 20 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index f695dae66..ea1752b77 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -21,7 +21,6 @@ disabled_rules: - mark - multiple_closures_with_trailing_closure - no_fallthrough_only - - opening_brace - orphaned_doc_comment - private_over_fileprivate - shorthand_operator diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 54009bf96..e91c8f4d4 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -9,8 +9,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, - GhosttyAppDelegate -{ + GhosttyAppDelegate { // The application logger. We should probably move this at some point to a dedicated // class/struct but for now it lives here! 🤷‍♂️ static let logger = Logger( @@ -948,7 +947,7 @@ class AppDelegate: NSObject, // Using AppIconActor to ensure this work // happens synchronously in the background @AppIconActor - private func updateAppIcon(from config: Ghostty.Config) async { + private func updateAppIcon(from config: Ghostty.Config) async { var appIcon: NSImage? var appIconName: String? = config.macosIcon.rawValue diff --git a/macos/Sources/Features/About/AboutView.swift b/macos/Sources/Features/About/AboutView.swift index 967eb16b0..d9a12e4dc 100644 --- a/macos/Sources/Features/About/AboutView.swift +++ b/macos/Sources/Features/About/AboutView.swift @@ -21,8 +21,7 @@ struct AboutView: View { init(material: NSVisualEffectView.Material, blendingMode: NSVisualEffectView.BlendingMode = .behindWindow, - isEmphasized: Bool = false) - { + isEmphasized: Bool = false) { self.material = material self.blendingMode = blendingMode self.isEmphasized = isEmphasized diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 820420c77..7a4dce780 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -340,8 +340,7 @@ class QuickTerminalController: BaseTerminalController { // we want to store it so we can restore state later. if !NSApp.isActive { if let previousApp = NSWorkspace.shared.frontmostApplication, - previousApp.bundleIdentifier != Bundle.main.bundleIdentifier - { + previousApp.bundleIdentifier != Bundle.main.bundleIdentifier { self.previousApp = previousApp } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 71be8174b..f27874207 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -31,8 +31,7 @@ class BaseTerminalController: NSWindowController, TerminalViewDelegate, TerminalViewModel, ClipboardConfirmationViewDelegate, - FullscreenDelegate -{ + FullscreenDelegate { /// The app instance that this terminal view will represent. let ghostty: Ghostty.App diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 58f5f4eb8..0c70217a5 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -449,8 +449,7 @@ class TerminalWindow: NSWindow { let forceOpaque = terminalController?.isBackgroundOpaque ?? false if !styleMask.contains(.fullScreen) && !forceOpaque && - (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) - { + (surfaceConfig.backgroundOpacity < 1 || surfaceConfig.backgroundBlur.isGlassStyle) { isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 6ed7f83a5..9a99ddbcb 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1993,8 +1993,7 @@ extension Ghostty { private static func configReload( _ app: ghostty_app_t, target: ghostty_target_s, - v: ghostty_action_reload_config_s) - { + v: ghostty_action_reload_config_s) { logger.info("config reload notification") guard let app_ud = ghostty_app_userdata(app) else { return } diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index b67c1932e..55888944e 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -39,8 +39,7 @@ extension NSEvent { key_ev.unshifted_codepoint = 0 if type == .keyDown || type == .keyUp { if let chars = characters(byApplyingModifiers: []), - let codepoint = chars.unicodeScalars.first - { + let codepoint = chars.unicodeScalars.first { key_ev.unshifted_codepoint = codepoint.value } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 30862237a..9084d0e2e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -965,8 +965,7 @@ extension Ghostty { !controller.commandPaletteIsShowing, window.isKeyWindow && !self.focused && - controller.focusFollowsMouse - { + controller.focusFollowsMouse { Ghostty.moveFocus(to: self) } } @@ -1944,8 +1943,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // we send it back through the event system so it can be encoded. if let lastPerformKeyEvent, let current = NSApp.currentEvent, - lastPerformKeyEvent == current.timestamp - { + lastPerformKeyEvent == current.timestamp { NSApp.sendEvent(current) return } diff --git a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift index 8d379bd99..cc8d49cf8 100644 --- a/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift +++ b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift @@ -5,11 +5,13 @@ import SwiftUI extension EventModifiers { init(nsFlags: NSEvent.ModifierFlags) { var result: SwiftUI.EventModifiers = [] + // swiftlint:disable opening_brace if nsFlags.contains(.shift) { result.insert(.shift) } if nsFlags.contains(.control) { result.insert(.control) } if nsFlags.contains(.option) { result.insert(.option) } if nsFlags.contains(.command) { result.insert(.command) } if nsFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } @@ -17,11 +19,13 @@ extension EventModifiers { extension NSEvent.ModifierFlags { init(swiftUIFlags: SwiftUI.EventModifiers) { var result: NSEvent.ModifierFlags = [] + // swiftlint:disable opening_brace if swiftUIFlags.contains(.shift) { result.insert(.shift) } if swiftUIFlags.contains(.control) { result.insert(.control) } if swiftUIFlags.contains(.option) { result.insert(.option) } if swiftUIFlags.contains(.command) { result.insert(.command) } if swiftUIFlags.contains(.capsLock) { result.insert(.capsLock) } + // swiftlint:enable opening_brace self = result } } From 25b38b291e8128df96258b342fff914f71f80cac Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:55:12 -0500 Subject: [PATCH 32/51] macos: swiftlint 'private_over_fileprivate' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../Command Palette/CommandPalette.swift | 8 ++++---- .../Command Palette/TerminalCommandPalette.swift | 2 +- .../Global Keybinds/GlobalEventTap.swift | 2 +- macos/Sources/Features/Splits/SplitTree.swift | 2 +- .../Features/Splits/TerminalSplitTreeView.swift | 4 ++-- .../Sources/Features/Terminal/TerminalView.swift | 2 +- .../TitlebarTabsVenturaTerminalWindow.swift | 8 ++++---- macos/Sources/Features/Update/UpdateBadge.swift | 2 +- .../Features/Update/UpdatePopoverView.swift | 16 ++++++++-------- macos/Sources/Helpers/MetalView.swift | 2 +- 12 files changed, 25 insertions(+), 26 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index ea1752b77..dc212c4f6 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -22,7 +22,6 @@ disabled_rules: - multiple_closures_with_trailing_closure - no_fallthrough_only - orphaned_doc_comment - - private_over_fileprivate - shorthand_operator - switch_case_alignment - syntactic_sugar diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index e91c8f4d4..d428aaa3e 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -1359,6 +1359,6 @@ private enum QuickTerminalState { } @globalActor -fileprivate actor AppIconActor: GlobalActor { +private actor AppIconActor: GlobalActor { static let shared = AppIconActor() } diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 46542a4b5..1d160d160 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -229,7 +229,7 @@ struct CommandPaletteView: View { } /// The text field for building the query for the command palette. -fileprivate struct CommandPaletteQuery: View { +private struct CommandPaletteQuery: View { @Binding var query: String var onEvent: ((KeyboardEvent) -> Void)? @FocusState private var isTextFieldFocused: Bool @@ -284,7 +284,7 @@ fileprivate struct CommandPaletteQuery: View { } } -fileprivate struct CommandTable: View { +private struct CommandTable: View { var options: [CommandOption] @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? @@ -332,7 +332,7 @@ fileprivate struct CommandTable: View { } /// A single row in the command palette. -fileprivate struct CommandRow: View { +private struct CommandRow: View { let option: CommandOption var isSelected: Bool @Binding var hoveredID: UUID? @@ -406,7 +406,7 @@ fileprivate struct CommandRow: View { } /// A row of Text representing a shortcut. -fileprivate struct ShortcutSymbolsView: View { +private struct ShortcutSymbolsView: View { let symbols: [String] var body: some View { diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 9bdf4b4ff..051fbe48f 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -171,7 +171,7 @@ struct TerminalCommandPaletteView: View { } /// This is done to ensure that the given view is in the responder chain. -fileprivate struct ResponderChainInjector: NSViewRepresentable { +private struct ResponderChainInjector: NSViewRepresentable { let responder: NSResponder func makeNSView(context: Context) -> NSView { diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index a57895a86..9d4023c2e 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -117,7 +117,7 @@ class GlobalEventTap { } } -fileprivate func cgEventFlagsChangedHandler( +private func cgEventFlagsChangedHandler( proxy: CGEventTapProxy, type: CGEventType, cgEvent: CGEvent, diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 5b776afbc..5fc49a24f 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -343,7 +343,7 @@ extension SplitTree { // MARK: SplitTree Codable -fileprivate enum CodingKeys: String, CodingKey { +private enum CodingKeys: String, CodingKey { case version case root case zoomed diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 635bea980..8b57a1a91 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -44,7 +44,7 @@ struct TerminalSplitTreeView: View { } } -fileprivate struct TerminalSplitSubtreeView: View { +private struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App let node: SplitTree.Node @@ -86,7 +86,7 @@ fileprivate struct TerminalSplitSubtreeView: View { } } -fileprivate struct TerminalSplitLeaf: View { +private struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 9f6a81e89..d369721dd 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -127,7 +127,7 @@ struct TerminalView: View { } } -fileprivate struct UpdateOverlay: View { +private struct UpdateOverlay: View { var body: some View { if let appDelegate = NSApp.delegate as? AppDelegate { VStack { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 0d138c4d9..faec8dcd3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -506,7 +506,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } // Passes mouseDown events from this view to window.performDrag so that you can drag the window by it. -fileprivate class WindowDragView: NSView { +private class WindowDragView: NSView { override public func mouseDown(with event: NSEvent) { // Drag the window for single left clicks, double clicks should bypass the drag handle. if event.type == .leftMouseDown && event.clickCount == 1 { @@ -535,7 +535,7 @@ fileprivate class WindowDragView: NSView { } // A view that matches the color of selected and unselected tabs in the adjacent tab bar. -fileprivate class WindowButtonsBackdropView: NSView { +private class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? @@ -588,7 +588,7 @@ fileprivate class WindowButtonsBackdropView: NSView { // Custom NSToolbar subclass that displays a centered window title, // in order to accommodate the titlebar tabs feature. -fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { +private class TerminalToolbar: NSToolbar, NSToolbarDelegate { private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") var titleText: String { @@ -674,7 +674,7 @@ fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { } /// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. -fileprivate class CenteredDynamicLabel: NSTextField { +private class CenteredDynamicLabel: NSTextField { override func viewDidMoveToSuperview() { // Configure the text field isEditable = false diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index 054fdf971..4b7e7c945 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -61,7 +61,7 @@ struct UpdateBadge: View { /// A circular progress indicator with a stroke-based ring design. /// /// Displays a partially filled circle that represents progress from 0.0 to 1.0. -fileprivate struct ProgressRingView: View { +private struct ProgressRingView: View { /// The current progress value, ranging from 0.0 (empty) to 1.0 (complete) let progress: Double diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index 87d76f801..b39fa707b 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -52,7 +52,7 @@ struct UpdatePopoverView: View { } } -fileprivate struct PermissionRequestView: View { +private struct PermissionRequestView: View { let request: UpdateState.PermissionRequest let dismiss: DismissAction @@ -93,7 +93,7 @@ fileprivate struct PermissionRequestView: View { } } -fileprivate struct CheckingView: View { +private struct CheckingView: View { let checking: UpdateState.Checking let dismiss: DismissAction @@ -120,7 +120,7 @@ fileprivate struct CheckingView: View { } } -fileprivate struct UpdateAvailableView: View { +private struct UpdateAvailableView: View { let update: UpdateState.UpdateAvailable let dismiss: DismissAction @@ -217,7 +217,7 @@ fileprivate struct UpdateAvailableView: View { } } -fileprivate struct DownloadingView: View { +private struct DownloadingView: View { let download: UpdateState.Downloading let dismiss: DismissAction @@ -255,7 +255,7 @@ fileprivate struct DownloadingView: View { } } -fileprivate struct ExtractingView: View { +private struct ExtractingView: View { let extracting: UpdateState.Extracting var body: some View { @@ -274,7 +274,7 @@ fileprivate struct ExtractingView: View { } } -fileprivate struct InstallingView: View { +private struct InstallingView: View { let installing: UpdateState.Installing let dismiss: DismissAction @@ -313,7 +313,7 @@ fileprivate struct InstallingView: View { } } -fileprivate struct NotFoundView: View { +private struct NotFoundView: View { let notFound: UpdateState.NotFound let dismiss: DismissAction @@ -343,7 +343,7 @@ fileprivate struct NotFoundView: View { } } -fileprivate struct UpdateErrorView: View { +private struct UpdateErrorView: View { let error: UpdateState.Error let dismiss: DismissAction diff --git a/macos/Sources/Helpers/MetalView.swift b/macos/Sources/Helpers/MetalView.swift index 6579f8863..e8c27b52b 100644 --- a/macos/Sources/Helpers/MetalView.swift +++ b/macos/Sources/Helpers/MetalView.swift @@ -10,7 +10,7 @@ struct MetalView: View { } } -fileprivate struct MetalViewRepresentable: NSViewRepresentable { +private struct MetalViewRepresentable: NSViewRepresentable { @Binding var metalView: V func makeNSView(context: Context) -> some NSView { From 6af959920e9815750998ea8e3dcb11a1264fee86 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:56:07 -0500 Subject: [PATCH 33/51] macos: swiftlint 'syntactic_sugar' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Ghostty/Surface View/SurfaceView.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index dc212c4f6..fc7ea7538 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -24,7 +24,6 @@ disabled_rules: - orphaned_doc_comment - shorthand_operator - switch_case_alignment - - syntactic_sugar - trailing_semicolon - trailing_whitespace - unneeded_synthesized_initializer diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index ba4239d12..2cc28d836 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -736,7 +736,7 @@ extension Ghostty { return try keys.withCStrings { keyCStrings in return try values.withCStrings { valueCStrings in // Create array of ghostty_env_var_s - var envVars = Array() + var envVars = [ghostty_env_var_s]() envVars.reserveCapacity(environmentVariables.count) for i in 0.. Date: Thu, 19 Feb 2026 18:56:25 -0500 Subject: [PATCH 34/51] macos: swiftlint 'trailing_semicolon' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/main.swift | 2 +- .../Terminal/BaseTerminalController.swift | 4 +- macos/Sources/Ghostty/Ghostty.Config.swift | 50 +++++++++---------- macos/Sources/Ghostty/Ghostty.Input.swift | 2 +- macos/Sources/Ghostty/Package.swift | 16 +++--- .../Ghostty/Surface View/InspectorView.swift | 2 +- .../Ghostty/Surface View/SurfaceView.swift | 10 ++-- .../Surface View/SurfaceView_AppKit.swift | 24 ++++----- .../Surface View/SurfaceView_UIKit.swift | 2 +- 10 files changed, 56 insertions(+), 57 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index fc7ea7538..1c71146ef 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -24,7 +24,6 @@ disabled_rules: - orphaned_doc_comment - shorthand_operator - switch_case_alignment - - trailing_semicolon - trailing_whitespace - unneeded_synthesized_initializer - unused_closure_parameter diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift index ad32f4e70..400f91c22 100644 --- a/macos/Sources/App/macOS/main.swift +++ b/macos/Sources/App/macOS/main.swift @@ -28,6 +28,6 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE // This will run the CLI action and exit if one was specified. A CLI // action is a command starting with a `+`, such as `ghostty +boo`. -ghostty_cli_try_action(); +ghostty_cli_try_action() _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index f27874207..87e63c348 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -530,14 +530,14 @@ class BaseTerminalController: NSWindowController, // then we let it stay that way. x: if newFrame.origin.x < visibleFrame.origin.x { if let savedFrame, savedFrame.window.origin.x < savedFrame.screen.origin.x { - break x; + break x } newFrame.origin.x = visibleFrame.origin.x } y: if newFrame.origin.y < visibleFrame.origin.y { if let savedFrame, savedFrame.window.origin.y < savedFrame.screen.origin.y { - break y; + break y } newFrame.origin.y = visibleFrame.origin.y diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 7acf44aff..3d3219663 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -22,7 +22,7 @@ extension Ghostty { var errors: [String] { guard let cfg = self.config else { return [] } - var diags: [String] = []; + var diags: [String] = [] let diagsCount = ghostty_config_diagnostics_count(cfg) for i in 0.. 0 { logger.warning("config error: \(diagsCount) configuration errors on reload") - var diags: [String] = []; + var diags: [String] = [] for i in 0.. Bool { switch self { - case .top_left, .top_center, .top_right: return true; - default: return false; + case .top_left, .top_center, .top_right: return true + default: return false } } func bottom() -> Bool { switch self { - case .bottom_left, .bottom_center, .bottom_right: return true; - default: return false; + case .bottom_left, .bottom_center, .bottom_right: return true + default: return false } } func left() -> Bool { switch self { - case .top_left, .bottom_left: return true; - default: return false; + case .top_left, .bottom_left: return true + default: return false } } func right() -> Bool { switch self { - case .top_right, .bottom_right: return true; - default: return false; + case .top_right, .bottom_right: return true + default: return false } } } diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 563e4a794..27f4d05dd 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -49,7 +49,7 @@ extension Ghostty { /// Returns the event modifier flags set for the Ghostty mods enum. static func eventModifierFlags(mods: ghostty_input_mods_e) -> NSEvent.ModifierFlags { - var flags = NSEvent.ModifierFlags(rawValue: 0); + var flags = NSEvent.ModifierFlags(rawValue: 0) if mods.rawValue & GHOSTTY_MODS_SHIFT.rawValue != 0 { flags.insert(.shift) } if mods.rawValue & GHOSTTY_MODS_CTRL.rawValue != 0 { flags.insert(.control) } if mods.rawValue & GHOSTTY_MODS_ALT.rawValue != 0 { flags.insert(.option) } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 41dcfad28..bdb64a6b5 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -198,13 +198,13 @@ extension Ghostty { static func from(direction: ghostty_action_resize_split_direction_e) -> Self? { switch direction { case GHOSTTY_RESIZE_SPLIT_UP: - return .up; + return .up case GHOSTTY_RESIZE_SPLIT_DOWN: - return .down; + return .down case GHOSTTY_RESIZE_SPLIT_LEFT: - return .left; + return .left case GHOSTTY_RESIZE_SPLIT_RIGHT: - return .right; + return .right default: return nil } @@ -213,13 +213,13 @@ extension Ghostty { func toNative() -> ghostty_action_resize_split_direction_e { switch self { case .up: - return GHOSTTY_RESIZE_SPLIT_UP; + return GHOSTTY_RESIZE_SPLIT_UP case .down: - return GHOSTTY_RESIZE_SPLIT_DOWN; + return GHOSTTY_RESIZE_SPLIT_DOWN case .left: - return GHOSTTY_RESIZE_SPLIT_LEFT; + return GHOSTTY_RESIZE_SPLIT_LEFT case .right: - return GHOSTTY_RESIZE_SPLIT_RIGHT; + return GHOSTTY_RESIZE_SPLIT_RIGHT } } } diff --git a/macos/Sources/Ghostty/Surface View/InspectorView.swift b/macos/Sources/Ghostty/Surface View/InspectorView.swift index 076a465f7..e7320c782 100644 --- a/macos/Sources/Ghostty/Surface View/InspectorView.swift +++ b/macos/Sources/Ghostty/Surface View/InspectorView.swift @@ -309,7 +309,7 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; + let mod: UInt32 switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 2cc28d836..fce8f3f4b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -221,7 +221,7 @@ extension Ghostty { // because we want to keep our focused surface dark even if we don't have window // focus. if isSplit && !surfaceFocus { - let overlayOpacity = ghostty.config.unfocusedSplitOpacity; + let overlayOpacity = ghostty.config.unfocusedSplitOpacity if overlayOpacity > 0 { Rectangle() .fill(ghostty.config.unfocusedSplitFill) @@ -328,15 +328,15 @@ extension Ghostty { lastSize = geoSize } - return true; + return true } } // Hidden depending on overlay config switch overlay { - case .never: return true; - case .always: return false; - case .after_first: return lastSize == nil; + case .never: return true + case .always: return false + case .after_first: return lastSize == nil } } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index 9084d0e2e..fab3ad8e7 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -991,8 +991,8 @@ extension Ghostty { if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. - x *= 2; - y *= 2; + x *= 2 + y *= 2 // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } @@ -1303,7 +1303,7 @@ extension Ghostty { } override func flagsChanged(with event: NSEvent) { - let mod: UInt32; + let mod: UInt32 switch event.keyCode { case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue @@ -1330,13 +1330,13 @@ extension Ghostty { let sidePressed: Bool switch event.keyCode { case 0x3C: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERSHIFTKEYMASK) != 0 case 0x3E: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCTLKEYMASK) != 0 case 0x3D: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERALTKEYMASK) != 0 case 0x36: - sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0; + sidePressed = event.modifierFlags.rawValue & UInt(NX_DEVICERCMDKEYMASK) != 0 default: sidePressed = true } @@ -1388,7 +1388,7 @@ extension Ghostty { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key: Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1401,7 +1401,7 @@ extension Ghostty { // Ghostty coordinate system is top-left, convert to bottom-left for AppKit let pt = NSPoint(x: text.tl_px_x, y: frame.size.height - text.tl_px_y) let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes) - self.showDefinition(for: str, at: pt); + self.showDefinition(for: str, at: pt) } override func menu(for event: NSEvent) -> NSMenu? { @@ -1830,7 +1830,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { // since we always have a primary font. The only scenario this doesn't // work is if someone is using a non-CoreText build which would be // unofficial. - var attributes: [ NSAttributedString.Key: Any ] = [:]; + var attributes: [ NSAttributedString.Key: Any ] = [:] if let fontRaw = ghostty_surface_quicklook_font(surface) { // Memory management here is wonky: ghostty_surface_quicklook_font // will create a copy of a CTFont, Swift will auto-retain the @@ -1868,8 +1868,8 @@ extension Ghostty.SurfaceView: NSTextInputClient { if ghostty_surface_read_selection(surface, &text) { // The -2/+2 here is subjective. QuickLook seems to offset the rectangle // a bit and I think these small adjustments make it look more natural. - x = text.tl_px_x - 2; - y = text.tl_px_y + 2; + x = text.tl_px_x - 2 + y = text.tl_px_y + 2 // Free our text ghostty_surface_free_text(surface, &text) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index 3b1c63d57..8e8a93b93 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -81,7 +81,7 @@ extension Ghostty { // TODO return } - self.surface = surface; + self.surface = surface } required init?(coder: NSCoder) { From b532cd55d626fd1e472c288ce42151f8e6945634 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:56:43 -0500 Subject: [PATCH 35/51] macos: swiftlint 'trailing_whitespace' rule --- macos/.swiftlint.yml | 1 - .../App/macOS/AppDelegate+Ghostty.swift | 4 +- macos/Sources/App/macOS/AppDelegate.swift | 44 ++++----- macos/Sources/App/macOS/main.swift | 4 +- .../App Intents/CloseTerminalIntent.swift | 2 +- .../App Intents/CommandPaletteIntent.swift | 2 +- .../App Intents/Entities/CommandEntity.swift | 2 +- .../App Intents/Entities/TerminalEntity.swift | 8 +- .../GetTerminalDetailsIntent.swift | 2 +- .../Features/App Intents/InputIntent.swift | 24 ++--- .../Features/App Intents/KeybindIntent.swift | 2 +- .../App Intents/NewTerminalIntent.swift | 2 +- .../App Intents/QuickTerminalIntent.swift | 2 +- .../ClipboardConfirmationView.swift | 6 +- .../Command Palette/CommandPalette.swift | 26 ++--- .../TerminalCommandPalette.swift | 16 +-- .../QuickTerminalController.swift | 12 +-- .../QuickTerminal/QuickTerminalPosition.swift | 12 +-- .../QuickTerminal/QuickTerminalScreen.swift | 4 +- .../QuickTerminalScreenStateCache.swift | 30 +++--- .../QuickTerminal/QuickTerminalWindow.swift | 8 +- .../Secure Input/SecureInputOverlay.swift | 6 +- macos/Sources/Features/Splits/SplitView.swift | 8 +- .../Splits/TerminalSplitTreeView.swift | 32 +++--- .../Terminal/BaseTerminalController.swift | 98 +++++++++---------- .../Terminal/TerminalRestorable.swift | 2 +- .../Features/Terminal/TerminalTabColor.swift | 4 +- .../Features/Terminal/TerminalView.swift | 10 +- .../HiddenTitlebarTerminalWindow.swift | 6 +- .../Window Styles/TerminalWindow.swift | 4 +- .../TitlebarTabsTahoeTerminalWindow.swift | 22 ++--- .../Sources/Features/Update/UpdateBadge.swift | 18 ++-- .../Features/Update/UpdateController.swift | 28 +++--- .../Features/Update/UpdateDelegate.swift | 2 +- .../Features/Update/UpdateDriver.swift | 66 ++++++------- .../Sources/Features/Update/UpdatePill.swift | 14 +-- .../Features/Update/UpdatePopoverView.swift | 92 ++++++++--------- .../Features/Update/UpdateSimulator.swift | 74 +++++++------- .../Features/Update/UpdateViewModel.swift | 64 ++++++------ macos/Sources/Ghostty/Ghostty.Action.swift | 22 ++--- macos/Sources/Ghostty/Ghostty.App.swift | 30 +++--- macos/Sources/Ghostty/Ghostty.Config.swift | 2 +- macos/Sources/Ghostty/Package.swift | 6 +- .../Surface View/SurfaceDragSource.swift | 60 ++++++------ .../Surface View/SurfaceGrabHandle.swift | 10 +- .../Surface View/SurfaceProgressBar.swift | 20 ++-- .../Surface View/SurfaceScrollView.swift | 56 +++++------ .../SurfaceView+Transferable.swift | 6 +- .../Ghostty/Surface View/SurfaceView.swift | 94 +++++++++--------- .../Surface View/SurfaceView_AppKit.swift | 8 +- .../Surface View/SurfaceView_UIKit.swift | 6 +- macos/Sources/Helpers/AnySortKey.swift | 6 +- macos/Sources/Helpers/Backport.swift | 2 +- .../Sources/Helpers/ExpiringUndoManager.swift | 6 +- .../Helpers/Extensions/Array+Extension.swift | 4 +- .../Extensions/NSPasteboard+Extension.swift | 4 +- .../Extensions/NSScreen+Extension.swift | 8 +- .../Helpers/Extensions/NSView+Extension.swift | 36 +++---- .../Extensions/NSWindow+Extension.swift | 10 +- .../Extensions/NSWorkspace+Extension.swift | 2 +- .../Extensions/Transferable+Extension.swift | 6 +- macos/Sources/Helpers/PermissionRequest.swift | 22 ++--- macos/Tests/NSPasteboardTests.swift | 4 +- macos/Tests/NSScreenTests.swift | 30 +++--- macos/Tests/Update/ReleaseNotesTests.swift | 34 +++---- macos/Tests/Update/UpdateStateTests.swift | 34 +++---- macos/Tests/Update/UpdateViewModelTests.swift | 28 +++--- 67 files changed, 659 insertions(+), 660 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 1c71146ef..171c7228c 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -24,7 +24,6 @@ disabled_rules: - orphaned_doc_comment - shorthand_operator - switch_case_alignment - - trailing_whitespace - unneeded_synthesized_initializer - unused_closure_parameter - unused_enumerated diff --git a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift index 4d798a1a5..66b95e06e 100644 --- a/macos/Sources/App/macOS/AppDelegate+Ghostty.swift +++ b/macos/Sources/App/macOS/AppDelegate+Ghostty.swift @@ -10,14 +10,14 @@ extension AppDelegate: Ghostty.Delegate { guard let controller = window.windowController as? BaseTerminalController else { continue } - + for surface in controller.surfaceTree { if surface.id == id { return surface } } } - + return nil } } diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index d428aaa3e..3a5511fe1 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -109,7 +109,7 @@ class AppDelegate: NSObject, switch quickTerminalControllerState { case .initialized(let controller): return controller - + case .pendingRestore(let state): let controller = QuickTerminalController( ghostty, @@ -119,7 +119,7 @@ class AppDelegate: NSObject, ) quickTerminalControllerState = .initialized(controller) return controller - + case .uninitialized: let controller = QuickTerminalController( ghostty, @@ -172,7 +172,7 @@ class AppDelegate: NSObject, // Disable the automatic full screen menu item because we handle // it manually. "NSFullScreenMenuItemEverywhere": false, - + // On macOS 26 RC1, the autofill heuristic controller causes unusable levels // of slowdowns and CPU usage in the terminal window under certain [unknown] // conditions. We don't know exactly why/how. This disables the full heuristic @@ -298,12 +298,12 @@ class AppDelegate: NSObject, case .app: // Don't have to do anything. break - + case .zig_run, .cli: // Part of launch services (clicking an app, using `open`, etc.) activates // the application and brings it to the front. When using the CLI we don't // get this behavior, so we have to do it manually. - + // This never gets called until we click the dock icon. This forces it // activate immediately. applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) @@ -353,7 +353,7 @@ class AppDelegate: NSObject, func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { let windows = NSApplication.shared.windows if windows.isEmpty { return .terminateNow } - + // If we've already accepted to install an update, then we don't need to // confirm quit. The user is already expecting the update to happen. if updateController.isInstalling { @@ -448,17 +448,17 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. - + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } - + // Set to true if confirmation is required before starting up the // new terminal. var requiresConfirm: Bool = false - + // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - + if isDirectory.boolValue { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. @@ -470,24 +470,24 @@ class AppDelegate: NSObject, // because there is a sandbox escape possible if a sandboxed application // somehow is tricked into `open`-ing a non-sandboxed application. requiresConfirm = true - + // When opening a file, we want to execute the file. To do this, we // don't override the command directly, because it won't load the // profile/rc files for the shell, which is super important on macOS // due to things like Homebrew. Instead, we set the command to // `; exit` which is what Terminal and iTerm2 do. config.initialInput = "\(Ghostty.Shell.quote(filename)); exit\n" - + // For commands executed directly, we want to ensure we wait after exit // because in most cases scripts don't block on exit and we don't want // the window to just flash closed once complete. config.waitAfterCommand = true - + // Set the parent directory to our working directory so that relative // paths in scripts work. config.workingDirectory = (filename as NSString).deletingLastPathComponent } - + if requiresConfirm { // Confirmation required. We use an app-wide NSAlert for now. In the future we // may want to show this as a sheet on the focused window (especially if we're @@ -500,12 +500,12 @@ class AppDelegate: NSObject, switch alert.runModal() { case .alertFirstButtonReturn: break - + default: return false } } - + switch ghostty.config.macosDockDropBehavior { case .new_tab: _ = TerminalController.newTab( @@ -515,7 +515,7 @@ class AppDelegate: NSObject, ) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } - + return true } @@ -1030,18 +1030,18 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) { Self.logger.debug("application will save window state") - + guard ghostty.config.windowSaveState != "never" else { return } - + // Encode our quick terminal state if we have it. switch quickTerminalControllerState { case .initialized(let controller) where controller.restorable: let data = QuickTerminalRestorableState(from: controller) data.encode(with: coder) - + case .pendingRestore(let state): state.encode(with: coder) - + default: break } @@ -1049,7 +1049,7 @@ class AppDelegate: NSObject, func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) { Self.logger.debug("application will restore window state") - + // Decode our quick terminal state. if ghostty.config.windowSaveState != "never", let state = QuickTerminalRestorableState(coder: coder) { diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift index 400f91c22..ade9bf3f0 100644 --- a/macos/Sources/App/macOS/main.swift +++ b/macos/Sources/App/macOS/main.swift @@ -7,7 +7,7 @@ import GhosttyKit // rest of the app. if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS { Ghostty.logger.critical("ghostty_init failed") - + // We also write to stderr if this is executed from the CLI or zig run switch Ghostty.launchSource { case .cli, .zig_run: @@ -18,7 +18,7 @@ if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCE "Actions start with the `+` character.\n\n" + "View all available actions by running `ghostty +help`.\n") exit(1) - + case .app: // For the app we exit immediately. We should handle this case more // gracefully in the future. diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 0155cf855..c3cca2514 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surfaceView = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index 2f07d7861..de6063564 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index 3c7745e7c..d78d75a5d 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -79,7 +79,7 @@ extension CommandEntity.ID: EntityIdentifierConvertible { static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? { .init(rawValue: entityIdentifierString) } - + var entityIdentifierString: String { rawValue } diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index e805466a2..a2c4abea0 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -52,7 +52,7 @@ struct TerminalEntity: AppEntity { if let nsImage = ImageRenderer(content: view.screenshot()).nsImage { self.screenshot = nsImage } - + // Determine the kind based on the window controller type if view.window?.windowController is QuickTerminalController { self.kind = .quick @@ -66,9 +66,9 @@ extension TerminalEntity { enum Kind: String, AppEnum { case normal case quick - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind") - + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .normal: .init(title: "Normal"), .quick: .init(title: "Quick") @@ -112,7 +112,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } - + // Get all our surfaces return controllers.flatMap { $0.surfaceTree.root?.leaves() ?? [] diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 563e3719b..99d6e39ba 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -31,7 +31,7 @@ struct GetTerminalDetailsIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + switch detail { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index e03eaf640..b77945ccc 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -34,7 +34,7 @@ struct InputTextIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -86,7 +86,7 @@ struct KeyEventIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -95,7 +95,7 @@ struct KeyEventIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let keyEvent = Ghostty.Input.KeyEvent( key: key, action: action, @@ -150,7 +150,7 @@ struct MouseButtonIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -159,7 +159,7 @@ struct MouseButtonIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mouseEvent = Ghostty.Input.MouseButtonEvent( action: action, button: button, @@ -184,7 +184,7 @@ struct MousePosIntent: AppIntent { var x: Double @Parameter( - title: "Y Position", + title: "Y Position", description: "The vertical position of the mouse cursor in pixels.", default: 0 ) @@ -213,7 +213,7 @@ struct MousePosIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -222,7 +222,7 @@ struct MousePosIntent: AppIntent { let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in result.union(mod.ghosttyMod) } - + let mousePosEvent = Ghostty.Input.MousePosEvent( x: x, y: y, @@ -283,7 +283,7 @@ struct MouseScrollIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -306,16 +306,16 @@ enum KeyEventMods: String, AppEnum, CaseIterable { case control case option case command - + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key") - + static var caseDisplayRepresentations: [KeyEventMods: DisplayRepresentation] = [ .shift: "Shift", .control: "Control", .option: "Option", .command: "Command" ] - + var ghosttyMod: Ghostty.Input.Mods { switch self { case .shift: .shift diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index a8cea8561..e4f41ebbd 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 6de9e1e7e..858d5ceb0 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -152,7 +152,7 @@ enum NewTerminalLocation: String { case splitRight = "split:right" case splitUp = "split:up" case splitDown = "split:down" - + var splitDirection: SplitTree.NewDirection? { switch self { case .splitLeft: return .left diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index 2048a3b88..df0fe17a5 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -15,7 +15,7 @@ struct QuickTerminalIntent: AppIntent { guard await requestIntentPermission() else { throw GhosttyIntentError.permissionDenied } - + guard let delegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } diff --git a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift index 839061092..17ab4aa24 100644 --- a/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift +++ b/macos/Sources/Features/ClipboardConfirmation/ClipboardConfirmationView.swift @@ -45,16 +45,16 @@ struct ClipboardConfirmationView: View { .font(.system(size: 42)) .padding() .frame(alignment: .center) - + Text(request.text()) .frame(maxWidth: .infinity, alignment: .leading) .padding() } - + TextEditor(text: .constant(contents)) .focusable(false) .font(.system(.body, design: .monospaced)) - + HStack { Spacer() Button(Action.text(.cancel, request)) { onCancel() } diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 1d160d160..10c56f8dd 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -23,7 +23,7 @@ struct CommandOption: Identifiable, Hashable { let sortKey: AnySortKey? /// The action to perform when this option is selected. let action: () -> Void - + init( title: String, subtitle: String? = nil, @@ -78,7 +78,7 @@ struct CommandPaletteView: View { ($0.subtitle?.localizedCaseInsensitiveContains(query) ?? false) || colorMatchScore(for: $0.leadingColor, query: query) > 0 } - + // Sort by color match score (higher scores first), then maintain original order return filtered.sorted { a, b in let scoreA = colorMatchScore(for: a.leadingColor, query: query) @@ -200,20 +200,20 @@ struct CommandPaletteView: View { isTextFieldFocused = isPresented } } - + /// Returns a score (0.0 to 1.0) indicating how well a color matches a search query color name. /// Returns 0 if no color name in the query matches, or if the color is nil. private func colorMatchScore(for color: Color?, query: String) -> Double { guard let color = color else { return 0 } - + let queryLower = query.lowercased() let nsColor = NSColor(color) - + var bestScore: Double = 0 for name in NSColor.colorNames { guard queryLower.contains(name), let systemColor = NSColor(named: name) else { continue } - + let distance = nsColor.distance(to: systemColor) // Max distance in weighted RGB space is ~3.0, so normalize and invert // Use a threshold to determine "close enough" matches @@ -223,7 +223,7 @@ struct CommandPaletteView: View { bestScore = max(bestScore, score) } } - + return bestScore } } @@ -346,26 +346,26 @@ private struct CommandRow: View { .fill(color) .frame(width: 8, height: 8) } - + if let icon = option.leadingIcon { Image(systemName: icon) .foregroundStyle(option.emphasis ? Color.accentColor : .secondary) .font(.system(size: 14, weight: .medium)) } - + VStack(alignment: .leading, spacing: 2) { Text(option.title) .fontWeight(option.emphasis ? .medium : .regular) - + if let subtitle = option.subtitle { Text(subtitle) .font(.caption) .foregroundStyle(.secondary) } } - + Spacer() - + if let badge = option.badge, !badge.isEmpty { Text(badge) .font(.caption2.weight(.medium)) @@ -376,7 +376,7 @@ private struct CommandRow: View { ) .foregroundStyle(Color.accentColor) } - + if let symbols = option.symbols { ShortcutSymbolsView(symbols: symbols) .foregroundStyle(.secondary) diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 051fbe48f..70d1273a2 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -11,7 +11,7 @@ struct TerminalCommandPaletteView: View { /// The configuration so we can lookup keyboard shortcuts. @ObservedObject var ghosttyConfig: Ghostty.Config - + /// The update view model for showing update commands. var updateViewModel: UpdateViewModel? @@ -54,13 +54,13 @@ struct TerminalCommandPaletteView: View { } } } - + /// All commands available in the command palette, combining update and terminal options. private var commandOptions: [CommandOption] { var options: [CommandOption] = [] // Updates always appear first options.append(contentsOf: updateOptions) - + // Sort the rest. We replace ":" with a character that sorts before space // so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker // for stable ordering when titles are equal. @@ -83,11 +83,11 @@ struct TerminalCommandPaletteView: View { /// Commands for installing or canceling available updates. private var updateOptions: [CommandOption] { var options: [CommandOption] = [] - + guard let updateViewModel, updateViewModel.state.isInstallable else { return options } - + // We override the update available one only because we want to properly // convey it'll go all the way through. let title: String @@ -96,7 +96,7 @@ struct TerminalCommandPaletteView: View { } else { title = updateViewModel.text } - + options.append(CommandOption( title: title, description: updateViewModel.description, @@ -106,14 +106,14 @@ struct TerminalCommandPaletteView: View { ) { (NSApp.delegate as? AppDelegate)?.updateController.installUpdate() }) - + options.append(CommandOption( title: "Cancel or Skip Update", description: "Dismiss the current update process" ) { updateViewModel.state.cancel() }) - + return options } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 7a4dce780..de1ea903d 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -29,7 +29,7 @@ class QuickTerminalController: BaseTerminalController { /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig - + /// Tracks if we're currently handling a manual resize to prevent recursion private var isHandlingResize: Bool = false @@ -135,14 +135,14 @@ class QuickTerminalController: BaseTerminalController { if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = window.frame } - + // Setup our content window.contentView = TerminalViewContainer( ghostty: self.ghostty, viewModel: self, delegate: self ) - + // Clear out our frame at this point, the fixup from above is complete. if let qtWindow = window as? QuickTerminalWindow { qtWindow.initialFrame = nil @@ -234,7 +234,7 @@ class QuickTerminalController: BaseTerminalController { // Prevent recursive loops isHandlingResize = true defer { isHandlingResize = false } - + switch position { case .top, .bottom, .center: // For centered positions (top, bottom, center), we need to recenter the window @@ -369,7 +369,7 @@ class QuickTerminalController: BaseTerminalController { } else { var config = Ghostty.SurfaceConfiguration() config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1" - + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config) surfaceTree = SplitTree(view: view) focusedSurface = view @@ -416,7 +416,7 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } - + // Grab our last closed frame to use from the cache. let closedFrame = screenStateCache.frame(for: screen) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index 9fd7d83bb..8742a7836 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -144,25 +144,25 @@ enum QuickTerminalPosition: String { x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .bottom: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: window.frame.origin.y // Keep the same Y position ) - + case .center: return CGPoint( x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .left, .right: // For left/right positions, only adjust horizontal centering if needed return window.frame.origin } } - + /// Calculate the vertically centered origin for side-positioned windows func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch self { @@ -171,13 +171,13 @@ enum QuickTerminalPosition: String { x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .right: return CGPoint( x: window.frame.origin.x, // Keep the same X position y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) ) - + case .top, .bottom, .center: // These positions don't need vertical recentering during resize return window.frame.origin diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift index 815bda9d6..70af0a505 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreen.swift @@ -12,10 +12,10 @@ enum QuickTerminalScreen { case "mouse": self = .mouse - + case "macos-menu-bar": self = .menuBar - + default: return nil } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift index a1c17abb9..301865561 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalScreenStateCache.swift @@ -8,15 +8,15 @@ import Cocoa /// to survive NSScreen garbage collection and automatically prunes stale entries. class QuickTerminalScreenStateCache { typealias Entries = [UUID: DisplayEntry] - + /// The maximum number of saved screen states we retain. This is to avoid some kind of /// pathological memory growth in case we get our screen state serializing wrong. I don't /// know anyone with more than 10 screens, so let's just arbitrarily go with that. private static let maxSavedScreens = 10 - + /// Time-to-live for screen entries that are no longer present (14 days). private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60 - + /// Keyed by display UUID to survive NSScreen garbage collection. private(set) var stateByDisplay: Entries = [:] @@ -28,11 +28,11 @@ class QuickTerminalScreenStateCache { name: NSApplication.didChangeScreenParametersNotification, object: nil) } - + deinit { NotificationCenter.default.removeObserver(self) } - + /// Save the window frame for a screen. func save(frame: NSRect, for screen: NSScreen) { guard let key = screen.displayUUID else { return } @@ -45,27 +45,27 @@ class QuickTerminalScreenStateCache { stateByDisplay[key] = entry pruneCapacity() } - + /// Retrieve the last closed frame for a screen, if valid. func frame(for screen: NSScreen) -> NSRect? { guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil } - + // Drop on dimension/scale change that makes the entry invalid if !entry.isValid(for: screen) { stateByDisplay.removeValue(forKey: key) return nil } - + entry.lastSeen = Date() stateByDisplay[key] = entry return entry.frame } - + @objc private func onScreensChanged(_ note: Notification) { let screens = NSScreen.screens let now = Date() let currentIDs = Set(screens.compactMap { $0.displayUUID }) - + for screen in screens { guard let key = screen.displayUUID else { continue } if var entry = stateByDisplay[key] { @@ -80,15 +80,15 @@ class QuickTerminalScreenStateCache { } } } - + // TTL prune for non-present screens stateByDisplay = stateByDisplay.filter { key, entry in currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL } - + pruneCapacity() } - + private func pruneCapacity() { guard stateByDisplay.count > Self.maxSavedScreens else { return } let toRemove = stateByDisplay @@ -98,13 +98,13 @@ class QuickTerminalScreenStateCache { stateByDisplay.removeValue(forKey: key) } } - + struct DisplayEntry: Codable { var frame: NSRect var screenSize: CGSize var scale: CGFloat var lastSeen: Date - + /// Returns true if this entry is still valid for the given screen. /// Valid if the scale matches and the cached size is not larger than the current screen size. /// This allows entries to persist when screens grow, but invalidates them when screens shrink. diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift index 680e6008a..507ec1baf 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalWindow.swift @@ -5,18 +5,18 @@ class QuickTerminalWindow: NSPanel { // still become key/main and receive events. override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } - + override func awakeFromNib() { super.awakeFromNib() // Note: almost all of this stuff can be done in the nib/xib directly // but I prefer to do it programmatically because the properties we // care about are less hidden. - + // Add a custom identifier so third party apps can use the Accessibility // API to apply special rules to the quick terminal. self.identifier = .init(rawValue: "com.mitchellh.ghostty.quickTerminal") - + // Set the correct AXSubrole of kAXFloatingWindowSubrole (allows // AeroSpace to treat the Quick Terminal as a floating window) self.setAccessibilitySubrole(.floatingWindow) @@ -33,7 +33,7 @@ class QuickTerminalWindow: NSPanel { /// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround /// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026 var initialFrame: NSRect? - + override func setFrame(_ frameRect: NSRect, display flag: Bool) { // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, diff --git a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift index 96f309de5..8d1332174 100644 --- a/macos/Sources/Features/Secure Input/SecureInputOverlay.swift +++ b/macos/Sources/Features/Secure Input/SecureInputOverlay.swift @@ -44,9 +44,9 @@ struct SecureInputOverlay: View { .padding(.trailing, 10) .popover(isPresented: $isPopover, arrowEdge: .bottom) { Text(""" - Secure Input is active. Secure Input is a macOS security feature that - prevents applications from reading keyboard events. This is enabled - automatically whenever Ghostty detects a password prompt in the terminal, + Secure Input is active. Secure Input is a macOS security feature that + prevents applications from reading keyboard events. This is enabled + automatically whenever Ghostty detects a password prompt in the terminal, or at all times if `Ghostty > Secure Keyboard Entry` is active. """) .padding(.all) diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index fe455d2df..e860c28c0 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -152,9 +152,9 @@ struct SplitView: View { return CGPoint(x: size.width / 2, y: leftRect.size.height) } } - + // MARK: Accessibility - + private var splitViewLabel: String { switch direction { case .horizontal: @@ -163,7 +163,7 @@ struct SplitView: View { return "Vertical split view" } } - + private var leftPaneLabel: String { switch direction { case .horizontal: @@ -172,7 +172,7 @@ struct SplitView: View { return "Top pane" } } - + private var rightPaneLabel: String { switch direction { case .horizontal: diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 8b57a1a91..5fa12edeb 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -7,19 +7,19 @@ import SwiftUI enum TerminalSplitOperation { case resize(Resize) case drop(Drop) - + struct Resize { let node: SplitTree.Node let ratio: Double } - + struct Drop { /// The surface being dragged. let payload: Ghostty.SurfaceView - + /// The surface it was dragged onto let destination: Ghostty.SurfaceView - + /// The zone it was dropped to determine how to split the destination. let zone: TerminalSplitDropZone } @@ -90,10 +90,10 @@ private struct TerminalSplitLeaf: View { let surfaceView: Ghostty.SurfaceView let isSplit: Bool let action: (TerminalSplitOperation) -> Void - + @State private var dropState: DropState = .idle @State private var isSelfDragging: Bool = false - + var body: some View { GeometryReader { geometry in Ghostty.InspectableSurface( @@ -129,26 +129,26 @@ private struct TerminalSplitLeaf: View { .accessibilityLabel("Terminal pane") } } - + private enum DropState: Equatable { case idle case dropping(TerminalSplitDropZone) } - + private struct SplitDropDelegate: DropDelegate { @Binding var dropState: DropState let viewSize: CGSize let destinationSurface: Ghostty.SurfaceView let action: (TerminalSplitOperation) -> Void - + func validateDrop(info: DropInfo) -> Bool { info.hasItemsConforming(to: [.ghosttySurfaceId]) } - + func dropEntered(info: DropInfo) { dropState = .dropping(.calculate(at: info.location, in: viewSize)) } - + func dropUpdated(info: DropInfo) -> DropProposal? { // For some reason dropUpdated is sent after performDrop is called // and we don't want to reset our drop zone to show it so we have @@ -157,11 +157,11 @@ private struct TerminalSplitLeaf: View { dropState = .dropping(.calculate(at: info.location, in: viewSize)) return DropProposal(operation: .move) } - + func dropExited(info: DropInfo) { dropState = .idle } - + func performDrop(info: DropInfo) -> Bool { let zone = TerminalSplitDropZone.calculate(at: info.location, in: viewSize) dropState = .idle @@ -169,7 +169,7 @@ private struct TerminalSplitLeaf: View { // Load the dropped surface asynchronously using Transferable let providers = info.itemProviders(for: [.ghosttySurfaceId]) guard let provider = providers.first else { return false } - + // Capture action before the async closure _ = provider.loadTransferable(type: Ghostty.SurfaceView.self) { [weak destinationSurface] result in switch result { @@ -180,12 +180,12 @@ private struct TerminalSplitLeaf: View { guard sourceSurface !== destinationSurface else { return } action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone))) } - + case .failure: break } } - + return true } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 87e63c348..302b3717d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -47,7 +47,7 @@ class BaseTerminalController: NSWindowController, /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false - + /// Set if the terminal view should show the update overlay. @Published var updateOverlayIsVisible: Bool = false @@ -423,7 +423,7 @@ class BaseTerminalController: NSWindowController, /// Goes to previous split unless we're the leftmost leaf, then goes to next. private func findNextFocusTargetAfterClosing(node: SplitTree.Node) -> Ghostty.SurfaceView? { guard let root = surfaceTree.root else { return nil } - + // If we're the leftmost, then we move to the next surface after closing. // Otherwise, we move to the previous. if root.leftmostLeaf() == node.leftmostLeaf() { @@ -432,7 +432,7 @@ class BaseTerminalController: NSWindowController, return surfaceTree.focusTarget(for: .previous, from: node) } } - + /// Remove a node from the surface tree and move focus appropriately. /// /// This also updates the undo manager to support restoring this node. @@ -470,13 +470,13 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: newView, from: oldView) } } - + // Setup our undo guard let undoManager else { return } if let undoAction { undoManager.setActionName(undoAction) } - + undoManager.registerUndo( withTarget: self, expiresAfter: undoExpiration @@ -487,7 +487,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: oldView, from: target.focusedSurface) } } - + undoManager.registerUndo( withTarget: target, expiresAfter: target.undoExpiration @@ -608,14 +608,14 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - + // Check if target surface is in current controller's tree guard surfaceTree.contains(target) else { return } - + // Equalize the splits surfaceTree = surfaceTree.equalized() } - + @objc private func ghosttyDidFocusSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -627,7 +627,7 @@ class BaseTerminalController: NSWindowController, // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Find the next surface to focus guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else { return @@ -648,7 +648,7 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: nextSurface, from: target) } } - + @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } @@ -676,19 +676,19 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: target) } } - + @objc private func ghosttyDidResizeSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Extract direction and amount from notification guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return } guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return } - + guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return } guard let amount = amountAny as? UInt16 else { return } - + // Convert Ghostty.SplitResizeDirection to SplitTree.Spatial.Direction let spatialDirection: SplitTree.Spatial.Direction switch direction { @@ -697,10 +697,10 @@ class BaseTerminalController: NSWindowController, case .left: spatialDirection = .left case .right: spatialDirection = .right } - + // Use viewBounds for the spatial calculation bounds let bounds = CGRect(origin: .zero, size: surfaceTree.viewBounds()) - + // Perform the resize using the new SplitTree resize method do { surfaceTree = try surfaceTree.resizing(node: targetNode, by: amount, in: spatialDirection, with: bounds) @@ -715,7 +715,7 @@ class BaseTerminalController: NSWindowController, // Bring the window to front and focus the surface. window?.makeKeyAndOrderFront(nil) - + // We use a small delay to ensure this runs after any UI cleanup // (e.g., command palette restoring focus to its original surface). Ghostty.moveFocus(to: target) @@ -728,11 +728,11 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttySurfaceDragEndedNoTarget(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // If our tree isn't split, then we never create a new window, because // it is already a single split. guard surfaceTree.isSplit else { return } - + // If we are removing our focused surface then we move it. We need to // keep track of our old one so undo sends focus back to the right place. let oldFocusedSurface = focusedSurface @@ -745,14 +745,14 @@ class BaseTerminalController: NSWindowController, // Create a new tree with the dragged surface and open a new window let newTree = SplitTree(view: target) - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + replaceSurfaceTree(removedTree, moveFocusFrom: oldFocusedSurface) _ = TerminalController.newWindow( ghostty, @@ -782,7 +782,7 @@ class BaseTerminalController: NSWindowController, if NSApp.mainWindow == window { surfaces = surfaces.filter { $0 != focusedSurface } } - + for surface in surfaces { surface.flagsChanged(with: event) } @@ -816,7 +816,7 @@ class BaseTerminalController: NSWindowController, titleDidChange(to: "👻") } } - + private func computeTitle(title: String, bell: Bool) -> String { var result = title if bell && ghostty.config.bellFeatures.contains(.title) { @@ -833,17 +833,17 @@ class BaseTerminalController: NSWindowController, private func applyTitleToWindow() { guard let window else { return } - + if let titleOverride { window.title = computeTitle( title: titleOverride, bell: focusedSurface?.bell ?? false) return } - + window.title = lastComputedTitle } - + func pwdDidChange(to: URL?) { guard let window else { return } @@ -895,7 +895,7 @@ class BaseTerminalController: NSWindowController, case .left: .left case .right: .right } - + // Check if source is in our tree if let sourceNode = surfaceTree.root?.node(view: source) { // Source is in our tree - same window move @@ -907,7 +907,7 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during drop: \(error)") return } - + replaceSurfaceTree( newTree, moveFocusTo: source, @@ -915,7 +915,7 @@ class BaseTerminalController: NSWindowController, undoAction: "Move Split") return } - + // Source is not in our tree - search other windows var sourceController: BaseTerminalController? var sourceNode: SplitTree.Node? @@ -928,12 +928,12 @@ class BaseTerminalController: NSWindowController, break } } - + guard let sourceController, let sourceNode else { Ghostty.logger.warning("source surface not found in any window during drop") return } - + // Remove from source controller's tree and add it to our tree. // We do this first because if there is an error then we can // abort. @@ -944,17 +944,17 @@ class BaseTerminalController: NSWindowController, Ghostty.logger.warning("failed to insert surface during cross-window drop: \(error)") return } - + // Treat our undo below as a full group. undoManager?.beginUndoGrouping() undoManager?.setActionName("Move Split") defer { undoManager?.endUndoGrouping() } - + // Remove the node from the source. sourceController.removeSurfaceNode(sourceNode) - + // Add in the surface to our tree replaceSurfaceTree( newTree, @@ -979,17 +979,17 @@ class BaseTerminalController: NSWindowController, func toggleBackgroundOpacity() { // Do nothing if config is already fully opaque guard ghostty.config.backgroundOpacity < 1 else { return } - + // Do nothing if in fullscreen (transparency doesn't apply in fullscreen) guard let window, !window.styleMask.contains(.fullScreen) else { return } // Toggle between transparent and opaque isBackgroundOpaque.toggle() - + // Update our appearance syncAppearance() } - + /// Override this to resync any appearance related properties. This will be called automatically /// when certain window properties change that affect appearance. The list below should be updated /// as we add new things: @@ -1051,7 +1051,7 @@ class BaseTerminalController: NSWindowController, func fullscreenDidChange() { guard let fullscreenStyle else { return } - + // When we enter fullscreen, we want to show the update overlay so that it // is easily visible. For native fullscreen this is visible by showing the // menubar but we don't want to rely on that. @@ -1060,7 +1060,7 @@ class BaseTerminalController: NSWindowController, } else { updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + // Always resync our appearance syncAppearance() } @@ -1145,26 +1145,26 @@ class BaseTerminalController: NSWindowController, fullscreenStyle = NativeFullscreen(window) fullscreenStyle?.delegate = self } - + // Set our update overlay state updateOverlayIsVisible = defaultUpdateOverlayVisibility() } - + func defaultUpdateOverlayVisibility() -> Bool { guard let window else { return true } - + // No titlebar we always show the update overlay because it can't support // updates in the titlebar guard window.styleMask.contains(.titled) else { return true } - + // If it's a non terminal window we can't trust it has an update accessory, // so we always want to show the overlay. guard let window = window as? TerminalWindow else { return true } - + // Show the overlay if the window isn't. return !window.supportsUpdateAccessory } @@ -1367,7 +1367,7 @@ class BaseTerminalController: NSWindowController, @IBAction func toggleCommandPalette(_ sender: Any?) { commandPaletteIsShowing.toggle() } - + @IBAction func find(_ sender: Any) { focusedSurface?.find(sender) } @@ -1383,11 +1383,11 @@ class BaseTerminalController: NSWindowController, @IBAction func findNext(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findPrevious(_ sender: Any) { focusedSurface?.findNext(sender) } - + @IBAction func findHide(_ sender: Any) { focusedSurface?.findHide(sender) } @@ -1429,7 +1429,7 @@ extension BaseTerminalController: NSMenuItemValidation { return true } } - + // MARK: - Surface Color Scheme /// Update the surface tree's color scheme only when it actually changes. diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index c3cc6961c..acc4d0532 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -137,7 +137,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { break } } - + if let view = foundView { c.focusedSurface = view restoreFocus(to: view, inWindow: window) diff --git a/macos/Sources/Features/Terminal/TerminalTabColor.swift b/macos/Sources/Features/Terminal/TerminalTabColor.swift index 08d89324c..2879822b3 100644 --- a/macos/Sources/Features/Terminal/TerminalTabColor.swift +++ b/macos/Sources/Features/Terminal/TerminalTabColor.swift @@ -122,7 +122,7 @@ struct TabColorMenuView: View { VStack(alignment: .leading, spacing: 3) { Text("Tab Color") .padding(.bottom, 2) - + ForEach(Self.paletteRows, id: \.self) { row in HStack(spacing: 2) { ForEach(row, id: \.self) { color in @@ -142,7 +142,7 @@ struct TabColorMenuView: View { .padding(.top, 4) .padding(.bottom, 4) } - + static let paletteRows: [[TerminalTabColor]] = [ [.none, .blue, .purple, .pink, .red], [.orange, .yellow, .green, .teal, .graphite], diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d369721dd..1aab8f497 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -17,7 +17,7 @@ protocol TerminalViewDelegate: AnyObject { /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) - + /// A split tree operation func performSplitAction(_ action: TerminalSplitOperation) } @@ -32,7 +32,7 @@ protocol TerminalViewModel: ObservableObject { /// The command palette state. var commandPaletteIsShowing: Bool { get set } - + /// The update overlay should be visible. var updateOverlayIsVisible: Bool { get } } @@ -46,7 +46,7 @@ struct TerminalView: View { // An optional delegate to receive information about terminal changes. weak var delegate: (any TerminalViewDelegate)? - + // The most recently focused surface, equal to focusedSurface when // it is non-nil. @State private var lastFocusedSurface: Weak = .init() @@ -116,7 +116,7 @@ struct TerminalView: View { self.delegate?.performAction(action, on: surfaceView) } } - + // Show update information above all else. if viewModel.updateOverlayIsVisible { UpdateOverlay() @@ -132,7 +132,7 @@ private struct UpdateOverlay: View { if let appDelegate = NSApp.delegate as? AppDelegate { VStack { Spacer() - + HStack { Spacer() UpdatePill(model: appDelegate.updateViewModel) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index dd8b258f3..766ec5857 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -3,7 +3,7 @@ import AppKit class HiddenTitlebarTerminalWindow: TerminalWindow { // No titlebar, we don't support accessories. override var supportsUpdateAccessory: Bool { false } - + override func awakeFromNib() { super.awakeFromNib() @@ -34,7 +34,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { .closable, .miniaturizable, ] - + /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { // If our window is fullscreen then we don't reapply the hidden style because @@ -43,7 +43,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { if terminalController?.fullscreenStyle?.isFullscreen ?? false { return } - + // Apply our style mask while preserving the .fullScreen option if styleMask.contains(.fullScreen) { styleMask = Self.hiddenStyleMask.union([.fullScreen]) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 0c70217a5..cde8d2747 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -33,7 +33,7 @@ class TerminalWindow: NSWindow { /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() - + /// Sets up our tab context menu private var tabMenuObserver: NSObjectProtocol? @@ -543,7 +543,7 @@ class TerminalWindow: NSWindow { NotificationCenter.default.removeObserver(observer) } } - + // MARK: Config struct DerivedConfig { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 21d5ef0dd..184614831 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -8,7 +8,7 @@ import SwiftUI class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() - + /// Titlebar tabs can't support the update accessory because of the way we layout /// the native tabs back into the menu bar. override var supportsUpdateAccessory: Bool { false } @@ -58,13 +58,13 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // Check if we have a tab bar and set it up if we have to. See the comment // on this function to learn why we need to check this here. setupTabBar() - + viewModel.isMainWindow = true } override func resignMain() { super.resignMain() - + viewModel.isMainWindow = false } @@ -84,18 +84,18 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.sendEvent(event) return } - + guard let tabBarView else { super.sendEvent(event) return } - + let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { super.sendEvent(event) return } - + tabBarView.rightMouseDown(with: event) } @@ -107,7 +107,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // After dragging a tab into a new window, `hasTabBar` needs to be // updated to properly review window title viewModel.hasTabBar = false - + super.addTitlebarAccessoryViewController(childViewController) return } @@ -116,7 +116,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // system will also try to add tab bar to this window, so we want to reset observer, // to put tab bar where we want again tabBarObserver = nil - + // Some setup needs to happen BEFORE it is added, such as layout. If // we don't do this before the call below, we'll trigger an AppKit // assertion. @@ -189,7 +189,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool guard let clipView = tabBarView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } guard let accessoryView = clipView.subviews[safe: 0] else { return } guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } - + // Make sure tabBar's height won't be stretched guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return } tabBarView.frame.size.height = newTabButton.frame.width @@ -282,7 +282,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // This is the documented way to avoid the glass view on an item. // We don't want glass on our title. item.isBordered = false - + return item default: return NSToolbarItem(itemIdentifier: itemIdentifier) @@ -327,7 +327,7 @@ extension TitlebarTabsTahoeTerminalWindow { Color.clear.frame(width: 1, height: 1) } } - + @ViewBuilder var titleText: some View { Text(title) diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index 4b7e7c945..ce98bd277 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -9,15 +9,15 @@ import SwiftUI struct UpdateBadge: View { /// The update view model that provides the current state and progress @ObservedObject var model: UpdateViewModel - + /// Current rotation angle for animated icon states @State private var rotationAngle: Double = 0 - + var body: some View { badgeContent .accessibilityLabel(model.text) } - + @ViewBuilder private var badgeContent: some View { switch model.state { @@ -28,10 +28,10 @@ struct UpdateBadge: View { } else { Image(systemName: "arrow.down.circle") } - + case .extracting(let extracting): ProgressRingView(progress: min(1, max(0, extracting.progress))) - + case .checking: if let iconName = model.iconName { Image(systemName: iconName) @@ -47,7 +47,7 @@ struct UpdateBadge: View { } else { EmptyView() } - + default: if let iconName = model.iconName { Image(systemName: iconName) @@ -64,15 +64,15 @@ struct UpdateBadge: View { private struct ProgressRingView: View { /// The current progress value, ranging from 0.0 (empty) to 1.0 (complete) let progress: Double - + /// The width of the progress ring stroke let lineWidth: CGFloat = 2 - + var body: some View { ZStack { Circle() .stroke(Color.primary.opacity(0.2), lineWidth: lineWidth) - + Circle() .trim(from: 0, to: progress) .stroke(Color.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 939eed420..1ca218c8b 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -11,16 +11,16 @@ class UpdateController { private(set) var updater: SPUUpdater private let userDriver: UpdateDriver private var installCancellable: AnyCancellable? - + var viewModel: UpdateViewModel { userDriver.viewModel } - + /// True if we're installing an update. var isInstalling: Bool { installCancellable != nil } - + /// Initialize a new update controller. init() { let hostBundle = Bundle.main @@ -34,11 +34,11 @@ class UpdateController { delegate: userDriver ) } - + deinit { installCancellable?.cancel() } - + /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, @@ -59,35 +59,35 @@ class UpdateController { )) } } - + /// Force install the current update. As long as we're in some "update available" state this will /// trigger all the steps necessary to complete the update. func installUpdate() { // Must be in an installable state guard viewModel.state.isInstallable else { return } - + // If we're already force installing then do nothing. guard installCancellable == nil else { return } - + // Setup a combine listener to listen for state changes and to always // confirm them. If we go to a non-installable state, cancel the listener. // The sink runs immediately with the current state, so we don't need to // manually confirm the first state. installCancellable = viewModel.$state.sink { [weak self] state in guard let self else { return } - + // If we move to a non-installable state (error, idle, etc.) then we // stop force installing. guard state.isInstallable else { self.installCancellable = nil return } - + // Continue the `yes` chain! state.confirm() } } - + /// Check for updates. /// /// This is typically connected to a menu item action. @@ -97,11 +97,11 @@ class UpdateController { updater.checkForUpdates() return } - + // If we're not idle then we need to cancel any prior state. installCancellable?.cancel() viewModel.state.cancel() - + // The above will take time to settle, so we delay the check for some time. // The 100ms is arbitrary and I'd rather not, but we have to wait more than // one loop tick it seems. @@ -109,7 +109,7 @@ class UpdateController { self?.updater.checkForUpdates() } } - + /// Validate the check for updates menu item. /// /// - Parameter item: The menu item to validate diff --git a/macos/Sources/Features/Update/UpdateDelegate.swift b/macos/Sources/Features/Update/UpdateDelegate.swift index cbfbdede0..72d54bd22 100644 --- a/macos/Sources/Features/Update/UpdateDelegate.swift +++ b/macos/Sources/Features/Update/UpdateDelegate.swift @@ -6,7 +6,7 @@ extension UpdateDriver: SPUUpdaterDelegate { guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } - + // Sparkle supports a native concept of "channels" but it requires that // you share a single appcast file. We don't want to do that so we // do this instead. diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index 3beb4c9be..b5f580f1b 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -5,23 +5,23 @@ import Sparkle class UpdateDriver: NSObject, SPUUserDriver { let viewModel: UpdateViewModel let standard: SPUStandardUserDriver - + init(viewModel: UpdateViewModel, hostBundle: Bundle) { self.viewModel = viewModel self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil) super.init() - + NotificationCenter.default.addObserver( self, selector: #selector(handleTerminalWindowWillClose), name: TerminalWindow.terminalWillCloseNotification, object: nil) } - + deinit { NotificationCenter.default.removeObserver(self) } - + @objc private func handleTerminalWindowWillClose() { // If we lost the ability to show unobtrusive states, cancel whatever // update state we're in. This will allow the manual `check for updates` @@ -36,7 +36,7 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel.state = .idle } } - + func show(_ request: SPUUpdatePermissionRequest, reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) { viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in @@ -47,7 +47,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.show(request, reply: reply) } } - + func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { viewModel.state = .checking(.init(cancel: cancellation)) @@ -55,7 +55,7 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.showUserInitiatedUpdateCheck(cancellation: cancellation) } } - + func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { @@ -64,25 +64,25 @@ class UpdateDriver: NSObject, SPUUserDriver { standard.showUpdateFound(with: appcastItem, state: state, reply: reply) } } - + func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { // We don't do anything with the release notes here because Ghostty // doesn't use the release notes feature of Sparkle currently. } - + func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) { // We don't do anything with release notes. See `showUpdateReleaseNotes` } - + func showUpdateNotFoundWithError(_ error: any Error, acknowledgement: @escaping () -> Void) { viewModel.state = .notFound(.init(acknowledgement: acknowledgement)) - + if !hasUnobtrusiveTarget { standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement) } } - + func showUpdaterError(_ error: any Error, acknowledgement: @escaping () -> Void) { viewModel.state = .error(.init( @@ -98,71 +98,71 @@ class UpdateDriver: NSObject, SPUUserDriver { dismiss: { [weak viewModel] in viewModel?.state = .idle })) - + if !hasUnobtrusiveTarget { standard.showUpdaterError(error, acknowledgement: acknowledgement) } else { acknowledgement() } } - + func showDownloadInitiated(cancellation: @escaping () -> Void) { viewModel.state = .downloading(.init( cancel: cancellation, expectedLength: nil, progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadInitiated(cancellation: cancellation) } } - + func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { guard case let .downloading(downloading) = viewModel.state else { return } - + viewModel.state = .downloading(.init( cancel: downloading.cancel, expectedLength: expectedContentLength, progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength) } } - + func showDownloadDidReceiveData(ofLength length: UInt64) { guard case let .downloading(downloading) = viewModel.state else { return } - + viewModel.state = .downloading(.init( cancel: downloading.cancel, expectedLength: downloading.expectedLength, progress: downloading.progress + length)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidReceiveData(ofLength: length) } } - + func showDownloadDidStartExtractingUpdate() { viewModel.state = .extracting(.init(progress: 0)) - + if !hasUnobtrusiveTarget { standard.showDownloadDidStartExtractingUpdate() } } - + func showExtractionReceivedProgress(_ progress: Double) { viewModel.state = .extracting(.init(progress: progress)) - + if !hasUnobtrusiveTarget { standard.showExtractionReceivedProgress(progress) } } - + func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) { if !hasUnobtrusiveTarget { standard.showReady(toInstallAndRelaunch: reply) @@ -170,7 +170,7 @@ class UpdateDriver: NSObject, SPUUserDriver { reply(.install) } } - + func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { viewModel.state = .installing(.init( retryTerminatingApplication: retryTerminatingApplication, @@ -178,30 +178,30 @@ class UpdateDriver: NSObject, SPUUserDriver { viewModel?.state = .idle } )) - + if !hasUnobtrusiveTarget { standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication) } } - + func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement) viewModel.state = .idle } - + func showUpdateInFocus() { if !hasUnobtrusiveTarget { standard.showUpdateInFocus() } } - + func dismissUpdateInstallation() { viewModel.state = .idle standard.dismissUpdateInstallation() } - + // MARK: No-Window Fallback - + /// True if there is a target that can render our unobtrusive update checker. var hasUnobtrusiveTarget: Bool { NSApp.windows.contains { window in diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index 29d1669e1..53dfbe842 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -4,16 +4,16 @@ import SwiftUI struct UpdatePill: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - + /// Whether the update popover is currently visible @State private var showPopover = false - + /// Task for auto-dismissing the "No Updates" state @State private var resetTask: Task? - + /// The font used for the pill text private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) - + var body: some View { if !model.state.isIdle { pillButton @@ -36,7 +36,7 @@ struct UpdatePill: View { } } } - + /// The pill-shaped button view that displays the update badge and text @ViewBuilder private var pillButton: some View { @@ -51,7 +51,7 @@ struct UpdatePill: View { HStack(spacing: 6) { UpdateBadge(model: model) .frame(width: 14, height: 14) - + Text(model.text) .font(Font(textFont)) .lineLimit(1) @@ -71,7 +71,7 @@ struct UpdatePill: View { .help(model.text) .accessibilityLabel(model.text) } - + /// Calculated width for the text to prevent resizing during progress updates private var textWidth: CGFloat? { let attributes: [NSAttributedString.Key: Any] = [.font: textFont] diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index b39fa707b..aa4e822f3 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -8,10 +8,10 @@ import Sparkle struct UpdatePopoverView: View { /// The update view model that provides the current state and information @ObservedObject var model: UpdateViewModel - + /// Environment value for dismissing the popover @Environment(\.dismiss) private var dismiss - + var body: some View { VStack(alignment: .leading, spacing: 0) { switch model.state { @@ -19,31 +19,31 @@ struct UpdatePopoverView: View { // Shouldn't happen in a well-formed view stack. Higher levels // should not call the popover for idles. EmptyView() - + case .permissionRequest(let request): PermissionRequestView(request: request, dismiss: dismiss) - + case .checking(let checking): CheckingView(checking: checking, dismiss: dismiss) - + case .updateAvailable(let update): UpdateAvailableView(update: update, dismiss: dismiss) - + case .downloading(let download): DownloadingView(download: download, dismiss: dismiss) - + case .extracting(let extracting): ExtractingView(extracting: extracting) - + case .installing(let installing): // This is only required when `installing.isAutoUpdate == true`, // but we keep it anyway, just in case something unexpected // happens during installing InstallingView(installing: installing, dismiss: dismiss) - + case .notFound(let notFound): NotFoundView(notFound: notFound, dismiss: dismiss) - + case .error(let error): UpdateErrorView(error: error, dismiss: dismiss) } @@ -55,19 +55,19 @@ struct UpdatePopoverView: View { private struct PermissionRequestView: View { let request: UpdateState.PermissionRequest let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Enable automatic updates?") .font(.system(size: 13, weight: .semibold)) - + Text("Ghostty can automatically check for updates in the background.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack(spacing: 8) { Button("Not Now") { request.reply(SUUpdatePermissionResponse( @@ -76,9 +76,9 @@ private struct PermissionRequestView: View { dismiss() } .keyboardShortcut(.cancelAction) - + Spacer() - + Button("Allow") { request.reply(SUUpdatePermissionResponse( automaticUpdateChecks: true, @@ -96,7 +96,7 @@ private struct PermissionRequestView: View { private struct CheckingView: View { let checking: UpdateState.Checking let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 10) { @@ -105,7 +105,7 @@ private struct CheckingView: View { Text("Checking for updates…") .font(.system(size: 13)) } - + HStack { Spacer() Button("Cancel") { @@ -123,16 +123,16 @@ private struct CheckingView: View { private struct UpdateAvailableView: View { let update: UpdateState.UpdateAvailable let dismiss: DismissAction - + private let labelWidth: CGFloat = 60 - + var body: some View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 8) { Text("Update Available") .font(.system(size: 13, weight: .semibold)) - + VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Version:") @@ -141,7 +141,7 @@ private struct UpdateAvailableView: View { Text(update.appcastItem.displayVersionString) } .font(.system(size: 11)) - + if update.appcastItem.contentLength > 0 { HStack(spacing: 6) { Text("Size:") @@ -151,7 +151,7 @@ private struct UpdateAvailableView: View { } .font(.system(size: 11)) } - + if let date = update.appcastItem.date { HStack(spacing: 6) { Text("Released:") @@ -164,23 +164,23 @@ private struct UpdateAvailableView: View { } .textSelection(.enabled) } - + HStack(spacing: 8) { Button("Skip") { update.reply(.skip) dismiss() } .controlSize(.small) - + Button("Later") { update.reply(.dismiss) dismiss() } .controlSize(.small) .keyboardShortcut(.cancelAction) - + Spacer() - + Button("Install and Relaunch") { update.reply(.install) dismiss() @@ -191,10 +191,10 @@ private struct UpdateAvailableView: View { } } .padding(16) - + if let notes = update.releaseNotes { Divider() - + Link(destination: notes.url) { HStack { Image(systemName: "doc.text") @@ -220,13 +220,13 @@ private struct UpdateAvailableView: View { private struct DownloadingView: View { let download: UpdateState.Downloading let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Downloading Update") .font(.system(size: 13, weight: .semibold)) - + if let expectedLength = download.expectedLength, expectedLength > 0 { let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) VStack(alignment: .leading, spacing: 6) { @@ -240,7 +240,7 @@ private struct DownloadingView: View { .controlSize(.small) } } - + HStack { Spacer() Button("Cancel") { @@ -257,12 +257,12 @@ private struct DownloadingView: View { private struct ExtractingView: View { let extracting: UpdateState.Extracting - + var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Preparing Update") .font(.system(size: 13, weight: .semibold)) - + VStack(alignment: .leading, spacing: 6) { ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0) Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100)) @@ -277,19 +277,19 @@ private struct ExtractingView: View { private struct InstallingView: View { let installing: UpdateState.Installing let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("Restart Required") .font(.system(size: 13, weight: .semibold)) - + Text("The update is ready. Please restart the application to complete the installation.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Button("Restart Later") { installing.dismiss() @@ -297,9 +297,9 @@ private struct InstallingView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Restart Now") { installing.retryTerminatingApplication() dismiss() @@ -316,19 +316,19 @@ private struct InstallingView: View { private struct NotFoundView: View { let notFound: UpdateState.NotFound let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { Text("No Updates Found") .font(.system(size: 13, weight: .semibold)) - + Text("You're already running the latest version.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Spacer() Button("OK") { @@ -346,7 +346,7 @@ private struct NotFoundView: View { private struct UpdateErrorView: View { let error: UpdateState.Error let dismiss: DismissAction - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -357,13 +357,13 @@ private struct UpdateErrorView: View { Text("Update Failed") .font(.system(size: 13, weight: .semibold)) } - + Text(error.error.localizedDescription) .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack(spacing: 8) { Button("OK") { error.dismiss() @@ -371,9 +371,9 @@ private struct UpdateErrorView: View { } .keyboardShortcut(.cancelAction) .controlSize(.small) - + Spacer() - + Button("Retry") { error.retry() dismiss() diff --git a/macos/Sources/Features/Update/UpdateSimulator.swift b/macos/Sources/Features/Update/UpdateSimulator.swift index bf168d9fc..c893993e0 100644 --- a/macos/Sources/Features/Update/UpdateSimulator.swift +++ b/macos/Sources/Features/Update/UpdateSimulator.swift @@ -9,31 +9,31 @@ import Sparkle enum UpdateSimulator { /// Complete successful update flow: checking → available → download → extract → ready → install → idle case happyPath - + /// No updates available: checking (2s) → "No Updates Available" (3s) → idle case notFound - + /// Error during check: checking (2s) → error with retry callback case error - + /// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install case slowDownload - + /// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted case permissionRequest - + /// User cancels during download: checking → available → download (5 steps) → cancels → idle case cancelDuringDownload - + /// User cancels while checking: checking (1s) → cancels → idle case cancelDuringChecking - + /// Shows the installing state with restart button: installing (stays until dismissed) case installing - + /// Simulates auto-update flow: goes directly to installing state without showing intermediate UI case autoUpdate - + func simulate(with viewModel: UpdateViewModel) { switch self { case .happyPath: @@ -56,12 +56,12 @@ enum UpdateSimulator { simulateAutoUpdate(viewModel) } } - + private func simulateHappyPath(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -75,28 +75,28 @@ enum UpdateSimulator { )) } } - + private func simulateNotFound(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .notFound(.init(acknowledgement: { // Acknowledgement called when dismissed })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { viewModel.state = .idle } } } - + private func simulateError(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .error(.init( error: NSError(domain: "UpdateError", code: 1, userInfo: [ @@ -111,12 +111,12 @@ enum UpdateSimulator { )) } } - + private func simulateSlowDownload(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -130,7 +130,7 @@ enum UpdateSimulator { )) } } - + private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -140,7 +140,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...20 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) { let updatedDownload = UpdateState.Downloading( @@ -149,7 +149,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 20 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -158,7 +158,7 @@ enum UpdateSimulator { } } } - + private func simulatePermissionRequest(_ viewModel: UpdateViewModel) { let request = SPUUpdatePermissionRequest(systemProfile: []) viewModel.state = .permissionRequest(.init( @@ -172,12 +172,12 @@ enum UpdateSimulator { } )) } - + private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { viewModel.state = .updateAvailable(.init( appcastItem: SUAppcastItem.empty(), @@ -191,7 +191,7 @@ enum UpdateSimulator { )) } } - + private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -201,7 +201,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -210,7 +210,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 5 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { viewModel.state = .idle @@ -219,17 +219,17 @@ enum UpdateSimulator { } } } - + private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) { viewModel.state = .checking(.init(cancel: { viewModel.state = .idle })) - + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { viewModel.state = .idle } } - + private func simulateDownload(_ viewModel: UpdateViewModel) { let download = UpdateState.Downloading( cancel: { @@ -239,7 +239,7 @@ enum UpdateSimulator { progress: 0 ) viewModel.state = .downloading(download) - + for i in 1...10 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) { let updatedDownload = UpdateState.Downloading( @@ -248,7 +248,7 @@ enum UpdateSimulator { progress: UInt64(i * 100) ) viewModel.state = .downloading(updatedDownload) - + if i == 10 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateExtract(viewModel) @@ -257,14 +257,14 @@ enum UpdateSimulator { } } } - + private func simulateExtract(_ viewModel: UpdateViewModel) { viewModel.state = .extracting(.init(progress: 0.0)) - + for j in 1...5 { DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) { viewModel.state = .extracting(.init(progress: Double(j) / 5.0)) - + if j == 5 { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { simulateInstalling(viewModel) @@ -273,7 +273,7 @@ enum UpdateSimulator { } } } - + private func simulateInstalling(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( retryTerminatingApplication: { @@ -285,7 +285,7 @@ enum UpdateSimulator { } )) } - + private func simulateAutoUpdate(_ viewModel: UpdateViewModel) { viewModel.state = .installing(.init( isAutoUpdate: true, diff --git a/macos/Sources/Features/Update/UpdateViewModel.swift b/macos/Sources/Features/Update/UpdateViewModel.swift index a3ef765cf..8e66f4a16 100644 --- a/macos/Sources/Features/Update/UpdateViewModel.swift +++ b/macos/Sources/Features/Update/UpdateViewModel.swift @@ -4,7 +4,7 @@ import Sparkle class UpdateViewModel: ObservableObject { @Published var state: UpdateState = .idle - + /// The text to display for the current update state. /// Returns an empty string for idle state, progress percentages for downloading/extracting, /// or descriptive text for other states. @@ -38,7 +38,7 @@ class UpdateViewModel: ObservableObject { return err.error.localizedDescription } } - + /// The maximum width text for states that show progress. /// Used to prevent the pill from resizing as percentages change. var maxWidthText: String { @@ -51,7 +51,7 @@ class UpdateViewModel: ObservableObject { return text } } - + /// The SF Symbol icon name for the current update state. var iconName: String? { switch state { @@ -75,7 +75,7 @@ class UpdateViewModel: ObservableObject { return "exclamationmark.triangle.fill" } } - + /// A longer description for the current update state. /// Used in contexts like the command palette where more detail is helpful. var description: String { @@ -100,7 +100,7 @@ class UpdateViewModel: ObservableObject { return "An error occurred during the update process" } } - + /// A badge to display for the current update state. /// Returns version numbers, progress percentages, or nil. var badge: String? { @@ -120,7 +120,7 @@ class UpdateViewModel: ObservableObject { return nil } } - + /// The color to apply to the icon for the current update state. var iconColor: Color { switch state { @@ -140,7 +140,7 @@ class UpdateViewModel: ObservableObject { return .orange } } - + /// The background color for the update pill. var backgroundColor: Color { switch state { @@ -156,7 +156,7 @@ class UpdateViewModel: ObservableObject { return Color(nsColor: .controlBackgroundColor) } } - + /// The foreground (text) color for the update pill. var foregroundColor: Color { switch state { @@ -184,12 +184,12 @@ enum UpdateState: Equatable { case downloading(Downloading) case extracting(Extracting) case installing(Installing) - + var isIdle: Bool { if case .idle = self { return true } return false } - + /// This is true if we're in a state that can be force installed. var isInstallable: Bool { switch self { @@ -199,12 +199,12 @@ enum UpdateState: Equatable { .extracting, .installing: return true - + default: return false } } - + func cancel() { switch self { case .checking(let checking): @@ -221,7 +221,7 @@ enum UpdateState: Equatable { break } } - + /// Confirms or accepts the current update state. /// - For available updates: begins installation /// - For ready-to-install: proceeds with installation @@ -233,7 +233,7 @@ enum UpdateState: Equatable { break } } - + static func == (lhs: UpdateState, rhs: UpdateState) -> Bool { switch (lhs, rhs) { case (.idle, .idle): @@ -258,38 +258,38 @@ enum UpdateState: Equatable { return false } } - + struct NotFound { let acknowledgement: () -> Void } - + struct PermissionRequest { let request: SPUUpdatePermissionRequest let reply: @Sendable (SUUpdatePermissionResponse) -> Void } - + struct Checking { let cancel: () -> Void } - + struct UpdateAvailable { let appcastItem: SUAppcastItem let reply: @Sendable (SPUUserUpdateChoice) -> Void - + var releaseNotes: ReleaseNotes? { let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit) } } - + enum ReleaseNotes { case commit(URL) case compareTip(URL) case tagged(URL) - + init?(displayVersionString: String, currentCommit: String?) { let version = displayVersionString - + // Check for semantic version (x.y.z) if let semver = Self.extractSemanticVersion(from: version) { let slug = semver.replacingOccurrences(of: ".", with: "-") @@ -298,12 +298,12 @@ enum UpdateState: Equatable { return } } - + // Fall back to git hash detection guard let newHash = Self.extractGitHash(from: version) else { return nil } - + if let currentHash = currentCommit, !currentHash.isEmpty, let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") { self = .compareTip(url) @@ -313,7 +313,7 @@ enum UpdateState: Equatable { return nil } } - + private static func extractSemanticVersion(from version: String) -> String? { let pattern = #"^\d+\.\d+\.\d+$"# if version.range(of: pattern, options: .regularExpression) != nil { @@ -321,7 +321,7 @@ enum UpdateState: Equatable { } return nil } - + private static func extractGitHash(from version: String) -> String? { let pattern = #"[0-9a-f]{7,40}"# if let range = version.range(of: pattern, options: .regularExpression) { @@ -329,7 +329,7 @@ enum UpdateState: Equatable { } return nil } - + var url: URL { switch self { case .commit(let url): return url @@ -337,7 +337,7 @@ enum UpdateState: Equatable { case .tagged(let url): return url } } - + var label: String { switch self { case .commit: return "View GitHub Commit" @@ -346,23 +346,23 @@ enum UpdateState: Equatable { } } } - + struct Error { let error: any Swift.Error let retry: () -> Void let dismiss: () -> Void } - + struct Downloading { let cancel: () -> Void let expectedLength: UInt64? let progress: UInt64 } - + struct Extracting { let progress: Double } - + struct Installing { /// True if this state is triggered by ``Ghostty/UpdateDriver/updater(_:willInstallUpdateOnQuit:immediateInstallationBlock:)`` var isAutoUpdate = false diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index 1bd197e7d..f3842fc56 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -40,13 +40,13 @@ extension Ghostty.Action { self.amount = c.amount } } - + struct OpenURL { enum Kind { case unknown case text case html - + init(_ c: ghostty_action_open_url_kind_e) { switch c { case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT: @@ -58,13 +58,13 @@ extension Ghostty.Action { } } } - + let kind: Kind let url: String - + init(c: ghostty_action_open_url_s) { self.kind = Kind(c.kind) - + if let urlCString = c.url { let data = Data(bytes: urlCString, count: Int(c.len)) self.url = String(data: data, encoding: .utf8) ?? "" @@ -81,7 +81,7 @@ extension Ghostty.Action { case error case indeterminate case pause - + init(_ c: ghostty_action_progress_report_state_e) { switch c { case GHOSTTY_PROGRESS_STATE_REMOVE: @@ -99,26 +99,26 @@ extension Ghostty.Action { } } } - + let state: State let progress: UInt8? } - + struct Scrollbar { let total: UInt64 let offset: UInt64 let len: UInt64 - + init(c: ghostty_action_scrollbar_s) { total = c.total - offset = c.offset + offset = c.offset len = c.len } } struct StartSearch { let needle: String? - + init(c: ghostty_action_start_search_s) { if let needleCString = c.needle { self.needle = String(cString: needleCString) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 9a99ddbcb..d09cd3184 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -379,25 +379,25 @@ extension Ghostty { let surface = self.surfaceUserdata(from: userdata) guard let pasteboard = NSPasteboard.ghostty(location) else { return } guard let content = content, len > 0 else { return } - + // Convert the C array to Swift array let contentArray = (0.. Bool { let action = Ghostty.Action.OpenURL(c: v) - + // If the URL doesn't have a valid scheme we assume its a file path. The URL // initializer will gladly take invalid URLs (e.g. plain file paths) and turn // them into schema-less URLs, but these won't open properly in text editors. @@ -697,7 +697,7 @@ extension Ghostty { } else { url = URL(filePath: action.url) } - + switch action.kind { case .text: // Open with the default editor for `*.ghostty` file or just system text editor @@ -706,15 +706,15 @@ extension Ghostty { NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) return true } - + case .html: // The extension will be HTML and we do the right thing automatically. break - + case .unknown: break } - + // Open with the default application for the URL NSWorkspace.shared.open(url) return true @@ -1850,7 +1850,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - + let progressReport = Ghostty.Action.ProgressReport(c: v) DispatchQueue.main.async { if progressReport.state == .remove { @@ -1877,7 +1877,7 @@ extension Ghostty { case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - + let scrollbar = Ghostty.Action.Scrollbar(c: v) NotificationCenter.default.post( name: .ghosttyDidUpdateScrollbar, @@ -1914,7 +1914,7 @@ extension Ghostty { } else { surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch) } - + NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView) } diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 3d3219663..d65bac27f 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -763,7 +763,7 @@ extension Ghostty.Config { static let navigation = SplitPreserveZoom(rawValue: 1 << 0) } - + enum MacDockDropBehavior: String { case new_tab = "new-tab" case new_window = "new-window" diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index bdb64a6b5..1e92eb8a1 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -299,17 +299,17 @@ extension Ghostty { } } } - + struct ClipboardContent { let mime: String let data: String - + static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? { guard let mimePtr = content.mime, let dataPtr = content.data else { return nil } - + return ClipboardContent( mime: String(cString: mimePtr), data: String(cString: dataPtr) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift index 8b312f5c0..dd2f3ef5e 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceDragSource.swift @@ -6,12 +6,12 @@ extension Ghostty { /// or nil if no surface is being dragged. struct DraggingSurfaceKey: PreferenceKey { static var defaultValue: SurfaceView.ID? - + static func reduce(value: inout SurfaceView.ID?, nextValue: () -> SurfaceView.ID?) { value = nextValue() ?? value } } - + /// A SwiftUI view that provides drag source functionality for terminal surfaces. /// /// This view wraps an AppKit-based drag source to enable drag-and-drop reordering @@ -24,13 +24,13 @@ extension Ghostty { struct SurfaceDragSource: View { /// The surface view that will be dragged. let surfaceView: SurfaceView - + /// Binding that reflects whether a drag session is currently active. @Binding var isDragging: Bool - + /// Binding that reflects whether the mouse is hovering over this view. @Binding var isHovering: Bool - + var body: some View { SurfaceDragSourceViewRepresentable( surfaceView: surfaceView, @@ -46,7 +46,7 @@ extension Ghostty { let surfaceView: SurfaceView @Binding var isDragging: Bool @Binding var isHovering: Bool - + func makeNSView(context: Context) -> SurfaceDragSourceView { let view = SurfaceDragSourceView() view.surfaceView = surfaceView @@ -60,7 +60,7 @@ extension Ghostty { } return view } - + func updateNSView(_ nsView: SurfaceDragSourceView, context: Context) { nsView.surfaceView = surfaceView nsView.onDragStateChanged = { dragging in @@ -73,7 +73,7 @@ extension Ghostty { } } } - + /// The underlying NSView that handles drag operations. /// /// This view manages mouse tracking and drag initiation for surface reordering. @@ -82,26 +82,26 @@ extension Ghostty { fileprivate class SurfaceDragSourceView: NSView, NSDraggingSource { /// Scale factor applied to the surface snapshot for the drag preview image. private static let previewScale: CGFloat = 0.2 - + /// The surface view that will be dragged. Its UUID is encoded into the /// pasteboard for drop targets to identify which surface is being moved. var surfaceView: SurfaceView? - + /// Callback invoked when the drag state changes. Called with `true` when /// a drag session begins, and `false` when it ends (completed or cancelled). var onDragStateChanged: ((Bool) -> Void)? - + /// Callback invoked when the mouse enters or exits this view's bounds. /// Used to update the hover state for visual feedback in the parent view. var onHoverChanged: ((Bool) -> Void)? - + /// Whether we are currently in a mouse tracking loop (between mouseDown /// and either mouseUp or drag initiation). Used to determine cursor state. private var isTracking: Bool = false - + /// Local event monitor to detect escape key presses during drag. private var escapeMonitor: Any? - + /// Whether the current drag was cancelled by pressing escape. private var dragCancelledByEscape: Bool = false @@ -137,26 +137,26 @@ extension Ghostty { userInfo: nil )) } - + override func resetCursorRects() { addCursorRect(bounds, cursor: isTracking ? .closedHand : .openHand) } - + override func mouseEntered(with event: NSEvent) { onHoverChanged?(true) } - + override func mouseExited(with event: NSEvent) { onHoverChanged?(false) } - + override func mouseDragged(with event: NSEvent) { guard !isTracking, let surfaceView = surfaceView else { return } - + // Create our dragging item from our transferable guard let pasteboardItem = surfaceView.pasteboardItem() else { return } let item = NSDraggingItem(pasteboardWriter: pasteboardItem) - + // Create a scaled preview image from the surface snapshot if let snapshot = surfaceView.asImage { let imageSize = NSSize( @@ -172,7 +172,7 @@ extension Ghostty { fraction: 1.0 ) scaledImage.unlockFocus() - + // Position the drag image so the mouse is at the center of the image. // I personally like the top middle or top left corner best but // this matches macOS native tab dragging behavior (at least, as of @@ -187,30 +187,30 @@ extension Ghostty { contents: scaledImage ) } - + onDragStateChanged?(true) let session = beginDraggingSession(with: [item], event: event, source: self) - + // We need to disable this so that endedAt happens immediately for our // drags outside of any targets. session.animatesToStartingPositionsOnCancelOrFail = false } - + // MARK: NSDraggingSource - + func draggingSession( _ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext ) -> NSDragOperation { return context == .withinApplication ? .move : [] } - + func draggingSession( _ session: NSDraggingSession, willBeginAt screenPoint: NSPoint ) { isTracking = true - + // Reset our escape tracking dragCancelledByEscape = false escapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in @@ -220,14 +220,14 @@ extension Ghostty { return event } } - + func draggingSession( _ session: NSDraggingSession, movedTo screenPoint: NSPoint ) { NSCursor.closedHand.set() } - + func draggingSession( _ session: NSDraggingSession, endedAt screenPoint: NSPoint, @@ -262,7 +262,7 @@ extension Notification.Name { /// released outside a valid drop target) and was not cancelled by the user /// pressing escape. The notification's object is the SurfaceView that was dragged. static let ghosttySurfaceDragEndedNoTarget = Notification.Name("ghosttySurfaceDragEndedNoTarget") - + /// Key for the screen point where the drag ended in the userInfo dictionary. static let ghosttySurfaceDragEndedNoTargetPointKey = "endedAtPoint" } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift index f3ee80874..ff751df10 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceGrabHandle.swift @@ -1,17 +1,17 @@ import AppKit import SwiftUI -extension Ghostty { +extension Ghostty { /// A grab handle overlay at the top of the surface for dragging the window. /// Only appears when hovering in the top region of the surface. struct SurfaceGrabHandle: View { private let handleHeight: CGFloat = 10 - + let surfaceView: SurfaceView - + @State private var isHovering: Bool = false @State private var isDragging: Bool = false - + var body: some View { VStack(spacing: 0) { Rectangle() @@ -32,7 +32,7 @@ extension Ghostty { isHovering: $isHovering ) } - + Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift index 82d26e681..2a5d48eaa 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift @@ -5,7 +5,7 @@ import SwiftUI /// control. struct SurfaceProgressBar: View { let report: Ghostty.Action.ProgressReport - + private var color: Color { switch report.state { case .error: return .red @@ -13,17 +13,17 @@ struct SurfaceProgressBar: View { default: return .accentColor } } - + private var progress: UInt8? { // If we have an explicit progress use that. if let v = report.progress { return v } - + // Otherwise, if we're in the pause state, we act as if we're at 100%. if report.state == .pause { return 100 } - + return nil } - + private var accessibilityLabel: String { switch report.state { case .error: return "Terminal progress - Error" @@ -32,7 +32,7 @@ struct SurfaceProgressBar: View { default: return "Terminal progress" } } - + private var accessibilityValue: String { if let progress { return "\(progress) percent complete" @@ -45,7 +45,7 @@ struct SurfaceProgressBar: View { } } } - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { @@ -78,15 +78,15 @@ struct SurfaceProgressBar: View { private struct BouncingProgressBar: View { let color: Color @State private var position: CGFloat = 0 - + private let barWidthRatio: CGFloat = 0.25 - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Rectangle() .fill(color.opacity(0.3)) - + Rectangle() .fill(color) .frame( diff --git a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift index b55f2e231..aab99c088 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceScrollView.swift @@ -19,12 +19,12 @@ class SurfaceScrollView: NSView { private var observers: [NSObjectProtocol] = [] private var cancellables: Set = [] private var isLiveScrolling = false - + /// The last row position sent via scroll_to_row action. Used to avoid /// sending redundant actions when the user drags the scrollbar but stays /// on the same row. private var lastSentRow: Int? - + init(contentSize: CGSize, surfaceView: Ghostty.SurfaceView) { self.surfaceView = surfaceView // The scroll view is our outermost view that controls all our scrollbar @@ -44,26 +44,26 @@ class SurfaceScrollView: NSView { // (we currently only use overlay scrollers, but might as well // configure the views correctly in case we change our mind) scrollView.contentView.clipsToBounds = false - + // The document view is what the scrollview is actually going // to be directly scrolling. We set it up to a "blank" NSView // with the desired content size. documentView = NSView(frame: NSRect(origin: .zero, size: contentSize)) scrollView.documentView = documentView - + // The document view contains our actual surface as a child. // We synchronize the scrolling of the document with this surface // so that our primary Ghostty renderer only needs to render the viewport. documentView.addSubview(surfaceView) - + super.init(frame: .zero) - + // Our scroll view is our only view addSubview(scrollView) - + // Apply initial scrollbar settings synchronizeAppearance() - + // We listen for scroll events through bounds notifications on our NSClipView. // This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/ scrollView.contentView.postsBoundsChangedNotifications = true @@ -74,7 +74,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollChange(notification) }) - + // Listen for scrollbar updates from Ghostty observers.append(NotificationCenter.default.addObserver( forName: .ghosttyDidUpdateScrollbar, @@ -83,7 +83,7 @@ class SurfaceScrollView: NSView { ) { [weak self] notification in self?.handleScrollbarUpdate(notification) }) - + // Listen for live scroll events observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.willStartLiveScrollNotification, @@ -92,7 +92,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = true }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didEndLiveScrollNotification, object: scrollView, @@ -100,7 +100,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.isLiveScrolling = false }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScrollView.didLiveScrollNotification, object: scrollView, @@ -108,7 +108,7 @@ class SurfaceScrollView: NSView { ) { [weak self] _ in self?.handleLiveScroll() }) - + observers.append(NotificationCenter.default.addObserver( forName: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil, @@ -150,11 +150,11 @@ class SurfaceScrollView: NSView { } .store(in: &cancellables) } - + required init?(coder: NSCoder) { fatalError("init(coder:) not implemented") } - + deinit { observers.forEach { NotificationCenter.default.removeObserver($0) } } @@ -163,10 +163,10 @@ class SurfaceScrollView: NSView { // insets. This is necessary for the content view to match the // surface view if we have the "hidden" titlebar style. override var safeAreaInsets: NSEdgeInsets { return NSEdgeInsetsZero } - + override func layout() { super.layout() - + // Fill entire bounds with scroll view scrollView.frame = bounds surfaceView.frame.size = scrollView.bounds.size @@ -174,13 +174,13 @@ class SurfaceScrollView: NSView { // We only set the width of the documentView here, as the height depends // on the scrollbar state and is updated in synchronizeScrollView documentView.frame.size.width = scrollView.bounds.width - + // When our scrollview changes make sure our scroller and surface views are synchronized synchronizeScrollView() synchronizeSurfaceView() synchronizeCoreSurface() } - + // MARK: Scrolling private func synchronizeAppearance() { @@ -220,7 +220,7 @@ class SurfaceScrollView: NSView { private func synchronizeScrollView() { // Update the document height to give our scroller the correct proportions documentView.frame.size.height = documentHeight() - + // Only update our actual scroll position if we're not actively scrolling. if !isLiveScrolling { // Convert row units to pixels using cell height, ignore zero height. @@ -236,13 +236,13 @@ class SurfaceScrollView: NSView { lastSentRow = Int(scrollbar.offset) } } - + // Always update our scrolled view with the latest dimensions scrollView.reflectScrolledClipView(scrollView.contentView) } - + // MARK: Notifications - + /// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized. private func handleScrollChange(_ notification: Notification) { synchronizeSurfaceView() @@ -259,7 +259,7 @@ class SurfaceScrollView: NSView { synchronizeAppearance() synchronizeCoreSurface() } - + /// Handles live scroll events (user actively dragging the scrollbar). /// /// Converts the current scroll position to a row number and sends a `scroll_to_row` action @@ -270,21 +270,21 @@ class SurfaceScrollView: NSView { // happen with a tiny terminal. let cellHeight = surfaceView.cellSize.height guard cellHeight > 0 else { return } - + // AppKit views are +Y going up, so we calculate from the bottom let visibleRect = scrollView.contentView.documentVisibleRect let documentHeight = documentView.frame.height let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height let row = Int(scrollOffset / cellHeight) - + // Only send action if the row changed to avoid action spam guard row != lastSentRow else { return } lastSentRow = row - + // Use the keybinding action to scroll. _ = surfaceView.surfaceModel?.perform(action: "scroll_to_row:\(row)") } - + /// Handles scrollbar state updates from the terminal core. /// /// Updates the document view size to reflect total scrollback and adjusts scroll position diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift index 509713309..106875813 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView+Transferable.swift @@ -17,11 +17,11 @@ extension Ghostty.SurfaceView: Transferable { let uuid = data.withUnsafeBytes { $0.load(as: UUID.self) } - + guard let imported = await Self.find(uuid: uuid) else { throw TransferError.invalidData } - + return imported } } @@ -29,7 +29,7 @@ extension Ghostty.SurfaceView: Transferable { enum TransferError: Error { case invalidData } - + @MainActor static func find(uuid: UUID) -> Self? { #if canImport(AppKit) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index fce8f3f4b..60d5caeaf 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -49,7 +49,7 @@ extension Ghostty { // True if we're hovering over the left URL view, so we can show it on the right. @State private var isHoveringURLLeft: Bool = false - + #if canImport(AppKit) // Observe SecureInput to detect when its enabled @ObservedObject private var secureInput = SecureInput.shared @@ -103,7 +103,7 @@ extension Ghostty { } } .ghosttySurfaceView(surfaceView) - + // Progress report if let progressReport = surfaceView.progressReport, progressReport.state != .remove { VStack(spacing: 0) { @@ -114,7 +114,7 @@ extension Ghostty { .allowsHitTesting(false) .transition(.opacity) } - + #if canImport(AppKit) // Readonly indicator badge if surfaceView.readonly { @@ -122,7 +122,7 @@ extension Ghostty { surfaceView.toggleReadonly(nil) } } - + // Show key state indicator for active key tables and/or pending key sequences KeyStateIndicator( keyTables: surfaceView.keyTables, @@ -404,9 +404,9 @@ extension Ghostty { @State private var dragOffset: CGSize = .zero @State private var barSize: CGSize = .zero @FocusState private var isSearchFieldFocused: Bool - + private let padding: CGFloat = 8 - + var body: some View { GeometryReader { geo in HStack(spacing: 4) { @@ -460,7 +460,7 @@ extension Ghostty { Image(systemName: "chevron.up") } .buttonStyle(SearchButtonStyle()) - + Button(action: { guard let surface = surfaceView.surface else { return } let action = "navigate_search:previous" @@ -469,7 +469,7 @@ extension Ghostty { Image(systemName: "chevron.down") } .buttonStyle(SearchButtonStyle()) - + Button(action: onClose) { Image(systemName: "xmark") } @@ -529,7 +529,7 @@ extension Ghostty { enum Corner { case topLeft, topRight, bottomLeft, bottomRight - + var alignment: Alignment { switch self { case .topLeft: return .topLeading @@ -539,11 +539,11 @@ extension Ghostty { } } } - + private func centerPosition(for corner: Corner, in containerSize: CGSize, barSize: CGSize) -> CGPoint { let halfWidth = barSize.width / 2 + padding let halfHeight = barSize.height / 2 + padding - + switch corner { case .topLeft: return CGPoint(x: halfWidth, y: halfHeight) @@ -555,21 +555,21 @@ extension Ghostty { return CGPoint(x: containerSize.width - halfWidth, y: containerSize.height - halfHeight) } } - + private func closestCorner(to point: CGPoint, in containerSize: CGSize) -> Corner { let midX = containerSize.width / 2 let midY = containerSize.height / 2 - + if point.x < midX { return point.y < midY ? .topLeft : .bottomLeft } else { return point.y < midY ? .topRight : .bottomRight } } - + struct SearchButtonStyle: ButtonStyle { @State private var isHovered = false - + func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundStyle(isHovered || configuration.isPressed ? .primary : .secondary) @@ -584,7 +584,7 @@ extension Ghostty { } .backport.pointerStyle(.link) } - + private func backgroundColor(isPressed: Bool) -> Color { if isPressed { return Color.primary.opacity(0.2) @@ -647,13 +647,13 @@ extension Ghostty { /// Explicit command to set var command: String? - + /// Environment variables to set for the terminal var environmentVariables: [String: String] = [:] /// Extra input to send as stdin var initialInput: String? - + /// Wait after the command var waitAfterCommand: Bool = false @@ -711,7 +711,7 @@ extension Ghostty { // Zero is our default value that means to inherit the font size. config.font_size = fontSize ?? 0 - + // Set wait after command config.wait_after_command = waitAfterCommand @@ -764,24 +764,24 @@ extension Ghostty { struct KeyStateIndicator: View { let keyTables: [String] let keySequence: [KeyboardShortcut] - + @State private var isShowingPopover = false @State private var position: Position = .bottom @State private var dragOffset: CGSize = .zero @State private var isDragging = false - + private let padding: CGFloat = 8 - + enum Position { case top, bottom - + var alignment: Alignment { switch self { case .top: return .top case .bottom: return .bottom } } - + var popoverEdge: Edge { switch self { case .top: return .top @@ -861,14 +861,14 @@ extension Ghostty { Divider() .frame(height: 14) } - + // Key sequence indicator if !keySequence.isEmpty { HStack(alignment: .center, spacing: 4) { ForEach(Array(keySequence.enumerated()), id: \.offset) { index, key in KeyCap(key.description) } - + // Animated ellipsis to indicate waiting for next key PendingIndicator(paused: isDragging) } @@ -898,11 +898,11 @@ extension Ghostty { .foregroundStyle(.secondary) } } - + if !keyTables.isEmpty && !keySequence.isEmpty { Divider() } - + if !keySequence.isEmpty { VStack(alignment: .leading, spacing: 4) { Label("Key Sequence", systemImage: "character.cursor.ibeam") @@ -921,15 +921,15 @@ extension Ghostty { isShowingPopover.toggle() } } - + /// A small keycap-style view for displaying keyboard shortcuts struct KeyCap: View { let text: String - + init(_ text: String) { self.text = text } - + var body: some View { Text(verbatim: text) .font(.system(size: 12, weight: .medium, design: .rounded)) @@ -946,7 +946,7 @@ extension Ghostty { ) } } - + /// Animated dots to indicate waiting for the next key struct PendingIndicator: View { @State private var animationPhase: Double = 0 @@ -967,7 +967,7 @@ extension Ghostty { } } } - + private func dotOpacity(for index: Int) -> Double { let phase = animationPhase let offset = Double(index) / 3.0 @@ -981,7 +981,7 @@ extension Ghostty { /// Visual overlay that shows a border around the edges when the bell rings with border feature enabled. struct BellBorderOverlay: View { let bell: Bool - + var body: some View { Rectangle() .strokeBorder( @@ -998,7 +998,7 @@ extension Ghostty { /// Uses a soft, soothing highlight with a pulsing border effect. struct HighlightOverlay: View { let highlighted: Bool - + @State private var borderPulse: Bool = false var body: some View { @@ -1051,21 +1051,21 @@ extension Ghostty { } // MARK: Readonly Badge - + /// A badge overlay that indicates a surface is in readonly mode. /// Positioned in the top-right corner and styled to be noticeable but unobtrusive. struct ReadonlyBadge: View { let onDisable: () -> Void - + @State private var showingPopover = false - + private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8) - + var body: some View { VStack { HStack { Spacer() - + HStack(spacing: 5) { Image(systemName: "eye.fill") .font(.system(size: 12)) @@ -1085,13 +1085,13 @@ extension Ghostty { } } .padding(8) - + Spacer() } .accessibilityElement(children: .ignore) .accessibilityLabel("Read-only terminal") } - + private var badgeBackground: some View { RoundedRectangle(cornerRadius: 6) .fill(.regularMaterial) @@ -1101,11 +1101,11 @@ extension Ghostty { ) } } - + struct ReadonlyPopoverView: View { let onDisable: () -> Void @Binding var isPresented: Bool - + var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 8) { @@ -1116,16 +1116,16 @@ extension Ghostty { Text("Read-Only Mode") .font(.system(size: 13, weight: .semibold)) } - + Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.") .font(.system(size: 11)) .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } - + HStack { Spacer() - + Button("Disable") { onDisable() isPresented = false diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index fab3ad8e7..b712e9785 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -1184,7 +1184,7 @@ extension Ghostty { // We only care about key down events. It might not even be possible // to receive any other event type here. guard event.type == .keyDown else { return false } - + // Only process events if we're focused. Some key events like C-/ macOS // appears to send to the first view in the hierarchy rather than the // the first responder (I don't know why). This prevents us from handling it. @@ -1194,7 +1194,7 @@ extension Ghostty { if !focused { return false } - + // Get information about if this is a binding. let bindingFlags = surfaceModel.flatMap { surface in var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) @@ -1203,7 +1203,7 @@ extension Ghostty { return surface.keyIsBinding(ghosttyEvent) } } - + // If this is a binding then we want to perform it. if let bindingFlags { // Attempt to trigger a menu item for this key binding. We only do this if: @@ -1220,7 +1220,7 @@ extension Ghostty { return true } } - + self.keyDown(with: event) return true } diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index 8e8a93b93..d8fe14830 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -32,7 +32,7 @@ extension Ghostty { // The hovered URL @Published var hoverUrl: String? - + // The progress report (if any) @Published var progressReport: Action.ProgressReport? @@ -42,7 +42,7 @@ extension Ghostty { /// True when the bell is active. This is set inactive on focus or event. @Published var bell: Bool = false - + // The current search state. When non-nil, the search overlay should be shown. @Published var searchState: SearchState? @@ -51,7 +51,7 @@ extension Ghostty { /// True when the surface is in readonly mode. @Published private(set) var readonly: Bool = false - + /// True when the surface should show a highlight effect (e.g., when presented via goto_split). @Published private(set) var highlighted: Bool = false diff --git a/macos/Sources/Helpers/AnySortKey.swift b/macos/Sources/Helpers/AnySortKey.swift index 6813ccf45..ffafb6b90 100644 --- a/macos/Sources/Helpers/AnySortKey.swift +++ b/macos/Sources/Helpers/AnySortKey.swift @@ -4,7 +4,7 @@ import Foundation struct AnySortKey: Comparable { private let value: Any private let comparator: (Any, Any) -> ComparisonResult - + init(_ value: T) { self.value = value self.comparator = { lhs, rhs in @@ -14,11 +14,11 @@ struct AnySortKey: Comparable { return .orderedSame } } - + static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedAscending } - + static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool { lhs.comparator(lhs.value, rhs.value) == .orderedSame } diff --git a/macos/Sources/Helpers/Backport.swift b/macos/Sources/Helpers/Backport.swift index 8c43652e4..28da6cce6 100644 --- a/macos/Sources/Helpers/Backport.swift +++ b/macos/Sources/Helpers/Backport.swift @@ -48,7 +48,7 @@ extension Backport where Content: View { return content #endif } - + /// Backported onKeyPress that works on macOS 14+ and is a no-op on macOS 13. func onKeyPress(_ key: KeyEquivalent, action: @escaping (EventModifiers) -> BackportKeyPressResult) -> some View { #if canImport(AppKit) diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 5fde0e870..3b1abd44a 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -98,10 +98,10 @@ class ExpiringUndoManager: UndoManager { private class ExpiringTarget { /// The actual target object for the undo operation, held weakly to avoid retain cycles. private(set) weak var target: AnyObject? - + /// Timer that triggers expiration after the specified duration. private var timer: Timer? - + /// The undo manager from which to remove actions when this target expires. private weak var undoManager: UndoManager? @@ -141,7 +141,7 @@ extension ExpiringTarget: Hashable, Equatable { static func == (lhs: ExpiringTarget, rhs: ExpiringTarget) -> Bool { return lhs === rhs } - + func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(self)) } diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index 4e8e39918..92beb0505 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -2,7 +2,7 @@ extension Array { subscript(safe index: Int) -> Element? { return indices.contains(index) ? self[index] : nil } - + /// Returns the index before i, with wraparound. Assumes i is a valid index. func indexWrapping(before i: Int) -> Int { if i == 0 { @@ -35,7 +35,7 @@ extension Array where Element == String { if index == count { return try body(accumulated) } - + return try self[index].withCString { cStr in var newAccumulated = accumulated newAccumulated.append(cStr) diff --git a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift index 11ab2b14a..a54735fde 100644 --- a/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift @@ -13,14 +13,14 @@ extension NSPasteboard.PasteboardType { default: break } - + // Try to get UTType from MIME type guard let utType = UTType(mimeType: mimeType) else { // Fallback: use the MIME type directly as identifier self.init(mimeType) return } - + // Use the UTType's identifier self.init(utType.identifier) } diff --git a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift index eea67ee2d..ca338f102 100644 --- a/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift @@ -5,7 +5,7 @@ extension NSScreen { var displayID: UInt32? { deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32 } - + /// The stable UUID for this display, suitable for tracking across reconnects and NSScreen garbage collection. var displayUUID: UUID? { guard let displayID = displayID else { return nil } @@ -48,7 +48,7 @@ extension NSScreen { // know any other situation this is true. return safeAreaInsets.top > 0 } - + /// Converts top-left offset coordinates to bottom-left origin coordinates for window positioning. /// - Parameters: /// - x: X offset from top-left corner @@ -57,11 +57,11 @@ extension NSScreen { /// - Returns: CGPoint suitable for setFrameOrigin that positions the window as requested func origin(fromTopLeftOffsetX x: CGFloat, offsetY y: CGFloat, windowSize: CGSize) -> CGPoint { let vf = visibleFrame - + // Convert top-left coordinates to bottom-left origin let originX = vf.minX + x let originY = vf.maxY - y - windowSize.height - + return CGPoint(x: originX, y: originY) } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index fb209e4ac..030de0d1d 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -131,12 +131,12 @@ extension NSView { /// This includes private views like title bar views. func firstViewFromRoot(withClassName name: String) -> NSView? { let root = rootView - + // Check if the root view itself matches if String(describing: type(of: root)) == name { return root } - + // Otherwise search descendants return root.firstDescendant(withClassName: name) } @@ -155,67 +155,67 @@ extension NSView { print("View Hierarchy from Root:") print(root.viewHierarchyDescription()) } - + /// Returns a string representation of the view hierarchy in a tree-like format. func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { var result = "" - + // Add the tree branch characters result += indent if !indent.isEmpty { result += isLast ? "└── " : "├── " } - + // Add the class name and optional identifier let className = String(describing: type(of: self)) result += className - + // Add identifier if present if let identifier = self.identifier { result += " (id: \(identifier.rawValue))" } - + // Add frame info result += " [frame: \(frame)]" - + // Add visual properties var properties: [String] = [] - + // Hidden status if isHidden { properties.append("hidden") } - + // Opaque status properties.append(isOpaque ? "opaque" : "transparent") - + // Layer backing if wantsLayer { properties.append("layer-backed") if let bgColor = layer?.backgroundColor { let color = NSColor(cgColor: bgColor) if let rgb = color?.usingColorSpace(.deviceRGB) { - properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", - rgb.redComponent * 255, - rgb.greenComponent * 255, - rgb.blueComponent * 255, + properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", + rgb.redComponent * 255, + rgb.greenComponent * 255, + rgb.blueComponent * 255, rgb.alphaComponent)) } else { properties.append("bg:\(bgColor)") } } } - + result += " [\(properties.joined(separator: ", "))]" result += "\n" - + // Process subviews for (index, subview) in subviews.enumerated() { let isLastSubview = index == subviews.count - 1 let newIndent = indent + (isLast ? " " : "│ ") result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview) } - + return result } } diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 5d1831f26..0fa330f1b 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -52,31 +52,31 @@ extension NSWindow { guard themeFrameView.responds(to: Selector(("titlebarView"))) else { return nil } return themeFrameView.value(forKey: "titlebarView") as? NSView } - + /// Returns the [private] NSTabBar view, if it exists. var tabBarView: NSView? { titlebarView?.firstDescendant(withClassName: "NSTabBar") } - + /// Returns the index of the tab button at the given screen point, if any. func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? { guard let tabBarView else { return nil } let locationInWindow = convertPoint(fromScreen: screenPoint) let locationInTabBar = tabBarView.convert(locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { return nil } - + // Find all tab buttons and sort by x position to get visual order. // The view hierarchy order doesn't match the visual tab order. let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton") .sorted { $0.frame.origin.x < $1.frame.origin.x } - + for (index, tabItemView) in tabItemViews.enumerated() { let locationInTab = tabItemView.convert(locationInWindow, from: nil) if tabItemView.bounds.contains(locationInTab) { return index } } - + return nil } } diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift index e87e0676c..c4f7ca5c1 100644 --- a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -24,7 +24,7 @@ extension NSWorkspace { nil )?.takeRetainedValue() as? URL } - + /// Returns the URL of the default application for opening files with the specified file extension. /// - Parameter ext: The file extension to find the default application for. /// - Returns: The URL of the default application, or nil if no default application is found. diff --git a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift index 3bcc9057f..a45cdc7a4 100644 --- a/macos/Sources/Helpers/Extensions/Transferable+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Transferable+Extension.swift @@ -40,16 +40,16 @@ private final class TransferableDataProvider: NSObject, NSPasteboardItemDataProv // to block until the async load completes. This is safe because AppKit // calls this method on a background thread during drag operations. let semaphore = DispatchSemaphore(value: 0) - + var result: Data? itemProvider.loadDataRepresentation(forTypeIdentifier: type.rawValue) { data, _ in result = data semaphore.signal() } - + // Wait for the data to load semaphore.wait() - + // Set it. I honestly don't know what happens here if this fails. if let data = result { item.setData(data, forType: type) diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 9c16c7163..29d1ab6d3 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -40,7 +40,7 @@ class PermissionRequest { completion(storedResult) return } - + let alert = NSAlert() alert.messageText = message alert.informativeText = informative @@ -59,7 +59,7 @@ class PermissionRequest { target: nil, action: nil) checkbox!.state = .off - + // Set checkbox as accessory view alert.accessoryView = checkbox } @@ -74,7 +74,7 @@ class PermissionRequest { handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } - + /// Handles the alert response and processes caching logic /// - Parameters: /// - response: The alert response from the user @@ -90,7 +90,7 @@ class PermissionRequest { allowDuration: AllowDuration, rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { - + let result: Bool switch response { case .alertFirstButtonReturn: // Allow @@ -100,7 +100,7 @@ class PermissionRequest { default: result = false } - + // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set if rememberDecision, let rememberDuration = rememberDuration { storeResult(result, for: key, duration: rememberDuration) @@ -118,10 +118,10 @@ class PermissionRequest { storeResult(result, for: key, duration: duration) } } - + completion(result) } - + /// Retrieves a cached permission decision if it hasn't expired /// - Parameter key: The UserDefaults key to check /// - Returns: The cached decision, or nil if no valid cached decision exists @@ -132,16 +132,16 @@ class PermissionRequest { ofClass: StoredPermission.self, from: data) else { return nil } - + if Date() > storedPermission.expiry { // Decision has expired, remove stored value userDefaults.removeObject(forKey: key) return nil } - + return storedPermission.result } - + /// Stores a permission decision in UserDefaults with an expiration date /// - Parameters: /// - result: The permission decision to store @@ -180,7 +180,7 @@ class PermissionRequest { return "Remember my decision for \(days) day\(days == 1 ? "" : "s")" } } - + /// Internal class for storing permission decisions with expiration dates in UserDefaults /// Conforms to NSSecureCoding for safe archiving/unarchiving @objc(StoredPermission) diff --git a/macos/Tests/NSPasteboardTests.swift b/macos/Tests/NSPasteboardTests.swift index d956ce733..9db17ca33 100644 --- a/macos/Tests/NSPasteboardTests.swift +++ b/macos/Tests/NSPasteboardTests.swift @@ -16,14 +16,14 @@ struct NSPasteboardTypeExtensionTests { #expect(pasteboardType != nil) #expect(pasteboardType == .string) } - + /// Test text/html MIME type converts to .html @Test func testTextHtmlMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "text/html") #expect(pasteboardType != nil) #expect(pasteboardType == .html) } - + /// Test image/png MIME type @Test func testImagePngMimeType() async throws { let pasteboardType = NSPasteboard.PasteboardType(mimeType: "image/png") diff --git a/macos/Tests/NSScreenTests.swift b/macos/Tests/NSScreenTests.swift index f7431bf05..6e67bb7e4 100644 --- a/macos/Tests/NSScreenTests.swift +++ b/macos/Tests/NSScreenTests.swift @@ -15,65 +15,65 @@ struct NSScreenExtensionTests { // Mock screen with 1000x800 visible frame starting at (0, 100) let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) - + // Mock window size let windowSize = CGSize(width: 400, height: 300) - + // Test top-left positioning: x=15, y=15 let origin = mockScreen.origin( fromTopLeftOffsetX: 15, offsetY: 15, windowSize: windowSize) - + // Expected: x = 0 + 15 = 15, y = (100 + 800) - 15 - 300 = 585 #expect(origin.x == 15) #expect(origin.y == 585) } - + /// Test zero coordinates (exact top-left corner) @Test func testZeroCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 100, width: 1000, height: 800) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 0, offsetY: 0, windowSize: windowSize) - + // Expected: x = 0, y = (100 + 800) - 0 - 300 = 600 #expect(origin.x == 0) #expect(origin.y == 600) } - + /// Test with offset screen (not starting at origin) @Test func testOffsetScreen() async throws { // Secondary monitor at position (1440, 0) with 1920x1080 resolution let mockScreenFrame = NSRect(x: 1440, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 600, height: 400) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 100, offsetY: 50, windowSize: windowSize) - + // Expected: x = 1440 + 100 = 1540, y = (0 + 1080) - 50 - 400 = 630 #expect(origin.x == 1540) #expect(origin.y == 630) } - + /// Test large coordinates @Test func testLargeCoordinates() async throws { let mockScreenFrame = NSRect(x: 0, y: 0, width: 1920, height: 1080) let mockScreen = MockNSScreen(visibleFrame: mockScreenFrame) let windowSize = CGSize(width: 400, height: 300) - + let origin = mockScreen.origin( fromTopLeftOffsetX: 500, offsetY: 200, windowSize: windowSize) - + // Expected: x = 0 + 500 = 500, y = (0 + 1080) - 200 - 300 = 580 #expect(origin.x == 500) #expect(origin.y == 580) @@ -83,16 +83,16 @@ struct NSScreenExtensionTests { /// Mock NSScreen class for testing coordinate conversion private class MockNSScreen: NSScreen { private let mockVisibleFrame: NSRect - + init(visibleFrame: NSRect) { self.mockVisibleFrame = visibleFrame super.init() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override var visibleFrame: NSRect { return mockVisibleFrame } diff --git a/macos/Tests/Update/ReleaseNotesTests.swift b/macos/Tests/Update/ReleaseNotesTests.swift index b029fa6bc..6c7d43ed5 100644 --- a/macos/Tests/Update/ReleaseNotesTests.swift +++ b/macos/Tests/Update/ReleaseNotesTests.swift @@ -9,7 +9,7 @@ struct ReleaseNotesTests { displayVersionString: "1.2.3", currentCommit: nil ) - + #expect(notes != nil) if case .tagged(let url) = notes { #expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3") @@ -18,14 +18,14 @@ struct ReleaseNotesTests { Issue.record("Expected tagged case") } } - + /// Test tip release comparison with current commit @Test func testTipReleaseComparison() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: "def5678" ) - + #expect(notes != nil) if case .compareTip(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") @@ -34,14 +34,14 @@ struct ReleaseNotesTests { Issue.record("Expected compareTip case") } } - + /// Test tip release without current commit @Test func testTipReleaseWithoutCurrentCommit() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: nil ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") @@ -50,14 +50,14 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test tip release with empty current commit @Test func testTipReleaseWithEmptyCurrentCommit() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-abc1234", currentCommit: "" ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234") @@ -65,14 +65,14 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test version with full 40-character hash @Test func testFullGitHash() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678", currentCommit: nil ) - + #expect(notes != nil) if case .commit(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678") @@ -80,46 +80,46 @@ struct ReleaseNotesTests { Issue.record("Expected commit case") } } - + /// Test version with no recognizable pattern @Test func testInvalidVersion() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "unknown-version", currentCommit: nil ) - + #expect(notes == nil) } - + /// Test semantic version with prerelease suffix should not match @Test func testSemanticVersionWithSuffix() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "1.2.3-beta", currentCommit: nil ) - + // Should not match semantic version pattern, falls back to hash detection #expect(notes == nil) } - + /// Test semantic version with 4 components should not match @Test func testSemanticVersionFourComponents() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "1.2.3.4", currentCommit: nil ) - + // Should not match pattern #expect(notes == nil) } - + /// Test version string with git hash embedded @Test func testVersionWithEmbeddedHash() async throws { let notes = UpdateState.ReleaseNotes( displayVersionString: "v2024.01.15-abc1234", currentCommit: "def5678" ) - + #expect(notes != nil) if case .compareTip(let url) = notes { #expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234") diff --git a/macos/Tests/Update/UpdateStateTests.swift b/macos/Tests/Update/UpdateStateTests.swift index 354d371c5..6aefa22a2 100644 --- a/macos/Tests/Update/UpdateStateTests.swift +++ b/macos/Tests/Update/UpdateStateTests.swift @@ -5,25 +5,25 @@ import Sparkle struct UpdateStateTests { // MARK: - Equatable Tests - + @Test func testIdleEquality() { let state1: UpdateState = .idle let state2: UpdateState = .idle #expect(state1 == state2) } - + @Test func testCheckingEquality() { let state1: UpdateState = .checking(.init(cancel: {})) let state2: UpdateState = .checking(.init(cancel: {})) #expect(state1 == state2) } - + @Test func testNotFoundEquality() { let state1: UpdateState = .notFound(.init(acknowledgement: {})) let state2: UpdateState = .notFound(.init(acknowledgement: {})) #expect(state1 == state2) } - + @Test func testInstallingEquality() { let state1: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) let state2: UpdateState = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -31,7 +31,7 @@ struct UpdateStateTests { let state3: UpdateState = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #expect(state3 != state2) } - + @Test func testPermissionRequestEquality() { let request1 = SPUUpdatePermissionRequest(systemProfile: []) let request2 = SPUUpdatePermissionRequest(systemProfile: []) @@ -39,43 +39,43 @@ struct UpdateStateTests { let state2: UpdateState = .permissionRequest(.init(request: request2, reply: { _ in })) #expect(state1 == state2) } - + @Test func testDownloadingEqualityWithSameProgress() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) #expect(state1 == state2) } - + @Test func testDownloadingInequalityWithDifferentProgress() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 600)) #expect(state1 != state2) } - + @Test func testDownloadingInequalityWithDifferentExpectedLength() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 2000, progress: 500)) #expect(state1 != state2) } - + @Test func testDownloadingEqualityWithNilExpectedLength() { let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) #expect(state1 == state2) } - + @Test func testExtractingEqualityWithSameProgress() { let state1: UpdateState = .extracting(.init(progress: 0.5)) let state2: UpdateState = .extracting(.init(progress: 0.5)) #expect(state1 == state2) } - + @Test func testExtractingInequalityWithDifferentProgress() { let state1: UpdateState = .extracting(.init(progress: 0.5)) let state2: UpdateState = .extracting(.init(progress: 0.6)) #expect(state1 != state2) } - + @Test func testErrorEqualityWithSameDescription() { let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error message"]) let error2 = NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error message"]) @@ -83,7 +83,7 @@ struct UpdateStateTests { let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) #expect(state1 == state2) } - + @Test func testErrorInequalityWithDifferentDescription() { let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 1"]) let error2 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 2"]) @@ -91,20 +91,20 @@ struct UpdateStateTests { let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {})) #expect(state1 != state2) } - + @Test func testDifferentStatesAreNotEqual() { let state1: UpdateState = .idle let state2: UpdateState = .checking(.init(cancel: {})) #expect(state1 != state2) } - + // MARK: - isIdle Tests - + @Test func testIsIdleTrue() { let state: UpdateState = .idle #expect(state.isIdle == true) } - + @Test func testIsIdleFalse() { let state: UpdateState = .checking(.init(cancel: {})) #expect(state.isIdle == false) diff --git a/macos/Tests/Update/UpdateViewModelTests.swift b/macos/Tests/Update/UpdateViewModelTests.swift index 529c2bc52..9b747f9ec 100644 --- a/macos/Tests/Update/UpdateViewModelTests.swift +++ b/macos/Tests/Update/UpdateViewModelTests.swift @@ -6,50 +6,50 @@ import Sparkle struct UpdateViewModelTests { // MARK: - Text Formatting Tests - + @Test func testIdleText() { let viewModel = UpdateViewModel() viewModel.state = .idle #expect(viewModel.text == "") } - + @Test func testPermissionRequestText() { let viewModel = UpdateViewModel() let request = SPUUpdatePermissionRequest(systemProfile: []) viewModel.state = .permissionRequest(.init(request: request, reply: { _ in })) #expect(viewModel.text == "Enable Automatic Updates?") } - + @Test func testCheckingText() { let viewModel = UpdateViewModel() viewModel.state = .checking(.init(cancel: {})) #expect(viewModel.text == "Checking for Updates…") } - + @Test func testDownloadingTextWithKnownLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500)) #expect(viewModel.text == "Downloading: 50%") } - + @Test func testDownloadingTextWithUnknownLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500)) #expect(viewModel.text == "Downloading…") } - + @Test func testDownloadingTextWithZeroExpectedLength() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 0, progress: 500)) #expect(viewModel.text == "Downloading…") } - + @Test func testExtractingText() { let viewModel = UpdateViewModel() viewModel.state = .extracting(.init(progress: 0.75)) #expect(viewModel.text == "Preparing: 75%") } - + @Test func testInstallingText() { let viewModel = UpdateViewModel() viewModel.state = .installing(.init(isAutoUpdate: false, retryTerminatingApplication: {}, dismiss: {})) @@ -57,34 +57,34 @@ struct UpdateViewModelTests { viewModel.state = .installing(.init(isAutoUpdate: true, retryTerminatingApplication: {}, dismiss: {})) #expect(viewModel.text == "Restart to Complete Update") } - + @Test func testNotFoundText() { let viewModel = UpdateViewModel() viewModel.state = .notFound(.init(acknowledgement: {})) #expect(viewModel.text == "No Updates Available") } - + @Test func testErrorText() { let viewModel = UpdateViewModel() let error = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"]) viewModel.state = .error(.init(error: error, retry: {}, dismiss: {})) #expect(viewModel.text == "Network error") } - + // MARK: - Max Width Text Tests - + @Test func testMaxWidthTextForDownloading() { let viewModel = UpdateViewModel() viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 50)) #expect(viewModel.maxWidthText == "Downloading: 100%") } - + @Test func testMaxWidthTextForExtracting() { let viewModel = UpdateViewModel() viewModel.state = .extracting(.init(progress: 0.5)) #expect(viewModel.maxWidthText == "Preparing: 100%") } - + @Test func testMaxWidthTextForNonProgressState() { let viewModel = UpdateViewModel() viewModel.state = .checking(.init(cancel: {})) From 540adb0da3494cfdae9264782e9e38281ca0997f Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:57:19 -0500 Subject: [PATCH 36/51] macos: swiftlint 'unneeded_synthesized_initializer' rule --- macos/.swiftlint.yml | 1 - .../Features/App Intents/Entities/CommandEntity.swift | 5 ----- 2 files changed, 6 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 171c7228c..43bd460f9 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -24,7 +24,6 @@ disabled_rules: - orphaned_doc_comment - shorthand_operator - switch_case_alignment - - unneeded_synthesized_initializer - unused_closure_parameter - unused_enumerated - unused_optional_binding diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift index d78d75a5d..f05b5d9b9 100644 --- a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -25,11 +25,6 @@ struct CommandEntity: AppEntity { struct ID: Hashable { let terminalId: TerminalEntity.ID let actionKey: String - - init(terminalId: TerminalEntity.ID, actionKey: String) { - self.terminalId = terminalId - self.actionKey = actionKey - } } static var typeDisplayRepresentation: TypeDisplayRepresentation { From a36d2f5420dee5b0ac1d0c55e83e1ea6dea3b879 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:57:43 -0500 Subject: [PATCH 37/51] macos: swiftlint 'unused_closure_parameter' rule --- macos/.swiftlint.yml | 1 - .../Window Styles/TransparentTitlebarTerminalWindow.swift | 4 ++-- macos/Sources/Ghostty/Surface View/SurfaceView.swift | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 43bd460f9..d4103690c 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -24,7 +24,6 @@ disabled_rules: - orphaned_doc_comment - shorthand_operator - switch_case_alignment - - unused_closure_parameter - unused_enumerated - unused_optional_binding - vertical_parameter_alignment diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index a72436d7f..a547d5286 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -151,7 +151,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabGroupWindowsObservation = tabGroup.observe( \.windows, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in // NOTE: At one point, I guarded this on only if we went from 0 to N // or N to 0 under the assumption that the tab bar would only get // replaced on those cases. This turned out to be false (Tahoe). @@ -175,7 +175,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabBarVisibleObservation = tabGroup?.observe( \.isTabBarVisible, options: [.new] - ) { [weak self] _, change in + ) { [weak self] _, _ in guard let self else { return } guard let lastSurfaceConfig else { return } self.syncAppearance(lastSurfaceConfig) diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 60d5caeaf..50575e482 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -865,7 +865,7 @@ extension Ghostty { // Key sequence indicator if !keySequence.isEmpty { HStack(alignment: .center, spacing: 4) { - ForEach(Array(keySequence.enumerated()), id: \.offset) { index, key in + ForEach(Array(keySequence.enumerated()), id: \.offset) { _, key in KeyCap(key.description) } From 629a656e5329b35c970fdba3ee10fc5b366d2ba0 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 18:58:28 -0500 Subject: [PATCH 38/51] macos: swiftlint 'vertical_whitespace' rule --- macos/.swiftlint.yml | 1 - .../App Intents/IntentPermission.swift | 1 - .../QuickTerminal/QuickTerminalSize.swift | 1 - macos/Sources/Features/Splits/SplitTree.swift | 1 - .../Terminal/BaseTerminalController.swift | 2 -- .../Features/Terminal/TerminalController.swift | 1 - .../Features/Terminal/TerminalRestorable.swift | 1 - .../TitlebarTabsVenturaTerminalWindow.swift | 1 - macos/Sources/Ghostty/Ghostty.App.swift | 18 ------------------ .../Surface View/SurfaceProgressBar.swift | 1 - .../Ghostty/Surface View/SurfaceView.swift | 2 -- .../Surface View/SurfaceView_AppKit.swift | 2 -- .../Helpers/Extensions/String+Extension.swift | 1 - 13 files changed, 33 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index d4103690c..0bd253efb 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -27,7 +27,6 @@ disabled_rules: - unused_enumerated - unused_optional_binding - vertical_parameter_alignment - - vertical_whitespace identifier_name: min_length: 1 diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 32c0a9b81..26a21e70b 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -43,7 +43,6 @@ func requestIntentPermission() async -> Bool { } } - PermissionRequest.show( "com.mitchellh.ghostty.shortcutsPermission", message: "Allow Shortcuts to interact with Ghostty?", diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift index 08bbcb8d9..2cd11e42e 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -48,7 +48,6 @@ struct QuickTerminalSize { } } - /// This is an almost direct port of th Zig function QuickTerminalSize.calculate func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize { let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 5fc49a24f..86932b1bb 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -728,7 +728,6 @@ extension SplitTree.Node { } } - /// Calculate the bounds of all views in this subtree based on split ratios func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { switch self { diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 302b3717d..1cff80c52 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -855,7 +855,6 @@ class BaseTerminalController: NSWindowController, } } - func cellSizeDidChange(to: NSSize) { guard derivedConfig.windowStepResize else { return } // Stage manager can sometimes present windows in such a way that the @@ -1294,7 +1293,6 @@ class BaseTerminalController: NSWindowController, ghostty.splitToggleZoom(surface: surface) } - @IBAction func splitMoveFocusPrevious(_ sender: Any) { splitMoveFocus(direction: .previous) } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ad1c68ddc..3b4242a86 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -54,7 +54,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig - /// The notification cancellable for focused surface property changes. private var surfaceAppearanceCancellables: Set = [] diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index acc4d0532..a42d4c2f6 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -192,4 +192,3 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { } } - diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index faec8dcd3..2e1ec1f95 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -340,7 +340,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - // HACK: hide the "collapsed items" marker from the toolbar if it's present. // idk why it appears in macOS 15.0+ but it does... so... make it go away. (sigh) private func hideToolbarOverflowButton() { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index d09cd3184..42d4368e7 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -312,7 +312,6 @@ extension Ghostty { ghostty_app_set_focus(app, false) } - // MARK: Ghostty Callbacks (macOS) static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) { @@ -782,7 +781,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -819,7 +817,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -848,7 +845,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -914,7 +910,6 @@ extension Ghostty { assertionFailure() } - default: assertionFailure() } @@ -969,7 +964,6 @@ extension Ghostty { ] ) - default: assertionFailure() } @@ -991,7 +985,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1014,7 +1007,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1296,7 +1288,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -1324,7 +1315,6 @@ extension Ghostty { ) return true - default: assertionFailure() return false @@ -1349,7 +1339,6 @@ extension Ghostty { userInfo: ["mode": mode] ) - default: assertionFailure() } @@ -1382,7 +1371,6 @@ extension Ghostty { surfaceView.showUserNotification(title: title, body: body) } - default: assertionFailure() } @@ -1609,7 +1597,6 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } surfaceView.setCursorShape(shape) - default: assertionFailure() } @@ -1638,7 +1625,6 @@ extension Ghostty { return } - default: assertionFailure() } @@ -1664,7 +1650,6 @@ extension Ghostty { let buffer = Data(bytes: v.url!, count: v.len) surfaceView.hoverUrl = String(data: buffer, encoding: .utf8) - default: assertionFailure() } @@ -1684,7 +1669,6 @@ extension Ghostty { guard let surfaceView = self.surfaceView(from: surface) else { return } surfaceView.initialSize = NSSize(width: Double(v.width), height: Double(v.height)) - default: assertionFailure() } @@ -1706,7 +1690,6 @@ extension Ghostty { object: surfaceView ) - default: assertionFailure() } @@ -2086,7 +2069,6 @@ extension Ghostty { } } - // MARK: User Notifications /// Handle a received user notification. This is called when a user notification is clicked or dismissed by the user diff --git a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift index 2a5d48eaa..0478bf2bf 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceProgressBar.swift @@ -110,4 +110,3 @@ private struct BouncingProgressBar: View { } } - diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView.swift b/macos/Sources/Ghostty/Surface View/SurfaceView.swift index 50575e482..221bc4c37 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView.swift @@ -286,8 +286,6 @@ extension Ghostty { } } - - // This is the resize overlay that shows on top of a surface to show the current // size during a resize operation. struct SurfaceResizeOverlay: View { diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift index b712e9785..6b3bfbfb4 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_AppKit.swift @@ -873,7 +873,6 @@ extension Ghostty { ghostty_surface_mouse_button(surface, GHOSTTY_MOUSE_RELEASE, button.cMouseButton, mods) } - override func rightMouseDown(with event: NSEvent) { guard let surface = self.surface else { return super.rightMouseDown(with: event) } @@ -1495,7 +1494,6 @@ extension Ghostty { } } - @IBAction func pasteAsPlainText(_ sender: Any?) { guard let surface = self.surface else { return } let action = "paste_from_clipboard" diff --git a/macos/Sources/Helpers/Extensions/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift index 03f715fd8..e28877ca8 100644 --- a/macos/Sources/Helpers/Extensions/String+Extension.swift +++ b/macos/Sources/Helpers/Extensions/String+Extension.swift @@ -27,5 +27,4 @@ extension String { } #endif - } From f4d70df34c8d564a7824144433d9bf30dfcef3e0 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 19:02:49 -0500 Subject: [PATCH 39/51] macos: swiftlint 'implicit_getter' rule --- macos/.swiftlint.yml | 1 - .../TitlebarTabsVenturaTerminalWindow.swift | 12 +++++------- .../Ghostty/Surface View/SurfaceView_UIKit.swift | 4 +--- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 0bd253efb..fe00ad9c4 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -16,7 +16,6 @@ disabled_rules: - deployment_target - for_where - force_cast - - implicit_getter - line_length - mark - multiple_closures_with_trailing_closure diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 2e1ec1f95..fe83fc5fd 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -20,13 +20,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { - get { - // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true - let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true - let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true - let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true - return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) - } + // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true + let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true + let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true + let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true + return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) } // MARK: NSWindow diff --git a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift index d8fe14830..9a4cf4d9b 100644 --- a/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/Surface View/SurfaceView_UIKit.swift @@ -122,9 +122,7 @@ extension Ghostty { // MARK: UIView override class var layerClass: AnyClass { - get { - return CAMetalLayer.self - } + return CAMetalLayer.self } override func didMoveToWindow() { From 9bae26ab455110a7b28872c65dc82efb9d352027 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 19:06:09 -0500 Subject: [PATCH 40/51] macos: swiftlint 'orphaned_doc_comment' rule --- macos/.swiftlint.yml | 1 - macos/Sources/App/macOS/AppDelegate.swift | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index fe00ad9c4..5b0a45b79 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -20,7 +20,6 @@ disabled_rules: - mark - multiple_closures_with_trailing_closure - no_fallthrough_only - - orphaned_doc_comment - shorthand_operator - switch_case_alignment - unused_enumerated diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 3a5511fe1..1aa597a25 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -879,7 +879,7 @@ class AppDelegate: NSObject, autoUpdate == .check || autoUpdate == .download updateController.updater.automaticallyDownloadsUpdates = autoUpdate == .download - /** + /* To test `auto-update` easily, uncomment the line below and delete `SUEnableAutomaticChecks` in Ghostty-Info.plist. From a7719a8db6bae10c57bad6a73b94def4826fff5b Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 19:07:29 -0500 Subject: [PATCH 41/51] macos: swiftlint 'shorthand_operator' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Features/Splits/SplitView.swift | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 5b0a45b79..8076d8875 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -20,7 +20,6 @@ disabled_rules: - mark - multiple_closures_with_trailing_closure - no_fallthrough_only - - shorthand_operator - switch_case_alignment - unused_enumerated - unused_optional_binding diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index e860c28c0..a19fdca6a 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -108,12 +108,12 @@ struct SplitView: View { var result = CGRect(x: 0, y: 0, width: size.width, height: size.height) switch direction { case .horizontal: - result.size.width = result.size.width * split + result.size.width *= split result.size.width -= splitterVisibleSize / 2 result.size.width -= result.size.width.truncatingRemainder(dividingBy: self.resizeIncrements.width) case .vertical: - result.size.height = result.size.height * split + result.size.height *= split result.size.height -= splitterVisibleSize / 2 result.size.height -= result.size.height.truncatingRemainder(dividingBy: self.resizeIncrements.height) } From c418e4b581d61969d3f02c2adae4e2e862f07b58 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 19:10:38 -0500 Subject: [PATCH 42/51] macos: swiftlint 'unused_optional_binding' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Helpers/AppInfo.swift | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 8076d8875..6713f9f64 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -22,7 +22,6 @@ disabled_rules: - no_fallthrough_only - switch_case_alignment - unused_enumerated - - unused_optional_binding - vertical_parameter_alignment identifier_name: diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift index 281bad18b..940d247d5 100644 --- a/macos/Sources/Helpers/AppInfo.swift +++ b/macos/Sources/Helpers/AppInfo.swift @@ -2,9 +2,5 @@ import Foundation /// True if we appear to be running in Xcode. func isRunningInXcode() -> Bool { - if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return true - } - - return false + ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] != nil } From dbf2e0e087ced90a8f844fc028d73d9b2e40e668 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Thu, 19 Feb 2026 19:11:36 -0500 Subject: [PATCH 43/51] macos: swiftlint 'vertical_parameter_alignment' rule --- macos/.swiftlint.yml | 1 - macos/Sources/Features/Terminal/TerminalController.swift | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/macos/.swiftlint.yml b/macos/.swiftlint.yml index 6713f9f64..8f76034af 100644 --- a/macos/.swiftlint.yml +++ b/macos/.swiftlint.yml @@ -22,7 +22,6 @@ disabled_rules: - no_fallthrough_only - switch_case_alignment - unused_enumerated - - vertical_parameter_alignment identifier_name: min_length: 1 diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 3b4242a86..2730a0436 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -935,9 +935,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr let tabColor: TerminalTabColor } - convenience init(_ ghostty: Ghostty.App, - with undoState: UndoState - ) { + convenience init(_ ghostty: Ghostty.App, with undoState: UndoState) { self.init(ghostty, withSurfaceTree: undoState.surfaceTree) // Show the window and restore its frame From f66a84b18a44838831af5819cdad1ba85d9592e4 Mon Sep 17 00:00:00 2001 From: Jake Stewart Date: Fri, 20 Feb 2026 07:55:47 +0800 Subject: [PATCH 44/51] improve light theme detection --- src/config/Config.zig | 18 ++++- src/terminal/color.zig | 146 +++++++++++++++++++++++++++++++++++------ src/termio/Termio.zig | 8 +-- 3 files changed, 141 insertions(+), 31 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b56aee004..df39835d1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -808,9 +808,21 @@ palette: Palette = .{}, /// Available since: 1.3.0 @"palette-generate": bool = true, -/// Whether to invert generated light themes colors (see `palette-generate`). -/// This helps give the 256-color palette more semantic meaning. -/// +/// Invert the palette colors generated when `palette-generate` is enabled, +/// so that the colors go in reverse order. This allows palette-based +/// applications to work well in both light and dark mode since the +/// palettes are always relatively good colors. +/// +/// This defaults to off because some legacy terminal applications +/// hardcode the assumption that palette indices 16–231 are ordered from +/// darkest to lightest, so enabling this would make them unreadable. +/// This is not a generally good assumption and we encourage modern +/// terminal applications to use the indices in a more semantic way. +/// +/// This has no effect if `palette-generate` is disabled. +/// +/// For more information see `palette-generate`. +/// /// Available since: 1.3.0 @"palette-harmonious": bool = false, diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 1daa50107..3b806f8b8 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -1,10 +1,9 @@ -const std = @import("std"); +const colorpkg = @This(); +const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const x11_color = @import("x11_color.zig"); -const colorpkg = @This(); - /// The default palette. pub const default: Palette = default: { var result: Palette = undefined; @@ -91,25 +90,32 @@ pub fn generate256Color( skip: PaletteMask, bg: RGB, fg: RGB, - harmonious: bool + harmonious: bool, ) Palette { // Convert the background, foreground, and 8 base theme colors into // CIELAB space so that all interpolation is perceptually uniform. - const bg_lab: LAB = .fromRgb(bg); - const fg_lab: LAB = .fromRgb(fg); + const base8_lab: [8]LAB = base8: { + var base8: [8]LAB = .{ + .fromRgb(bg), + LAB.fromRgb(base[1]), + LAB.fromRgb(base[2]), + LAB.fromRgb(base[3]), + LAB.fromRgb(base[4]), + LAB.fromRgb(base[5]), + LAB.fromRgb(base[6]), + .fromRgb(fg), + }; - const is_light_theme = bg_lab.l > 50; - const invert = is_light_theme and !harmonious; + // For light themes (where the foreground is darker than the + // background), the cube's dark-to-light orientation is inverted + // relative to the base color mapping. When `harmonious` is false, + // swap bg and fg so the cube still runs from black (16) to + // white (231). + const is_light_theme = base8[7].l < base8[0].l; + const invert = is_light_theme and !harmonious; + if (invert) std.mem.swap(LAB, &base8[0], &base8[7]); - const base8_lab: [8]LAB = .{ - if (invert) fg_lab else bg_lab, - LAB.fromRgb(base[1]), - LAB.fromRgb(base[2]), - LAB.fromRgb(base[3]), - LAB.fromRgb(base[4]), - LAB.fromRgb(base[5]), - LAB.fromRgb(base[6]), - if (invert) bg_lab else fg_lab, + break :base8 base8; }; // Start from the base palette so indices 0–15 are preserved as-is. @@ -937,7 +943,7 @@ test "generate256Color: base16 preserved" { const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // The first 16 colors (base16) must remain unchanged. for (0..16) |i| { @@ -950,7 +956,7 @@ test "generate256Color: cube corners match base colors" { const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // Index 16 is cube (0,0,0) which should equal bg. try testing.expectEqual(bg, palette[16]); @@ -959,12 +965,43 @@ test "generate256Color: cube corners match base colors" { try testing.expectEqual(fg, palette[231]); } +test "generate256Color: cube corners black/white with harmonious=false" { + const testing = std.testing; + + const black = RGB{ .r = 0, .g = 0, .b = 0 }; + const white = RGB{ .r = 255, .g = 255, .b = 255 }; + + // Dark theme: bg=black, fg=white. + const dark = generate256Color(default, .initEmpty(), black, white, false); + try testing.expectEqual(black, dark[16]); + try testing.expectEqual(white, dark[231]); + + // Light theme: bg=white, fg=black. The bg/red swap ensures + // the cube still runs from black (16) to white (231). + const light = generate256Color(default, .initEmpty(), white, black, false); + try testing.expectEqual(black, light[16]); + try testing.expectEqual(white, light[231]); +} + +test "generate256Color: light theme cube corners with harmonious=true" { + const testing = std.testing; + + const white = RGB{ .r = 255, .g = 255, .b = 255 }; + const black = RGB{ .r = 0, .g = 0, .b = 0 }; + + // harmonious=true skips the bg/fg swap, so the cube preserves the + // original orientation: (0,0,0)=bg=white, (5,5,5)=fg=black. + const palette = generate256Color(default, .initEmpty(), white, black, true); + try testing.expectEqual(white, palette[16]); + try testing.expectEqual(black, palette[231]); +} + test "generate256Color: grayscale ramp monotonic luminance" { const testing = std.testing; const bg = RGB{ .r = 0, .g = 0, .b = 0 }; const fg = RGB{ .r = 255, .g = 255, .b = 255 }; - const palette = generate256Color(default, .initEmpty(), bg, fg); + const palette = generate256Color(default, .initEmpty(), bg, fg, false); // The grayscale ramp (232–255) should have monotonically increasing // luminance from near-black to near-white. @@ -988,7 +1025,7 @@ test "generate256Color: skip mask preserves original colors" { skip.set(100); skip.set(240); - const palette = generate256Color(default, skip, bg, fg); + const palette = generate256Color(default, skip, bg, fg, false); try testing.expectEqual(default[20], palette[20]); try testing.expectEqual(default[100], palette[100]); try testing.expectEqual(default[240], palette[240]); @@ -997,6 +1034,73 @@ test "generate256Color: skip mask preserves original colors" { try testing.expect(!palette[21].eql(default[21])); } +test "generate256Color: dark theme harmonious has no effect" { + const testing = std.testing; + + // For a dark theme (fg lighter than bg), harmonious should not change + // the output because the inversion is only relevant for light themes. + const bg = RGB{ .r = 0, .g = 0, .b = 0 }; + const fg = RGB{ .r = 255, .g = 255, .b = 255 }; + const normal = generate256Color(default, .initEmpty(), bg, fg, false); + const harmonious = generate256Color(default, .initEmpty(), bg, fg, true); + + for (16..256) |i| { + try testing.expectEqual(normal[i], harmonious[i]); + } +} + +test "generate256Color: light theme harmonious skips inversion" { + const testing = std.testing; + + // For a light theme (fg darker than bg), harmonious=true skips the + // bg/red swap, producing different cube colors than harmonious=false. + const bg = RGB{ .r = 255, .g = 255, .b = 255 }; + const fg = RGB{ .r = 0, .g = 0, .b = 0 }; + const inverted = generate256Color(default, .initEmpty(), bg, fg, false); + const harmonious = generate256Color(default, .initEmpty(), bg, fg, true); + + // Cube origin (0,0,0) at index 16: without harmonious, bg and red are + // swapped so it becomes the red base; with harmonious it stays as bg. + try testing.expectEqual(bg, harmonious[16]); + try testing.expect(!inverted[16].eql(bg)); + + // At least some cube colors should differ between the two modes. + var differ: usize = 0; + for (16..232) |i| { + if (!inverted[i].eql(harmonious[i])) differ += 1; + } + try testing.expect(differ > 0); +} + +test "generate256Color: light theme harmonious grayscale ramp" { + const testing = std.testing; + + const bg = RGB{ .r = 255, .g = 255, .b = 255 }; + const fg = RGB{ .r = 0, .g = 0, .b = 0 }; + + // harmonious=false swaps bg/fg, so the ramp runs black→white (increasing). + { + const palette = generate256Color(default, .initEmpty(), bg, fg, false); + var prev_lum: f64 = 0.0; + for (232..256) |i| { + const lum = palette[i].luminance(); + try testing.expect(lum >= prev_lum); + prev_lum = lum; + } + } + + // harmonious=true keeps original order, so the ramp runs white→black (decreasing). + { + const palette = generate256Color(default, .initEmpty(), bg, fg, true); + var prev_lum: f64 = 1.0; + for (232..256) |i| { + const lum = palette[i].luminance(); + try testing.expect(lum <= prev_lum); + prev_lum = lum; + } + } +} + test "LAB.toRgb" { const testing = std.testing; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 80ab4d7c7..092871af5 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -184,13 +184,7 @@ pub const DerivedConfig = struct { break :generate; } - break :palette terminalpkg.color.generate256Color( - config.palette.value, - config.palette.mask, - config.background.toTerminalRGB(), - config.foreground.toTerminalRGB(), - config.@"palette-harmonious" - ); + break :palette terminalpkg.color.generate256Color(config.palette.value, config.palette.mask, config.background.toTerminalRGB(), config.foreground.toTerminalRGB(), config.@"palette-harmonious"); } break :palette config.palette.value; From acf8cc74195f8f778e933a3e8c218891d79a36e4 Mon Sep 17 00:00:00 2001 From: TicClick Date: Fri, 20 Feb 2026 05:24:29 +0100 Subject: [PATCH 45/51] i18n: Update and slightly clean up Russian translation (#10633) as per #10632 --- po/ru_RU.UTF-8.po | 55 +++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index e4e314141..d64229eed 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2025-09-03 01:50+0300\n" +"PO-Revision-Date: 2025-02-18 10:20+0100\n" "Last-Translator: Ivan Bastrakov \n" "Language-Team: Russian \n" "Language: ru\n" @@ -21,7 +21,7 @@ msgstr "" #: dist/linux/ghostty_nautilus.py:53 msgid "Open in Ghostty" -msgstr "" +msgstr "Открыть в Ghostty" #: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 #: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 @@ -68,8 +68,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Конфигурация содержит ошибки. Проверьте их ниже, а затем либо перезагрузите " -"конфигурацию, либо проигнорируйте ошибки." +"В конфигурации обнаружены перечисленные ниже ошибки. При необходимости " +"исправьте их, а затем перезагрузите конфигурацию." #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Ignore" @@ -78,7 +78,7 @@ msgstr "Игнорировать" #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:11 #: src/apprt/gtk/ui/1.2/surface.blp:366 src/apprt/gtk/ui/1.5/window.blp:300 msgid "Reload Configuration" -msgstr "Обновить конфигурацию" +msgstr "Перезагрузить конфигурацию" #: src/apprt/gtk/ui/1.2/debug-warning.blp:7 #: src/apprt/gtk/ui/1.3/debug-warning.blp:6 @@ -94,23 +94,23 @@ msgstr "Ghostty: инспектор терминала" #: src/apprt/gtk/ui/1.2/search-overlay.blp:29 msgid "Find…" -msgstr "" +msgstr "Найти…" #: src/apprt/gtk/ui/1.2/search-overlay.blp:64 msgid "Previous Match" -msgstr "" +msgstr "Предыдущий результат" #: src/apprt/gtk/ui/1.2/search-overlay.blp:74 msgid "Next Match" -msgstr "" +msgstr "Следующий результат" #: src/apprt/gtk/ui/1.2/surface.blp:6 msgid "Oh, no." -msgstr "" +msgstr "Ой!" #: src/apprt/gtk/ui/1.2/surface.blp:7 msgid "Unable to acquire an OpenGL context for rendering." -msgstr "" +msgstr "Не удалось получить доступ к контексту OpenGL для отрисовки." #: src/apprt/gtk/ui/1.2/surface.blp:97 msgid "" @@ -118,10 +118,13 @@ msgid "" "through the content, but no input events will be sent to the running " "application." msgstr "" +"Терминал работает в режиме только для чтения: его содержимое можно " +"прокручивать и выделять, но запущенное приложение не будет получать события " +"ввода." #: src/apprt/gtk/ui/1.2/surface.blp:107 msgid "Read-only" -msgstr "" +msgstr "Режим только для чтения" #: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200 msgid "Copy" @@ -133,7 +136,7 @@ msgstr "Вставить" #: src/apprt/gtk/ui/1.2/surface.blp:270 msgid "Notify on Next Command Finish" -msgstr "" +msgstr "Сообщить о завершении следующей команды" #: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273 msgid "Clear" @@ -149,7 +152,7 @@ msgstr "Сплит" #: src/apprt/gtk/ui/1.2/surface.blp:292 src/apprt/gtk/ui/1.5/window.blp:245 msgid "Change Title…" -msgstr "Изменить заголовок…" +msgstr "Переименовать…" #: src/apprt/gtk/ui/1.2/surface.blp:297 src/apprt/gtk/ui/1.5/window.blp:177 #: src/apprt/gtk/ui/1.5/window.blp:250 @@ -178,7 +181,7 @@ msgstr "Вкладка" #: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 #: src/apprt/gtk/ui/1.5/window.blp:320 msgid "Change Tab Title…" -msgstr "" +msgstr "Переименовать вкладку…" #: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 #: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 @@ -211,7 +214,7 @@ msgstr "Открыть конфигурационный файл" #: src/apprt/gtk/ui/1.5/title-dialog.blp:5 msgid "Leave blank to restore the default title." -msgstr "Оставьте пустым, чтобы восстановить исходный заголовок." +msgstr "Оставьте поле пустым, чтобы вернуть название по умолчанию." #: src/apprt/gtk/ui/1.5/title-dialog.blp:9 msgid "OK" @@ -262,8 +265,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Приложение пытается прочитать данные из буфера обмена. Эти данные отображены " -"ниже." +"Приложение пытается прочитать данные из буфера обмена. Его содержимое " +"показано ниже." #: src/apprt/gtk/class/clipboard_confirmation_dialog.zig:205 msgid "Warning: Potentially Unsafe Paste" @@ -274,12 +277,12 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Вставка этого текста в терминал может быть опасной. Это выглядит как " -"команды, которые могут быть исполнены." +"Этот текст может быть опасен: его вставка в терминал приведёт к выполнению " +"команд." #: src/apprt/gtk/class/close_confirmation_dialog.zig:184 msgid "Quit Ghostty?" -msgstr "Закрыть Ghostty?" +msgstr "Выйти из Ghostty?" #: src/apprt/gtk/class/close_confirmation_dialog.zig:185 msgid "Close Tab?" @@ -311,15 +314,15 @@ msgstr "Процесс, работающий в этой сплит-област #: src/apprt/gtk/class/surface.zig:1108 msgid "Command Finished" -msgstr "" +msgstr "Команда завершилась" #: src/apprt/gtk/class/surface.zig:1109 msgid "Command Succeeded" -msgstr "" +msgstr "Команда выполнена успешно" #: src/apprt/gtk/class/surface.zig:1110 msgid "Command Failed" -msgstr "" +msgstr "Команда завершилась с ошибкой" #: src/apprt/gtk/class/surface_child_exited.zig:109 msgid "Command succeeded" @@ -331,15 +334,15 @@ msgstr "Команда завершилась с ошибкой" #: src/apprt/gtk/class/title_dialog.zig:225 msgid "Change Terminal Title" -msgstr "Изменить заголовок терминала" +msgstr "Переименовать терминал" #: src/apprt/gtk/class/title_dialog.zig:226 msgid "Change Tab Title" -msgstr "" +msgstr "Переименовать вкладку" #: src/apprt/gtk/class/window.zig:1007 msgid "Reloaded the configuration" -msgstr "Конфигурация была обновлена" +msgstr "Конфигурация перезагружена" #: src/apprt/gtk/class/window.zig:1566 msgid "Copied to clipboard" From 2863849fcae7ef46342e14af30fc3d850cd2109a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 20:28:28 -0800 Subject: [PATCH 46/51] ci: milestone workflow should use our vouch app token This increases our rate limits and the vouch app already has the permissions required for the milestone workflow. --- .github/workflows/milestone.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index 49bba4e6b..dc8061a08 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -9,18 +9,26 @@ on: pull_request_target: types: [closed] +permissions: {} + jobs: update-milestone: runs-on: namespace-profile-ghostty-sm name: Milestone Update steps: + - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + id: app-token + with: + app-id: ${{ secrets.VOUCH_APP_ID }} + private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} + - name: Set Milestone for PR uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1 if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue @@ -29,4 +37,4 @@ jobs: with: action: bind-issue env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} From df47dc1a98b9df29152ee1b24daea5f50883c99a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 20:28:28 -0800 Subject: [PATCH 47/51] ci: milestone workflow should use our vouch app token This increases our rate limits and the vouch app already has the permissions required for the milestone workflow. --- .github/workflows/milestone.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index dc8061a08..ad6623fd2 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -27,8 +27,7 @@ jobs: if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + github-token: ${{ steps.app-token.outputs.token }} # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue @@ -36,5 +35,4 @@ jobs: if: github.event.issue.state == 'closed' with: action: bind-issue - env: - GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + github-token: ${{ steps.app-token.outputs.token }} From a6b603385236bad5a592eb078d3e72a39c8215c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Feb 2026 20:39:50 -0800 Subject: [PATCH 48/51] ci: pass milestone token via github-token parameter If I am reading the upstream action right, even if you set GITHUB_TOKEN env var its defaulting to `github.token`, so we need to specify as a param. --- .github/workflows/milestone.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml index ad6623fd2..33a074159 100644 --- a/.github/workflows/milestone.yml +++ b/.github/workflows/milestone.yml @@ -9,25 +9,17 @@ on: pull_request_target: types: [closed] -permissions: {} - jobs: update-milestone: runs-on: namespace-profile-ghostty-sm name: Milestone Update steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 - id: app-token - with: - app-id: ${{ secrets.VOUCH_APP_ID }} - private-key: ${{ secrets.VOUCH_APP_PRIVATE_KEY }} - - name: Set Milestone for PR uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1 if: github.event.pull_request.merged == true with: action: bind-pr # `bind-pr` is the default action - github-token: ${{ steps.app-token.outputs.token }} + github-token: ${{ secrets.GITHUB_TOKEN }} # Bind milestone to closed issue that has a merged PR fix - name: Set Milestone for Issue @@ -35,4 +27,4 @@ jobs: if: github.event.issue.state == 'closed' with: action: bind-issue - github-token: ${{ steps.app-token.outputs.token }} + github-token: ${{ secrets.GITHUB_TOKEN }} From b0f00a65edcd029a0670a669506de778edc39cfd Mon Sep 17 00:00:00 2001 From: Nico Ritschel Date: Thu, 19 Feb 2026 20:45:52 -0800 Subject: [PATCH 49/51] Add ghostty_config_get tests I mostly did this to familiarize myself with the codebase and figured it doesn't hurt to cover this with tests if I add more in this area, despite receiving indirect coverage elsewhere. --- src/config/CApi.zig | 98 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/config/CApi.zig b/src/config/CApi.zig index 4ea9ea63f..ca15ce4e8 100644 --- a/src/config/CApi.zig +++ b/src/config/CApi.zig @@ -144,3 +144,101 @@ export fn ghostty_config_open_path() c.String { const Diagnostic = extern struct { message: [*:0]const u8 = "", }; + +test "ghostty_config_get: bool" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.maximize = true; + + var out = false; + const key = "maximize"; + try testing.expect(ghostty_config_get(&cfg, &out, key, key.len)); + try testing.expect(out); +} + +test "ghostty_config_get: enum" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"window-theme" = .dark; + + var out: [*:0]const u8 = undefined; + const key = "window-theme"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + const str = std.mem.sliceTo(out, 0); + try testing.expectEqualStrings("dark", str); +} + +test "ghostty_config_get: optional null returns false" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"unfocused-split-fill" = null; + + var out: Config.Color.C = undefined; + const key = "unfocused-split-fill"; + try testing.expect(!ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); +} + +test "ghostty_config_get: unknown key returns false" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + + var out = false; + const key = "not-a-real-key"; + try testing.expect(!ghostty_config_get(&cfg, &out, key, key.len)); +} + +test "ghostty_config_get: optional string null returns true" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.title = null; + + var out: ?[*:0]const u8 = undefined; + const key = "title"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + try testing.expect(out == null); +} + +test "ghostty_config_get: float" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.@"background-opacity" = 0.42; + + var out: f64 = 0; + const key = "background-opacity"; + try testing.expect(ghostty_config_get(&cfg, &out, key, key.len)); + try testing.expectApproxEqAbs(@as(f64, 0.42), out, 0.000001); +} + +test "ghostty_config_get: struct cval conversion" { + const testing = std.testing; + const alloc = testing.allocator; + + var cfg = try Config.default(alloc); + defer cfg.deinit(); + cfg.background = .{ .r = 12, .g = 34, .b = 56 }; + + var out: Config.Color.C = undefined; + const key = "background"; + try testing.expect(ghostty_config_get(&cfg, @ptrCast(&out), key, key.len)); + try testing.expectEqual(@as(u8, 12), out.r); + try testing.expectEqual(@as(u8, 34), out.g); + try testing.expectEqual(@as(u8, 56), out.b); +} From 585bf3fcd25be3d1d70383a0f1b55e0f6744d639 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Fri, 20 Feb 2026 06:55:39 -0300 Subject: [PATCH 50/51] Update es_AR translations with additional strings for 1.3 (#10861) - Adds the three new string - Changes one string for better wording --- po/es_AR.UTF-8.po | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 930d8ada5..3182ebf48 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2026-02-17 23:16+0100\n" -"PO-Revision-Date: 2026-02-09 17:50-0300\n" +"PO-Revision-Date: 2026-02-19 13:34-0300\n" "Last-Translator: Alan Moyano \n" "Language-Team: Argentinian \n" "Language: es_AR\n" @@ -19,7 +19,7 @@ msgstr "" #: dist/linux/ghostty_nautilus.py:53 msgid "Open in Ghostty" -msgstr "" +msgstr "Abrir en Ghostty" #: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12 #: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12 @@ -179,7 +179,7 @@ msgstr "Pestaña" #: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224 #: src/apprt/gtk/ui/1.5/window.blp:320 msgid "Change Tab Title…" -msgstr "" +msgstr "Cambiar título de la pestaña…" #: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57 #: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229 @@ -332,11 +332,11 @@ msgstr "Comando fallido" #: src/apprt/gtk/class/title_dialog.zig:225 msgid "Change Terminal Title" -msgstr "Cambiar el título de la terminal" +msgstr "Cambiar título de la terminal" #: src/apprt/gtk/class/title_dialog.zig:226 msgid "Change Tab Title" -msgstr "" +msgstr "Cambiar título de la pestaña" #: src/apprt/gtk/class/window.zig:1007 msgid "Reloaded the configuration" From b6c1a264378ea8616d9e71d941731ded400ca83e Mon Sep 17 00:00:00 2001 From: "ghostty-vouch[bot]" <262049992+ghostty-vouch[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:37:50 +0000 Subject: [PATCH 51/51] Update VOUCHED list (#10887) Triggered by [discussion comment](https://github.com/ghostty-org/ghostty/discussions/10882) from @jcollie. Vouch: @brentschroeter Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .github/VOUCHED.td | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/VOUCHED.td b/.github/VOUCHED.td index 356731c56..ef4a8f25d 100644 --- a/.github/VOUCHED.td +++ b/.github/VOUCHED.td @@ -31,6 +31,7 @@ beryesa bitigchi bkircher bo2themax +brentschroeter charliie-dev chernetskyi craziestowl