mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-15 03:52:39 +00:00
Merge remote-tracking branch 'upstream/main' into grapheme-width-changes
This commit is contained in:
63
.github/VOUCHED.td
vendored
63
.github/VOUCHED.td
vendored
@@ -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
|
||||
|
||||
6
.github/workflows/milestone.yml
vendored
6
.github/workflows/milestone.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/nix.yml
vendored
2
.github/workflows/nix.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/release-tip.yml
vendored
2
.github/workflows/release-tip.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/snap.yml
vendored
2
.github/workflows/snap.yml
vendored
@@ -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
|
||||
|
||||
217
.github/workflows/test.yml
vendored
217
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/update-colorschemes.yml
vendored
2
.github/workflows/update-colorschemes.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/vouch-check-issue.yml
vendored
2
.github/workflows/vouch-check-issue.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/vouch-check-pr.yml
vendored
2
.github/workflows/vouch-check-pr.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
2
.github/workflows/vouch-manage-by-issue.yml
vendored
2
.github/workflows/vouch-manage-by-issue.yml
vendored
@@ -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 }}
|
||||
|
||||
32
.github/workflows/vouch-sync-codeowners.yml
vendored
Normal file
32
.github/workflows/vouch-sync-codeowners.yml
vendored
Normal 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
1
.gitignore
vendored
@@ -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
2
.swiftlint.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
included: macos
|
||||
child_config: macos/.swiftlint.yml
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
25
HACKING.md
25
HACKING.md
@@ -186,6 +186,31 @@ shellcheck \
|
||||
$(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort)
|
||||
```
|
||||
|
||||
### SwiftLint
|
||||
|
||||
Swift code is linted using [SwiftLint](https://github.com/realm/SwiftLint). A
|
||||
SwiftLint CI check will fail builds with improper formatting. Therefore, if you
|
||||
are modifying Swift code, you may want to install it locally and run this from
|
||||
the repo root before you commit:
|
||||
|
||||
```
|
||||
swiftlint lint --fix
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
14
build.zig.zon.json
generated
@@ -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
14
build.zig.zon.nix
generated
@@ -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
6
build.zig.zon.txt
generated
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
36
macos/.swiftlint.yml
Normal 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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,7 @@ struct CloseTerminalIntent: AppIntent {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
|
||||
guard let surfaceView = terminal.surfaceView else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct CommandPaletteIntent: AppIntent {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() ?? []
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -26,7 +26,7 @@ struct KeybindIntent: AppIntent {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 ?? ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user