Merge remote-tracking branch 'upstream/main' into grapheme-width-changes

This commit is contained in:
Jacob Sandlund
2026-02-23 08:39:10 -05:00
155 changed files with 2652 additions and 1662 deletions

63
.github/VOUCHED.td vendored
View File

@@ -18,45 +18,108 @@
# 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
alexfeijoo44
andrejdaskalov
atomk
balazs-szucs
bennettp123
benodiwal
bernsno
beryesa
bitigchi
bkircher
bo2themax
brentschroeter
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
jacobsandlund
jake-stewart
jcollie
johnslavik
josephmart
jparise
juniqlim
kawarimidoll
kenvandine
khipp
kirwiisp
kjvdven
kloneets
kristina8888
kristofersoler
laxystem
liby
lonsagisawa
mahnokropotkinvich
marijagjorgjieva
marrocco-simone
matkotiric
miguelelgallo
mikailmm
misairuzame
mitchellh
miupa
mtak
natesmyth
neo773
nicosuave
nwehg
oshdubh
pan93412
pangoraw
peilingjiang
peterdavehello
phush0
piedrahitac
pluiedev
pouwerkerk
priyans-hu
prsweet
qwerasd205
reo101
rgehan
rmengelbrecht
rmunn
rockorager
rpfaeffle
secrus
silveirapf
slsrepo
sunshine-syz
tdslot
ticclick
tnagatomi
trag1c
tristan957
tweedbeetle
uhojin
uzaaft
vlsi
yamshta
zenyr
zeshi09

View File

