mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-14 03:25:50 +00:00
Merge branch 'ghostty-org:main' into fix-tabbing-from-tab-overview
This commit is contained in:
12
.github/VOUCHED.td
vendored
12
.github/VOUCHED.td
vendored
@@ -19,11 +19,13 @@
|
||||
# discussion by the author. Maintainers can denounce users by commenting
|
||||
# "!denounce" or "!denounce [username]" on a discussion.
|
||||
00-kat
|
||||
aalhendi
|
||||
abudvytis
|
||||
aindriu80
|
||||
alanmoyano
|
||||
alexfeijoo44
|
||||
andrejdaskalov
|
||||
atomk
|
||||
balazs-szucs
|
||||
bennettp123
|
||||
benodiwal
|
||||
@@ -36,6 +38,7 @@ brentschroeter
|
||||
charliie-dev
|
||||
chernetskyi
|
||||
craziestowl
|
||||
curtismoncoq
|
||||
d-dudas
|
||||
daiimus
|
||||
damyanbogoev
|
||||
@@ -60,6 +63,7 @@ jacobsandlund
|
||||
jake-stewart
|
||||
jcollie
|
||||
johnslavik
|
||||
josephmart
|
||||
jparise
|
||||
juniqlim
|
||||
kawarimidoll
|
||||
@@ -68,8 +72,10 @@ khipp
|
||||
kirwiisp
|
||||
kjvdven
|
||||
kloneets
|
||||
koranir
|
||||
kristina8888
|
||||
kristofersoler
|
||||
laxystem
|
||||
liby
|
||||
lonsagisawa
|
||||
mahnokropotkinvich
|
||||
@@ -81,7 +87,10 @@ mikailmm
|
||||
misairuzame
|
||||
mitchellh
|
||||
miupa
|
||||
mrmage
|
||||
mtak
|
||||
natesmyth
|
||||
neo773
|
||||
nicosuave
|
||||
nwehg
|
||||
oshdubh
|
||||
@@ -94,9 +103,9 @@ piedrahitac
|
||||
pluiedev
|
||||
pouwerkerk
|
||||
priyans-hu
|
||||
prsweet
|
||||
qwerasd205
|
||||
reo101
|
||||
rgehan
|
||||
rmengelbrecht
|
||||
rmunn
|
||||
rockorager
|
||||
@@ -104,6 +113,7 @@ rpfaeffle
|
||||
secrus
|
||||
silveirapf
|
||||
slsrepo
|
||||
sunshine-syz
|
||||
tdslot
|
||||
ticclick
|
||||
tnagatomi
|
||||
|
||||
15
.github/workflows/release-tag.yml
vendored
15
.github/workflows/release-tag.yml
vendored
@@ -130,10 +130,20 @@ jobs:
|
||||
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
|
||||
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
|
||||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
@@ -174,7 +184,9 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
xcodebuild -target Ghostty -configuration Release \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
# Add all our metadata to Info.plist so we can reference it later.
|
||||
- name: Update Info.plist
|
||||
@@ -219,6 +231,7 @@ jobs:
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"
|
||||
|
||||
# Codesign the app bundle
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
|
||||
|
||||
50
.github/workflows/release-tip.yml
vendored
50
.github/workflows/release-tip.yml
vendored
@@ -37,6 +37,11 @@ jobs:
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
@@ -219,6 +224,8 @@ jobs:
|
||||
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
|
||||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -226,6 +233,14 @@ jobs:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -263,7 +278,9 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
xcodebuild -target Ghostty -configuration Release \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -309,6 +326,7 @@ jobs:
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"
|
||||
|
||||
# Codesign the app bundle
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
|
||||
@@ -462,6 +480,8 @@ jobs:
|
||||
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
|
||||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -469,6 +489,14 @@ jobs:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -506,7 +534,9 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
xcodebuild -target Ghostty -configuration Release \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -552,6 +582,7 @@ jobs:
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"
|
||||
|
||||
# Codesign the app bundle
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
|
||||
@@ -646,6 +677,8 @@ jobs:
|
||||
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
|
||||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -653,6 +686,14 @@ jobs:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -690,7 +731,9 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
xcodebuild -target Ghostty -configuration Release \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
# This will be a monotonically always increasing build number that we use.
|
||||
@@ -736,6 +779,7 @@ jobs:
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/PlugIns/DockTilePlugin.plugin"
|
||||
|
||||
# Codesign the app bundle
|
||||
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
|
||||
|
||||
136
.github/workflows/test.yml
vendored
136
.github/workflows/test.yml
vendored
@@ -22,22 +22,29 @@ jobs:
|
||||
# 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.outputs.actions_pins }}
|
||||
blueprints: ${{ steps.filter.outputs.blueprints }}
|
||||
macos: ${{ steps.filter.outputs.macos }}
|
||||
nix: ${{ steps.filter.outputs.nix }}
|
||||
shell: ${{ steps.filter.outputs.shell }}
|
||||
zig: ${{ steps.filter.outputs.zig }}
|
||||
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
|
||||
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/**'
|
||||
@@ -64,7 +71,7 @@ jobs:
|
||||
- id: determine
|
||||
name: Determine skip
|
||||
run: |
|
||||
if [ "${{ steps.filter.outputs.code }}" = "false" ]; then
|
||||
if [ "${{ steps.filter_every.outputs.code }}" = "false" ]; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -81,6 +88,7 @@ jobs:
|
||||
- build-examples
|
||||
- build-flatpak
|
||||
- build-libghostty-vt
|
||||
- build-libghostty-vt-android
|
||||
- build-libghostty-vt-macos
|
||||
- build-linux
|
||||
- build-linux-libghostty
|
||||
@@ -322,10 +330,21 @@ jobs:
|
||||
target: [aarch64-macos, x86_64-macos, aarch64-ios]
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -343,6 +362,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@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
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
|
||||
@@ -534,10 +601,21 @@ jobs:
|
||||
build-macos:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -566,21 +644,38 @@ jobs:
|
||||
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
|
||||
# Nix breaks xcodebuild so this has to be run outside.
|
||||
- name: Build Ghostty.app
|
||||
run: cd macos && xcodebuild -target Ghostty
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
# Build the iOS target without code signing just to verify it works.
|
||||
- name: Build Ghostty iOS
|
||||
run: |
|
||||
cd macos
|
||||
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
|
||||
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" \
|
||||
COMPILATION_CACHE_CAS_PATH=/Users/runner/Library/Developer/Xcode/DerivedData/CompilationCache.noindex \
|
||||
COMPILATION_CACHE_KEEP_CAS_DIRECTORY=YES
|
||||
|
||||
build-macos-freetype:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -843,10 +938,21 @@ jobs:
|
||||
test-macos:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /Users/runner/zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /Users/runner/zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -1001,6 +1107,14 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
cache: |
|
||||
xcode
|
||||
path: |
|
||||
/Users/runner/zig
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
@@ -1204,7 +1318,7 @@ jobs:
|
||||
needs: [test, build-dist]
|
||||
steps:
|
||||
- name: Install and configure Namespace CLI
|
||||
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
||||
uses: namespacelabs/nscloud-setup@f378676225212387f1283f4da878712af2c4cd60 # v0.0.11
|
||||
|
||||
- name: Configure Namespace powered Buildx
|
||||
uses: namespacelabs/nscloud-setup-buildx-action@f5814dcf37a16cce0624d5bec2ab879654294aa0 # v0.0.22
|
||||
|
||||
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@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0
|
||||
- uses: mitchellh/vouch/action/check-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
|
||||
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@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0
|
||||
- uses: mitchellh/vouch/action/check-pr@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
|
||||
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@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0
|
||||
- uses: mitchellh/vouch/action/manage-by-discussion@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
|
||||
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@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0
|
||||
- uses: mitchellh/vouch/action/manage-by-issue@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
issue-id: ${{ github.event.issue.number }}
|
||||
|
||||
2
.github/workflows/vouch-sync-codeowners.yml
vendored
2
.github/workflows/vouch-sync-codeowners.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- uses: mitchellh/vouch/action/sync-codeowners@daa39f90448eb4054ad334d7f53959a7efb5005b # v1.4.0
|
||||
- uses: mitchellh/vouch/action/sync-codeowners@c6d80ead49839655b61b422700b7a3bc9d0804a9 # v1.4.2
|
||||
with:
|
||||
repo: ${{ github.repository }}
|
||||
pull-request: "true"
|
||||
|
||||
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
|
||||
|
||||
@@ -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,7 +39,7 @@
|
||||
},
|
||||
.uucode = .{
|
||||
// jacobsandlund/uucode
|
||||
.url = "https://github.com/jacobsandlund/uucode/archive/refs/tags/v0.2.0.tar.gz",
|
||||
.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 = .{
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
10
build.zig.zon.json
generated
10
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",
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
"uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9": {
|
||||
"name": "uucode",
|
||||
"url": "https://github.com/jacobsandlund/uucode/archive/refs/tags/v0.2.0.tar.gz",
|
||||
"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": {
|
||||
@@ -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": {
|
||||
|
||||
10
build.zig.zon.nix
generated
10
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=";
|
||||
};
|
||||
}
|
||||
{
|
||||
@@ -278,7 +278,7 @@ in
|
||||
name = "uucode-0.2.0-ZZjBPqZVVABQepOqZHR7vV_NcaN-wats0IB6o-Exj6m9";
|
||||
path = fetchZigArtifact {
|
||||
name = "uucode";
|
||||
url = "https://github.com/jacobsandlund/uucode/archive/refs/tags/v0.2.0.tar.gz";
|
||||
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,16 +21,16 @@ 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-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
|
||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||
https://github.com/jacobsandlund/uucode/archive/refs/tags/v0.2.0.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
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
packageOverrides = pyfinal: pyprev: {
|
||||
blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {};
|
||||
ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {};
|
||||
wcwidth = pyfinal.callPackage ./nix/pkgs/wcwidth.nix {};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,7 +145,7 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/jacobsandlund/uucode/archive/refs/tags/v0.2.0.tar.gz",
|
||||
"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"
|
||||
},
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -11,18 +11,12 @@ disabled_rules:
|
||||
- function_body_length
|
||||
- line_length
|
||||
- nesting
|
||||
- no_fallthrough_only
|
||||
- todo
|
||||
- trailing_comma
|
||||
- trailing_newline
|
||||
- type_body_length
|
||||
|
||||
# TODO
|
||||
- for_where
|
||||
- force_cast
|
||||
- multiple_closures_with_trailing_closure
|
||||
- no_fallthrough_only
|
||||
- switch_case_alignment
|
||||
|
||||
identifier_name:
|
||||
min_length: 1
|
||||
allowed_symbols: ["_"]
|
||||
|
||||
3
macos/AGENTS.md
Normal file
3
macos/AGENTS.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# macOS Ghostty Application
|
||||
|
||||
- Use `swiftlint` for formatting and linting Swift code.
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSDockTilePlugIn</key>
|
||||
<string>DockTilePlugin.plugin</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */ = {isa = PBXBuildFile; fileRef = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
||||
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
@@ -35,6 +37,13 @@
|
||||
remoteGlobalIDString = A5B30530299BEAAA0047F10C;
|
||||
remoteInfo = Ghostty;
|
||||
};
|
||||
819324672F2502FB00A9ED8F /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 8193244C2F24E6C000A9ED8F;
|
||||
remoteInfo = DockTilePlugin;
|
||||
};
|
||||
A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
@@ -44,12 +53,27 @@
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
819324572F24E74E00A9ED8F /* Copy DockTilePlugin */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */,
|
||||
);
|
||||
name = "Copy DockTilePlugin";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = "<group>"; };
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||
A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = "<group>"; };
|
||||
@@ -70,6 +94,22 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
"Features/Custom App Icon/AppIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Features/Custom App Icon/Extensions/Notification+AppIcon.swift",
|
||||
"Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift",
|
||||
Ghostty/Ghostty.ConfigTypes.swift,
|
||||
Ghostty/GhosttyPackageMeta.swift,
|
||||
Helpers/CrossKit.swift,
|
||||
"Helpers/Extensions/NSImage+Extension.swift",
|
||||
"Helpers/Extensions/OSColor+Extension.swift",
|
||||
);
|
||||
target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */;
|
||||
};
|
||||
81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@@ -80,6 +120,7 @@
|
||||
Features/About/About.xib,
|
||||
Features/About/AboutController.swift,
|
||||
Features/About/AboutView.swift,
|
||||
Features/About/AboutViewModel.swift,
|
||||
Features/About/CyclingIconView.swift,
|
||||
"Features/App Intents/CloseTerminalIntent.swift",
|
||||
"Features/App Intents/CommandPaletteIntent.swift",
|
||||
@@ -96,11 +137,15 @@
|
||||
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIconImage.swift",
|
||||
"Features/Colorized Ghostty Icon/ColorizedGhosttyIconView.swift",
|
||||
"Features/Command Palette/CommandPalette.swift",
|
||||
"Features/Command Palette/TerminalCommandPalette.swift",
|
||||
"Features/Custom App Icon/AppIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIcon.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIconImage.swift",
|
||||
"Features/Custom App Icon/ColorizedGhosttyIconView.swift",
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Features/Custom App Icon/Extensions/Notification+AppIcon.swift",
|
||||
"Features/Custom App Icon/Extensions/UserDefaults+AppIcon.swift",
|
||||
"Features/Global Keybinds/GlobalEventTap.swift",
|
||||
Features/QuickTerminal/QuickTerminal.xib,
|
||||
Features/QuickTerminal/QuickTerminalController.swift,
|
||||
@@ -190,6 +235,7 @@
|
||||
Helpers/Private/CGS.swift,
|
||||
Helpers/Private/Dock.swift,
|
||||
Helpers/TabGroupCloseCoordinator.swift,
|
||||
Helpers/TabTitleEditor.swift,
|
||||
Helpers/VibrantLayer.m,
|
||||
Helpers/Weak.swift,
|
||||
);
|
||||
@@ -199,6 +245,7 @@
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
App/iOS/iOSApp.swift,
|
||||
"Features/Custom App Icon/DockTilePlugin.swift",
|
||||
"Ghostty/Surface View/SurfaceView_UIKit.swift",
|
||||
);
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
@@ -207,7 +254,7 @@
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
810ACCA02E9D3302004F8F92 /* GhosttyUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = GhosttyUITests; sourceTree = "<group>"; };
|
||||
81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
|
||||
81F82BC72E82815D001EDFA7 /* Sources */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (81F82CB12E8281F9001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 81F82CB02E8281F5001EDFA7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, 8193245D2F24E80800A9ED8F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Sources; sourceTree = "<group>"; };
|
||||
A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
@@ -219,6 +266,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
8193244A2F24E6C000A9ED8F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F02E1F047A0046BD5C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -289,6 +343,7 @@
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */,
|
||||
A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */,
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */,
|
||||
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -328,6 +383,25 @@
|
||||
productReference = 810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
8193244C2F24E6C000A9ED8F /* DockTilePlugin */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */;
|
||||
buildPhases = (
|
||||
819324492F24E6C000A9ED8F /* Sources */,
|
||||
8193244A2F24E6C000A9ED8F /* Frameworks */,
|
||||
8193244B2F24E6C000A9ED8F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = DockTilePlugin;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = DockTilePlugin;
|
||||
productReference = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */;
|
||||
productType = "com.apple.product-type.bundle";
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */;
|
||||
@@ -359,10 +433,12 @@
|
||||
A5B3052D299BEAAA0047F10C /* Sources */,
|
||||
A5B3052E299BEAAA0047F10C /* Frameworks */,
|
||||
A5B3052F299BEAAA0047F10C /* Resources */,
|
||||
819324572F24E74E00A9ED8F /* Copy DockTilePlugin */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
819324682F2502FB00A9ED8F /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
81F82BC72E82815D001EDFA7 /* Sources */,
|
||||
@@ -409,6 +485,9 @@
|
||||
CreatedOnToolsVersion = 26.1;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
};
|
||||
8193244C2F24E6C000A9ED8F = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
};
|
||||
A54F45F22E1F047A0046BD5C = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
@@ -440,6 +519,7 @@
|
||||
targets = (
|
||||
A5B30530299BEAAA0047F10C /* Ghostty */,
|
||||
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */,
|
||||
8193244C2F24E6C000A9ED8F /* DockTilePlugin */,
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */,
|
||||
810ACC9E2E9D3301004F8F92 /* GhosttyUITests */,
|
||||
);
|
||||
@@ -454,6 +534,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
8193244B2F24E6C000A9ED8F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45F12E1F047A0046BD5C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -522,6 +610,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
819324492F24E6C000A9ED8F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A54F45EF2E1F047A0046BD5C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -551,6 +646,11 @@
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
targetProxy = 810ACCA52E9D3302004F8F92 /* PBXContainerItemProxy */;
|
||||
};
|
||||
819324682F2502FB00A9ED8F /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 8193244C2F24E6C000A9ED8F /* DockTilePlugin */;
|
||||
targetProxy = 819324672F2502FB00A9ED8F /* PBXContainerItemProxy */;
|
||||
};
|
||||
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
@@ -683,7 +783,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -706,6 +806,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -728,6 +829,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
@@ -736,6 +838,93 @@
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
8193244E2F24E6C000A9ED8F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DOCK_TILE_PLUGIN DEBUG";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
8193244F2F24E6C000A9ED8F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
819324502F24E6C000A9ED8F /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty Dock Tile Plugin";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSPrincipalClass = "";
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles";
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||
MARKETING_VERSION = 0.1;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.mitchellh.ghostty-dock-tile";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DOCK_TILE_PLUGIN;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 6.0;
|
||||
WRAPPER_EXTENSION = plugin;
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
A54F45F92E1F047A0046BD5C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -1162,6 +1351,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
819324512F24E6C000A9ED8F /* Build configuration list for PBXNativeTarget "DockTilePlugin" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
8193244E2F24E6C000A9ED8F /* Debug */,
|
||||
8193244F2F24E6C000A9ED8F /* Release */,
|
||||
819324502F24E6C000A9ED8F /* ReleaseLocal */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -11,10 +11,8 @@ extension AppDelegate: Ghostty.Delegate {
|
||||
continue
|
||||
}
|
||||
|
||||
for surface in controller.surfaceTree {
|
||||
if surface.id == id {
|
||||
return surface
|
||||
}
|
||||
for surface in controller.surfaceTree where surface.id == id {
|
||||
return surface
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -380,13 +380,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
if let why = event.attributeDescriptor(forKeyword: keyword) {
|
||||
switch why.typeCodeValue {
|
||||
case kAEShutDown:
|
||||
fallthrough
|
||||
|
||||
case kAERestart:
|
||||
fallthrough
|
||||
|
||||
case kAEReallyLogOut:
|
||||
case kAEShutDown, kAERestart, kAEReallyLogOut:
|
||||
return .terminateNow
|
||||
|
||||
default:
|
||||
@@ -934,9 +928,8 @@ class AppDelegate: NSObject,
|
||||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
Task {
|
||||
await updateAppIcon(from: config)
|
||||
}
|
||||
|
||||
updateAppIcon(from: config)
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
@@ -944,81 +937,15 @@ class AppDelegate: NSObject,
|
||||
NSApplication.shared.appearance = .init(ghosttyConfig: config)
|
||||
}
|
||||
|
||||
// Using AppIconActor to ensure this work
|
||||
// happens synchronously in the background
|
||||
@AppIconActor
|
||||
private func updateAppIcon(from config: Ghostty.Config) async {
|
||||
var appIcon: NSImage?
|
||||
var appIconName: String? = config.macosIcon.rawValue
|
||||
|
||||
switch config.macosIcon {
|
||||
case let icon where icon.assetName != nil:
|
||||
appIcon = NSImage(named: icon.assetName!)!
|
||||
|
||||
case .custom:
|
||||
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
|
||||
appIcon = userIcon
|
||||
appIconName = config.macosCustomIcon
|
||||
} else {
|
||||
appIcon = nil // Revert back to official icon if invalid location
|
||||
appIconName = nil // Discard saved icon name
|
||||
}
|
||||
|
||||
case .customStyle:
|
||||
// Discard saved icon name
|
||||
// if no valid colours were found
|
||||
appIconName = nil
|
||||
guard let ghostColor = config.macosIconGhostColor else { break }
|
||||
guard let screenColors = config.macosIconScreenColor else { break }
|
||||
guard let icon = ColorizedGhosttyIcon(
|
||||
screenColors: screenColors,
|
||||
ghostColor: ghostColor,
|
||||
frame: config.macosIconFrame
|
||||
).makeImage() else { break }
|
||||
appIcon = icon
|
||||
let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString)
|
||||
appIconName = (colorStrings + [config.macosIconFrame.rawValue])
|
||||
.joined(separator: "_")
|
||||
|
||||
default:
|
||||
// Discard saved icon name
|
||||
appIconName = nil
|
||||
private func updateAppIcon(from config: Ghostty.Config) {
|
||||
// Since this is called after `DockTilePlugin` has been running,
|
||||
// clean it up here to trigger a correct update of the current config.
|
||||
UserDefaults.standard.removeObject(forKey: "CustomGhosttyIcon")
|
||||
DispatchQueue.global().async {
|
||||
UserDefaults.standard.appIcon = AppIcon(config: config)
|
||||
DistributedNotificationCenter.default()
|
||||
.postNotificationName(.ghosttyIconDidChange, object: nil, userInfo: nil, deliverImmediately: true)
|
||||
}
|
||||
|
||||
// Only change the icon if it has actually changed from the current one,
|
||||
// or if the app build has changed (e.g. after an update that reset the icon)
|
||||
let cachedIconName = UserDefaults.standard.string(forKey: "CustomGhosttyIcon")
|
||||
let cachedIconBuild = UserDefaults.standard.string(forKey: "CustomGhosttyIconBuild")
|
||||
let currentBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
let buildChanged = cachedIconBuild != currentBuild
|
||||
|
||||
guard cachedIconName != appIconName || buildChanged else {
|
||||
#if DEBUG
|
||||
if appIcon == nil {
|
||||
await MainActor.run {
|
||||
// Changing the app bundle's icon will corrupt code signing.
|
||||
// We only use the default blueprint icon for the dock,
|
||||
// so developers don't need to clean and re-build every time.
|
||||
NSApplication.shared.applicationIconImage = NSImage(named: "BlueprintImage")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return
|
||||
}
|
||||
// make it immutable, so Swift 6 won't complain
|
||||
let newIcon = appIcon
|
||||
|
||||
let appPath = Bundle.main.bundlePath
|
||||
guard NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: []) else { return }
|
||||
NSWorkspace.shared.noteFileSystemChanged(appPath)
|
||||
|
||||
await MainActor.run {
|
||||
self.appIcon = newIcon
|
||||
NSApplication.shared.applicationIconImage = newIcon
|
||||
}
|
||||
|
||||
UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon")
|
||||
UserDefaults.standard.set(currentBuild, forKey: "CustomGhosttyIconBuild")
|
||||
}
|
||||
|
||||
// MARK: - Restorable State
|
||||
@@ -1082,10 +1009,8 @@ class AppDelegate: NSObject,
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1357,8 +1282,3 @@ private enum QuickTerminalState {
|
||||
/// Controller has been initialized.
|
||||
case initialized(QuickTerminalController)
|
||||
}
|
||||
|
||||
@globalActor
|
||||
private actor AppIconActor: GlobalActor {
|
||||
static let shared = AppIconActor()
|
||||
}
|
||||
|
||||
@@ -5,19 +5,21 @@ import SwiftUI
|
||||
class AboutController: NSWindowController, NSWindowDelegate {
|
||||
static let shared: AboutController = AboutController()
|
||||
|
||||
private let viewModel = AboutViewModel()
|
||||
override var windowNibName: NSNib.Name? { "About" }
|
||||
|
||||
override func windowDidLoad() {
|
||||
guard let window = window else { return }
|
||||
window.center()
|
||||
window.isMovableByWindowBackground = true
|
||||
window.contentView = NSHostingView(rootView: AboutView())
|
||||
window.contentView = NSHostingView(rootView: AboutView().environmentObject(viewModel))
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
func show() {
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
viewModel.startCyclingIcons()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
@@ -38,4 +40,8 @@ class AboutController: NSWindowController, NSWindowDelegate {
|
||||
@objc func cancel(_ sender: Any?) {
|
||||
close()
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
viewModel.stopCyclingIcons()
|
||||
}
|
||||
}
|
||||
|
||||
40
macos/Sources/Features/About/AboutViewModel.swift
Normal file
40
macos/Sources/Features/About/AboutViewModel.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import Combine
|
||||
|
||||
class AboutViewModel: ObservableObject {
|
||||
@Published var currentIcon: Ghostty.MacOSIcon?
|
||||
@Published var isHovering: Bool = false
|
||||
|
||||
private var timerCancellable: AnyCancellable?
|
||||
|
||||
private let icons: [Ghostty.MacOSIcon] = [
|
||||
.official,
|
||||
.blueprint,
|
||||
.chalkboard,
|
||||
.microchip,
|
||||
.glass,
|
||||
.holographic,
|
||||
.paper,
|
||||
.retro,
|
||||
.xray,
|
||||
]
|
||||
|
||||
func startCyclingIcons() {
|
||||
timerCancellable = Timer.publish(every: 3, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
guard let self, !isHovering else { return }
|
||||
advanceToNextIcon()
|
||||
}
|
||||
}
|
||||
|
||||
func stopCyclingIcons() {
|
||||
timerCancellable = nil
|
||||
currentIcon = nil
|
||||
}
|
||||
|
||||
func advanceToNextIcon() {
|
||||
let currentIndex = currentIcon.flatMap(icons.firstIndex(of:)) ?? 0
|
||||
let nextIndex = icons.indexWrapping(after: currentIndex)
|
||||
currentIcon = icons[nextIndex]
|
||||
}
|
||||
}
|
||||
@@ -1,50 +1,38 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
import Combine
|
||||
|
||||
/// A view that cycles through Ghostty's official icon variants.
|
||||
struct CyclingIconView: View {
|
||||
@State private var currentIcon: Ghostty.MacOSIcon = .official
|
||||
@State private var isHovering: Bool = false
|
||||
|
||||
private let icons: [Ghostty.MacOSIcon] = [
|
||||
.official,
|
||||
.blueprint,
|
||||
.chalkboard,
|
||||
.microchip,
|
||||
.glass,
|
||||
.holographic,
|
||||
.paper,
|
||||
.retro,
|
||||
.xray,
|
||||
]
|
||||
private let timerPublisher = Timer.publish(every: 3, on: .main, in: .common)
|
||||
@EnvironmentObject var viewModel: AboutViewModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
iconView(for: currentIcon)
|
||||
.id(currentIcon)
|
||||
iconView(for: viewModel.currentIcon)
|
||||
.id(viewModel.currentIcon)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.5), value: currentIcon)
|
||||
.animation(.easeInOut(duration: 0.5), value: viewModel.currentIcon)
|
||||
.frame(height: 128)
|
||||
.onReceive(timerPublisher.autoconnect()) { _ in
|
||||
if !isHovering {
|
||||
advanceToNextIcon()
|
||||
}
|
||||
}
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
viewModel.isHovering = hovering
|
||||
}
|
||||
.onTapGesture {
|
||||
advanceToNextIcon()
|
||||
viewModel.advanceToNextIcon()
|
||||
}
|
||||
.contextMenu {
|
||||
if let currentIcon = viewModel.currentIcon {
|
||||
Button("Copy Icon Config") {
|
||||
NSPasteboard.general.setString("macos-icon = \(currentIcon.rawValue)", forType: .string)
|
||||
}
|
||||
}
|
||||
}
|
||||
.help("macos-icon = \(currentIcon.rawValue)")
|
||||
.accessibilityLabel("Ghostty Application Icon")
|
||||
.accessibilityHint("Click to cycle through icon variants")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func iconView(for icon: Ghostty.MacOSIcon) -> some View {
|
||||
let iconImage: Image = switch icon.assetName {
|
||||
private func iconView(for icon: Ghostty.MacOSIcon?) -> some View {
|
||||
let iconImage: Image = switch icon?.assetName {
|
||||
case let assetName?: Image(assetName)
|
||||
case nil: ghosttyIconImage()
|
||||
}
|
||||
@@ -53,10 +41,4 @@ struct CyclingIconView: View {
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
|
||||
private func advanceToNextIcon() {
|
||||
let currentIndex = icons.firstIndex(of: currentIcon) ?? 0
|
||||
let nextIndex = icons.indexWrapping(after: currentIndex)
|
||||
currentIcon = icons[nextIndex]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import Cocoa
|
||||
|
||||
struct ColorizedGhosttyIcon {
|
||||
/// The colors that make up the gradient of the screen.
|
||||
let screenColors: [NSColor]
|
||||
|
||||
/// The color of the ghost.
|
||||
let ghostColor: NSColor
|
||||
|
||||
/// The frame type to use
|
||||
let frame: Ghostty.MacOSIconFrame
|
||||
|
||||
/// Make a custom colorized ghostty icon.
|
||||
func makeImage() -> NSImage? {
|
||||
// All of our layers (not in order)
|
||||
guard let screen = NSImage(named: "CustomIconScreen") else { return nil }
|
||||
guard let screenMask = NSImage(named: "CustomIconScreenMask") else { return nil }
|
||||
guard let ghost = NSImage(named: "CustomIconGhost") else { return nil }
|
||||
guard let crt = NSImage(named: "CustomIconCRT") else { return nil }
|
||||
guard let gloss = NSImage(named: "CustomIconGloss") else { return nil }
|
||||
|
||||
let baseName = switch frame {
|
||||
case .aluminum: "CustomIconBaseAluminum"
|
||||
case .beige: "CustomIconBaseBeige"
|
||||
case .chrome: "CustomIconBaseChrome"
|
||||
case .plastic: "CustomIconBasePlastic"
|
||||
}
|
||||
guard let base = NSImage(named: baseName) else { return nil }
|
||||
|
||||
// Apply our color in various ways to our layers.
|
||||
// NOTE: These functions are not built-in, they're implemented as an extension
|
||||
// to NSImage in NSImage+Extension.swift.
|
||||
guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil }
|
||||
guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil }
|
||||
|
||||
// Combine our layers using the proper blending modes
|
||||
return.combine(images: [
|
||||
base,
|
||||
screen,
|
||||
screenGradient,
|
||||
ghost,
|
||||
tintedGhost,
|
||||
crt,
|
||||
gloss,
|
||||
], blendingModes: [
|
||||
.normal,
|
||||
.normal,
|
||||
.color,
|
||||
.normal,
|
||||
.color,
|
||||
.overlay,
|
||||
.normal,
|
||||
])
|
||||
}
|
||||
}
|
||||
86
macos/Sources/Features/Custom App Icon/AppIcon.swift
Normal file
86
macos/Sources/Features/Custom App Icon/AppIcon.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
import AppKit
|
||||
import System
|
||||
|
||||
/// The icon style for the Ghostty App.
|
||||
enum AppIcon: Equatable, Codable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
case xray
|
||||
/// Save full image data to avoid sandboxing issues
|
||||
case custom(_ iconFile: Data)
|
||||
case customStyle(_ icon: ColorizedGhosttyIcon)
|
||||
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
init?(config: Ghostty.Config) {
|
||||
switch config.macosIcon {
|
||||
case .official:
|
||||
return nil
|
||||
case .blueprint:
|
||||
self = .blueprint
|
||||
case .chalkboard:
|
||||
self = .chalkboard
|
||||
case .glass:
|
||||
self = .glass
|
||||
case .holographic:
|
||||
self = .holographic
|
||||
case .microchip:
|
||||
self = .microchip
|
||||
case .paper:
|
||||
self = .paper
|
||||
case .retro:
|
||||
self = .retro
|
||||
case .xray:
|
||||
self = .xray
|
||||
case .custom:
|
||||
if let data = try? Data(contentsOf: URL(filePath: config.macosCustomIcon, relativeTo: nil)) {
|
||||
self = .custom(data)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case .customStyle:
|
||||
// Discard saved icon name
|
||||
// if no valid colours were found
|
||||
guard
|
||||
let ghostColor = config.macosIconGhostColor,
|
||||
let screenColors = config.macosIconScreenColor
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
self = .customStyle(ColorizedGhosttyIcon(screenColors: screenColors, ghostColor: ghostColor, frame: config.macosIconFrame))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
func image(in bundle: Bundle) -> NSImage? {
|
||||
switch self {
|
||||
case .official:
|
||||
return nil
|
||||
case .blueprint:
|
||||
return bundle.image(forResource: "BlueprintImage")!
|
||||
case .chalkboard:
|
||||
return bundle.image(forResource: "ChalkboardImage")!
|
||||
case .glass:
|
||||
return bundle.image(forResource: "GlassImage")!
|
||||
case .holographic:
|
||||
return bundle.image(forResource: "HolographicImage")!
|
||||
case .microchip:
|
||||
return bundle.image(forResource: "MicrochipImage")!
|
||||
case .paper:
|
||||
return bundle.image(forResource: "PaperImage")!
|
||||
case .retro:
|
||||
return bundle.image(forResource: "RetroImage")!
|
||||
case .xray:
|
||||
return bundle.image(forResource: "XrayImage")!
|
||||
case let .custom(file):
|
||||
return NSImage(data: file)
|
||||
case let .customStyle(customIcon):
|
||||
return customIcon.makeImage(in: bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import Cocoa
|
||||
|
||||
struct ColorizedGhosttyIcon {
|
||||
/// The colors that make up the gradient of the screen.
|
||||
let screenColors: [NSColor]
|
||||
|
||||
/// The color of the ghost.
|
||||
let ghostColor: NSColor
|
||||
|
||||
/// The frame type to use
|
||||
let frame: Ghostty.MacOSIconFrame
|
||||
|
||||
/// Make a custom colorized ghostty icon.
|
||||
func makeImage(in bundle: Bundle) -> NSImage? {
|
||||
// All of our layers (not in order)
|
||||
guard let screen = bundle.image(forResource: "CustomIconScreen") else { return nil }
|
||||
guard let screenMask = bundle.image(forResource: "CustomIconScreenMask") else { return nil }
|
||||
guard let ghost = bundle.image(forResource: "CustomIconGhost") else { return nil }
|
||||
guard let crt = bundle.image(forResource: "CustomIconCRT") else { return nil }
|
||||
guard let gloss = bundle.image(forResource: "CustomIconGloss") else { return nil }
|
||||
|
||||
let baseName = switch frame {
|
||||
case .aluminum: "CustomIconBaseAluminum"
|
||||
case .beige: "CustomIconBaseBeige"
|
||||
case .chrome: "CustomIconBaseChrome"
|
||||
case .plastic: "CustomIconBasePlastic"
|
||||
}
|
||||
guard let base = bundle.image(forResource: baseName) else { return nil }
|
||||
|
||||
// Apply our color in various ways to our layers.
|
||||
// NOTE: These functions are not built-in, they're implemented as an extension
|
||||
// to NSImage in NSImage+Extension.swift.
|
||||
guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil }
|
||||
guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil }
|
||||
|
||||
// Combine our layers using the proper blending modes
|
||||
return.combine(images: [
|
||||
base,
|
||||
screen,
|
||||
screenGradient,
|
||||
ghost,
|
||||
tintedGhost,
|
||||
crt,
|
||||
gloss,
|
||||
], blendingModes: [
|
||||
.normal,
|
||||
.normal,
|
||||
.color,
|
||||
.normal,
|
||||
.color,
|
||||
.overlay,
|
||||
.normal,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
|
||||
extension ColorizedGhosttyIcon: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case screenColors
|
||||
case ghostColor
|
||||
case frame
|
||||
|
||||
static let currentVersion: Int = 1
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// If no version exists then this is the legacy v0 format.
|
||||
let version = try container.decodeIfPresent(Int.self, forKey: .version) ?? 0
|
||||
guard version == 0 || version == CodingKeys.currentVersion else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: "Unsupported ColorizedGhosttyIcon version: \(version)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let screenColorHexes = try container.decode([String].self, forKey: .screenColors)
|
||||
let screenColors = screenColorHexes.compactMap(NSColor.init(hex:))
|
||||
let ghostColorHex = try container.decode(String.self, forKey: .ghostColor)
|
||||
guard let ghostColor = NSColor(hex: ghostColorHex) else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .ghostColor,
|
||||
in: container,
|
||||
debugDescription: "Failed to decode ghost color from \(ghostColorHex)"
|
||||
)
|
||||
}
|
||||
let frame = try container.decode(Ghostty.MacOSIconFrame.self, forKey: .frame)
|
||||
self.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame)
|
||||
}
|
||||
|
||||
func encode(to encoder: any Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(CodingKeys.currentVersion, forKey: .version)
|
||||
try container.encode(screenColors.compactMap(\.hexString), forKey: .screenColors)
|
||||
try container.encode(ghostColor.hexString, forKey: .ghostColor)
|
||||
try container.encode(frame, forKey: .frame)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension ColorizedGhosttyIcon: Equatable {
|
||||
static func == (lhs: Self, rhs: Self) -> Bool {
|
||||
lhs.frame == rhs.frame &&
|
||||
lhs.screenColors.compactMap(\.hexString) == rhs.screenColors.compactMap(\.hexString) &&
|
||||
lhs.ghostColor.hexString == rhs.ghostColor.hexString
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,6 @@ struct ColorizedGhosttyIconView: View {
|
||||
screenColors: [.purple, .blue],
|
||||
ghostColor: .yellow,
|
||||
frame: .aluminum
|
||||
).makeImage()!)
|
||||
).makeImage(in: .main)!)
|
||||
}
|
||||
}
|
||||
118
macos/Sources/Features/Custom App Icon/DockTilePlugin.swift
Normal file
118
macos/Sources/Features/Custom App Icon/DockTilePlugin.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
import AppKit
|
||||
|
||||
class DockTilePlugin: NSObject, NSDockTilePlugIn {
|
||||
// WARNING: An instance of this class is alive as long as Ghostty's icon is
|
||||
// in the doc (running or not!), so keep any state and processing to a
|
||||
// minimum to respect resource usage.
|
||||
|
||||
private let pluginBundle = Bundle(for: DockTilePlugin.self)
|
||||
|
||||
// Separate defaults based on debug vs release builds so we can test icons
|
||||
// without messing up releases.
|
||||
#if DEBUG
|
||||
private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty.debug")
|
||||
#else
|
||||
private let ghosttyUserDefaults = UserDefaults(suiteName: "com.mitchellh.ghostty")
|
||||
#endif
|
||||
|
||||
private var iconChangeObserver: Any?
|
||||
|
||||
/// The path to the Ghostty.app, determined based on the bundle path of this plugin.
|
||||
var ghosttyAppPath: String {
|
||||
var url = pluginBundle.bundleURL
|
||||
// Remove "/Contents/PlugIns/DockTilePlugIn.bundle" from the bundle URL to reach Ghostty.app.
|
||||
while url.lastPathComponent != "Ghostty.app", !url.lastPathComponent.isEmpty {
|
||||
url.deleteLastPathComponent()
|
||||
}
|
||||
return url.path
|
||||
}
|
||||
|
||||
/// The primary NSDockTilePlugin function.
|
||||
func setDockTile(_ dockTile: NSDockTile?) {
|
||||
// If no dock tile or no access to Ghostty defaults, we can't do anything.
|
||||
guard let dockTile, let ghosttyUserDefaults else {
|
||||
iconChangeObserver = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Try to restore the previous icon on launch.
|
||||
iconDidChange(ghosttyUserDefaults.appIcon, dockTile: dockTile)
|
||||
|
||||
// Setup a new observer for when the icon changes so we can update. This message
|
||||
// is sent by the primary Ghostty app.
|
||||
iconChangeObserver = DistributedNotificationCenter
|
||||
.default()
|
||||
.publisher(for: .ghosttyIconDidChange)
|
||||
.map { [weak self] _ in self?.ghosttyUserDefaults?.appIcon }
|
||||
.receive(on: DispatchQueue.global())
|
||||
.sink { [weak self] newIcon in self?.iconDidChange(newIcon, dockTile: dockTile) }
|
||||
}
|
||||
|
||||
private func iconDidChange(_ newIcon: AppIcon?, dockTile: NSDockTile) {
|
||||
guard let appIcon = newIcon?.image(in: pluginBundle) else {
|
||||
resetIcon(dockTile: dockTile)
|
||||
return
|
||||
}
|
||||
|
||||
let appBundlePath = self.ghosttyAppPath
|
||||
NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath)
|
||||
NSWorkspace.shared.noteFileSystemChanged(appBundlePath)
|
||||
|
||||
dockTile.setIcon(appIcon)
|
||||
}
|
||||
|
||||
/// Reset the application icon and dock tile icon to the default.
|
||||
private func resetIcon(dockTile: NSDockTile) {
|
||||
let appBundlePath = self.ghosttyAppPath
|
||||
let appIcon: NSImage
|
||||
if #available(macOS 26.0, *) {
|
||||
// Reset to the default (glassy) icon.
|
||||
NSWorkspace.shared.setIcon(nil, forFile: appBundlePath)
|
||||
|
||||
#if DEBUG
|
||||
// Use the `Blueprint` icon to distinguish Debug from Release builds.
|
||||
appIcon = pluginBundle.image(forResource: "BlueprintImage")!
|
||||
#else
|
||||
// Get the composed icon from the app bundle.
|
||||
if let iconRep = NSWorkspace.shared.icon(forFile: appBundlePath)
|
||||
.bestRepresentation(
|
||||
for: CGRect(origin: .zero, size: dockTile.size),
|
||||
context: nil,
|
||||
hints: nil
|
||||
) {
|
||||
appIcon = NSImage(size: dockTile.size)
|
||||
appIcon.addRepresentation(iconRep)
|
||||
} else {
|
||||
// If something unexpected happens on macOS 26,
|
||||
// fall back to a bundled icon.
|
||||
appIcon = pluginBundle.image(forResource: "AppIconImage")!
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// Use the bundled icon to keep the corner radius consistent with pre-Tahoe apps.
|
||||
appIcon = pluginBundle.image(forResource: "AppIconImage")!
|
||||
NSWorkspace.shared.setIcon(appIcon, forFile: appBundlePath)
|
||||
}
|
||||
|
||||
// Notify Finder/Dock so icon caches refresh immediately.
|
||||
NSWorkspace.shared.noteFileSystemChanged(appBundlePath)
|
||||
dockTile.setIcon(appIcon)
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSDockTile {
|
||||
func setIcon(_ newIcon: NSImage) {
|
||||
// Update the Dock tile on the main thread.
|
||||
DispatchQueue.main.async {
|
||||
let iconView = NSImageView(frame: CGRect(origin: .zero, size: self.size))
|
||||
iconView.wantsLayer = true
|
||||
iconView.image = newIcon
|
||||
self.contentView = iconView
|
||||
self.display()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is required because of the DispatchQueue call above. This doesn't
|
||||
// feel right but I don't know a better way to solve this.
|
||||
extension NSDockTile: @unchecked @retroactive Sendable {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import AppKit
|
||||
|
||||
extension Notification.Name {
|
||||
static let ghosttyIconDidChange = Notification.Name("com.mitchellh.ghostty.iconDidChange")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import AppKit
|
||||
|
||||
extension UserDefaults {
|
||||
private static let customIconKeyOld = "CustomGhosttyIcon"
|
||||
private static let customIconKeyNew = "CustomGhosttyIcon2"
|
||||
|
||||
var appIcon: AppIcon? {
|
||||
get {
|
||||
// Always remove our old pre-docktileplugin values.
|
||||
defer {
|
||||
removeObject(forKey: Self.customIconKeyOld)
|
||||
}
|
||||
|
||||
// Check if we have the new key for our dock tile plugin format.
|
||||
guard let data = data(forKey: Self.customIconKeyNew) else {
|
||||
return nil
|
||||
}
|
||||
return try? JSONDecoder().decode(AppIcon.self, from: data)
|
||||
}
|
||||
|
||||
set {
|
||||
guard let newData = try? JSONEncoder().encode(newValue) else {
|
||||
return
|
||||
}
|
||||
|
||||
set(newData, forKey: Self.customIconKeyNew)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,14 @@ enum QuickTerminalSpaceBehavior {
|
||||
|
||||
init?(fromGhosttyConfig string: String) {
|
||||
switch string {
|
||||
case "move":
|
||||
self = .move
|
||||
case "move":
|
||||
self = .move
|
||||
|
||||
case "remain":
|
||||
self = .remain
|
||||
case "remain":
|
||||
self = .remain
|
||||
|
||||
default:
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ enum QuickTerminalSpaceBehavior {
|
||||
]
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1265,6 +1265,17 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
@IBAction func changeTabTitle(_ sender: Any) {
|
||||
if let targetWindow = window {
|
||||
let inlineHostWindow =
|
||||
targetWindow.tabbedWindows?
|
||||
.first(where: { $0.tabBarView != nil }) as? TerminalWindow
|
||||
?? (targetWindow as? TerminalWindow)
|
||||
|
||||
if let inlineHostWindow, inlineHostWindow.beginInlineTabTitleEdit(for: targetWindow) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
promptTabTitle()
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +131,9 @@ 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 {
|
||||
|
||||
@@ -6,9 +6,8 @@ import SwiftUI
|
||||
class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
|
||||
private let terminalView: NSView
|
||||
|
||||
/// Glass effect view for liquid glass background when transparency is enabled
|
||||
/// Combined glass effect and inactive tint overlay view
|
||||
private var glassEffectView: NSView?
|
||||
private var glassTopConstraint: NSLayoutConstraint?
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) {
|
||||
@@ -27,6 +26,10 @@ class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
/// To make ``TerminalController/DefaultSize/contentIntrinsicSize``
|
||||
/// work in ``TerminalController/windowDidLoad()``,
|
||||
/// we override this to provide the correct size.
|
||||
@@ -50,6 +53,20 @@ class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidBecomeKey(_:)),
|
||||
name: NSWindow.didBecomeKeyNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidResignKey(_:)),
|
||||
name: NSWindow.didResignKeyNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
@@ -72,36 +89,139 @@ class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
|
||||
derivedConfig = newValue
|
||||
DispatchQueue.main.async(execute: updateGlassEffectIfNeeded)
|
||||
}
|
||||
|
||||
@objc private func windowDidBecomeKey(_ notification: Notification) {
|
||||
guard let window = notification.object as? NSWindow,
|
||||
window == self.window else { return }
|
||||
updateGlassTintOverlay(isKeyWindow: true)
|
||||
}
|
||||
|
||||
@objc private func windowDidResignKey(_ notification: Notification) {
|
||||
guard let window = notification.object as? NSWindow,
|
||||
window == self.window else { return }
|
||||
updateGlassTintOverlay(isKeyWindow: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Glass
|
||||
|
||||
/// An `NSView` that contains a liquid glass background effect and
|
||||
/// an inactive-window tint overlay.
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
private class TerminalGlassView: NSView {
|
||||
private let glassEffectView: NSGlassEffectView
|
||||
private var glassTopConstraint: NSLayoutConstraint?
|
||||
private let tintOverlay: NSView
|
||||
private var tintTopConstraint: NSLayoutConstraint?
|
||||
|
||||
init(topOffset: CGFloat) {
|
||||
self.glassEffectView = NSGlassEffectView()
|
||||
self.tintOverlay = NSView()
|
||||
super.init(frame: .zero)
|
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Glass effect view fills this view.
|
||||
glassEffectView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(glassEffectView)
|
||||
glassTopConstraint = glassEffectView.topAnchor.constraint(
|
||||
equalTo: topAnchor,
|
||||
constant: topOffset
|
||||
)
|
||||
if let glassTopConstraint {
|
||||
NSLayoutConstraint.activate([
|
||||
glassTopConstraint,
|
||||
glassEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
glassEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
glassEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
// Tint overlay sits above the glass effect.
|
||||
tintOverlay.translatesAutoresizingMaskIntoConstraints = false
|
||||
tintOverlay.wantsLayer = true
|
||||
tintOverlay.alphaValue = 0
|
||||
addSubview(tintOverlay, positioned: .above, relativeTo: glassEffectView)
|
||||
tintTopConstraint = tintOverlay.topAnchor.constraint(
|
||||
equalTo: topAnchor,
|
||||
constant: topOffset
|
||||
)
|
||||
if let tintTopConstraint {
|
||||
NSLayoutConstraint.activate([
|
||||
tintTopConstraint,
|
||||
tintOverlay.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
tintOverlay.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
tintOverlay.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// Configures the glass effect style, tint color, corner radius, and
|
||||
/// updates the inactive tint overlay based on window key status.
|
||||
func configure(
|
||||
style: NSGlassEffectView.Style,
|
||||
backgroundColor: NSColor,
|
||||
backgroundOpacity: Double,
|
||||
cornerRadius: CGFloat?,
|
||||
isKeyWindow: Bool
|
||||
) {
|
||||
glassEffectView.style = style
|
||||
glassEffectView.tintColor = backgroundColor.withAlphaComponent(backgroundOpacity)
|
||||
if let cornerRadius {
|
||||
glassEffectView.cornerRadius = cornerRadius
|
||||
}
|
||||
updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor)
|
||||
}
|
||||
|
||||
/// Updates the top inset offset for both the glass effect and tint overlay.
|
||||
/// Call this when the safe area insets change (e.g., during layout).
|
||||
func updateTopInset(_ offset: CGFloat) {
|
||||
glassTopConstraint?.constant = offset
|
||||
tintTopConstraint?.constant = offset
|
||||
}
|
||||
|
||||
/// Updates the tint overlay visibility based on window key status.
|
||||
func updateKeyStatus(_ isKeyWindow: Bool, backgroundColor: NSColor) {
|
||||
let tint = tintProperties(for: backgroundColor)
|
||||
tintOverlay.layer?.backgroundColor = tint.color.cgColor
|
||||
tintOverlay.alphaValue = isKeyWindow ? 0 : tint.opacity
|
||||
}
|
||||
|
||||
/// Computes a saturation-boosted tint color and opacity for the inactive overlay.
|
||||
private func tintProperties(for color: NSColor) -> (color: NSColor, opacity: CGFloat) {
|
||||
let isLight = color.isLightColor
|
||||
let vibrant = color.adjustingSaturation(by: 1.2)
|
||||
let overlayOpacity: CGFloat = isLight ? 0.35 : 0.85
|
||||
return (vibrant, overlayOpacity)
|
||||
}
|
||||
}
|
||||
#endif // compiler(>=6.2)
|
||||
|
||||
private extension TerminalViewContainer {
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
func addGlassEffectViewIfNeeded() -> NSGlassEffectView? {
|
||||
if let existed = glassEffectView as? NSGlassEffectView {
|
||||
func addGlassEffectViewIfNeeded() -> TerminalGlassView? {
|
||||
if let existed = glassEffectView as? TerminalGlassView {
|
||||
updateGlassEffectTopInsetIfNeeded()
|
||||
return existed
|
||||
}
|
||||
guard let themeFrameView = window?.contentView?.superview else {
|
||||
return nil
|
||||
}
|
||||
let effectView = NSGlassEffectView()
|
||||
let effectView = TerminalGlassView(topOffset: -themeFrameView.safeAreaInsets.top)
|
||||
addSubview(effectView, positioned: .below, relativeTo: terminalView)
|
||||
effectView.translatesAutoresizingMaskIntoConstraints = false
|
||||
glassTopConstraint = effectView.topAnchor.constraint(
|
||||
equalTo: topAnchor,
|
||||
constant: -themeFrameView.safeAreaInsets.top
|
||||
)
|
||||
if let glassTopConstraint {
|
||||
NSLayoutConstraint.activate([
|
||||
glassTopConstraint,
|
||||
effectView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
effectView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
effectView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
effectView.topAnchor.constraint(equalTo: topAnchor),
|
||||
effectView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
effectView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
effectView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
])
|
||||
glassEffectView = effectView
|
||||
return effectView
|
||||
}
|
||||
@@ -112,26 +232,35 @@ private extension TerminalViewContainer {
|
||||
guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else {
|
||||
glassEffectView?.removeFromSuperview()
|
||||
glassEffectView = nil
|
||||
glassTopConstraint = nil
|
||||
return
|
||||
}
|
||||
guard let effectView = addGlassEffectViewIfNeeded() else {
|
||||
return
|
||||
}
|
||||
|
||||
let style: NSGlassEffectView.Style
|
||||
switch derivedConfig.backgroundBlur {
|
||||
case .macosGlassRegular:
|
||||
effectView.style = NSGlassEffectView.Style.regular
|
||||
style = NSGlassEffectView.Style.regular
|
||||
case .macosGlassClear:
|
||||
effectView.style = NSGlassEffectView.Style.clear
|
||||
style = NSGlassEffectView.Style.clear
|
||||
default:
|
||||
break
|
||||
style = NSGlassEffectView.Style.regular
|
||||
}
|
||||
let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor)
|
||||
effectView.tintColor = backgroundColor
|
||||
.withAlphaComponent(derivedConfig.backgroundOpacity)
|
||||
if let window, window.responds(to: Selector(("_cornerRadius"))), let cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat {
|
||||
effectView.cornerRadius = cornerRadius
|
||||
|
||||
var cornerRadius: CGFloat?
|
||||
if let window, window.responds(to: Selector(("_cornerRadius"))) {
|
||||
cornerRadius = window.value(forKey: "_cornerRadius") as? CGFloat
|
||||
}
|
||||
|
||||
effectView.configure(
|
||||
style: style,
|
||||
backgroundColor: backgroundColor,
|
||||
backgroundOpacity: derivedConfig.backgroundOpacity,
|
||||
cornerRadius: cornerRadius,
|
||||
isKeyWindow: window?.isKeyWindow ?? true
|
||||
)
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
@@ -142,7 +271,16 @@ private extension TerminalViewContainer {
|
||||
}
|
||||
guard glassEffectView != nil else { return }
|
||||
guard let themeFrameView = window?.contentView?.superview else { return }
|
||||
glassTopConstraint?.constant = -themeFrameView.safeAreaInsets.top
|
||||
(glassEffectView as? TerminalGlassView)?.updateTopInset(-themeFrameView.safeAreaInsets.top)
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
func updateGlassTintOverlay(isKeyWindow: Bool) {
|
||||
#if compiler(>=6.2)
|
||||
guard #available(macOS 26.0, *) else { return }
|
||||
guard glassEffectView != nil else { return }
|
||||
let backgroundColor = (window as? TerminalWindow)?.preferredBackgroundColor ?? NSColor(derivedConfig.backgroundColor)
|
||||
(glassEffectView as? TerminalGlassView)?.updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor)
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ class TerminalWindow: NSWindow {
|
||||
/// Sets up our tab context menu
|
||||
private var tabMenuObserver: NSObjectProtocol?
|
||||
|
||||
/// Handles inline tab title editing for this host window.
|
||||
private lazy var tabTitleEditor = TabTitleEditor(
|
||||
hostWindow: self,
|
||||
delegate: self
|
||||
)
|
||||
|
||||
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||
/// window should determine how to show update notifications.
|
||||
var supportsUpdateAccessory: Bool {
|
||||
@@ -174,7 +180,16 @@ class TerminalWindow: NSWindow {
|
||||
override var canBecomeKey: Bool { return true }
|
||||
override var canBecomeMain: Bool { return true }
|
||||
|
||||
override func sendEvent(_ event: NSEvent) {
|
||||
if tabTitleEditor.handleDoubleClick(event) {
|
||||
return
|
||||
}
|
||||
|
||||
super.sendEvent(event)
|
||||
}
|
||||
|
||||
override func close() {
|
||||
tabTitleEditor.finishEditing(commit: true)
|
||||
NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
|
||||
super.close()
|
||||
}
|
||||
@@ -207,6 +222,21 @@ class TerminalWindow: NSWindow {
|
||||
viewModel.isMainWindow = false
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func beginInlineTabTitleEdit(for targetWindow: NSWindow) -> Bool {
|
||||
tabTitleEditor.beginEditing(for: targetWindow)
|
||||
}
|
||||
|
||||
@objc private func renameTabFromContextMenu(_ sender: NSMenuItem) {
|
||||
let targetWindow = sender.representedObject as? NSWindow ?? self
|
||||
if beginInlineTabTitleEdit(for: targetWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
guard let targetController = targetWindow.windowController as? BaseTerminalController else { return }
|
||||
targetController.promptTabTitle()
|
||||
}
|
||||
|
||||
override func mergeAllWindows(_ sender: Any?) {
|
||||
super.mergeAllWindows(sender)
|
||||
|
||||
@@ -731,10 +761,11 @@ extension TerminalWindow {
|
||||
separator.identifier = Self.tabColorSeparatorIdentifier
|
||||
menu.addItem(separator)
|
||||
|
||||
// Change Title...
|
||||
let changeTitleItem = NSMenuItem(title: "Change Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "")
|
||||
// Rename Tab...
|
||||
let changeTitleItem = NSMenuItem(title: "Rename Tab...", action: #selector(TerminalWindow.renameTabFromContextMenu(_:)), keyEquivalent: "")
|
||||
changeTitleItem.identifier = Self.changeTitleMenuItemIdentifier
|
||||
changeTitleItem.target = target
|
||||
changeTitleItem.target = self
|
||||
changeTitleItem.representedObject = target?.window
|
||||
changeTitleItem.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
menu.addItem(changeTitleItem)
|
||||
|
||||
@@ -760,3 +791,42 @@ private func makeTabColorPaletteView(
|
||||
hostingView.frame.size = hostingView.intrinsicContentSize
|
||||
return hostingView
|
||||
}
|
||||
|
||||
// MARK: - Inline Tab Title Editing
|
||||
|
||||
extension TerminalWindow: TabTitleEditorDelegate {
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
canRenameTabFor targetWindow: NSWindow
|
||||
) -> Bool {
|
||||
targetWindow.windowController is BaseTerminalController
|
||||
}
|
||||
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
titleFor targetWindow: NSWindow
|
||||
) -> String {
|
||||
guard let targetController = targetWindow.windowController as? BaseTerminalController else {
|
||||
return targetWindow.title
|
||||
}
|
||||
|
||||
return targetController.titleOverride ?? targetWindow.title
|
||||
}
|
||||
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
didCommitTitle editedTitle: String,
|
||||
for targetWindow: NSWindow
|
||||
) {
|
||||
guard let targetController = targetWindow.windowController as? BaseTerminalController else { return }
|
||||
targetController.titleOverride = editedTitle.isEmpty ? nil : editedTitle
|
||||
}
|
||||
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
performFallbackRenameFor targetWindow: NSWindow
|
||||
) {
|
||||
guard let targetController = targetWindow.windowController as? BaseTerminalController else { return }
|
||||
targetController.promptTabTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ struct UpdatePill: View {
|
||||
} else {
|
||||
showPopover.toggle()
|
||||
}
|
||||
}) {
|
||||
}, label: {
|
||||
HStack(spacing: 6) {
|
||||
UpdateBadge(model: model)
|
||||
.frame(width: 14, height: 14)
|
||||
@@ -66,7 +66,7 @@ struct UpdatePill: View {
|
||||
)
|
||||
.foregroundColor(model.foregroundColor)
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
})
|
||||
.buttonStyle(.plain)
|
||||
.help(model.text)
|
||||
.accessibilityLabel(model.text)
|
||||
|
||||
@@ -694,7 +694,10 @@ 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 {
|
||||
|
||||
43
macos/Sources/Ghostty/Ghostty.ConfigTypes.swift
Normal file
43
macos/Sources/Ghostty/Ghostty.ConfigTypes.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
// This file contains the configuration types for Ghostty so that alternate targets
|
||||
// can get typed information without depending on all the dependencies of GhosttyKit.
|
||||
|
||||
extension Ghostty {
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String, Sendable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
case xray
|
||||
case custom
|
||||
case customStyle = "custom-style"
|
||||
|
||||
/// Bundled asset name for built-in icons
|
||||
var assetName: String? {
|
||||
switch self {
|
||||
case .official: return nil
|
||||
case .blueprint: return "BlueprintImage"
|
||||
case .chalkboard: return "ChalkboardImage"
|
||||
case .microchip: return "MicrochipImage"
|
||||
case .glass: return "GlassImage"
|
||||
case .holographic: return "HolographicImage"
|
||||
case .paper: return "PaperImage"
|
||||
case .retro: return "RetroImage"
|
||||
case .xray: return "XrayImage"
|
||||
case .custom, .customStyle: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon-frame
|
||||
enum MacOSIconFrame: String, Codable {
|
||||
case aluminum
|
||||
case beige
|
||||
case plastic
|
||||
case chrome
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,6 @@ import os
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
struct Ghostty {
|
||||
// The primary logger used by the GhosttyKit libraries.
|
||||
static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "ghostty"
|
||||
)
|
||||
|
||||
// All the notifications that will be emitted will be put here.
|
||||
struct Notification {}
|
||||
|
||||
// The user notification category identifier
|
||||
static let userNotificationCategory = "com.mitchellh.ghostty.userNotification"
|
||||
|
||||
// The user notification "Show" action
|
||||
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
|
||||
}
|
||||
|
||||
// MARK: C Extensions
|
||||
|
||||
/// A command is fully self-contained so it is Sendable.
|
||||
@@ -28,6 +11,14 @@ extension ghostty_command_s: @unchecked @retroactive Sendable {}
|
||||
/// may be unsafe but the value itself is safe to send across threads.
|
||||
extension ghostty_surface_t: @unchecked @retroactive Sendable {}
|
||||
|
||||
extension Ghostty {
|
||||
// The user notification category identifier
|
||||
static let userNotificationCategory = "com.mitchellh.ghostty.userNotification"
|
||||
|
||||
// The user notification "Show" action
|
||||
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
|
||||
}
|
||||
|
||||
// MARK: Build Info
|
||||
|
||||
extension Ghostty {
|
||||
@@ -317,45 +308,6 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon
|
||||
enum MacOSIcon: String, Sendable {
|
||||
case official
|
||||
case blueprint
|
||||
case chalkboard
|
||||
case glass
|
||||
case holographic
|
||||
case microchip
|
||||
case paper
|
||||
case retro
|
||||
case xray
|
||||
case custom
|
||||
case customStyle = "custom-style"
|
||||
|
||||
/// Bundled asset name for built-in icons
|
||||
var assetName: String? {
|
||||
switch self {
|
||||
case .official: return nil
|
||||
case .blueprint: return "BlueprintImage"
|
||||
case .chalkboard: return "ChalkboardImage"
|
||||
case .microchip: return "MicrochipImage"
|
||||
case .glass: return "GlassImage"
|
||||
case .holographic: return "HolographicImage"
|
||||
case .paper: return "PaperImage"
|
||||
case .retro: return "RetroImage"
|
||||
case .xray: return "XrayImage"
|
||||
case .custom, .customStyle: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// macos-icon-frame
|
||||
enum MacOSIconFrame: String {
|
||||
case aluminum
|
||||
case beige
|
||||
case plastic
|
||||
case chrome
|
||||
}
|
||||
|
||||
/// Enum for the macos-window-buttons config option
|
||||
enum MacOSWindowButtons: String {
|
||||
case visible
|
||||
16
macos/Sources/Ghostty/GhosttyPackageMeta.swift
Normal file
16
macos/Sources/Ghostty/GhosttyPackageMeta.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
import os
|
||||
|
||||
// This defines the minimal information required so all other files can do
|
||||
// `extension Ghostty` to add more to it. This purposely has minimal
|
||||
// dependencies so things like our dock tile plugin can use it.
|
||||
enum Ghostty {
|
||||
// The primary logger used by the GhosttyKit libraries.
|
||||
static let logger = Logger(
|
||||
subsystem: Bundle.main.bundleIdentifier!,
|
||||
category: "ghostty"
|
||||
)
|
||||
|
||||
// All the notifications that will be emitted will be put here.
|
||||
struct Notification {}
|
||||
}
|
||||
@@ -1,41 +1,37 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
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
|
||||
@ObservedObject var surfaceView: SurfaceView
|
||||
|
||||
@State private var isHovering: Bool = false
|
||||
@State private var isDragging: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Rectangle()
|
||||
.fill(Color.primary.opacity(isHovering || isDragging ? 0.15 : 0))
|
||||
.frame(height: handleHeight)
|
||||
.overlay(alignment: .center) {
|
||||
if isHovering || isDragging {
|
||||
Image(systemName: "ellipsis")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(.primary.opacity(0.5))
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.overlay {
|
||||
SurfaceDragSource(
|
||||
surfaceView: surfaceView,
|
||||
isDragging: $isDragging,
|
||||
isHovering: $isHovering
|
||||
)
|
||||
}
|
||||
private var ellipsisVisible: Bool {
|
||||
surfaceView.mouseOverSurface && surfaceView.cursorVisible
|
||||
}
|
||||
|
||||
Spacer()
|
||||
var body: some View {
|
||||
ZStack {
|
||||
SurfaceDragSource(
|
||||
surfaceView: surfaceView,
|
||||
isDragging: $isDragging,
|
||||
isHovering: $isHovering
|
||||
)
|
||||
.frame(width: 80, height: 12)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
if ellipsisVisible {
|
||||
Image(systemName: "ellipsis")
|
||||
.font(.system(size: 10, weight: .semibold))
|
||||
.foregroundColor(.primary.opacity(isHovering ? 0.8 : 0.3))
|
||||
.offset(y: -3)
|
||||
.allowsHitTesting(false)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,18 +454,18 @@ 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) {
|
||||
|
||||
@@ -116,6 +116,12 @@ extension Ghostty {
|
||||
// Whether the pointer should be visible or not
|
||||
@Published private(set) var pointerStyle: CursorStyle = .horizontalText
|
||||
|
||||
// Whether the mouse is currently over this surface
|
||||
@Published private(set) var mouseOverSurface: Bool = false
|
||||
|
||||
// Whether the cursor is currently visible (not hidden by typing, etc.)
|
||||
@Published private(set) var cursorVisible: Bool = true
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
@Published private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
@@ -533,6 +539,7 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
func setCursorVisibility(_ visible: Bool) {
|
||||
cursorVisible = visible
|
||||
// Technically this action could be called anytime we want to
|
||||
// change the mouse visibility but at the time of writing this
|
||||
// mouse-hide-while-typing is the only use case so this is the
|
||||
@@ -910,6 +917,7 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
mouseOverSurface = true
|
||||
super.mouseEntered(with: event)
|
||||
|
||||
guard let surfaceModel else { return }
|
||||
@@ -928,6 +936,7 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
mouseOverSurface = false
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// If the mouse is being dragged then we don't have to emit
|
||||
|
||||
@@ -24,6 +24,14 @@ extension NSColor {
|
||||
appleColorList?.allKeys.map { $0.lowercased() } ?? []
|
||||
}
|
||||
|
||||
/// Returns a new color with its saturation multiplied by the given factor, clamped to [0, 1].
|
||||
func adjustingSaturation(by factor: CGFloat) -> NSColor {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
let hsbColor = self.usingColorSpace(.sRGB) ?? self
|
||||
hsbColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
|
||||
return NSColor(hue: h, saturation: min(max(s * factor, 0), 1), brightness: b, alpha: a)
|
||||
}
|
||||
|
||||
/// Calculates the perceptual distance to another color in RGB space.
|
||||
func distance(to other: NSColor) -> Double {
|
||||
guard let a = self.usingColorSpace(.sRGB),
|
||||
|
||||
@@ -52,10 +52,8 @@ extension NSView {
|
||||
return true
|
||||
}
|
||||
|
||||
for subview in subviews {
|
||||
if subview.contains(view) {
|
||||
return true
|
||||
}
|
||||
for subview in subviews where subview.contains(view) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -67,10 +65,8 @@ extension NSView {
|
||||
return true
|
||||
}
|
||||
|
||||
for subview in subviews {
|
||||
if subview.contains(className: name) {
|
||||
return true
|
||||
}
|
||||
for subview in subviews where subview.contains(className: name) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -76,25 +76,33 @@ extension NSWindow {
|
||||
titlebarView?.firstDescendant(withClassName: "NSTabBar")
|
||||
}
|
||||
|
||||
/// Returns the index of the tab button at the given screen point, if any.
|
||||
func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? {
|
||||
/// Returns tab button views in visual order from left to right.
|
||||
func tabButtonsInVisualOrder() -> [NSView] {
|
||||
guard let tabBarView else { return [] }
|
||||
return tabBarView
|
||||
.descendants(withClassName: "NSTabButton")
|
||||
.sorted { $0.frame.minX < $1.frame.minX }
|
||||
}
|
||||
|
||||
/// Returns the visual tab index and matching tab button at the given screen point.
|
||||
func tabButtonHit(atScreenPoint screenPoint: NSPoint) -> (index: Int, tabButton: NSView)? {
|
||||
guard let tabBarView else { return nil }
|
||||
let locationInWindow = convertPoint(fromScreen: screenPoint)
|
||||
let locationInTabBar = tabBarView.convert(locationInWindow, from: nil)
|
||||
guard tabBarView.bounds.contains(locationInTabBar) else { return nil }
|
||||
|
||||
// Find all tab buttons and sort by x position to get visual order.
|
||||
// The view hierarchy order doesn't match the visual tab order.
|
||||
let tabItemViews = tabBarView.descendants(withClassName: "NSTabButton")
|
||||
.sorted { $0.frame.origin.x < $1.frame.origin.x }
|
||||
|
||||
for (index, tabItemView) in tabItemViews.enumerated() {
|
||||
let locationInTab = tabItemView.convert(locationInWindow, from: nil)
|
||||
if tabItemView.bounds.contains(locationInTab) {
|
||||
return index
|
||||
for (index, tabButton) in tabButtonsInVisualOrder().enumerated() {
|
||||
let locationInTabButton = tabButton.convert(locationInWindow, from: nil)
|
||||
if tabButton.bounds.contains(locationInTabButton) {
|
||||
return (index, tabButton)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Returns the index of the tab button at the given screen point, if any.
|
||||
func tabIndex(atScreenPoint screenPoint: NSPoint) -> Int? {
|
||||
tabButtonHit(atScreenPoint: screenPoint)?.index
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Foundation
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
import GhosttyKit
|
||||
#endif
|
||||
|
||||
extension OSColor {
|
||||
var isLightColor: Bool {
|
||||
@@ -92,7 +94,7 @@ extension OSColor {
|
||||
}
|
||||
|
||||
// MARK: Ghostty Types
|
||||
|
||||
#if !DOCK_TILE_PLUGIN
|
||||
extension OSColor {
|
||||
/// Create a color from a Ghostty color.
|
||||
convenience init(ghostty: ghostty_config_color_s) {
|
||||
@@ -102,3 +104,4 @@ extension OSColor {
|
||||
self.init(red: red, green: green, blue: blue, alpha: 1)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
336
macos/Sources/Helpers/TabTitleEditor.swift
Normal file
336
macos/Sources/Helpers/TabTitleEditor.swift
Normal file
@@ -0,0 +1,336 @@
|
||||
import AppKit
|
||||
|
||||
/// Delegate used by ``TabTitleEditor`` to resolve tab-specific behavior.
|
||||
protocol TabTitleEditorDelegate: AnyObject {
|
||||
/// Returns whether inline rename should be allowed for the given tab window.
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
canRenameTabFor targetWindow: NSWindow
|
||||
) -> Bool
|
||||
|
||||
/// Returns the current title value to seed into the inline editor.
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
titleFor targetWindow: NSWindow
|
||||
) -> String
|
||||
|
||||
/// Called when inline editing commits a title for a target tab window.
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
didCommitTitle editedTitle: String,
|
||||
for targetWindow: NSWindow
|
||||
)
|
||||
|
||||
/// Called when inline editing could not start and the host should show a fallback flow.
|
||||
func tabTitleEditor(
|
||||
_ editor: TabTitleEditor,
|
||||
performFallbackRenameFor targetWindow: NSWindow
|
||||
)
|
||||
}
|
||||
|
||||
/// Handles inline tab title editing for native AppKit window tabs.
|
||||
final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
/// Host window containing the tab bar where editing occurs.
|
||||
private weak var hostWindow: NSWindow?
|
||||
/// Delegate that provides and commits title data for target tab windows.
|
||||
private weak var delegate: TabTitleEditorDelegate?
|
||||
|
||||
/// Active inline editor view, if editing is in progress.
|
||||
private weak var inlineTitleEditor: NSTextField?
|
||||
/// Tab window currently being edited.
|
||||
private weak var inlineTitleTargetWindow: NSWindow?
|
||||
/// Original hidden state for title labels that are temporarily hidden while editing.
|
||||
private var hiddenLabels: [(label: NSTextField, wasHidden: Bool)] = []
|
||||
/// Original button title state restored once editing finishes.
|
||||
private var buttonState: (button: NSButton, title: String, attributedTitle: NSAttributedString?)?
|
||||
/// Deferred begin-editing work used to avoid visual flicker on double-click.
|
||||
private var pendingEditWorkItem: DispatchWorkItem?
|
||||
|
||||
/// Creates a coordinator bound to a host window and rename delegate.
|
||||
init(hostWindow: NSWindow, delegate: TabTitleEditorDelegate) {
|
||||
self.hostWindow = hostWindow
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
/// Handles double-click events from the host window and begins inline edit if possible. If this
|
||||
/// returns true then the double click was handled by the coordinator.
|
||||
func handleDoubleClick(_ event: NSEvent) -> Bool {
|
||||
// We only want double-clicks
|
||||
guard event.type == .leftMouseDown, event.clickCount == 2 else { return false }
|
||||
|
||||
// If we don't have a host window to look up the click, we do nothing.
|
||||
guard let hostWindow else { return false }
|
||||
|
||||
// Find the tab window that is being clicked.
|
||||
let locationInScreen = hostWindow.convertPoint(toScreen: event.locationInWindow)
|
||||
guard let tabIndex = hostWindow.tabIndex(atScreenPoint: locationInScreen),
|
||||
let targetWindow = hostWindow.tabbedWindows?[safe: tabIndex],
|
||||
delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true
|
||||
else { return false }
|
||||
|
||||
// We need to start editing in a separate event loop tick, so set that up.
|
||||
pendingEditWorkItem?.cancel()
|
||||
let workItem = DispatchWorkItem { [weak self, weak targetWindow] in
|
||||
guard let self, let targetWindow else { return }
|
||||
if self.beginEditing(for: targetWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
// Inline editing failed, so trigger fallback rename whatever it is.
|
||||
self.delegate?.tabTitleEditor(self, performFallbackRenameFor: targetWindow)
|
||||
}
|
||||
|
||||
pendingEditWorkItem = workItem
|
||||
DispatchQueue.main.async(execute: workItem)
|
||||
return true
|
||||
}
|
||||
|
||||
/// Begins editing the given target tab window title. Returns true if we're able to start the
|
||||
/// inline edit.
|
||||
@discardableResult
|
||||
func beginEditing(for targetWindow: NSWindow) -> Bool {
|
||||
// Resolve the visual tab button for the target tab window. We rely on visual order
|
||||
// since native tab view hierarchy order does not necessarily match what is on screen.
|
||||
guard let hostWindow,
|
||||
let tabbedWindows = hostWindow.tabbedWindows,
|
||||
let tabIndex = tabbedWindows.firstIndex(of: targetWindow),
|
||||
let tabButton = hostWindow.tabButtonsInVisualOrder()[safe: tabIndex],
|
||||
delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true
|
||||
else { return false }
|
||||
|
||||
// If we have a pending edit, we need to cancel it because we got
|
||||
// called to start edit explicitly.
|
||||
pendingEditWorkItem?.cancel()
|
||||
pendingEditWorkItem = nil
|
||||
finishEditing(commit: true)
|
||||
|
||||
// Build the editor using title text and style derived from the tab's existing label.
|
||||
let titleLabels = tabButton
|
||||
.descendants(withClassName: "NSTextField")
|
||||
.compactMap { $0 as? NSTextField }
|
||||
let editedTitle = delegate?.tabTitleEditor(self, titleFor: targetWindow) ?? targetWindow.title
|
||||
let sourceLabel = sourceTabTitleLabel(from: titleLabels, matching: editedTitle)
|
||||
let editorFrame = tabTitleEditorFrame(for: tabButton, sourceLabel: sourceLabel)
|
||||
guard editorFrame.width >= 20, editorFrame.height >= 14 else { return false }
|
||||
|
||||
let editor = NSTextField(frame: editorFrame)
|
||||
editor.delegate = self
|
||||
editor.stringValue = editedTitle
|
||||
editor.alignment = sourceLabel?.alignment ?? .center
|
||||
editor.isBordered = false
|
||||
editor.isBezeled = false
|
||||
editor.drawsBackground = false
|
||||
editor.focusRingType = .none
|
||||
editor.lineBreakMode = .byClipping
|
||||
if let editorCell = editor.cell as? NSTextFieldCell {
|
||||
editorCell.wraps = false
|
||||
editorCell.usesSingleLineMode = true
|
||||
editorCell.isScrollable = true
|
||||
}
|
||||
if let sourceLabel {
|
||||
applyTextStyle(to: editor, from: sourceLabel, title: editedTitle)
|
||||
}
|
||||
|
||||
// Hide it until the tab button has finished layout so we can avoid flicker.
|
||||
editor.isHidden = true
|
||||
|
||||
inlineTitleEditor = editor
|
||||
inlineTitleTargetWindow = targetWindow
|
||||
|
||||
// Temporarily hide native title label views while editing so only the text field is visible.
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
hiddenLabels = titleLabels.map { ($0, $0.isHidden) }
|
||||
for label in titleLabels {
|
||||
label.isHidden = true
|
||||
}
|
||||
if let tabButton = tabButton as? NSButton {
|
||||
buttonState = (tabButton, tabButton.title, tabButton.attributedTitle)
|
||||
tabButton.title = ""
|
||||
tabButton.attributedTitle = NSAttributedString(string: "")
|
||||
} else {
|
||||
buttonState = nil
|
||||
}
|
||||
tabButton.layoutSubtreeIfNeeded()
|
||||
tabButton.displayIfNeeded()
|
||||
tabButton.addSubview(editor)
|
||||
CATransaction.commit()
|
||||
|
||||
// Focus after insertion so AppKit has created the field editor for this text field.
|
||||
DispatchQueue.main.async { [weak hostWindow, weak editor] in
|
||||
guard let hostWindow, let editor else { return }
|
||||
editor.isHidden = false
|
||||
hostWindow.makeFirstResponder(editor)
|
||||
if let fieldEditor = editor.currentEditor() as? NSTextView,
|
||||
let editorFont = editor.font {
|
||||
fieldEditor.font = editorFont
|
||||
var typingAttributes = fieldEditor.typingAttributes
|
||||
typingAttributes[.font] = editorFont
|
||||
fieldEditor.typingAttributes = typingAttributes
|
||||
}
|
||||
editor.currentEditor()?.selectAll(nil)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/// Finishes any in-flight inline edit and optionally commits the edited title.
|
||||
func finishEditing(commit: Bool) {
|
||||
// If we're pending starting a new edit, cancel it.
|
||||
pendingEditWorkItem?.cancel()
|
||||
pendingEditWorkItem = nil
|
||||
|
||||
// To finish editing we need a current editor.
|
||||
guard let editor = inlineTitleEditor else { return }
|
||||
let editedTitle = editor.stringValue
|
||||
let targetWindow = inlineTitleTargetWindow
|
||||
|
||||
// Clear coordinator references first so re-entrant paths don't see stale state.
|
||||
editor.delegate = nil
|
||||
inlineTitleEditor = nil
|
||||
inlineTitleTargetWindow = nil
|
||||
|
||||
// Make sure the window grabs focus again
|
||||
if let hostWindow {
|
||||
if let currentEditor = editor.currentEditor(), hostWindow.firstResponder === currentEditor {
|
||||
hostWindow.makeFirstResponder(nil)
|
||||
} else if hostWindow.firstResponder === editor {
|
||||
hostWindow.makeFirstResponder(nil)
|
||||
}
|
||||
}
|
||||
|
||||
editor.removeFromSuperview()
|
||||
|
||||
// Restore original tab title presentation.
|
||||
for (label, wasHidden) in hiddenLabels {
|
||||
label.isHidden = wasHidden
|
||||
}
|
||||
hiddenLabels.removeAll()
|
||||
|
||||
if let buttonState {
|
||||
buttonState.button.title = buttonState.title
|
||||
buttonState.button.attributedTitle = buttonState.attributedTitle ?? NSAttributedString(string: buttonState.title)
|
||||
}
|
||||
self.buttonState = nil
|
||||
|
||||
// Delegate owns title persistence semantics (including empty-title handling).
|
||||
guard commit, let targetWindow else { return }
|
||||
delegate?.tabTitleEditor(self, didCommitTitle: editedTitle, for: targetWindow)
|
||||
}
|
||||
|
||||
/// Chooses an editor frame that aligns with the tab title within the tab button.
|
||||
private func tabTitleEditorFrame(for tabButton: NSView, sourceLabel: NSTextField?) -> NSRect {
|
||||
let bounds = tabButton.bounds
|
||||
let horizontalInset: CGFloat = 6
|
||||
var frame = bounds.insetBy(dx: horizontalInset, dy: 0)
|
||||
|
||||
if let sourceLabel {
|
||||
let labelFrame = tabButton.convert(sourceLabel.bounds, from: sourceLabel)
|
||||
frame.origin.y = labelFrame.minY
|
||||
frame.size.height = labelFrame.height
|
||||
}
|
||||
|
||||
return frame.integral
|
||||
}
|
||||
|
||||
/// Selects the best title label candidate from private tab button subviews.
|
||||
private func sourceTabTitleLabel(from labels: [NSTextField], matching title: String) -> NSTextField? {
|
||||
let expected = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !expected.isEmpty {
|
||||
// Prefer a visible exact title match when we can find one.
|
||||
if let exactVisible = labels.first(where: {
|
||||
!$0.isHidden &&
|
||||
$0.alphaValue > 0.01 &&
|
||||
$0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) == expected
|
||||
}) {
|
||||
return exactVisible
|
||||
}
|
||||
|
||||
// Fall back to any exact match, including hidden labels.
|
||||
if let exactAny = labels.first(where: {
|
||||
$0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) == expected
|
||||
}) {
|
||||
return exactAny
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise heuristically choose the largest visible, centered label first.
|
||||
let visibleNonEmpty = labels.filter {
|
||||
!$0.isHidden &&
|
||||
$0.alphaValue > 0.01 &&
|
||||
!$0.stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
|
||||
if let centeredVisible = visibleNonEmpty
|
||||
.filter({ $0.alignment == .center })
|
||||
.max(by: { $0.bounds.width < $1.bounds.width }) {
|
||||
return centeredVisible
|
||||
}
|
||||
|
||||
if let visible = visibleNonEmpty.max(by: { $0.bounds.width < $1.bounds.width }) {
|
||||
return visible
|
||||
}
|
||||
|
||||
return labels.max(by: { $0.bounds.width < $1.bounds.width })
|
||||
}
|
||||
|
||||
/// Copies text styling from the source tab label onto the inline editor.
|
||||
private func applyTextStyle(to editor: NSTextField, from label: NSTextField, title: String) {
|
||||
var attributes: [NSAttributedString.Key: Any] = [:]
|
||||
if label.attributedStringValue.length > 0 {
|
||||
attributes = label.attributedStringValue.attributes(at: 0, effectiveRange: nil)
|
||||
}
|
||||
|
||||
if attributes[.font] == nil, let font = label.font {
|
||||
attributes[.font] = font
|
||||
}
|
||||
|
||||
if attributes[.foregroundColor] == nil {
|
||||
attributes[.foregroundColor] = label.textColor
|
||||
}
|
||||
|
||||
if let font = attributes[.font] as? NSFont {
|
||||
editor.font = font
|
||||
}
|
||||
|
||||
if let textColor = attributes[.foregroundColor] as? NSColor {
|
||||
editor.textColor = textColor
|
||||
}
|
||||
|
||||
if !attributes.isEmpty {
|
||||
editor.attributedStringValue = NSAttributedString(string: title, attributes: attributes)
|
||||
} else {
|
||||
editor.stringValue = title
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSTextFieldDelegate
|
||||
|
||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
guard control === inlineTitleEditor else { return false }
|
||||
|
||||
// Enter commits and exits inline edit.
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||
finishEditing(commit: true)
|
||||
return true
|
||||
}
|
||||
|
||||
// Escape cancels and restores the previous tab title.
|
||||
if commandSelector == #selector(NSResponder.cancelOperation(_:)) {
|
||||
finishEditing(commit: false)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func controlTextDidEndEditing(_ obj: Notification) {
|
||||
guard let inlineTitleEditor,
|
||||
let finishedEditor = obj.object as? NSTextField,
|
||||
finishedEditor === inlineTitleEditor
|
||||
else { return }
|
||||
|
||||
// Blur/end-edit commits, matching standard NSTextField behavior.
|
||||
finishEditing(commit: true)
|
||||
}
|
||||
}
|
||||
144
macos/Tests/ColorizedGhosttyIconTests.swift
Normal file
144
macos/Tests/ColorizedGhosttyIconTests.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
|
||||
struct ColorizedGhosttyIconTests {
|
||||
private func makeIcon(
|
||||
screenColors: [NSColor] = [
|
||||
NSColor(hex: "#112233")!,
|
||||
NSColor(hex: "#AABBCC")!,
|
||||
],
|
||||
ghostColor: NSColor = NSColor(hex: "#445566")!,
|
||||
frame: Ghostty.MacOSIconFrame = .aluminum
|
||||
) -> ColorizedGhosttyIcon {
|
||||
.init(screenColors: screenColors, ghostColor: ghostColor, frame: frame)
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
@Test func codableRoundTripPreservesIcon() throws {
|
||||
let icon = makeIcon(frame: .chrome)
|
||||
let data = try JSONEncoder().encode(icon)
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
|
||||
#expect(decoded == icon)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
#expect(decoded.ghostColor.hexString == "#445566")
|
||||
#expect(decoded.frame == .chrome)
|
||||
}
|
||||
|
||||
@Test func encodingWritesVersionAndHexColors() throws {
|
||||
let icon = makeIcon(frame: .plastic)
|
||||
let data = try JSONEncoder().encode(icon)
|
||||
|
||||
let payload = try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
||||
#expect(payload["version"] as? Int == 1)
|
||||
#expect(payload["screenColors"] as? [String] == ["#112233", "#AABBCC"])
|
||||
#expect(payload["ghostColor"] as? String == "#445566")
|
||||
#expect(payload["frame"] as? String == "plastic")
|
||||
}
|
||||
|
||||
@Test func decodesLegacyV0PayloadWithoutVersion() throws {
|
||||
let data = Data("""
|
||||
{
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "beige"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
#expect(decoded.ghostColor.hexString == "#445566")
|
||||
#expect(decoded.frame == .beige)
|
||||
}
|
||||
|
||||
@Test func decodingUnsupportedVersionThrowsDataCorrupted() {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 99,
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
do {
|
||||
_ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
Issue.record("Expected decode to fail for unsupported version")
|
||||
} catch let DecodingError.dataCorrupted(context) {
|
||||
#expect(context.debugDescription.contains("Unsupported ColorizedGhosttyIcon version"))
|
||||
} catch {
|
||||
Issue.record("Expected DecodingError.dataCorrupted, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func decodingInvalidGhostColorThrows() {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 1,
|
||||
"screenColors": ["#112233", "#AABBCC"],
|
||||
"ghostColor": "not-a-color",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
do {
|
||||
_ = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
Issue.record("Expected decode to fail for invalid ghost color")
|
||||
} catch let DecodingError.dataCorrupted(context) {
|
||||
#expect(context.debugDescription.contains("Failed to decode ghost color"))
|
||||
} catch {
|
||||
Issue.record("Expected DecodingError.dataCorrupted, got: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func decodingInvalidScreenColorsDropsInvalidEntries() throws {
|
||||
let data = Data("""
|
||||
{
|
||||
"version": 1,
|
||||
"screenColors": ["#112233", "invalid", "#AABBCC"],
|
||||
"ghostColor": "#445566",
|
||||
"frame": "chrome"
|
||||
}
|
||||
""".utf8)
|
||||
|
||||
let decoded = try JSONDecoder().decode(ColorizedGhosttyIcon.self, from: data)
|
||||
#expect(decoded.screenColors.compactMap(\.hexString) == ["#112233", "#AABBCC"])
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
@Test func equatableUsesHexColorAndFrameValues() {
|
||||
let lhs = makeIcon(
|
||||
screenColors: [
|
||||
NSColor(red: 0x11 / 255.0, green: 0x22 / 255.0, blue: 0x33 / 255.0, alpha: 1.0),
|
||||
NSColor(red: 0xAA / 255.0, green: 0xBB / 255.0, blue: 0xCC / 255.0, alpha: 1.0),
|
||||
],
|
||||
ghostColor: NSColor(red: 0x44 / 255.0, green: 0x55 / 255.0, blue: 0x66 / 255.0, alpha: 1.0),
|
||||
frame: .chrome
|
||||
)
|
||||
let rhs = makeIcon(frame: .chrome)
|
||||
|
||||
#expect(lhs == rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentFrame() {
|
||||
let lhs = makeIcon(frame: .aluminum)
|
||||
let rhs = makeIcon(frame: .chrome)
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentScreenColors() {
|
||||
let lhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#AABBCC")!])
|
||||
let rhs = makeIcon(screenColors: [NSColor(hex: "#112233")!, NSColor(hex: "#CCBBAA")!])
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
|
||||
@Test func equatableReturnsFalseForDifferentGhostColor() {
|
||||
let lhs = makeIcon(ghostColor: NSColor(hex: "#445566")!)
|
||||
let rhs = makeIcon(ghostColor: NSColor(hex: "#665544")!)
|
||||
#expect(lhs != rhs)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchPypi,
|
||||
fetchFromGitHub,
|
||||
pythonOlder,
|
||||
flit-core,
|
||||
six,
|
||||
wcwidth,
|
||||
}:
|
||||
buildPythonPackage rec {
|
||||
buildPythonPackage {
|
||||
pname = "blessed";
|
||||
version = "1.23.0";
|
||||
version = "unstable-2026-02-23";
|
||||
pyproject = true;
|
||||
|
||||
disabled = pythonOlder "3.7";
|
||||
disabled = pythonOlder "3.8";
|
||||
|
||||
src = fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-VlkaMpZvcE9hMfFACvQVHZ6PX0FEEzpcoDQBl2Pe53s=";
|
||||
src = fetchFromGitHub {
|
||||
owner = "jquast";
|
||||
repo = "blessed";
|
||||
rev = "master";
|
||||
hash = "sha256-ROd/O9pfqnF5DHXqoz+tkl1jQJSZad3Ta1h+oC3+gvY=";
|
||||
};
|
||||
|
||||
build-system = [flit-core];
|
||||
@@ -27,6 +29,7 @@ buildPythonPackage rec {
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
dontCheckRuntimeDeps = true;
|
||||
|
||||
meta = with lib; {
|
||||
homepage = "https://github.com/jquast/blessed";
|
||||
|
||||
@@ -1,36 +1,42 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchPypi,
|
||||
fetchFromGitHub,
|
||||
pythonOlder,
|
||||
setuptools,
|
||||
hatchling,
|
||||
# Dependencies
|
||||
blessed,
|
||||
wcwidth,
|
||||
pyyaml,
|
||||
prettytable,
|
||||
requests,
|
||||
}:
|
||||
buildPythonPackage rec {
|
||||
buildPythonPackage {
|
||||
pname = "ucs-detect";
|
||||
version = "1.0.8";
|
||||
version = "unstable-2026-02-23";
|
||||
pyproject = true;
|
||||
|
||||
disabled = pythonOlder "3.8";
|
||||
|
||||
src = fetchPypi {
|
||||
inherit version;
|
||||
pname = "ucs_detect";
|
||||
hash = "sha256-ihB+tZCd6ykdeXYxc6V1Q6xALQ+xdCW5yqSL7oppqJc=";
|
||||
src = fetchFromGitHub {
|
||||
owner = "jquast";
|
||||
repo = "ucs-detect";
|
||||
rev = "master";
|
||||
hash = "sha256-x7BD14n1/mP9bzjM6DPqc5R1Fk/HLLycl4o41KV+xAE=";
|
||||
};
|
||||
|
||||
dependencies = [
|
||||
blessed
|
||||
wcwidth
|
||||
pyyaml
|
||||
prettytable
|
||||
requests
|
||||
];
|
||||
|
||||
nativeBuildInputs = [setuptools];
|
||||
nativeBuildInputs = [hatchling];
|
||||
|
||||
doCheck = false;
|
||||
dontCheckRuntimeDeps = true;
|
||||
|
||||
meta = with lib; {
|
||||
description = "Measures number of Terminal column cells of wide-character codes";
|
||||
|
||||
27
nix/pkgs/wcwidth.nix
Normal file
27
nix/pkgs/wcwidth.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchPypi,
|
||||
hatchling,
|
||||
}:
|
||||
buildPythonPackage rec {
|
||||
pname = "wcwidth";
|
||||
version = "0.6.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchPypi {
|
||||
inherit pname version;
|
||||
hash = "sha256-zcTkJi1u+aGlfgGDhMvrEgjYq7xkF2An4sJFXIExMVk=";
|
||||
};
|
||||
|
||||
build-system = [hatchling];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
meta = with lib; {
|
||||
description = "Measures the displayed width of unicode strings in a terminal";
|
||||
homepage = "https://github.com/jquast/wcwidth";
|
||||
license = licenses.mit;
|
||||
maintainers = [];
|
||||
};
|
||||
}
|
||||
207
pkg/android-ndk/build.zig
Normal file
207
pkg/android-ndk/build.zig
Normal file
@@ -0,0 +1,207 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
pub fn build(_: *std.Build) !void {}
|
||||
|
||||
// Configure the step to point to the Android NDK for libc and include
|
||||
// paths. This requires the Android NDK installed in the system and
|
||||
// setting the appropriate environment variables or installing the NDK
|
||||
// in the default location.
|
||||
//
|
||||
// The environment variables can be set as follows:
|
||||
// - `ANDROID_NDK_HOME`: Directly points to the NDK path, including the version.
|
||||
// - `ANDROID_HOME` or `ANDROID_SDK_ROOT`: Points to the Android SDK path;
|
||||
// latest available NDK will be automatically selected.
|
||||
//
|
||||
// NB: This is a workaround until zig natively supports bionic
|
||||
// cross-compilation (ziglang/zig#23906).
|
||||
pub fn addPaths(b: *std.Build, step: *std.Build.Step.Compile) !void {
|
||||
const Cache = struct {
|
||||
const Key = struct {
|
||||
arch: std.Target.Cpu.Arch,
|
||||
abi: std.Target.Abi,
|
||||
api_level: u32,
|
||||
};
|
||||
|
||||
var map: std.AutoHashMapUnmanaged(Key, ?struct {
|
||||
libc: std.Build.LazyPath,
|
||||
cpp_include: std.Build.LazyPath,
|
||||
lib: std.Build.LazyPath,
|
||||
}) = .empty;
|
||||
};
|
||||
|
||||
const target = step.rootModuleTarget();
|
||||
const gop = try Cache.map.getOrPut(b.allocator, .{
|
||||
.arch = target.cpu.arch,
|
||||
.abi = target.abi,
|
||||
.api_level = target.os.version_range.linux.android,
|
||||
});
|
||||
|
||||
if (!gop.found_existing) {
|
||||
const ndk_path = findNDKPath(b) orelse return error.AndroidNDKNotFound;
|
||||
|
||||
const ndk_triple = ndkTriple(target) orelse {
|
||||
gop.value_ptr.* = null;
|
||||
return error.AndroidNDKUnsupportedTarget;
|
||||
};
|
||||
|
||||
const host = hostTag() orelse {
|
||||
gop.value_ptr.* = null;
|
||||
return error.AndroidNDKUnsupportedHost;
|
||||
};
|
||||
|
||||
const sysroot = b.pathJoin(&.{
|
||||
ndk_path,
|
||||
"toolchains",
|
||||
"llvm",
|
||||
"prebuilt",
|
||||
host,
|
||||
"sysroot",
|
||||
});
|
||||
const include_dir = b.pathJoin(&.{
|
||||
sysroot,
|
||||
"usr",
|
||||
"include",
|
||||
});
|
||||
const sys_include_dir = b.pathJoin(&.{
|
||||
sysroot,
|
||||
"usr",
|
||||
"include",
|
||||
ndk_triple,
|
||||
});
|
||||
const c_runtime_dir = b.pathJoin(&.{
|
||||
sysroot,
|
||||
"usr",
|
||||
"lib",
|
||||
ndk_triple,
|
||||
b.fmt("{d}", .{target.os.version_range.linux.android}),
|
||||
});
|
||||
const lib = b.pathJoin(&.{
|
||||
sysroot,
|
||||
"usr",
|
||||
"lib",
|
||||
ndk_triple,
|
||||
});
|
||||
const cpp_include = b.pathJoin(&.{
|
||||
sysroot,
|
||||
"usr",
|
||||
"include",
|
||||
"c++",
|
||||
"v1",
|
||||
});
|
||||
|
||||
const libc_txt = b.fmt(
|
||||
\\include_dir={s}
|
||||
\\sys_include_dir={s}
|
||||
\\crt_dir={s}
|
||||
\\msvc_lib_dir=
|
||||
\\kernel32_lib_dir=
|
||||
\\gcc_dir=
|
||||
, .{ include_dir, sys_include_dir, c_runtime_dir });
|
||||
|
||||
const wf = b.addWriteFiles();
|
||||
const libc_path = wf.add("libc.txt", libc_txt);
|
||||
|
||||
gop.value_ptr.* = .{
|
||||
.libc = libc_path,
|
||||
.cpp_include = .{ .cwd_relative = cpp_include },
|
||||
.lib = .{ .cwd_relative = lib },
|
||||
};
|
||||
}
|
||||
|
||||
const value = gop.value_ptr.* orelse return error.AndroidNDKNotFound;
|
||||
|
||||
step.setLibCFile(value.libc);
|
||||
step.root_module.addSystemIncludePath(value.cpp_include);
|
||||
step.root_module.addLibraryPath(value.lib);
|
||||
}
|
||||
|
||||
fn findNDKPath(b: *std.Build) ?[]const u8 {
|
||||
// Check if user has set the environment variable for the NDK path.
|
||||
if (std.process.getEnvVarOwned(b.allocator, "ANDROID_NDK_HOME") catch null) |value| {
|
||||
if (value.len == 0) return null;
|
||||
var dir = std.fs.openDirAbsolute(value, .{}) catch return null;
|
||||
defer dir.close();
|
||||
return value;
|
||||
}
|
||||
|
||||
// Check the common environment variables for the Android SDK path and look for the NDK inside it.
|
||||
inline for (.{ "ANDROID_HOME", "ANDROID_SDK_ROOT" }) |env| {
|
||||
if (std.process.getEnvVarOwned(b.allocator, env) catch null) |sdk| {
|
||||
if (sdk.len > 0) {
|
||||
if (findLatestNDK(b, sdk)) |ndk| return ndk;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As a fallback, we assume the most common/default SDK path based on the OS.
|
||||
const home = std.process.getEnvVarOwned(
|
||||
b.allocator,
|
||||
if (builtin.os.tag == .windows) "LOCALAPPDATA" else "HOME",
|
||||
) catch return null;
|
||||
|
||||
const default_sdk_path = b.pathJoin(
|
||||
&.{
|
||||
home,
|
||||
switch (builtin.os.tag) {
|
||||
.linux => "Android/sdk",
|
||||
.macos => "Library/Android/Sdk",
|
||||
.windows => "Android/Sdk",
|
||||
else => return null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return findLatestNDK(b, default_sdk_path);
|
||||
}
|
||||
|
||||
fn findLatestNDK(b: *std.Build, sdk_path: []const u8) ?[]const u8 {
|
||||
const ndk_dir = b.pathJoin(&.{ sdk_path, "ndk" });
|
||||
var dir = std.fs.openDirAbsolute(ndk_dir, .{ .iterate = true }) catch return null;
|
||||
defer dir.close();
|
||||
|
||||
var latest_: ?struct {
|
||||
name: []const u8,
|
||||
version: std.SemanticVersion,
|
||||
} = null;
|
||||
var iterator = dir.iterate();
|
||||
|
||||
while (iterator.next() catch null) |file| {
|
||||
if (file.kind != .directory) continue;
|
||||
const version = std.SemanticVersion.parse(file.name) catch continue;
|
||||
if (latest_) |latest| {
|
||||
if (version.order(latest.version) != .gt) continue;
|
||||
}
|
||||
latest_ = .{
|
||||
.name = file.name,
|
||||
.version = version,
|
||||
};
|
||||
}
|
||||
|
||||
const latest = latest_ orelse return null;
|
||||
|
||||
return b.pathJoin(&.{ sdk_path, "ndk", latest.name });
|
||||
}
|
||||
|
||||
fn hostTag() ?[]const u8 {
|
||||
return switch (builtin.os.tag) {
|
||||
.linux => "linux-x86_64",
|
||||
// All darwin hosts use the same prebuilt binaries
|
||||
// (https://developer.android.com/ndk/guides/other_build_systems).
|
||||
.macos => "darwin-x86_64",
|
||||
.windows => "windows-x86_64",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
// We must map the target architecture to the corresponding NDK triple following the NDK
|
||||
// documentation: https://android.googlesource.com/platform/ndk/+/master/docs/BuildSystemMaintainers.md#architectures
|
||||
fn ndkTriple(target: std.Target) ?[]const u8 {
|
||||
return switch (target.cpu.arch) {
|
||||
.arm => "arm-linux-androideabi",
|
||||
.aarch64 => "aarch64-linux-android",
|
||||
.x86 => "i686-linux-android",
|
||||
.x86_64 => "x86_64-linux-android",
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
10
pkg/android-ndk/build.zig.zon
Normal file
10
pkg/android-ndk/build.zig.zon
Normal file
@@ -0,0 +1,10 @@
|
||||
.{
|
||||
.name = .android_ndk,
|
||||
.version = "0.0.2",
|
||||
.fingerprint = 0xee68d62c5a97b68b,
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
},
|
||||
}
|
||||
@@ -31,6 +31,11 @@ pub fn build(b: *std.Build) !void {
|
||||
try apple_sdk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
if (target.result.abi.isAndroid()) {
|
||||
const android_ndk = @import("android_ndk");
|
||||
try android_ndk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
try flags.appendSlice(b.allocator, &.{
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
.android_ndk = .{ .path = "../android-ndk" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,11 @@ pub fn build(b: *std.Build) !void {
|
||||
try apple_sdk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
if (target.result.abi.isAndroid()) {
|
||||
const android_ndk = @import("android_ndk");
|
||||
try android_ndk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
// Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
.paths = .{""},
|
||||
.dependencies = .{
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
.android_ndk = .{ .path = "../android-ndk" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ pub fn build(b: *std.Build) !void {
|
||||
try apple_sdk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
if (target.result.abi.isAndroid()) {
|
||||
const android_ndk = @import("android_ndk");
|
||||
try android_ndk.addPaths(b, lib);
|
||||
}
|
||||
|
||||
var flags: std.ArrayList([]const u8) = .empty;
|
||||
defer flags.deinit(b.allocator);
|
||||
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
},
|
||||
|
||||
.apple_sdk = .{ .path = "../apple-sdk" },
|
||||
.android_ndk = .{ .path = "../android-ndk" },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Italian translations for com.mitchellh.ghostty package
|
||||
# Italian translations for com.mitchellh.ghostty package.
|
||||
# Traduzioni italiane per il pacchetto com.mitchellh.ghostty.
|
||||
# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors"
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
@@ -20,7 +20,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "Apri in Ghostty"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
@@ -93,23 +93,23 @@ msgstr "Ghostty: Ispettore del terminale"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:29
|
||||
msgid "Find…"
|
||||
msgstr ""
|
||||
msgstr "Cerca…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:64
|
||||
msgid "Previous Match"
|
||||
msgstr ""
|
||||
msgstr "Corrispondenza precedente"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/search-overlay.blp:74
|
||||
msgid "Next Match"
|
||||
msgstr ""
|
||||
msgstr "Corrispondenza successiva"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:6
|
||||
msgid "Oh, no."
|
||||
msgstr ""
|
||||
msgstr "Oh no!"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:7
|
||||
msgid "Unable to acquire an OpenGL context for rendering."
|
||||
msgstr ""
|
||||
msgstr "Impossibile ottenere un contesto OpenGL per il rendering."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:97
|
||||
msgid ""
|
||||
@@ -117,10 +117,13 @@ msgid ""
|
||||
"through the content, but no input events will be sent to the running "
|
||||
"application."
|
||||
msgstr ""
|
||||
"Questo terminale è in modalità di sola lettura. Puoi ancora vedere, "
|
||||
"selezionare e scorrere il contenuto, ma non verrà inviato alcun evento "
|
||||
"di input all'applicazione in esecuzione."
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:107
|
||||
msgid "Read-only"
|
||||
msgstr ""
|
||||
msgstr "Sola lettura"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:260 src/apprt/gtk/ui/1.5/window.blp:200
|
||||
msgid "Copy"
|
||||
@@ -132,7 +135,7 @@ msgstr "Incolla"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:270
|
||||
msgid "Notify on Next Command Finish"
|
||||
msgstr ""
|
||||
msgstr "Notifica al termine del prossimo comando"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:277 src/apprt/gtk/ui/1.5/window.blp:273
|
||||
msgid "Clear"
|
||||
@@ -177,7 +180,7 @@ msgstr "Scheda"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:320
|
||||
msgid "Change Tab Title…"
|
||||
msgstr ""
|
||||
msgstr "Cambia titolo scheda…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
|
||||
@@ -311,15 +314,15 @@ msgstr ""
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1108
|
||||
msgid "Command Finished"
|
||||
msgstr ""
|
||||
msgstr "Comando terminato"
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1109
|
||||
msgid "Command Succeeded"
|
||||
msgstr ""
|
||||
msgstr "Comando riuscito"
|
||||
|
||||
#: src/apprt/gtk/class/surface.zig:1110
|
||||
msgid "Command Failed"
|
||||
msgstr ""
|
||||
msgstr "Comando fallito"
|
||||
|
||||
#: src/apprt/gtk/class/surface_child_exited.zig:109
|
||||
msgid "Command succeeded"
|
||||
@@ -335,7 +338,7 @@ msgstr "Cambia il titolo del terminale"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:226
|
||||
msgid "Change Tab Title"
|
||||
msgstr ""
|
||||
msgstr "Cambia il titolo della scheda"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1007
|
||||
msgid "Reloaded the configuration"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2026-02-17 23:16+0100\n"
|
||||
"PO-Revision-Date: 2026-02-10 08:14+0100\n"
|
||||
"PO-Revision-Date: 2026-02-20 12:13+0100\n"
|
||||
"Last-Translator: Tadas Lotuzas <tdslot@gmail.com>\n"
|
||||
"Language-Team: Language LT\n"
|
||||
"Language: LT\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "Atidaryti su Ghostty"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
@@ -177,7 +177,7 @@ msgstr "Kortelė"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:320
|
||||
msgid "Change Tab Title…"
|
||||
msgstr ""
|
||||
msgstr "Keisti kortelės pavadinimą…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
|
||||
@@ -334,7 +334,7 @@ msgstr "Keisti terminalo pavadinimą"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:226
|
||||
msgid "Change Tab Title"
|
||||
msgstr ""
|
||||
msgstr "Keisti kortelės pavadinimą"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1007
|
||||
msgid "Reloaded the configuration"
|
||||
|
||||
@@ -19,7 +19,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "Отвори во Ghostty"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
@@ -179,7 +179,7 @@ msgstr "Јазиче"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:320
|
||||
msgid "Change Tab Title…"
|
||||
msgstr ""
|
||||
msgstr "Промени наслов на јазиче…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
|
||||
@@ -336,7 +336,7 @@ msgstr "Промени наслов на терминал"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:226
|
||||
msgid "Change Tab Title"
|
||||
msgstr ""
|
||||
msgstr "Промени наслов на јазиче"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1007
|
||||
msgid "Reloaded the configuration"
|
||||
|
||||
@@ -9,7 +9,7 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2026-02-17 23:16+0100\n"
|
||||
"PO-Revision-Date: 2026-02-09 20:39+0100\n"
|
||||
"PO-Revision-Date: 2026-02-18 20:59+0100\n"
|
||||
"Last-Translator: Nico Geesink <geesinknico@gmail.com>\n"
|
||||
"Language-Team: Dutch <vertaling@vrijschrift.org>\n"
|
||||
"Language: nl\n"
|
||||
@@ -20,7 +20,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "Open in Ghostty"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
@@ -180,7 +180,7 @@ msgstr "Tabblad"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:320
|
||||
msgid "Change Tab Title…"
|
||||
msgstr ""
|
||||
msgstr "Wijzig tabbladtitel…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
|
||||
@@ -337,7 +337,7 @@ msgstr "Titel van de terminal wijzigen"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:226
|
||||
msgid "Change Tab Title"
|
||||
msgstr ""
|
||||
msgstr "Wijzig tabbladtitel"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1007
|
||||
msgid "Reloaded the configuration"
|
||||
|
||||
@@ -19,7 +19,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "在 Ghostty 中打开"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
@@ -175,7 +175,7 @@ msgstr "标签页"
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:325 src/apprt/gtk/ui/1.5/window.blp:224
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:320
|
||||
msgid "Change Tab Title…"
|
||||
msgstr ""
|
||||
msgstr "更改标签页标题…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.2/surface.blp:330 src/apprt/gtk/ui/1.5/window.blp:57
|
||||
#: src/apprt/gtk/ui/1.5/window.blp:107 src/apprt/gtk/ui/1.5/window.blp:229
|
||||
@@ -326,7 +326,7 @@ msgstr "更改终端标题"
|
||||
|
||||
#: src/apprt/gtk/class/title_dialog.zig:226
|
||||
msgid "Change Tab Title"
|
||||
msgstr ""
|
||||
msgstr "更改标签页标题"
|
||||
|
||||
#: src/apprt/gtk/class/window.zig:1007
|
||||
msgid "Reloaded the configuration"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2026-02-17 23:16+0100\n"
|
||||
"PO-Revision-Date: 2026-02-10 15:32+0800\n"
|
||||
"PO-Revision-Date: 2026-02-18 13:58+0800\n"
|
||||
"Last-Translator: Yi-Jyun Pan <me@pan93.com>\n"
|
||||
"Language-Team: Chinese (traditional)\n"
|
||||
"Language: zh_TW\n"
|
||||
@@ -18,7 +18,7 @@ msgstr ""
|
||||
|
||||
#: dist/linux/ghostty_nautilus.py:53
|
||||
msgid "Open in Ghostty"
|
||||
msgstr ""
|
||||
msgstr "在 Ghostty 中開啟"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/clipboard-confirmation-dialog.blp:12
|
||||
#: src/apprt/gtk/ui/1.4/clipboard-confirmation-dialog.blp:12
|
||||
|
||||
@@ -330,6 +330,10 @@ pub const Tab = extern struct {
|
||||
glib.free(@ptrCast(@constCast(v)));
|
||||
priv.title = null;
|
||||
}
|
||||
if (priv.title_override) |v| {
|
||||
glib.free(@ptrCast(@constCast(v)));
|
||||
priv.title_override = null;
|
||||
}
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
|
||||
@@ -339,6 +339,7 @@ pub const Window = extern struct {
|
||||
.init("close-tab", actionCloseTab, s_variant_type),
|
||||
.init("new-tab", actionNewTab, null),
|
||||
.init("new-window", actionNewWindow, null),
|
||||
.init("prompt-surface-title", actionPromptSurfaceTitle, null),
|
||||
.init("prompt-tab-title", actionPromptTabTitle, null),
|
||||
.init("prompt-context-tab-title", actionPromptContextTabTitle, null),
|
||||
.init("ring-bell", actionRingBell, null),
|
||||
@@ -1803,6 +1804,14 @@ pub const Window = extern struct {
|
||||
tab.promptTabTitle();
|
||||
}
|
||||
|
||||
fn actionPromptSurfaceTitle(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.prompt_surface_title);
|
||||
}
|
||||
|
||||
fn actionPromptTabTitle(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
|
||||
@@ -243,7 +243,7 @@ menu main_menu {
|
||||
|
||||
item {
|
||||
label: _("Change Title…");
|
||||
action: "win.prompt-title";
|
||||
action: "win.prompt-surface-title";
|
||||
}
|
||||
|
||||
item {
|
||||
|
||||
@@ -62,6 +62,13 @@ pub fn initShared(
|
||||
.{ .include_extensions = &.{".h"} },
|
||||
);
|
||||
|
||||
if (lib.rootModuleTarget().abi.isAndroid()) {
|
||||
// Support 16kb page sizes, required for Android 15+.
|
||||
lib.link_z_max_page_size = 16384; // 16kb
|
||||
|
||||
try @import("android_ndk").addPaths(b, lib);
|
||||
}
|
||||
|
||||
if (lib.rootModuleTarget().os.tag.isDarwin()) {
|
||||
// Self-hosted x86_64 doesn't work for darwin. It may not work
|
||||
// for other platforms too but definitely darwin.
|
||||
|
||||
@@ -100,6 +100,7 @@ pub const tables = [_]config.Table{
|
||||
},
|
||||
.fields = &.{
|
||||
width.field("width"),
|
||||
wcwidth.field("wcwidth_zero_in_grapheme"),
|
||||
grapheme_break_no_control.field("grapheme_break_no_control"),
|
||||
is_symbol.field("is_symbol"),
|
||||
d.field("is_emoji_vs_base"),
|
||||
|
||||
@@ -39,6 +39,7 @@ pub const Path = @import("path.zig").Path;
|
||||
pub const RepeatablePath = @import("path.zig").RepeatablePath;
|
||||
const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig");
|
||||
const KeyRemapSet = @import("../input/key_mods.zig").RemapSet;
|
||||
const string = @import("string.zig");
|
||||
|
||||
// We do this instead of importing all of terminal/main.zig to
|
||||
// limit the dependency graph. This is important because some things
|
||||
@@ -1857,6 +1858,12 @@ class: ?[:0]const u8 = null,
|
||||
/// If an invalid key is pressed, the sequence ends but the table remains
|
||||
/// active.
|
||||
///
|
||||
/// * Chain actions work within tables, the `chain` keyword applies to
|
||||
/// the most recently defined binding in the table. e.g. if you set
|
||||
/// `table/ctrl+a=new_window` you can chain by using `chain=text:hello`.
|
||||
/// Important: chain itself doesn't get prefixed with the table name,
|
||||
/// since it applies to the most recent binding in any table.
|
||||
///
|
||||
/// * Prefixes like `global:` work within tables:
|
||||
/// `foo/global:ctrl+a=new_window`.
|
||||
///
|
||||
@@ -2939,6 +2946,20 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// * `vec4 iPreviousCursorColor` - Color of the previous terminal cursor.
|
||||
///
|
||||
/// * `vec4 iCurrentCursorStyle` - Style of the terminal cursor
|
||||
///
|
||||
/// Macros simplified use are defined for the various cursor styles:
|
||||
///
|
||||
/// - `CURSORSTYLE_BLOCK` or `0`
|
||||
/// - `CURSORSTYLE_BLOCK_HOLLOW` or `1`
|
||||
/// - `CURSORSTYLE_BAR` or `2`
|
||||
/// - `CURSORSTYLE_UNDERLINE` or `3`
|
||||
/// - `CURSORSTYLE_LOCK` or `4`
|
||||
///
|
||||
/// * `vec4 iPreviousCursorStyle` - Style of the previous terminal cursor
|
||||
///
|
||||
/// * `vec4 iCursorVisible` - Visibility of the terminal cursor.
|
||||
///
|
||||
/// * `float iTimeCursorChange` - Timestamp of terminal cursor change.
|
||||
///
|
||||
/// When the terminal cursor changes position or color, this is set to
|
||||
@@ -5965,22 +5986,15 @@ pub const SelectionWordChars = struct {
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
||||
const value = input orelse return error.ValueRequired;
|
||||
|
||||
// Parse UTF-8 string into codepoints
|
||||
// Parse string with Zig escape sequence support into codepoints
|
||||
var list: std.ArrayList(u21) = .empty;
|
||||
defer list.deinit(alloc);
|
||||
|
||||
// Always include null as first boundary
|
||||
try list.append(alloc, 0);
|
||||
|
||||
// Parse the UTF-8 string
|
||||
const utf8_view = std.unicode.Utf8View.init(value) catch {
|
||||
// Invalid UTF-8, just use null boundary
|
||||
self.codepoints = try list.toOwnedSlice(alloc);
|
||||
return;
|
||||
};
|
||||
|
||||
var utf8_it = utf8_view.iterator();
|
||||
while (utf8_it.nextCodepoint()) |codepoint| {
|
||||
var it = string.codepointIterator(value);
|
||||
while (it.next() catch return error.InvalidValue) |codepoint| {
|
||||
try list.append(alloc, codepoint);
|
||||
}
|
||||
|
||||
@@ -6033,6 +6047,56 @@ pub const SelectionWordChars = struct {
|
||||
try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]);
|
||||
try testing.expectEqual(@as(u21, ','), chars.codepoints[4]);
|
||||
}
|
||||
|
||||
test "parseCLI escape sequences" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// \t escape should be parsed as tab
|
||||
var chars: Self = .{};
|
||||
try chars.parseCLI(alloc, " \\t;,");
|
||||
|
||||
try testing.expectEqual(@as(usize, 5), chars.codepoints.len);
|
||||
try testing.expectEqual(@as(u21, 0), chars.codepoints[0]);
|
||||
try testing.expectEqual(@as(u21, ' '), chars.codepoints[1]);
|
||||
try testing.expectEqual(@as(u21, '\t'), chars.codepoints[2]);
|
||||
try testing.expectEqual(@as(u21, ';'), chars.codepoints[3]);
|
||||
try testing.expectEqual(@as(u21, ','), chars.codepoints[4]);
|
||||
}
|
||||
|
||||
test "parseCLI backslash escape" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// \\ should be parsed as a single backslash
|
||||
var chars: Self = .{};
|
||||
try chars.parseCLI(alloc, "\\\\;");
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), chars.codepoints.len);
|
||||
try testing.expectEqual(@as(u21, 0), chars.codepoints[0]);
|
||||
try testing.expectEqual(@as(u21, '\\'), chars.codepoints[1]);
|
||||
try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]);
|
||||
}
|
||||
|
||||
test "parseCLI unicode escape" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// \u{2502} should be parsed as │
|
||||
var chars: Self = .{};
|
||||
try chars.parseCLI(alloc, "\\u{2502};");
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), chars.codepoints.len);
|
||||
try testing.expectEqual(@as(u21, 0), chars.codepoints[0]);
|
||||
try testing.expectEqual(@as(u21, '│'), chars.codepoints[1]);
|
||||
try testing.expectEqual(@as(u21, ';'), chars.codepoints[2]);
|
||||
}
|
||||
};
|
||||
|
||||
/// FontVariation is a repeatable configuration value that sets a single
|
||||
@@ -6169,6 +6233,15 @@ pub const Keybinds = struct {
|
||||
/// which allows all table names to be available without reservation.
|
||||
tables: std.StringArrayHashMapUnmanaged(inputpkg.Binding.Set) = .empty,
|
||||
|
||||
/// The most recent binding target for `chain=` additions.
|
||||
///
|
||||
/// This is intentionally tracked at the Keybinds level so that chains can
|
||||
/// apply across table boundaries according to parse order.
|
||||
chain_target: union(enum) {
|
||||
root,
|
||||
table: []const u8,
|
||||
} = .root,
|
||||
|
||||
pub fn init(self: *Keybinds, alloc: Allocator) !void {
|
||||
// We don't clear the memory because it's in the arena and unlikely
|
||||
// to be free-able anyways (since arenas can only clear the last
|
||||
@@ -6176,6 +6249,7 @@ pub const Keybinds = struct {
|
||||
// will be freed when the config is freed.
|
||||
self.set = .{};
|
||||
self.tables = .empty;
|
||||
self.chain_target = .root;
|
||||
|
||||
// keybinds for opening and reloading config
|
||||
try self.set.put(
|
||||
@@ -6958,6 +7032,7 @@ pub const Keybinds = struct {
|
||||
log.info("config has 'keybind = clear', all keybinds cleared", .{});
|
||||
self.set = .{};
|
||||
self.tables = .empty;
|
||||
self.chain_target = .root;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6995,16 +7070,39 @@ pub const Keybinds = struct {
|
||||
if (binding.len == 0) {
|
||||
log.debug("config has 'keybind = {s}/', table cleared", .{table_name});
|
||||
gop.value_ptr.* = .{};
|
||||
self.chain_target = .root;
|
||||
return;
|
||||
}
|
||||
|
||||
// Chains are only allowed at the root level. Their target is
|
||||
// tracked globally by parse order in `self.chain_target`.
|
||||
if (std.mem.startsWith(u8, binding, "chain=")) {
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
// Parse and add the binding to the table
|
||||
try gop.value_ptr.parseAndPut(alloc, binding);
|
||||
self.chain_target = .{ .table = gop.key_ptr.* };
|
||||
return;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, value, "chain=")) {
|
||||
switch (self.chain_target) {
|
||||
.root => try self.set.parseAndPut(alloc, value),
|
||||
.table => |table_name| {
|
||||
const table = self.tables.getPtr(table_name) orelse {
|
||||
self.chain_target = .root;
|
||||
return error.InvalidFormat;
|
||||
};
|
||||
try table.parseAndPut(alloc, value);
|
||||
},
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse into default set
|
||||
try self.set.parseAndPut(alloc, value);
|
||||
self.chain_target = .root;
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
@@ -7446,6 +7544,63 @@ pub const Keybinds = struct {
|
||||
try testing.expect(keybinds.tables.contains("mytable"));
|
||||
}
|
||||
|
||||
test "parseCLI chain without prior parsed binding is invalid" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try testing.expectError(
|
||||
error.InvalidFormat,
|
||||
keybinds.parseCLI(alloc, "chain=new_tab"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI table chain syntax is invalid" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try keybinds.parseCLI(alloc, "foo/a=text:hello");
|
||||
try testing.expectError(
|
||||
error.InvalidFormat,
|
||||
keybinds.parseCLI(alloc, "foo/chain=deactivate_key_table"),
|
||||
);
|
||||
}
|
||||
|
||||
test "parseCLI chain applies to most recent table binding" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var keybinds: Keybinds = .{};
|
||||
|
||||
try keybinds.parseCLI(alloc, "ctrl+n=activate_key_table:foo");
|
||||
try keybinds.parseCLI(alloc, "foo/a=text:hello");
|
||||
try keybinds.parseCLI(alloc, "chain=deactivate_key_table");
|
||||
|
||||
const root_entry = keybinds.set.get(.{
|
||||
.mods = .{ .ctrl = true },
|
||||
.key = .{ .unicode = 'n' },
|
||||
}).?.value_ptr.*;
|
||||
try testing.expect(root_entry == .leaf);
|
||||
try testing.expect(root_entry.leaf.action == .activate_key_table);
|
||||
|
||||
const foo_entry = keybinds.tables.get("foo").?.get(.{
|
||||
.key = .{ .unicode = 'a' },
|
||||
}).?.value_ptr.*;
|
||||
try testing.expect(foo_entry == .leaf_chained);
|
||||
try testing.expectEqual(@as(usize, 2), foo_entry.leaf_chained.actions.items.len);
|
||||
try testing.expect(foo_entry.leaf_chained.actions.items[0] == .text);
|
||||
try testing.expect(foo_entry.leaf_chained.actions.items[1] == .deactivate_key_table);
|
||||
}
|
||||
|
||||
test "clone with tables" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
|
||||
@@ -36,6 +36,40 @@ pub fn parse(out: []u8, bytes: []const u8) ![]u8 {
|
||||
return out[0..dst_i];
|
||||
}
|
||||
|
||||
/// Creates an iterator that requires no allocation to extract codepoints
|
||||
/// from the string literal, parsing escape sequences as it goes.
|
||||
pub fn codepointIterator(bytes: []const u8) CodepointIterator {
|
||||
return .{ .bytes = bytes, .i = 0 };
|
||||
}
|
||||
|
||||
pub const CodepointIterator = struct {
|
||||
bytes: []const u8,
|
||||
i: usize,
|
||||
|
||||
pub fn next(self: *CodepointIterator) error{InvalidString}!?u21 {
|
||||
if (self.i >= self.bytes.len) return null;
|
||||
switch (self.bytes[self.i]) {
|
||||
// An escape sequence
|
||||
'\\' => return switch (std.zig.string_literal.parseEscapeSequence(
|
||||
self.bytes,
|
||||
&self.i,
|
||||
)) {
|
||||
.failure => error.InvalidString,
|
||||
.success => |cp| cp,
|
||||
},
|
||||
|
||||
// Not an escape, parse as UTF-8
|
||||
else => |start| {
|
||||
const cp_len = std.unicode.utf8ByteSequenceLength(start) catch
|
||||
return error.InvalidString;
|
||||
defer self.i += cp_len;
|
||||
return std.unicode.utf8Decode(self.bytes[self.i..][0..cp_len]) catch
|
||||
return error.InvalidString;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "parse: empty" {
|
||||
const testing = std.testing;
|
||||
|
||||
@@ -65,3 +99,48 @@ test "parse: escapes" {
|
||||
try testing.expectEqualStrings("hello\u{1F601}world", result);
|
||||
}
|
||||
}
|
||||
|
||||
test "codepointIterator: empty" {
|
||||
var it = codepointIterator("");
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
test "codepointIterator: ascii no escapes" {
|
||||
var it = codepointIterator("abc");
|
||||
try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, 'c'), (try it.next()).?);
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
test "codepointIterator: multibyte utf8" {
|
||||
// │ is U+2502 (3 bytes in UTF-8)
|
||||
var it = codepointIterator("a│b");
|
||||
try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, '│'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?);
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
test "codepointIterator: escape sequences" {
|
||||
var it = codepointIterator("a\\tb\\n\\\\");
|
||||
try std.testing.expectEqual(@as(u21, 'a'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, '\t'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, 'b'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, '\n'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, '\\'), (try it.next()).?);
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
test "codepointIterator: unicode escape" {
|
||||
var it = codepointIterator("\\u{2502}x");
|
||||
try std.testing.expectEqual(@as(u21, '│'), (try it.next()).?);
|
||||
try std.testing.expectEqual(@as(u21, 'x'), (try it.next()).?);
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
test "codepointIterator: emoji unicode escape" {
|
||||
var it = codepointIterator("\\u{1F601}");
|
||||
try std.testing.expectEqual(@as(u21, 0x1F601), (try it.next()).?);
|
||||
try std.testing.expectEqual(null, try it.next());
|
||||
}
|
||||
|
||||
@@ -1895,7 +1895,7 @@ test "shape Bengali ligatures with out of order vowels" {
|
||||
try testing.expectEqual(@as(u16, 0), cells[0].x);
|
||||
try testing.expectEqual(@as(u16, 0), cells[1].x);
|
||||
// See the giant "We need to reset the `cell_offset`" comment, but here
|
||||
// we should technically have the rest of these be `x` of 1, but that
|
||||
// we should technically have the rest of these be `x` of 2, but that
|
||||
// would require going back in the stream to adjust past cells, and
|
||||
// we don't take on that complexity.
|
||||
try testing.expectEqual(@as(u16, 0), cells[2].x);
|
||||
|
||||
@@ -1447,12 +1447,12 @@ test "shape Bengali ligatures with out of order vowels" {
|
||||
|
||||
// Whereas CoreText puts everything all into the first cell (see the
|
||||
// corresponding test), HarfBuzz splits into two clusters.
|
||||
try testing.expectEqual(@as(u16, 1), cells[2].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[3].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[4].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[5].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[6].x);
|
||||
try testing.expectEqual(@as(u16, 1), cells[7].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[2].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[3].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[4].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[5].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[6].x);
|
||||
try testing.expectEqual(@as(u16, 2), cells[7].x);
|
||||
|
||||
// The vowel sign E renders before the SSA:
|
||||
try testing.expect(cells[2].x_offset < cells[3].x_offset);
|
||||
|
||||
@@ -570,12 +570,9 @@ pub const Action = union(enum) {
|
||||
toggle_tab_overview,
|
||||
|
||||
/// Change the title of the current focused surface via a pop-up prompt.
|
||||
///
|
||||
/// This requires libadwaita 1.5 or newer on Linux. The current libadwaita
|
||||
/// version can be found by running `ghostty +version`.
|
||||
prompt_surface_title,
|
||||
|
||||
/// Change the title of the current tab/window via a pop-up prompt. The
|
||||
/// Change the title of the current tab via a pop-up prompt. The
|
||||
/// title set via this prompt overrides any title set by the terminal
|
||||
/// and persists across focus changes within the tab.
|
||||
prompt_tab_title,
|
||||
|
||||
@@ -440,13 +440,13 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||
|
||||
.prompt_surface_title => comptime &.{.{
|
||||
.action = .prompt_surface_title,
|
||||
.title = "Change Terminal Title...",
|
||||
.title = "Change Terminal Title…",
|
||||
.description = "Prompt for a new title for the current terminal.",
|
||||
}},
|
||||
|
||||
.prompt_tab_title => comptime &.{.{
|
||||
.action = .prompt_tab_title,
|
||||
.title = "Change Tab Title...",
|
||||
.title = "Change Tab Title…",
|
||||
.description = "Prompt for a new title for the current tab.",
|
||||
}},
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ pub const Style = enum {
|
||||
lock,
|
||||
|
||||
/// Create a cursor style from the terminal style request.
|
||||
pub fn fromTerminal(term: terminal.CursorStyle) ?Style {
|
||||
pub fn fromTerminal(term: terminal.CursorStyle) Style {
|
||||
return switch (term) {
|
||||
.bar => .bar,
|
||||
.block => .block,
|
||||
|
||||
@@ -756,6 +756,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
.previous_cursor = @splat(0),
|
||||
.current_cursor_color = @splat(0),
|
||||
.previous_cursor_color = @splat(0),
|
||||
.current_cursor_style = 0,
|
||||
.previous_cursor_style = 0,
|
||||
.cursor_visible = 0,
|
||||
.cursor_change_time = 0,
|
||||
.time_focus = 0,
|
||||
.focus = 1, // assume focused initially
|
||||
@@ -1226,6 +1229,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// kitty state on every frame because any cell change can move
|
||||
// an image.
|
||||
if (self.images.kittyRequiresUpdate(state.terminal)) {
|
||||
// We need to grab the draw mutex since this updates
|
||||
// our image state that drawFrame uses.
|
||||
self.draw_mutex.lock();
|
||||
defer self.draw_mutex.unlock();
|
||||
self.images.kittyUpdate(
|
||||
self.alloc,
|
||||
state.terminal,
|
||||
@@ -2007,11 +2014,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// Only update when terminal state is dirty.
|
||||
if (self.terminal_state.dirty == .false) return;
|
||||
|
||||
const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
|
||||
const colors: *const terminal.RenderState.Colors = &self.terminal_state.colors;
|
||||
|
||||
// 256-color palette
|
||||
for (colors.palette, 0..) |color, i| {
|
||||
self.custom_shader_uniforms.palette[i] = .{
|
||||
uniforms.palette[i] = .{
|
||||
@as(f32, @floatFromInt(color.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(color.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(color.b)) / 255.0,
|
||||
@@ -2020,7 +2028,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
}
|
||||
|
||||
// Background color
|
||||
self.custom_shader_uniforms.background_color = .{
|
||||
uniforms.background_color = .{
|
||||
@as(f32, @floatFromInt(colors.background.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(colors.background.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(colors.background.b)) / 255.0,
|
||||
@@ -2028,7 +2036,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
};
|
||||
|
||||
// Foreground color
|
||||
self.custom_shader_uniforms.foreground_color = .{
|
||||
uniforms.foreground_color = .{
|
||||
@as(f32, @floatFromInt(colors.foreground.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(colors.foreground.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(colors.foreground.b)) / 255.0,
|
||||
@@ -2037,7 +2045,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
// Cursor color
|
||||
if (colors.cursor) |cursor_color| {
|
||||
self.custom_shader_uniforms.cursor_color = .{
|
||||
uniforms.cursor_color = .{
|
||||
@as(f32, @floatFromInt(cursor_color.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(cursor_color.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(cursor_color.b)) / 255.0,
|
||||
@@ -2051,7 +2059,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
// Cursor text color
|
||||
if (self.config.cursor_text) |cursor_text| {
|
||||
self.custom_shader_uniforms.cursor_text = .{
|
||||
uniforms.cursor_text = .{
|
||||
@as(f32, @floatFromInt(cursor_text.color.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(cursor_text.color.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(cursor_text.color.b)) / 255.0,
|
||||
@@ -2061,7 +2069,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
// Selection background color
|
||||
if (self.config.selection_background) |selection_bg| {
|
||||
self.custom_shader_uniforms.selection_background_color = .{
|
||||
uniforms.selection_background_color = .{
|
||||
@as(f32, @floatFromInt(selection_bg.color.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(selection_bg.color.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(selection_bg.color.b)) / 255.0,
|
||||
@@ -2071,13 +2079,21 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
// Selection foreground color
|
||||
if (self.config.selection_foreground) |selection_fg| {
|
||||
self.custom_shader_uniforms.selection_foreground_color = .{
|
||||
uniforms.selection_foreground_color = .{
|
||||
@as(f32, @floatFromInt(selection_fg.color.r)) / 255.0,
|
||||
@as(f32, @floatFromInt(selection_fg.color.g)) / 255.0,
|
||||
@as(f32, @floatFromInt(selection_fg.color.b)) / 255.0,
|
||||
1.0,
|
||||
};
|
||||
}
|
||||
|
||||
// Cursor visibility
|
||||
uniforms.cursor_visible = @intFromBool(self.terminal_state.cursor.visible);
|
||||
|
||||
// Cursor style
|
||||
const cursor_style: renderer.CursorStyle = .fromTerminal(self.terminal_state.cursor.visual_style);
|
||||
uniforms.previous_cursor_style = uniforms.current_cursor_style;
|
||||
uniforms.current_cursor_style = @as(i32, @intFromEnum(cursor_style));
|
||||
}
|
||||
|
||||
/// Update per-frame custom shader uniforms.
|
||||
@@ -2087,7 +2103,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// We only need to do this if we have custom shaders.
|
||||
if (!self.has_custom_shaders) return;
|
||||
|
||||
const uniforms = &self.custom_shader_uniforms;
|
||||
const uniforms: *shadertoy.Uniforms = &self.custom_shader_uniforms;
|
||||
|
||||
const now = try std.time.Instant.now();
|
||||
defer self.last_frame_time = now;
|
||||
@@ -2121,7 +2137,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
0,
|
||||
};
|
||||
|
||||
// Update custom cursor uniforms, if we have a cursor.
|
||||
if (self.cells.getCursorGlyph()) |cursor| {
|
||||
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
|
||||
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
|
||||
|
||||
@@ -844,7 +844,7 @@ pub const Image = union(enum) {
|
||||
/// Converts the image data to a format that can be uploaded to the GPU.
|
||||
/// If the data is already in a format that can be uploaded, this is a
|
||||
/// no-op.
|
||||
pub fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||
fn convert(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||
const p = self.getPendingPointer().?;
|
||||
// As things stand, we currently convert all images to RGBA before
|
||||
// uploading to the GPU. This just makes things easier. In the future
|
||||
@@ -867,7 +867,7 @@ pub const Image = union(enum) {
|
||||
|
||||
/// Prepare the pending image data for upload to the GPU.
|
||||
/// This doesn't need GPU access so is safe to call any time.
|
||||
pub fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||
fn prepForUpload(self: *Image, alloc: Allocator) wuffs.Error!void {
|
||||
assert(self.isPending());
|
||||
try self.convert(alloc);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ layout(binding = 1, std140) uniform Globals {
|
||||
uniform vec4 iPreviousCursor;
|
||||
uniform vec4 iCurrentCursorColor;
|
||||
uniform vec4 iPreviousCursorColor;
|
||||
uniform int iCurrentCursorStyle;
|
||||
uniform int iPreviousCursorStyle;
|
||||
uniform int iCursorVisible;
|
||||
uniform float iTimeCursorChange;
|
||||
uniform float iTimeFocus;
|
||||
uniform int iFocus;
|
||||
@@ -27,6 +30,12 @@ layout(binding = 1, std140) uniform Globals {
|
||||
uniform vec3 iSelectionBackgroundColor;
|
||||
};
|
||||
|
||||
#define CURSORSTYLE_BLOCK 0
|
||||
#define CURSORSTYLE_BLOCK_HOLLOW 1
|
||||
#define CURSORSTYLE_BAR 2
|
||||
#define CURSORSTYLE_UNDERLINE 3
|
||||
#define CURSORSTYLE_LOCK 4
|
||||
|
||||
layout(binding = 0) uniform sampler2D iChannel0;
|
||||
|
||||
// These are unused currently by Ghostty:
|
||||
|
||||
@@ -24,6 +24,9 @@ pub const Uniforms = extern struct {
|
||||
previous_cursor: [4]f32 align(16),
|
||||
current_cursor_color: [4]f32 align(16),
|
||||
previous_cursor_color: [4]f32 align(16),
|
||||
current_cursor_style: i32 align(4),
|
||||
previous_cursor_style: i32 align(4),
|
||||
cursor_visible: i32 align(4),
|
||||
cursor_change_time: f32 align(4),
|
||||
time_focus: f32 align(4),
|
||||
focus: i32 align(4),
|
||||
|
||||
@@ -329,12 +329,16 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
@branchHint(.unlikely);
|
||||
// We need the previous cell to determine if we're at a grapheme
|
||||
// break or not. If we are NOT, then we are still combining the
|
||||
// same grapheme. Otherwise, we can stay in this cell.
|
||||
// same grapheme, and will be appending to prev.cell. Otherwise, we are
|
||||
// in a new cell.
|
||||
const Prev = struct { cell: *Cell, left: size.CellCountInt };
|
||||
const prev: Prev = prev: {
|
||||
var prev: Prev = prev: {
|
||||
const left: size.CellCountInt = left: {
|
||||
// If we have wraparound, then we always use the prev col
|
||||
if (self.modes.get(.wraparound)) break :left 1;
|
||||
// If we have wraparound, then we use the prev col unless
|
||||
// there's a pending wrap, in which case we use the current.
|
||||
if (self.modes.get(.wraparound)) {
|
||||
break :left @intFromBool(!self.screens.active.cursor.pending_wrap);
|
||||
}
|
||||
|
||||
// If we do not have wraparound, the logic is trickier. If
|
||||
// we're not on the last column, then we just use the previous
|
||||
@@ -380,6 +384,8 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// If we can NOT break, this means that "c" is part of a grapheme
|
||||
// with the previous char.
|
||||
if (!grapheme_break) {
|
||||
var desired_wide: enum { no_change, wide, narrow } = .no_change;
|
||||
|
||||
// If this is an emoji variation selector then we need to modify
|
||||
// the cell width accordingly. VS16 makes the character wide and
|
||||
// VS15 makes it narrow.
|
||||
@@ -390,71 +396,132 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
if (!prev_props.emoji_vs_base) return;
|
||||
|
||||
switch (c) {
|
||||
0xFE0F => wide: {
|
||||
if (prev.cell.wide == .wide) break :wide;
|
||||
0xFE0F => desired_wide = .wide,
|
||||
0xFE0E => desired_wide = .narrow,
|
||||
else => unreachable,
|
||||
}
|
||||
} else if (!unicode.table.get(c).width_zero_in_grapheme) {
|
||||
// If we have a code point that contributes to the width of a
|
||||
// grapheme, it necessarily means that we're at least at width
|
||||
// 2, since the first code point must be at least width 1 to
|
||||
// start. (Note that Prepend code points could effectively mean
|
||||
// the first code point should be width 0, but we don't handle
|
||||
// that yet.)
|
||||
desired_wide = .wide;
|
||||
}
|
||||
|
||||
// Move our cursor back to the previous. We'll move
|
||||
// the cursor within this block to the proper location.
|
||||
self.screens.active.cursorLeft(prev.left);
|
||||
switch (desired_wide) {
|
||||
.wide => wide: {
|
||||
if (prev.cell.wide == .wide) break :wide;
|
||||
|
||||
// If we don't have space for the wide char, we need
|
||||
// to insert spacers and wrap. Then we just print the wide
|
||||
// char as normal.
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
if (!self.modes.get(.wraparound)) return;
|
||||
// Move our cursor back to the previous. We'll move
|
||||
// the cursor within this block to the proper location.
|
||||
self.screens.active.cursorLeft(prev.left);
|
||||
|
||||
// If we don't have space for the wide char, we need to
|
||||
// insert spacers and wrap. We need special handling if the
|
||||
// previous cell has grapheme data.
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
if (!self.modes.get(.wraparound)) return;
|
||||
|
||||
const prev_cp = prev.cell.content.codepoint;
|
||||
|
||||
if (prev.cell.hasGrapheme()) {
|
||||
// This is like printCell but without clearing the
|
||||
// grapheme data from the cell, so we can move it
|
||||
// later.
|
||||
prev.cell.wide = if (right_limit == self.cols) .spacer_head else .narrow;
|
||||
prev.cell.content.codepoint = 0;
|
||||
|
||||
try self.printWrap();
|
||||
self.printCell(prev_cp, .wide);
|
||||
|
||||
const new_pin = self.screens.active.cursor.page_pin.*;
|
||||
const new_rac = new_pin.rowAndCell();
|
||||
|
||||
transfer_graphemes: {
|
||||
var old_pin = self.screens.active.cursor.page_pin.up(1) orelse break :transfer_graphemes;
|
||||
old_pin.x = right_limit - 1;
|
||||
const old_rac = old_pin.rowAndCell();
|
||||
|
||||
if (new_pin.node == old_pin.node) {
|
||||
new_pin.node.data.moveGrapheme(prev.cell, new_rac.cell);
|
||||
prev.cell.content_tag = .codepoint;
|
||||
new_rac.cell.content_tag = .codepoint_grapheme;
|
||||
new_rac.row.grapheme = true;
|
||||
} else {
|
||||
const cps = old_pin.node.data.lookupGrapheme(old_rac.cell).?;
|
||||
for (cps) |cp| {
|
||||
try self.screens.active.appendGrapheme(new_rac.cell, cp);
|
||||
}
|
||||
old_pin.node.data.clearGrapheme(old_rac.cell);
|
||||
}
|
||||
|
||||
old_pin.node.data.updateRowGraphemeFlag(old_rac.row);
|
||||
}
|
||||
|
||||
// Point prev.cell to our new previous cell that
|
||||
// we'll be appending graphemes to
|
||||
prev.cell = new_rac.cell;
|
||||
} else {
|
||||
self.printCell(
|
||||
0,
|
||||
if (right_limit == self.cols) .spacer_head else .narrow,
|
||||
);
|
||||
try self.printWrap();
|
||||
self.printCell(prev_cp, .wide);
|
||||
|
||||
// Point prev.cell to our new previous cell that
|
||||
// we'll be appending graphemes to
|
||||
prev.cell = self.screens.active.cursor.page_cell;
|
||||
}
|
||||
} else {
|
||||
prev.cell.wide = .wide;
|
||||
}
|
||||
|
||||
self.printCell(prev.cell.content.codepoint, .wide);
|
||||
// Write our spacer, since prev.cell is now wide
|
||||
self.screens.active.cursorRight(1);
|
||||
self.printCell(0, .spacer_tail);
|
||||
|
||||
// Write our spacer
|
||||
// Move the cursor again so we're beyond our spacer
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
self.screens.active.cursor.pending_wrap = true;
|
||||
} else {
|
||||
self.screens.active.cursorRight(1);
|
||||
self.printCell(0, .spacer_tail);
|
||||
}
|
||||
},
|
||||
|
||||
// Move the cursor again so we're beyond our spacer
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
self.screens.active.cursor.pending_wrap = true;
|
||||
} else {
|
||||
self.screens.active.cursorRight(1);
|
||||
}
|
||||
},
|
||||
.narrow => narrow: {
|
||||
// Prev cell is no longer wide
|
||||
if (prev.cell.wide != .wide) break :narrow;
|
||||
prev.cell.wide = .narrow;
|
||||
|
||||
0xFE0E => narrow: {
|
||||
// Prev cell is no longer wide
|
||||
if (prev.cell.wide != .wide) break :narrow;
|
||||
prev.cell.wide = .narrow;
|
||||
// Remove the wide spacer tail
|
||||
const cell = self.screens.active.cursorCellLeft(prev.left - 1);
|
||||
cell.wide = .narrow;
|
||||
|
||||
// Remove the wide spacer tail
|
||||
const cell = self.screens.active.cursorCellLeft(prev.left - 1);
|
||||
cell.wide = .narrow;
|
||||
// Back track the cursor so that we don't end up with
|
||||
// an extra space after the character. Since xterm is
|
||||
// not VS aware, it cannot be used as a reference for
|
||||
// this behavior; but it does follow the principle of
|
||||
// least surprise, and also matches the behavior that
|
||||
// can be observed in Kitty, which is one of the only
|
||||
// other VS aware terminals.
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
// If we're already at the right edge, we stay
|
||||
// here and set the pending wrap to false since
|
||||
// when we pend a wrap, we only move our cursor once
|
||||
// even for wide chars (tests verify).
|
||||
self.screens.active.cursor.pending_wrap = false;
|
||||
} else {
|
||||
// Otherwise, move back.
|
||||
self.screens.active.cursorLeft(1);
|
||||
}
|
||||
|
||||
// Back track the cursor so that we don't end up with
|
||||
// an extra space after the character. Since xterm is
|
||||
// not VS aware, it cannot be used as a reference for
|
||||
// this behavior; but it does follow the principle of
|
||||
// least surprise, and also matches the behavior that
|
||||
// can be observed in Kitty, which is one of the only
|
||||
// other VS aware terminals.
|
||||
if (self.screens.active.cursor.x == right_limit - 1) {
|
||||
// If we're already at the right edge, we stay
|
||||
// here and set the pending wrap to false since
|
||||
// when we pend a wrap, we only move our cursor once
|
||||
// even for wide chars (tests verify).
|
||||
self.screens.active.cursor.pending_wrap = false;
|
||||
} else {
|
||||
// Otherwise, move back.
|
||||
self.screens.active.cursorLeft(1);
|
||||
}
|
||||
break :narrow;
|
||||
},
|
||||
|
||||
break :narrow;
|
||||
},
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
else => {},
|
||||
}
|
||||
|
||||
log.debug("c={X} grapheme attach to left={} primary_cp={X}", .{
|
||||
@@ -3834,19 +3901,23 @@ test "Terminal: print invalid VS15 in emoji ZWJ sequence" {
|
||||
}
|
||||
|
||||
test "Terminal: VS15 to make narrow character with pending wrap" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 2 });
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 4 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
try testing.expect(t.modes.get(.wraparound));
|
||||
|
||||
try t.print(0x1F34B); // Lemon, width=2
|
||||
try t.print(0x2614); // Umbrella with rain drops, width=2
|
||||
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
|
||||
t.clearDirty();
|
||||
|
||||
// We only move one because we're in a pending wrap state.
|
||||
// We only move to the end of the line because we're in a pending wrap
|
||||
// state.
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x);
|
||||
try testing.expect(t.screens.active.cursor.pending_wrap);
|
||||
|
||||
try t.print(0xFE0E); // VS15 to make narrow
|
||||
@@ -3855,17 +3926,17 @@ test "Terminal: VS15 to make narrow character with pending wrap" {
|
||||
|
||||
// VS15 should clear the pending wrap state
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 3), t.screens.active.cursor.x);
|
||||
try testing.expect(!t.screens.active.cursor.pending_wrap);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expectEqualStrings("☔︎", str);
|
||||
try testing.expectEqualStrings("🍋☔︎", str);
|
||||
}
|
||||
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x2614), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
@@ -3873,6 +3944,102 @@ test "Terminal: VS15 to make narrow character with pending wrap" {
|
||||
const cps = list_cell.node.data.lookupGrapheme(cell).?;
|
||||
try testing.expectEqual(@as(usize, 1), cps.len);
|
||||
}
|
||||
|
||||
// VS15 should not affect the previous grapheme
|
||||
{
|
||||
const lemon_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?.cell;
|
||||
try testing.expectEqual(@as(u21, 0x1F34B), lemon_cell.content.codepoint);
|
||||
try testing.expectEqual(Cell.Wide.wide, lemon_cell.wide);
|
||||
const spacer_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?.cell;
|
||||
try testing.expectEqual(@as(u21, 0), spacer_cell.content.codepoint);
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, spacer_cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: VS16 to make wide character on next line" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
t.cursorRight(2);
|
||||
try t.print('#');
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(t.screens.active.cursor.pending_wrap);
|
||||
try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } }));
|
||||
t.clearDirty();
|
||||
|
||||
try t.print(0xFE0F); // VS16 to make wide
|
||||
|
||||
try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 0 } }));
|
||||
t.clearDirty();
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(!t.screens.active.cursor.pending_wrap);
|
||||
|
||||
{
|
||||
// Previous cell turns into spacer_head
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
|
||||
}
|
||||
{
|
||||
// '#' cell is wide
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, '#'), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
// spacer_tail
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: VS16 to make wide character with pending wrap" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
t.cursorRight(1);
|
||||
try t.print('#');
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(!t.screens.active.cursor.pending_wrap);
|
||||
|
||||
try t.print(0xFE0F); // VS16 to make wide
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expect(t.screens.active.cursor.pending_wrap);
|
||||
|
||||
{
|
||||
// '#' cell is wide
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, '#'), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{0xFE0F}, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
// spacer_tail
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: VS16 to make wide character with mode 2027" {
|
||||
@@ -4013,6 +4180,173 @@ test "Terminal: print invalid VS16 with second char" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print grapheme ò (o with nonspacing mark) should be narrow" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
try t.print('o');
|
||||
try t.print(0x0300); // combining grave accent
|
||||
|
||||
// We should have 1 cell taken up.
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.x);
|
||||
|
||||
// Assert various properties about our screen to verify
|
||||
// we have all expected cells.
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 'o'), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{0x0300}, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print Devanagari grapheme should be wide" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
// क्ष
|
||||
try t.print(0x0915);
|
||||
try t.print(0x094D);
|
||||
try t.print(0x200D);
|
||||
try t.print(0x0937);
|
||||
|
||||
// We should have 2 cells taken up. It is one character but "wide".
|
||||
try testing.expectEqual(@as(usize, 0), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
|
||||
// Assert various properties about our screen to verify
|
||||
// we have all expected cells.
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print Devanagari grapheme should be wide on next line" {
|
||||
var t = try init(testing.allocator, .{ .rows = 5, .cols = 3 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
t.cursorRight(2);
|
||||
|
||||
// क्ष
|
||||
try t.print(0x0915);
|
||||
try t.print(0x094D);
|
||||
try t.print(0x200D);
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(t.screens.active.cursor.pending_wrap);
|
||||
|
||||
// This one increases the width to wide
|
||||
try t.print(0x0937);
|
||||
|
||||
// We should have 2 cells taken up. It is one character but "wide".
|
||||
try testing.expectEqual(@as(usize, 1), t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(!t.screens.active.cursor.pending_wrap);
|
||||
|
||||
{
|
||||
// Previous cell turns into spacer_head
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 2, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
|
||||
}
|
||||
{
|
||||
// Devanagari grapheme is wide
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 0, .y = 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .screen = .{ .x = 1, .y = 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print Devanagari grapheme should be wide on next page" {
|
||||
const rows = pagepkg.std_capacity.rows;
|
||||
const cols = pagepkg.std_capacity.cols;
|
||||
var t = try init(testing.allocator, .{ .rows = rows, .cols = cols });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
t.cursorDown(rows - 1);
|
||||
|
||||
for (rows..t.screens.active.pages.pages.first.?.data.capacity.rows) |_| {
|
||||
try t.index();
|
||||
}
|
||||
|
||||
t.cursorRight(cols - 1);
|
||||
|
||||
try testing.expectEqual(cols - 1, t.screens.active.cursor.x);
|
||||
try testing.expectEqual(rows - 1, t.screens.active.cursor.y);
|
||||
|
||||
// क्ष
|
||||
try t.print(0x0915);
|
||||
try t.print(0x094D);
|
||||
try t.print(0x200D);
|
||||
try testing.expectEqual(cols - 1, t.screens.active.cursor.x);
|
||||
try testing.expect(t.screens.active.cursor.pending_wrap);
|
||||
|
||||
// This one increases the width to wide
|
||||
try t.print(0x0937);
|
||||
|
||||
// We should have 2 cells taken up. It is one character but "wide".
|
||||
try testing.expectEqual(rows - 1, t.screens.active.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 2), t.screens.active.cursor.x);
|
||||
try testing.expect(!t.screens.active.cursor.pending_wrap);
|
||||
|
||||
{
|
||||
// Previous cell turns into spacer_head
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = cols - 1, .y = rows - 2 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.spacer_head, cell.wide);
|
||||
}
|
||||
{
|
||||
// Devanagari grapheme is wide
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 0, .y = rows - 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x0915), cell.content.codepoint);
|
||||
try testing.expect(cell.hasGrapheme());
|
||||
try testing.expectEqualSlices(u21, &.{ 0x094D, 0x200D, 0x0937 }, list_cell.node.data.lookupGrapheme(cell).?);
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screens.active.pages.getCell(.{ .active = .{ .x = 1, .y = rows - 1 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print invalid VS16 with second char (combining)" {
|
||||
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
@@ -1556,7 +1556,7 @@ pub const Page = struct {
|
||||
/// WARNING: This will NOT change the content_tag on the cells because
|
||||
/// there are scenarios where we want to move graphemes without changing
|
||||
/// the content tag. Callers beware but assertIntegrity should catch this.
|
||||
inline fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void {
|
||||
pub inline fn moveGrapheme(self: *Page, src: *Cell, dst: *Cell) void {
|
||||
if (build_options.slow_runtime_safety) {
|
||||
assert(src.hasGrapheme());
|
||||
assert(!dst.hasGrapheme());
|
||||
|
||||
@@ -13,6 +13,10 @@ pub const Properties = packed struct {
|
||||
/// becomes a 2-em dash).
|
||||
width: u2 = 0,
|
||||
|
||||
/// Whether the code point does not contribute to the width of a grapheme
|
||||
/// cluster (not used for single code point cells).
|
||||
width_zero_in_grapheme: bool = false,
|
||||
|
||||
/// Grapheme break property.
|
||||
grapheme_break: uucode.x.types.GraphemeBreakNoControl = .other,
|
||||
|
||||
@@ -22,6 +26,7 @@ pub const Properties = packed struct {
|
||||
// Needed for lut.Generator
|
||||
pub fn eql(a: Properties, b: Properties) bool {
|
||||
return a.width == b.width and
|
||||
a.width_zero_in_grapheme == b.width_zero_in_grapheme and
|
||||
a.grapheme_break == b.grapheme_break and
|
||||
a.emoji_vs_base == b.emoji_vs_base;
|
||||
}
|
||||
@@ -34,11 +39,13 @@ pub const Properties = packed struct {
|
||||
try writer.print(
|
||||
\\.{{
|
||||
\\ .width= {},
|
||||
\\ .width_zero_in_grapheme= {},
|
||||
\\ .grapheme_break= .{s},
|
||||
\\ .emoji_vs_base= {},
|
||||
\\}}
|
||||
, .{
|
||||
self.width,
|
||||
self.width_zero_in_grapheme,
|
||||
@tagName(self.grapheme_break),
|
||||
self.emoji_vs_base,
|
||||
});
|
||||
|
||||
@@ -8,12 +8,14 @@ const Properties = @import("props.zig").Properties;
|
||||
pub fn get(cp: u21) Properties {
|
||||
if (cp > uucode.config.max_code_point) return .{
|
||||
.width = 1,
|
||||
.width_zero_in_grapheme = true,
|
||||
.grapheme_break = .other,
|
||||
.emoji_vs_base = false,
|
||||
};
|
||||
|
||||
return .{
|
||||
.width = uucode.get(.width, cp),
|
||||
.width_zero_in_grapheme = uucode.get(.wcwidth_zero_in_grapheme, cp),
|
||||
.grapheme_break = uucode.get(.grapheme_break_no_control, cp),
|
||||
.emoji_vs_base = uucode.get(.is_emoji_vs_base, cp),
|
||||
};
|
||||
|
||||
@@ -42,6 +42,8 @@ extend-ignore-re = [
|
||||
"draw[0-9A-F]+(_[0-9A-F]+)?\\(",
|
||||
# Ignore test data in src/input/paste.zig
|
||||
"\"hel\\\\x",
|
||||
# Ignore long hex-like IDs such as 815E26BA2EF1E00F005C67B1
|
||||
"[0-9A-F]{12,}",
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
|
||||
Reference in New Issue
Block a user