@@ -19,8 +19,7 @@ jobs:
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: ${{ secrets.GITHUB_TOKEN }}
# Bind milestone to closed issue that has a merged PR fix
- name: Set Milestone for Issue
@@ -28,5 +27,4 @@ jobs:
if: github.event.issue.state == 'closed'
with:
action: bind-issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -11,15 +11,84 @@ concurrency:
cancel-in-progress: true
jobs:
# Determines whether other jobs should be skipped. Modify this if there
# are other fast skip conditions, and add it as an output. Then modify
# other tests `needs/if` to check them. Document the outputs.
skip:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-xsm
outputs:
# 'true' when all changed files are non-code (e.g. only VOUCHED.td),
# signaling that all other jobs can be skipped entirely.
skip: ${{ steps.determine.outputs.skip }}
# Path-based filters to gate specific linter/formatter jobs.
actions_pins: ${{ steps.filter_any.outputs.actions_pins }}
blueprints: ${{ steps.filter_any.outputs.blueprints }}
macos: ${{ steps.filter_any.outputs.macos }}
nix: ${{ steps.filter_any.outputs.nix }}
shell: ${{ steps.filter_any.outputs.shell }}
zig: ${{ steps.filter_any.outputs.zig }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter_every
with:
token: ${{ secrets.GITHUB_TOKEN }}
predicate-quantifier: "every"
filters: |
code:
- '**'
- '!.github/VOUCHED.td'
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter_any
with:
token: ${{ secrets.GITHUB_TOKEN }}
filters: |
macos:
- '.swiftlint.yml'
- 'macos/**'
actions_pins:
- '.github/workflows/**'
- '.github/pinact.yml'
shell:
- '**/*.sh'
- '**/*.bash'
nix:
- 'nix/**'
- '*.nix'
- 'flake.nix'
- 'flake.lock'
- 'default.nix'
- 'shell.nix'
zig:
- '**/*.zig'
- 'build.zig*'
blueprints:
- 'src/apprt/gtk/**/*.blp'
- 'nix/build-support/check-blueprints.sh'
- id: determine
name: Determine skip
run: |
if [ "${{ steps.filter_every.outputs.code }}" = "false" ]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
required:
name: "Required Checks: Test"
if: always()
runs-on: namespace-profile-ghostty-xsm
needs:
- skip
- build-bench
- build-dist
- build-examples
- build-flatpak
- build-libghostty-vt
- build-libghostty-vt-android
- build-libghostty-vt-macos
- build-linux
- build-linux-libghostty
@@ -36,6 +105,7 @@ jobs:
- test-macos
- pinact
- prettier
- swiftlint
- alejandra
- typos
- shellcheck
@@ -78,7 +148,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 +191,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 +224,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 +258,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 +302,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
@@ -281,6 +351,54 @@ jobs:
nix develop -c zig build lib-vt \
-Dtarget=${{ matrix.target }}
# lib-vt requires the Android NDK for Android builds
build-libghostty-vt-android:
strategy:
matrix:
target:
[aarch64-linux-android, x86_64-linux-android, arm-linux-androideabi]
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
ANDROID_NDK_VERSION: r29
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@4d61c33d0b4333a518e975a0c4de7633d28713bb # v1.4.1
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Setup Android NDK
uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0
id: setup-ndk
with:
ndk-version: r29
add-to-path: false
link-to-sdk: false
local-cache: true
- name: Build
run: |
nix develop -c zig build lib-vt \
-Dtarget=${{ matrix.target }}
env:
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
build-linux:
strategy:
fail-fast: false
@@ -296,7 +414,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 +443,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 +476,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 +522,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
@@ -616,7 +734,8 @@ jobs:
run: Get-Content -Path ".\build.log"
test:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-md
outputs:
zig_version: ${{ steps.zig.outputs.version }}
@@ -633,7 +752,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 +794,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 +842,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 +877,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
@@ -811,7 +930,7 @@ jobs:
fail-fast: false
matrix:
i18n: ["true", "false"]
name: Build -Di18n=${{ matrix.simd }}
name: Build -Di18n=${{ matrix.i18n }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
@@ -822,7 +941,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
@@ -842,7 +961,8 @@ jobs:
nix develop -c zig build -Di18n=${{ matrix.i18n }}
zig-fmt:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.zig == 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -851,7 +971,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
@@ -870,7 +990,8 @@ jobs:
pinact:
name: "GitHub Actions Pins"
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.actions_pins == 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
permissions:
@@ -881,7 +1002,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
@@ -901,7 +1022,8 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
prettier:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -910,7 +1032,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
@@ -927,8 +1049,31 @@ jobs:
- name: prettier check
run: nix develop -c prettier --check .
swiftlint:
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.macos == 'true'
runs-on: namespace-profile-ghostty-macos-tahoe
needs: skip
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
alejandra:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.nix == 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -937,7 +1082,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
@@ -955,7 +1100,8 @@ jobs:
run: nix develop -c alejandra --check .
typos:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -964,7 +1110,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
@@ -982,7 +1128,8 @@ jobs:
run: nix develop -c typos
shellcheck:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.shell == 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -991,7 +1138,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
@@ -1014,7 +1161,8 @@ jobs:
$(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort)
translations:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -1023,7 +1171,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
@@ -1041,7 +1189,8 @@ jobs:
run: nix develop -c .github/scripts/check-translations.sh
blueprint-compiler:
if: github.repository == 'ghostty-org/ghostty'
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.blueprints == 'true'
needs: skip
runs-on: namespace-profile-ghostty-xsm
timeout-minutes: 60
env:
@@ -1050,7 +1199,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 +1234,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 +1296,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@
zig-cache/
.zig-cache/
zig-out/
/build.zig.zon.bak
/result*
/.nixos-test-history
example/*.wasm

2
.swiftlint.yml Normal file
View File

@@ -0,0 +1,2 @@
included: macos
child_config: macos/.swiftlint.yml

View File

@@ -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=<test name>`
- **Formatting (Zig)**: `zig fmt .`
- **Formatting (Swift)**: `swiftlint lint --fix`
- **Formatting (other)**: `prettier -w .`
## Directory Structure

View File

@@ -137,6 +137,7 @@
/dist/macos/ @ghostty-org/macos
/pkg/apple-sdk/ @ghostty-org/macos
/pkg/macos/ @ghostty-org/macos
/.swiftlint.yml @ghostty-org/macos
# Renderer
/src/renderer.zig @ghostty-org/renderer

View File

@@ -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
```
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
```
To check for violations without auto-fixing:
```
nix develop -c swiftlint lint --strict
```
### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output

View File

@@ -21,7 +21,7 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz",
.url = "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz",
.hash = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
.lazy = true,
},
@@ -39,8 +39,8 @@
},
.uucode = .{
// jacobsandlund/uucode
.url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
.url = "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz",
.hash = "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9",
},
.zig_wayland = .{
// codeberg ifreund/zig-wayland
@@ -115,9 +115,10 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.android_ndk = .{ .path = "./pkg/android-ndk" },
.iterm2_themes = .{
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz",
.hash = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z",
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz",
.hash = "N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF",
.lazy = true,
},
},

View File

@@ -1,124 +0,0 @@
.{
.name = .ghostty,
.version = "1.3.0-dev",
.paths = .{""},
.fingerprint = 0x64407a2a0b4147e5,
.minimum_zig_version = "0.15.2",
.dependencies = .{
// Zig libs
.libxev = .{
// mitchellh/libxev
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
.lazy = true,
},
.vaxis = .{
// rockorager/libvaxis
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
.lazy = true,
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
.lazy = true,
},
.zig_objc = .{
// mitchellh/zig-objc
.url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
.hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
.lazy = true,
},
.zig_js = .{
// mitchellh/zig-js
.url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
.hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
.lazy = true,
},
.uucode = .{
// jacobsandlund/uucode
.url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
},
.zig_wayland = .{
// codeberg ifreund/zig-wayland
.url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
.hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
.lazy = true,
},
.zf = .{
// natecraddock/zf
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
.lazy = true,
},
.gobject = .{
// https://github.com/ghostty-org/zig-gobject based on zig_gobject
// Temporary until we generate them at build time automatically.
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
.lazy = true,
},
// C libs
.cimgui = .{ .path = "./pkg/cimgui", .lazy = true },
.fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true },
.freetype = .{ .path = "./pkg/freetype", .lazy = true },
.gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true },
.harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true },
.highway = .{ .path = "./pkg/highway", .lazy = true },
.libintl = .{ .path = "./pkg/libintl", .lazy = true },
.libpng = .{ .path = "./pkg/libpng", .lazy = true },
.macos = .{ .path = "./pkg/macos", .lazy = true },
.oniguruma = .{ .path = "./pkg/oniguruma", .lazy = true },
.opengl = .{ .path = "./pkg/opengl", .lazy = true },
.sentry = .{ .path = "./pkg/sentry", .lazy = true },
.simdutf = .{ .path = "./pkg/simdutf", .lazy = true },
.utfcpp = .{ .path = "./pkg/utfcpp", .lazy = true },
.wuffs = .{ .path = "./pkg/wuffs", .lazy = true },
.zlib = .{ .path = "./pkg/zlib", .lazy = true },
// Shader translation
.glslang = .{ .path = "./pkg/glslang", .lazy = true },
.spirv_cross = .{ .path = "./pkg/spirv-cross", .lazy = true },
// Wayland
.wayland = .{
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
.hash = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t",
.lazy = true,
},
.wayland_protocols = .{
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
.hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S",
.lazy = true,
},
.plasma_wayland_protocols = .{
.url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
.hash = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs",
.lazy = true,
},
// Fonts
.jetbrains_mono = .{
.url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
.hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
.lazy = true,
},
.nerd_fonts_symbols_only = .{
.url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
.hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
.lazy = true,
},
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
.hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
.lazy = true,
},
},
}

14
build.zig.zon.json generated
View File

@@ -54,10 +54,10 @@
"url": "https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz",
"hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="
},
"N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z": {
"N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF": {
"name": "iterm2_themes",
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz",
"hash": "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg="
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz",
"hash": "sha256-FCALuGoMgUq2lgnVALKAs5a20uuDXt8Gdt5KeJwKqP0="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
@@ -119,10 +119,10 @@
"url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
"hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="
},
"uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": {
"uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9": {
"name": "uucode",
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="
"url": "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz",
"hash": "sha256-0KvuD0+L1urjwFF3fhbnxC2JZKqqAVWRxOVlcD9GX5U="
},
"vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
"name": "vaxis",
@@ -146,7 +146,7 @@
},
"z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz",
"url": "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz",
"hash": "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c="
},
"zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {

14
build.zig.zon.nix generated
View File

@@ -171,11 +171,11 @@ in
};
}
{
name = "N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z";
name = "N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz";
hash = "sha256-xN+3iQaN3uIJ/BzkgFxLojgHqeuz1htNcVjcjWR7Qjg=";
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz";
hash = "sha256-FCALuGoMgUq2lgnVALKAs5a20uuDXt8Gdt5KeJwKqP0=";
};
}
{
@@ -275,11 +275,11 @@ in
};
}
{
name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E";
name = "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9";
path = fetchZigArtifact {
name = "uucode";
url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=";
url = "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz";
hash = "sha256-0KvuD0+L1urjwFF3fhbnxC2JZKqqAVWRxOVlcD9GX5U=";
};
}
{
@@ -318,7 +318,7 @@ in
name = "z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz";
url = "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz";
hash = "sha256-afIdou/V7gk3/lXE0J5Ir8T7L5GgHvFnyMJ1rgRnl/c=";
};
}

6
build.zig.zon.txt generated
View File

@@ -6,7 +6,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
https://deps.files.ghostty.org/gettext-0.24.tar.gz
https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz
https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
@@ -21,11 +21,12 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz
https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz
https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz
https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
@@ -33,4 +34,3 @@ https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
https://github.com/ocornut/imgui/archive/refs/tags/v1.92.5-docking.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz

View File

@@ -67,9 +67,9 @@
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260202-151632-49169e9.tgz",
"dest": "vendor/p/N-V-__8AAKlTAwClvdUfsAoiLgM6sb2NPZNnS6wzCiIs252Z",
"sha256": "c4dfb789068ddee209fc1ce4805c4ba23807a9ebb3d61b4d7158dc8d647b4238"
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260216-151611-fc73ce3.tgz",
"dest": "vendor/p/N-V-__8AABVbAwBwDRyZONfx553tvMW8_A2OKUoLzPUSRiLF",
"sha256": "14200bb86a0c814ab69609d500b280b396b6d2eb835edf0676de4a789c0aa8fd"
},
{
"type": "archive",
@@ -145,9 +145,9 @@
},
{
"type": "archive",
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
"sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e"
"url": "https://deps.files.ghostty.org/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9.tar.gz",
"dest": "vendor/p/uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9",
"sha256": "d0abee0f4f8bd6eae3c051777e16e7c42d8964aaaa015591c4e565703f465f95"
},
{
"type": "archive",
@@ -175,7 +175,7 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.10.0.tar.gz",
"url": "https://deps.files.ghostty.org/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ.tar.gz",
"dest": "vendor/p/z2d-0.10.0-j5P_Hu-6FgBsZNgwphIqh17jDnj8_yPtD8yzjO6PpHRQ",
"sha256": "69f21da2efd5ee0937fe55c4d09e48afc4fb2f91a01ef167c8c275ae046797f7"
},

View File

@@ -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,

36
macos/.swiftlint.yml Normal file
View File

@@ -0,0 +1,36 @@
# SwiftLint <https://realm.github.io/SwiftLint/>
#
check_for_updates: false
excluded:
- build
disabled_rules:
- cyclomatic_complexity
- file_length
- function_body_length
- line_length
- nesting
- no_fallthrough_only
- todo
- trailing_comma
- trailing_newline
- type_body_length
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

View File

@@ -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\nfi\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
810ACC9B2E9D3301004F8F92 /* Sources */ = {
isa = PBXSourcesBuildPhase;

View File

@@ -10,14 +10,12 @@ extension AppDelegate: Ghostty.Delegate {
guard let controller = window.windowController as? BaseTerminalController else {
continue
}
for surface in controller.surfaceTree {
if surface.id == id {
return surface
}
for surface in controller.surfaceTree where surface.id == id {
return surface
}
}
return nil
}
}

View File

@@ -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(
@@ -110,7 +109,7 @@ class AppDelegate: NSObject,
switch quickTerminalControllerState {
case .initialized(let controller):
return controller
case .pendingRestore(let state):
let controller = QuickTerminalController(
ghostty,
@@ -120,7 +119,7 @@ class AppDelegate: NSObject,
)
quickTerminalControllerState = .initialized(controller)
return controller
case .uninitialized:
let controller = QuickTerminalController(
ghostty,
@@ -144,16 +143,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
@@ -166,14 +165,14 @@ class AppDelegate: NSObject,
ghostty.delegate = self
}
//MARK: - NSApplicationDelegate
// MARK: - NSApplicationDelegate
func applicationWillFinishLaunching(_ notification: Notification) {
UserDefaults.standard.register(defaults: [
// 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
@@ -197,7 +196,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 +279,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
@@ -299,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))
@@ -332,7 +331,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,8 +352,8 @@ 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.
if updateController.isInstalling {
@@ -380,14 +379,8 @@ class AppDelegate: NSObject,
guard let keyword = AEKeyword("why?") else { break why }
if let why = event.attributeDescriptor(forKeyword: keyword) {
switch (why.typeCodeValue) {
case kAEShutDown:
fallthrough
case kAERestart:
fallthrough
case kAEReallyLogOut:
switch why.typeCodeValue {
case kAEShutDown, kAERestart, kAEReallyLogOut:
return .terminateNow
default:
@@ -397,7 +390,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 +399,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
@@ -449,18 +442,18 @@ 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) {
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
@@ -471,24 +464,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
// `<filename>; 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
@@ -498,15 +491,15 @@ class AppDelegate: NSObject,
alert.addButton(withTitle: "Allow")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
switch (alert.runModal()) {
switch alert.runModal() {
case .alertFirstButtonReturn:
break
default:
return false
}
}
switch ghostty.config.macosDockDropBehavior {
case .new_tab:
_ = TerminalController.newTab(
@@ -516,7 +509,7 @@ class AppDelegate: NSObject,
)
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
return true
}
@@ -746,7 +739,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 +754,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 +770,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 +854,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
@@ -880,14 +873,14 @@ 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.
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
@@ -900,7 +893,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 +904,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()
@@ -948,11 +941,11 @@ 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
switch (config.macosIcon) {
switch config.macosIcon {
case let icon where icon.assetName != nil:
appIcon = NSImage(named: icon.assetName!)!
@@ -1022,7 +1015,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 {
@@ -1031,18 +1024,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
}
@@ -1050,7 +1043,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) {
@@ -1058,7 +1051,7 @@ class AppDelegate: NSObject,
}
}
//MARK: - UNUserNotificationCenterDelegate
// MARK: - UNUserNotificationCenterDelegate
func userNotificationCenter(
_ center: UNUserNotificationCenter,
@@ -1079,21 +1072,19 @@ class AppDelegate: NSObject,
withCompletionHandler(options)
}
//MARK: - GhosttyAppDelegate
// MARK: - GhosttyAppDelegate
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? {
for c in TerminalController.all {
for view in c.surfaceTree {
if view.id == uuid {
return view
}
for view in c.surfaceTree where view.id == uuid {
return view
}
}
return nil
}
//MARK: - Dock Menu
// MARK: - Dock Menu
private func reloadDockMenu() {
let newWindow = NSMenuItem(title: "New Window", action: #selector(newWindow), keyEquivalent: "")
@@ -1104,11 +1095,11 @@ class AppDelegate: NSObject,
dockMenu.addItem(newTab)
}
//MARK: - Global State
// MARK: - Global State
func setSecureInput(_ mode: Ghostty.SetSecureInput) {
let input = SecureInput.shared
switch (mode) {
switch mode {
case .on:
input.global = true
@@ -1118,11 +1109,11 @@ 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")
}
//MARK: - IB Actions
// MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) {
Ghostty.App.openConfig()
@@ -1134,7 +1125,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?) {
@@ -1288,7 +1279,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)
@@ -1360,6 +1351,6 @@ private enum QuickTerminalState {
}
@globalActor
fileprivate actor AppIconActor: GlobalActor {
private actor AppIconActor: GlobalActor {
static let shared = AppIconActor()
}

View File

@@ -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.
@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surfaceView = terminal.surfaceView else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surface = terminal.surfaceModel else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -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 {
@@ -79,7 +74,7 @@ extension CommandEntity.ID: EntityIdentifierConvertible {
static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? {
.init(rawValue: entityIdentifierString)
}
var entityIdentifierString: String {
rawValue
}

View File

@@ -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() ?? []

View File

@@ -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)

View File

@@ -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] = [
static var caseDisplayRepresentations: [KeyEventMods: DisplayRepresentation] = [
.shift: "Shift",
.control: "Control",
.option: "Option",
.command: "Command"
]
var ghosttyMod: Ghostty.Input.Mods {
switch self {
case .shift: .shift

View File

@@ -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
@@ -43,7 +43,6 @@ func requestIntentPermission() async -> Bool {
}
}
PermissionRequest.show(
"com.mitchellh.ghostty.shortcutsPermission",
message: "Allow Shortcuts to interact with Ghostty?",

View File

@@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent {
guard await requestIntentPermission() else {
throw GhosttyIntentError.permissionDenied
}
guard let surface = terminal.surfaceModel else {
throw GhosttyIntentError.surfaceNotFound
}

View File

@@ -152,7 +152,7 @@ enum NewTerminalLocation: String {
case splitRight = "split:right"
case splitUp = "split:up"
case splitDown = "split:down"
var splitDirection: SplitTree<Ghostty.SurfaceView>.NewDirection? {
switch self {
case .splitLeft: return .left

View File

@@ -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
}

View File

@@ -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
@@ -28,12 +28,12 @@ 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 }
switch (request) {
switch request {
case .paste:
window.title = "Warning: Potentially Unsafe Paste"
case .osc_52_read, .osc_52_write:

View File

@@ -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
@@ -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
@@ -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() }
@@ -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()
}
}

View File

@@ -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"

View File

@@ -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)
@@ -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
@@ -128,7 +128,7 @@ struct CommandPaletteView: View {
? 0
: current + 1
case .move(_):
case .move:
// Unknown, ignore
break
}
@@ -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,15 +223,15 @@ struct CommandPaletteView: View {
bestScore = max(bestScore, score)
}
}
return bestScore
}
}
/// 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)? = nil
var onEvent: ((KeyboardEvent) -> Void)?
@FocusState private var isTextFieldFocused: Bool
init(query: Binding<String>, isTextFieldFocused: FocusState<Bool>, onEvent: ((KeyboardEvent) -> Void)? = nil) {
@@ -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?
@@ -346,26 +346,26 @@ fileprivate 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 @@ fileprivate struct CommandRow: View {
)
.foregroundStyle(Color.accentColor)
}
if let symbols = option.symbols {
ShortcutSymbolsView(symbols: symbols)
.foregroundStyle(.secondary)
@@ -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 {

View File

@@ -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
}
@@ -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 {

View File

@@ -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() {}
@@ -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
}
@@ -117,7 +117,7 @@ class GlobalEventTap {
}
}
fileprivate func cgEventFlagsChangedHandler(
private func cgEventFlagsChangedHandler(
proxy: CGEventTapProxy,
type: CGEventType,
cgEvent: CGEvent,
@@ -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
}

View File

@@ -16,20 +16,20 @@ 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
/// 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
@@ -316,7 +316,7 @@ class QuickTerminalController: BaseTerminalController {
// MARK: Methods
func toggle() {
if (visible) {
if visible {
animateOut()
} else {
animateIn()
@@ -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
}
}
@@ -370,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
@@ -417,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)
@@ -441,7 +440,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 +674,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

View File

@@ -1,6 +1,6 @@
import Cocoa
enum QuickTerminalPosition : String {
enum QuickTerminalPosition: String {
case top
case bottom
case left
@@ -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),
@@ -86,13 +86,13 @@ 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)
}
}
/// 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
@@ -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

View File

@@ -6,23 +6,23 @@ enum QuickTerminalScreen {
case menuBar
init?(fromGhosttyConfig string: String) {
switch (string) {
switch string {
case "main":
self = .main
case "mouse":
self = .mouse
case "macos-menu-bar":
self = .menuBar
default:
return nil
}
}
var screen: NSScreen? {
switch (self) {
switch self {
case .main:
return NSScreen.main

View File

@@ -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.

View File

@@ -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)

View File

@@ -6,15 +6,15 @@ enum QuickTerminalSpaceBehavior {
case move
init?(fromGhosttyConfig string: String) {
switch (string) {
case "move":
self = .move
switch string {
case "move":
self = .move
case "remain":
self = .remain
case "remain":
self = .remain
default:
return nil
default:
return nil
}
}
@@ -24,13 +24,13 @@ enum QuickTerminalSpaceBehavior {
.fullScreenAuxiliary
]
switch (self) {
case .move:
// We want this to move the window to the active space.
return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior)
case .remain:
// We want this to remain the window in the current space.
return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior)
switch self {
case .move:
// We want this to move the window to the active space.
return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior)
case .remain:
// We want this to remain the window in the current space.
return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior)
}
}
}

View File

@@ -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)
@@ -32,8 +32,8 @@ 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
// seems to have a "hiccup" and corrupts the frameRect,

View File

@@ -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(
@@ -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

View File

@@ -2,8 +2,8 @@ import SwiftUI
struct SecureInputOverlay: View {
// Animations
@State private var shadowAngle: Angle = .degrees(0)
@State private var shadowWidth: CGFloat = 6
@State private var gradientAngle: Angle = .degrees(0)
@State private var gradientOpacity: CGFloat = 0.5
// Popover explainer text
@State private var isPopover = false
@@ -20,18 +20,32 @@ struct SecureInputOverlay: View {
.foregroundColor(.primary)
.padding(5)
.background(
RoundedRectangle(cornerRadius: 12)
Rectangle()
.fill(.background)
.innerShadow(
using: RoundedRectangle(cornerRadius: 12),
stroke: AngularGradient(
gradient: Gradient(colors: [.cyan, .blue, .yellow, .blue, .cyan]),
center: .center,
angle: shadowAngle
),
width: shadowWidth
.overlay(
Rectangle()
.fill(
AngularGradient(
gradient: Gradient(
colors: [.cyan, .blue, .yellow, .blue, .cyan]
),
center: .center,
angle: gradientAngle
)
)
.blur(radius: 4, opaque: true)
.mask(
RadialGradient(
colors: [.clear, .black],
center: .center,
startRadius: 0,
endRadius: 25
)
)
.opacity(gradientOpacity)
)
)
.mask(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.gray, lineWidth: 1)
@@ -44,9 +58,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)
@@ -57,11 +71,11 @@ struct SecureInputOverlay: View {
}
.onAppear {
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) {
shadowAngle = .degrees(360)
gradientAngle = .degrees(360)
}
withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: true)) {
shadowWidth = 12
gradientOpacity = 1
}
}
}

View File

@@ -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)

View File

@@ -12,13 +12,13 @@ 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)
}
}
}
//MARK: - NSWindowController
// MARK: - NSWindowController
override func windowWillLoad() {
shouldCascadeWindows = false

View File

@@ -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()
}
@@ -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
@@ -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
@@ -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 {

View File

@@ -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:

View File

@@ -90,7 +90,7 @@ struct SplitView<L: View, R: View>: 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,14 +106,14 @@ struct SplitView<L: View, R: View>: 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 *= 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)
}
@@ -125,7 +125,7 @@ struct SplitView<L: View, R: View>: 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<L: View, R: View>: 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)
@@ -152,9 +152,9 @@ struct SplitView<L: View, R: View>: 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<L: View, R: View>: View {
return "Vertical split view"
}
}
private var leftPaneLabel: String {
switch direction {
case .horizontal:
@@ -172,7 +172,7 @@ struct SplitView<L: View, R: View>: View {
return "Top pane"
}
}
private var rightPaneLabel: String {
switch direction {
case .horizontal:

View File

@@ -7,19 +7,19 @@ import SwiftUI
enum TerminalSplitOperation {
case resize(Resize)
case drop(Drop)
struct Resize {
let node: SplitTree<Ghostty.SurfaceView>.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
}
@@ -44,7 +44,7 @@ struct TerminalSplitTreeView: View {
}
}
fileprivate struct TerminalSplitSubtreeView: View {
private struct TerminalSplitSubtreeView: View {
@EnvironmentObject var ghostty: Ghostty.App
let node: SplitTree<Ghostty.SurfaceView>.Node
@@ -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
}
@@ -86,14 +86,14 @@ fileprivate struct TerminalSplitSubtreeView: View {
}
}
fileprivate struct TerminalSplitLeaf: View {
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 @@ fileprivate 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 @@ fileprivate 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 @@ fileprivate 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 @@ fileprivate struct TerminalSplitLeaf: View {
guard sourceSurface !== destinationSurface else { return }
action(.drop(.init(payload: sourceSurface, destination: destinationSurface, zone: zone)))
}
case .failure:
break
}
}
return true
}
}

View File

@@ -31,13 +31,12 @@ class BaseTerminalController: NSWindowController,
TerminalViewDelegate,
TerminalViewModel,
ClipboardConfirmationViewDelegate,
FullscreenDelegate
{
FullscreenDelegate {
/// The app instance that this terminal view will represent.
let ghostty: Ghostty.App
/// The currently focused surface.
var focusedSurface: Ghostty.SurfaceView? = nil {
var focusedSurface: Ghostty.SurfaceView? {
didSet { syncFocusToSurfaceTree() }
}
@@ -48,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
@@ -58,19 +57,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 +85,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() }
}
@@ -281,7 +280,7 @@ class BaseTerminalController: NSWindowController,
/// Subclasses should call super first.
func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
// If our surface tree becomes empty then we have no focused surface.
if (to.isEmpty) {
if to.isEmpty {
focusedSurface = nil
}
}
@@ -424,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<Ghostty.SurfaceView>.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() {
@@ -433,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.
@@ -471,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
@@ -488,7 +487,7 @@ class BaseTerminalController: NSWindowController,
Ghostty.moveFocus(to: oldView, from: target.focusedSurface)
}
}
undoManager.registerUndo(
withTarget: target,
expiresAfter: target.undoExpiration
@@ -531,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
@@ -596,7 +595,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<Ghostty.SurfaceView>.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
@@ -609,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 }
@@ -628,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
@@ -649,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 }
@@ -677,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<Ghostty.SurfaceView>.Spatial.Direction
switch direction {
@@ -698,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)
@@ -716,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)
@@ -729,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
@@ -746,14 +745,14 @@ class BaseTerminalController: NSWindowController,
// Create a new tree with the dragged surface and open a new window
let newTree = SplitTree<Ghostty.SurfaceView>(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,
@@ -783,7 +782,7 @@ class BaseTerminalController: NSWindowController,
if NSApp.mainWindow == window {
surfaces = surfaces.filter { $0 != focusedSurface }
}
for surface in surfaces {
surface.flagsChanged(with: event)
}
@@ -817,10 +816,10 @@ class BaseTerminalController: NSWindowController,
titleDidChange(to: "👻")
}
}
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)"
}
@@ -834,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 }
@@ -856,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
@@ -896,7 +894,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
@@ -908,7 +906,7 @@ class BaseTerminalController: NSWindowController,
Ghostty.logger.warning("failed to insert surface during drop: \(error)")
return
}
replaceSurfaceTree(
newTree,
moveFocusTo: source,
@@ -916,7 +914,7 @@ class BaseTerminalController: NSWindowController,
undoAction: "Move Split")
return
}
// Source is not in our tree - search other windows
var sourceController: BaseTerminalController?
var sourceNode: SplitTree<Ghostty.SurfaceView>.Node?
@@ -929,12 +927,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.
@@ -945,17 +943,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,
@@ -966,7 +964,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))
}
@@ -980,17 +978,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:
@@ -1052,7 +1050,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.
@@ -1061,7 +1059,7 @@ class BaseTerminalController: NSWindowController,
} else {
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
}
// Always resync our appearance
syncAppearance()
}
@@ -1109,7 +1107,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 +1115,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 = ""
@@ -1146,26 +1144,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
}
@@ -1295,7 +1293,6 @@ class BaseTerminalController: NSWindowController,
ghostty.splitToggleZoom(surface: surface)
}
@IBAction func splitMoveFocusPrevious(_ sender: Any) {
splitMoveFocus(direction: .previous)
}
@@ -1368,7 +1365,7 @@ class BaseTerminalController: NSWindowController,
@IBAction func toggleCommandPalette(_ sender: Any?) {
commandPaletteIsShowing.toggle()
}
@IBAction func find(_ sender: Any) {
focusedSurface?.find(sender)
}
@@ -1384,11 +1381,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)
}
@@ -1430,7 +1427,7 @@ extension BaseTerminalController: NSMenuItemValidation {
return true
}
}
// MARK: - Surface Color Scheme
/// Update the surface tree's color scheme only when it actually changes.

View File

@@ -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,32 @@ 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<AnyCancellable> = []
/// 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,
withSurfaceTree tree: SplitTree<Ghostty.SurfaceView>? = nil,
@@ -72,12 +71,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 +133,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<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
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) {
if to.isEmpty {
self.window?.close()
}
}
override func replaceSurfaceTree(
_ newTree: SplitTree<Ghostty.SurfaceView>,
moveFocusTo newView: Ghostty.SurfaceView? = nil,
@@ -177,7 +176,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeTabImmediately()
return
}
super.replaceSurfaceTree(
newTree,
moveFocusTo: newView,
@@ -210,7 +209,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(
@@ -224,27 +223,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)
}
}
}
@@ -255,7 +252,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)
}
}
@@ -392,7 +389,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.
@@ -407,7 +404,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":
@@ -483,8 +480,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
return controller
}
//MARK: - Methods
// MARK: - Methods
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
// Get our managed configuration object out
@@ -493,7 +490,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)
@@ -564,7 +561,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.
@@ -909,7 +906,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)
@@ -938,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
@@ -965,7 +960,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 }) {
@@ -996,7 +991,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.
@@ -1015,7 +1010,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))
}
@@ -1037,7 +1032,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)
@@ -1073,7 +1068,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).
@@ -1103,7 +1098,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 }
@@ -1133,7 +1128,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)
@@ -1317,7 +1312,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
ghostty.toggleTerminalInspector(surface: surface)
}
//MARK: - TerminalViewDelegate
// MARK: - TerminalViewDelegate
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
super.focusedSurfaceDidChange(to: to)
@@ -1349,7 +1344,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 }
@@ -1432,23 +1427,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
@@ -1549,25 +1544,25 @@ extension TerminalController {
case #selector(closeTabsOnTheRight):
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 }
return tabGroup.windows.indices.contains { $0 > 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)
}

View File

@@ -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
}
@@ -131,13 +131,11 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
// Find the focused surface in surfaceTree
if let focusedStr = state.focusedSurface {
var foundView: Ghostty.SurfaceView?
for view in c.surfaceTree {
if view.id.uuidString == focusedStr {
foundView = view
break
}
for view in c.surfaceTree where view.id.uuidString == focusedStr {
foundView = view
break
}
if let view = foundView {
c.focusedSurface = view
restoreFocus(to: view, inWindow: window)
@@ -161,9 +159,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,11 +183,10 @@ 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)
}
}
}
}

View File

@@ -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],

View File

@@ -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 }
}
@@ -45,8 +45,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: 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.
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init()
@@ -76,7 +76,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: 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()
}
@@ -116,7 +116,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
self.delegate?.performAction(action, on: surfaceView)
}
}
// Show update information above all else.
if viewModel.updateOverlayIsVisible {
UpdateOverlay()
@@ -127,12 +127,12 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
}
}
fileprivate struct UpdateOverlay: View {
private struct UpdateOverlay: View {
var body: some View {
if let appDelegate = NSApp.delegate as? AppDelegate {
VStack {
Spacer()
HStack {
Spacer()
UpdatePill(model: appDelegate.updateViewModel)

View File

@@ -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])

View File

@@ -33,9 +33,9 @@ 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? = 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.
@@ -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
@@ -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 {
@@ -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
@@ -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
@@ -459,7 +458,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())
@@ -510,7 +509,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()
}
@@ -544,7 +543,7 @@ class TerminalWindow: NSWindow {
NotificationCenter.default.removeObserver(observer)
}
}
// MARK: Config
struct DerivedConfig {

View File

@@ -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
@@ -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
}
@@ -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)

View File

@@ -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
@@ -159,7 +157,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()
@@ -172,7 +170,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
backgroundColor.luminance < 0.05
}
private var newTabButtonImageLayer: VibrantLayer? = nil
private var newTabButtonImageLayer: VibrantLayer?
func updateTabBar() {
newTabButtonImageLayer = nil
@@ -251,7 +249,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
@@ -286,9 +284,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 {
@@ -340,7 +338,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() {
@@ -359,7 +356,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 +371,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
super.addTitlebarAccessoryViewController(childViewController)
if (isTabBar) {
if isTabBar {
pushTabsToTitlebar(childViewController)
}
}
@@ -382,7 +379,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 +400,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()
}
@@ -506,10 +503,10 @@ 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) {
if event.type == .leftMouseDown && event.clickCount == 1 {
window?.performDrag(with: event)
NSCursor.closedHand.set()
} else {
@@ -535,7 +532,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 +585,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 +671,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

View File

@@ -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)

View File

@@ -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)
@@ -61,18 +61,18 @@ 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
/// 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))

View File

@@ -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

View File

@@ -6,11 +6,11 @@ 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.
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"
}

View File

@@ -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

View File

@@ -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<Void, Never>?
/// 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 {
@@ -47,11 +47,11 @@ struct UpdatePill: View {
} else {
showPopover.toggle()
}
}) {
}, label: {
HStack(spacing: 6) {
UpdateBadge(model: model)
.frame(width: 14, height: 14)
Text(model.text)
.font(Font(textFont))
.lineLimit(1)
@@ -66,12 +66,12 @@ struct UpdatePill: View {
)
.foregroundColor(model.foregroundColor)
.contentShape(Capsule())
}
})
.buttonStyle(.plain)
.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]

View File

@@ -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)
}
@@ -52,22 +52,22 @@ struct UpdatePopoverView: View {
}
}
fileprivate struct PermissionRequestView: 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 @@ fileprivate struct PermissionRequestView: View {
dismiss()
}
.keyboardShortcut(.cancelAction)
Spacer()
Button("Allow") {
request.reply(SUUpdatePermissionResponse(
automaticUpdateChecks: true,
@@ -93,10 +93,10 @@ fileprivate struct PermissionRequestView: View {
}
}
fileprivate struct CheckingView: 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 @@ fileprivate struct CheckingView: View {
Text("Checking for updates…")
.font(.system(size: 13))
}
HStack {
Spacer()
Button("Cancel") {
@@ -120,19 +120,19 @@ fileprivate struct CheckingView: View {
}
}
fileprivate struct UpdateAvailableView: 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 @@ fileprivate 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 @@ fileprivate struct UpdateAvailableView: View {
}
.font(.system(size: 11))
}
if let date = update.appcastItem.date {
HStack(spacing: 6) {
Text("Released:")
@@ -164,23 +164,23 @@ fileprivate 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 @@ fileprivate struct UpdateAvailableView: View {
}
}
.padding(16)
if let notes = update.releaseNotes {
Divider()
Link(destination: notes.url) {
HStack {
Image(systemName: "doc.text")
@@ -217,16 +217,16 @@ fileprivate struct UpdateAvailableView: View {
}
}
fileprivate struct DownloadingView: 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 @@ fileprivate struct DownloadingView: View {
.controlSize(.small)
}
}
HStack {
Spacer()
Button("Cancel") {
@@ -255,14 +255,14 @@ fileprivate struct DownloadingView: View {
}
}
fileprivate struct ExtractingView: 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))
@@ -274,22 +274,22 @@ fileprivate struct ExtractingView: View {
}
}
fileprivate struct InstallingView: 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 @@ fileprivate struct InstallingView: View {
}
.keyboardShortcut(.cancelAction)
.controlSize(.small)
Spacer()
Button("Restart Now") {
installing.retryTerminatingApplication()
dismiss()
@@ -313,22 +313,22 @@ fileprivate struct InstallingView: View {
}
}
fileprivate struct NotFoundView: 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") {
@@ -343,10 +343,10 @@ fileprivate struct NotFoundView: View {
}
}
fileprivate struct UpdateErrorView: 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 @@ fileprivate 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 @@ fileprivate struct UpdateErrorView: View {
}
.keyboardShortcut(.cancelAction)
.controlSize(.small)
Spacer()
Button("Retry") {
error.retry()
dismiss()

View File

@@ -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,

View File

@@ -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,27 +184,27 @@ 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) {
switch self {
case .checking,
.updateAvailable,
.downloading,
.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,32 +337,32 @@ enum UpdateState: Equatable {
case .tagged(let url): return url
}
}
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"
}
}
}
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

View File

@@ -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:
@@ -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)

View File

@@ -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)
@@ -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)")
}
}
@@ -312,7 +312,6 @@ extension Ghostty {
ghostty_app_set_focus(app, false)
}
// MARK: Ghostty Callbacks (macOS)
static func closeSurface(_ userdata: UnsafeMutableRawPointer?, processAlive: Bool) {
@@ -379,25 +378,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..<len).compactMap { i in
Ghostty.ClipboardContent.from(content: content[i])
}
guard !contentArray.isEmpty else { return }
// Assert there is only one text/plain entry. For security reasons we need
// to guarantee this for now since our confirmation dialog only shows one.
assert(contentArray.filter({ $0.mime == "text/plain" }).count <= 1,
"clipboard contents should have at most one text/plain entry")
if !confirm {
// Declare all types
let types = contentArray.compactMap { item in
NSPasteboard.PasteboardType(mimeType: item.mime)
}
pasteboard.declareTypes(types, owner: nil)
// Set data for each type
for item in contentArray {
guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
@@ -410,7 +409,7 @@ extension Ghostty {
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
return
}
NotificationCenter.default.post(
name: Notification.confirmClipboard,
object: surface,
@@ -463,7 +462,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 +472,7 @@ extension Ghostty {
}
// Action dispatch
switch (action.tag) {
switch action.tag {
case GHOSTTY_ACTION_QUIT:
quit(app)
@@ -605,7 +604,7 @@ extension Ghostty {
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app)
case GHOSTTY_ACTION_OPEN_URL:
return openURL(action.action.open_url)
@@ -681,12 +680,12 @@ extension Ghostty {
appDelegate.checkForUpdates(nil)
}
}
private static func openURL(
_ v: ghostty_action_open_url_s
) -> 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.
@@ -695,9 +694,12 @@ extension Ghostty {
if let candidate = URL(string: action.url), candidate.scheme != nil {
url = candidate
} else {
url = URL(filePath: action.url)
// Expand ~ to the user's home directory so that file paths
// like ~/Documents/file.txt resolve correctly.
let expandedPath = NSString(string: action.url).standardizingPath
url = URL(filePath: expandedPath)
}
switch action.kind {
case .text:
// Open with the default editor for `*.ghostty` file or just system text editor
@@ -706,15 +708,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
@@ -722,7 +724,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 +745,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 +765,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,
@@ -782,14 +784,13 @@ extension Ghostty {
]
)
default:
assertionFailure()
}
}
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,
@@ -819,7 +820,6 @@ extension Ghostty {
]
)
default:
assertionFailure()
}
@@ -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")
@@ -848,7 +848,6 @@ extension Ghostty {
]
)
default:
assertionFailure()
}
@@ -858,7 +857,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 +878,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 +887,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,
@@ -914,14 +913,13 @@ extension Ghostty {
assertionFailure()
}
default:
assertionFailure()
}
}
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 +947,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
@@ -969,7 +967,6 @@ extension Ghostty {
]
)
default:
assertionFailure()
}
@@ -978,7 +975,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
@@ -991,7 +988,6 @@ extension Ghostty {
object: surfaceView
)
default:
assertionFailure()
}
@@ -1001,7 +997,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
@@ -1014,7 +1010,6 @@ extension Ghostty {
object: surfaceView
)
default:
assertionFailure()
}
@@ -1031,7 +1026,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 +1051,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 +1076,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 +1107,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 +1139,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 +1245,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 +1278,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
@@ -1296,7 +1291,6 @@ extension Ghostty {
object: surfaceView
)
default:
assertionFailure()
}
@@ -1305,7 +1299,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
@@ -1324,7 +1318,6 @@ extension Ghostty {
)
return true
default:
assertionFailure()
return false
@@ -1335,7 +1328,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
@@ -1349,7 +1342,6 @@ extension Ghostty {
userInfo: ["mode": mode]
)
default:
assertionFailure()
}
@@ -1359,7 +1351,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
@@ -1377,12 +1369,11 @@ extension Ghostty {
}
}
center.getNotificationSettings() { settings in
center.getNotificationSettings { settings in
guard settings.authorizationStatus == .authorized else { return }
surfaceView.showUserNotification(title: title, body: body)
}
default:
assertionFailure()
}
@@ -1395,7 +1386,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 +1396,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 +1420,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 +1444,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 +1455,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 +1483,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 +1502,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 +1525,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 +1542,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 +1570,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 +1590,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
@@ -1609,7 +1600,6 @@ extension Ghostty {
guard let surfaceView = self.surfaceView(from: surface) else { return }
surfaceView.setCursorShape(shape)
default:
assertionFailure()
}
@@ -1619,7 +1609,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 +1617,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)
@@ -1638,7 +1628,6 @@ extension Ghostty {
return
}
default:
assertionFailure()
}
@@ -1648,7 +1637,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
@@ -1664,7 +1653,6 @@ extension Ghostty {
let buffer = Data(bytes: v.url!, count: v.len)
surfaceView.hoverUrl = String(data: buffer, encoding: .utf8)
default:
assertionFailure()
}
@@ -1674,7 +1662,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
@@ -1682,8 +1670,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:
assertionFailure()
@@ -1693,7 +1680,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
@@ -1706,7 +1693,6 @@ extension Ghostty {
object: surfaceView
)
default:
assertionFailure()
}
@@ -1716,7 +1702,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 +1724,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 +1746,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 +1771,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 +1803,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 +1828,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
@@ -1850,7 +1836,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 {
@@ -1869,7 +1855,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
@@ -1877,7 +1863,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,
@@ -1896,7 +1882,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
@@ -1914,7 +1900,7 @@ extension Ghostty {
} else {
surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch)
}
NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView)
}
@@ -1926,7 +1912,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 +1934,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 +1957,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
@@ -1993,14 +1979,13 @@ 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 }
let ghostty = Unmanaged<App>.fromOpaque(app_ud).takeUnretainedValue()
switch (target.tag) {
switch target.tag {
case GHOSTTY_TARGET_APP:
ghostty.reloadConfig(soft: v.soft)
return
@@ -2026,7 +2011,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 +2051,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
@@ -2087,7 +2072,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
@@ -2097,7 +2081,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)

View File

@@ -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 }
@@ -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..<diagsCount {
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
@@ -73,10 +73,10 @@ extension Ghostty {
// We only load CLI args when not running in Xcode because in Xcode we
// pass some special parameters to control the debugger.
if !isRunningInXcode() {
ghostty_config_load_cli_args(cfg);
ghostty_config_load_cli_args(cfg)
}
ghostty_config_load_recursive_files(cfg);
ghostty_config_load_recursive_files(cfg)
#endif
// TODO: we'd probably do some config loading here... for now we'd
@@ -92,7 +92,7 @@ extension Ghostty {
let diagsCount = ghostty_config_diagnostics_count(cfg)
if diagsCount > 0 {
logger.warning("config error: \(diagsCount) configuration errors on reload")
var diags: [String] = [];
var diags: [String] = []
for i in 0..<diagsCount {
let diag = ghostty_config_get_diagnostic(cfg, UInt32(i))
let message = String(cString: diag.message)
@@ -144,7 +144,7 @@ extension Ghostty {
var initialWindow: Bool {
guard let config = self.config else { return true }
var v = true;
var v = true
let key = "initial-window"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -152,7 +152,7 @@ extension Ghostty {
var shouldQuitAfterLastWindowClosed: Bool {
guard let config = self.config else { return true }
var v = false;
var v = false
let key = "quit-after-last-window-closed"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -160,7 +160,7 @@ extension Ghostty {
var title: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -227,19 +227,51 @@ 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<Int8>?
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<Int8>?
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
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -261,7 +293,7 @@ extension Ghostty {
var windowTitleFontFamily: String? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -271,7 +303,7 @@ extension Ghostty {
var macosWindowButtons: MacOSWindowButtons {
let defaultValue = MacOSWindowButtons.visible
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -282,7 +314,7 @@ extension Ghostty {
var macosTitlebarStyle: String {
let defaultValue = "transparent"
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -292,7 +324,7 @@ extension Ghostty {
var macosTitlebarProxyIcon: MacOSTitlebarProxyIcon {
let defaultValue = MacOSTitlebarProxyIcon.visible
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -303,7 +335,7 @@ extension Ghostty {
var macosDockDropBehavior: MacDockDropBehavior {
let defaultValue = MacDockDropBehavior.new_tab
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -313,7 +345,7 @@ extension Ghostty {
var macosWindowShadow: Bool {
guard let config = self.config else { return false }
var v = false;
var v = false
let key = "macos-window-shadow"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -322,7 +354,7 @@ extension Ghostty {
var macosIcon: MacOSIcon {
let defaultValue = MacOSIcon.official
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -334,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<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -348,7 +380,7 @@ extension Ghostty {
var macosIconFrame: MacOSIconFrame {
let defaultValue = MacOSIconFrame.aluminum
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -376,7 +408,7 @@ extension Ghostty {
var macosHidden: MacHidden {
guard let config = self.config else { return .never }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -384,18 +416,18 @@ extension Ghostty {
return MacHidden(rawValue: str) ?? .never
}
var focusFollowsMouse : Bool {
var focusFollowsMouse: Bool {
guard let config = self.config else { return false }
var v = false;
var v = false
let key = "focus-follows-mouse"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
var backgroundColor: Color {
var color: ghostty_config_color_s = .init();
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)
@@ -417,7 +449,7 @@ extension Ghostty {
var v: Double = 1
let key = "background-opacity"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v;
return v
}
var backgroundBlur: BackgroundBlur {
@@ -439,11 +471,11 @@ extension Ghostty {
var unfocusedSplitFill: Color {
guard let config = self.config else { return .white }
var color: ghostty_config_color_s = .init();
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)));
_ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8)))
}
return .init(
@@ -460,9 +492,9 @@ extension Ghostty {
guard let config = self.config else { return Color(newColor) }
var color: ghostty_config_color_s = .init();
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)
}
@@ -476,7 +508,7 @@ extension Ghostty {
#if canImport(AppKit)
var quickTerminalPosition: QuickTerminalPosition {
guard let config = self.config else { return .top }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -486,7 +518,7 @@ extension Ghostty {
var quickTerminalScreen: QuickTerminalScreen {
guard let config = self.config else { return .main }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -512,7 +544,7 @@ extension Ghostty {
var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior {
guard let config = self.config else { return .move }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -531,7 +563,7 @@ extension Ghostty {
var resizeOverlay: ResizeOverlay {
guard let config = self.config else { return .after_first }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -542,7 +574,7 @@ extension Ghostty {
var resizeOverlayPosition: ResizeOverlayPosition {
let defaultValue = ResizeOverlayPosition.center
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -555,7 +587,7 @@ extension Ghostty {
var v: UInt = 0
let key = "resize-overlay-duration"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v;
return v
}
var undoTimeout: Duration {
@@ -568,7 +600,7 @@ extension Ghostty {
var autoUpdate: AutoUpdate? {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -579,7 +611,7 @@ extension Ghostty {
var autoUpdateChannel: AutoUpdateChannel {
let defaultValue = AutoUpdateChannel.stable
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -589,7 +621,7 @@ extension Ghostty {
var autoSecureInput: Bool {
guard let config = self.config else { return true }
var v = false;
var v = false
let key = "macos-auto-secure-input"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -597,7 +629,7 @@ extension Ghostty {
var secureInputIndication: Bool {
guard let config = self.config else { return true }
var v = false;
var v = false
let key = "macos-secure-input-indication"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -605,7 +637,7 @@ extension Ghostty {
var maximize: Bool {
guard let config = self.config else { return true }
var v = false;
var v = false
let key = "maximize"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
@@ -614,7 +646,7 @@ extension Ghostty {
var macosShortcuts: MacShortcuts {
let defaultValue = MacShortcuts.ask
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -625,7 +657,7 @@ extension Ghostty {
var scrollbar: Scrollbar {
let defaultValue = Scrollbar.system
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
var v: UnsafePointer<Int8>?
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 }
@@ -648,7 +680,7 @@ extension Ghostty {
// MARK: Configuration Enums
extension Ghostty.Config {
enum AutoUpdate : String {
enum AutoUpdate: String {
case off
case check
case download
@@ -731,13 +763,13 @@ extension Ghostty.Config {
static let navigation = SplitPreserveZoom(rawValue: 1 << 0)
}
enum MacDockDropBehavior: String {
case new_tab = "new-tab"
case new_window = "new-window"
}
enum MacHidden : String {
enum MacHidden: String {
case never
case always
}
@@ -753,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"
@@ -769,30 +801,30 @@ extension Ghostty.Config {
case bottom_right = "bottom-right"
func top() -> Bool {
switch (self) {
case .top_left, .top_center, .top_right: return true;
default: return false;
switch self {
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;
switch self {
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;
switch self {
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;
switch self {
case .top_right, .bottom_right: return true
default: return false
}
}
}

View File

@@ -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.
@@ -49,11 +49,11 @@ 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) }
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) }
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)
}
@@ -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",

View File

@@ -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))
}

View File

@@ -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
}
}

View File

@@ -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,30 +196,30 @@ 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;
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
}
}
func toNative() -> ghostty_action_resize_split_direction_e {
switch (self) {
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
}
}
}
@@ -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:
@@ -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)
@@ -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 {}

View File

@@ -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
@@ -94,7 +94,7 @@ extension Ghostty {
class InspectorView: MTKView, NSTextInputClient {
let commandQueue: MTLCommandQueue
var surfaceView: SurfaceView? = nil {
var surfaceView: SurfaceView? {
didSet { surfaceViewDidChange() }
}
@@ -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:
@@ -309,8 +309,8 @@ extension Ghostty {
}
override func flagsChanged(with event: NSEvent) {
let mod: UInt32;
switch (event.keyCode) {
let mod: UInt32
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)
}
@@ -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) {
@@ -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)
}

View File

@@ -5,13 +5,13 @@ 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
}
}
/// 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"
}

View File

@@ -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)

View File

@@ -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(
@@ -110,4 +110,3 @@ private struct BouncingProgressBar: View {
}
}

View File

@@ -19,12 +19,12 @@ class SurfaceScrollView: NSView {
private var observers: [NSObjectProtocol] = []
private var cancellables: Set<AnyCancellable> = []
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

View File

@@ -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)

View File

@@ -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
@@ -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
}
}
@@ -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,
@@ -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) {
let overlayOpacity = ghostty.config.unfocusedSplitOpacity;
if (overlayOpacity > 0) {
if isSplit && !surfaceFocus {
let overlayOpacity = ghostty.config.unfocusedSplitOpacity
if overlayOpacity > 0 {
Rectangle()
.fill(ghostty.config.unfocusedSplitFill)
.allowsHitTesting(false)
@@ -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 {
@@ -300,7 +298,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.
@@ -312,42 +310,42 @@ 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 {
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;
switch overlay {
case .never: return true
case .always: return false
case .after_first: return lastSize == nil
}
}
var body: some View {
VStack {
if (!position.top()) {
if !position.top() {
Spacer()
}
HStack {
if (!position.left()) {
if !position.left() {
Spacer()
}
@@ -361,12 +359,12 @@ extension Ghostty {
.lineLimit(1)
.truncationMode(.tail)
if (!position.right()) {
if !position.right() {
Spacer()
}
}
if (!position.bottom()) {
if !position.bottom() {
Spacer()
}
}
@@ -386,7 +384,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)
}
@@ -404,9 +402,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) {
@@ -456,20 +454,20 @@ extension Ghostty {
guard let surface = surfaceView.surface else { return }
let action = "navigate_search:next"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}) {
}, label: {
Image(systemName: "chevron.up")
}
})
.buttonStyle(SearchButtonStyle())
Button(action: {
guard let surface = surfaceView.surface else { return }
let action = "navigate_search:previous"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}) {
}, label: {
Image(systemName: "chevron.down")
}
})
.buttonStyle(SearchButtonStyle())
Button(action: onClose) {
Image(systemName: "xmark")
}
@@ -529,7 +527,7 @@ extension Ghostty {
enum Corner {
case topLeft, topRight, bottomLeft, bottomRight
var alignment: Alignment {
switch self {
case .topLeft: return .topLeading
@@ -539,11 +537,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 +553,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 +582,7 @@ extension Ghostty {
}
.backport.pointerStyle(.link)
}
private func backgroundColor(isPressed: Bool) -> Color {
if isPressed {
return Color.primary.opacity(0.2)
@@ -640,20 +638,20 @@ 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
@@ -711,7 +709,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
@@ -736,7 +734,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<ghostty_env_var_s>()
var envVars = [ghostty_env_var_s]()
envVars.reserveCapacity(environmentVariables.count)
for i in 0..<environmentVariables.count {
envVars.append(ghostty_env_var_s(
@@ -764,24 +762,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 +859,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
ForEach(Array(keySequence.enumerated()), id: \.offset) { _, key in
KeyCap(key.description)
}
// Animated ellipsis to indicate waiting for next key
PendingIndicator(paused: isDragging)
}
@@ -898,11 +896,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 +919,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 +944,7 @@ extension Ghostty {
)
}
}
/// Animated dots to indicate waiting for the next key
struct PendingIndicator: View {
@State private var animationPhase: Double = 0
@@ -967,7 +965,7 @@ extension Ghostty {
}
}
}
private func dotOpacity(for index: Int) -> Double {
let phase = animationPhase
let offset = Double(index) / 3.0
@@ -981,7 +979,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 +996,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 +1049,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 +1083,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 +1099,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 +1114,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
@@ -1252,8 +1250,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 ?? ""

View File

@@ -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.
@@ -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)
}
@@ -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<String>
/// 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 }
@@ -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
@@ -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
}
@@ -873,17 +873,16 @@ 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) }
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 +895,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,10 +962,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)
}
}
@@ -992,8 +990,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?
}
@@ -1048,7 +1046,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 +1059,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 +1091,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 +1106,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
}
@@ -1185,17 +1183,17 @@ 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.
// 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
}
// Get information about if this is a binding.
let bindingFlags = surfaceModel.flatMap { surface in
var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)
@@ -1204,7 +1202,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:
@@ -1221,17 +1219,17 @@ extension Ghostty {
return true
}
}
self.keyDown(with: event)
return true
}
let equivalent: String
switch (event.charactersIgnoringModifiers) {
switch event.charactersIgnoringModifiers {
case "\r":
// Pass C-<return> through verbatim
// (prevent the default context menu equivalent)
if (!event.modifierFlags.contains(.control)) {
if !event.modifierFlags.contains(.control) {
return false
}
@@ -1240,8 +1238,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 +1263,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
@@ -1304,8 +1302,8 @@ extension Ghostty {
}
override func flagsChanged(with event: NSEvent) {
let mod: UInt32;
switch (event.keyCode) {
let mod: UInt32
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,26 +1321,26 @@ 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;
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
}
if (sidePressed) {
if sidePressed {
action = GHOSTTY_ACTION_PRESS
}
}
@@ -1389,7 +1387,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
@@ -1400,9 +1398,9 @@ 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);
self.showDefinition(for: str, at: pt)
}
override func menu(for event: NSEvent) -> NSMenu? {
@@ -1483,7 +1481,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,16 +1489,15 @@ 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)")
}
}
@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 +1505,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 +1513,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 +1521,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 +1529,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 +1537,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 +1545,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 +1553,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 +1561,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 +1569,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 +1605,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 +1613,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 +1654,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)
@@ -1831,7 +1828,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
@@ -1850,7 +1847,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.
@@ -1869,8 +1866,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)
@@ -1892,11 +1889,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)
@@ -1913,7 +1910,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:
@@ -1944,8 +1941,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
}
@@ -2052,7 +2048,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))
@@ -2134,7 +2130,7 @@ extension Ghostty.SurfaceView {
DispatchQueue.main.async {
self.insertText(
content,
replacementRange: NSMakeRange(0, 0)
replacementRange: NSRange(location: 0, length: 0)
)
}
return true

View File

@@ -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,30 +28,30 @@ 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] = []
/// 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
@@ -81,7 +81,7 @@ extension Ghostty {
// TODO
return
}
self.surface = surface;
self.surface = surface
}
required init?(coder: NSCoder) {
@@ -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
}
}
@@ -122,9 +122,7 @@ extension Ghostty {
// MARK: UIView
override class var layerClass: AnyClass {
get {
return CAMetalLayer.self
}
return CAMetalLayer.self
}
override func didMoveToWindow() {

View File

@@ -4,7 +4,7 @@ import Foundation
struct AnySortKey: Comparable {
private let value: Any
private let comparator: (Any, Any) -> ComparisonResult
init<T: Comparable>(_ 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
}

View File

@@ -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
}

View File

@@ -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)

Some files were not shown because too many files have changed in this diff Show More