mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-19 20:18:07 +00:00
Merge branch 'ghostty-org:main' into i18n-update-translation-lt-for-v1.4
This commit is contained in:
30
.github/VOUCHED.td
vendored
30
.github/VOUCHED.td
vendored
@@ -46,11 +46,13 @@ alexfeijoo44
|
||||
alexjuca
|
||||
alosarjos
|
||||
amadeus
|
||||
and-rs
|
||||
andrejdaskalov
|
||||
anhthang
|
||||
anmitalidev
|
||||
anthonyzhoon
|
||||
aprilnea
|
||||
arneshbanerjee
|
||||
arvin7liu
|
||||
athaapa
|
||||
atomk
|
||||
@@ -60,6 +62,7 @@ b0uks
|
||||
b1nar10
|
||||
balazs-szucs
|
||||
barutsrb
|
||||
basteez
|
||||
bch
|
||||
bennettp123
|
||||
benodiwal
|
||||
@@ -72,6 +75,7 @@ bo2themax
|
||||
bousii
|
||||
brentschroeter
|
||||
brianc442
|
||||
by-nelson
|
||||
c0x0o
|
||||
carldaws
|
||||
carlvillads
|
||||
@@ -97,9 +101,14 @@ deblasis
|
||||
dervedro
|
||||
devsunb
|
||||
diaaeddin
|
||||
-didouougha slingshotting LLM output
|
||||
diego-moment
|
||||
diegoarmstrong
|
||||
dkinzler
|
||||
dmehala
|
||||
dmunozv04
|
||||
dobbylee
|
||||
dolzenko
|
||||
doprz
|
||||
douglance
|
||||
douglas
|
||||
@@ -108,6 +117,7 @@ drepper
|
||||
dzhlobo
|
||||
ekaterinepapava
|
||||
elias8
|
||||
elitex45
|
||||
-enkr1
|
||||
enzowilliam
|
||||
ephemera
|
||||
@@ -147,9 +157,11 @@ hulet
|
||||
i999rri
|
||||
icodesign
|
||||
illiakrauchanka
|
||||
j-c-m
|
||||
j0hnm4r5
|
||||
jacobsandlund
|
||||
jake-stewart
|
||||
jakeriksen
|
||||
-jamesarch Bad AI etiquette
|
||||
jamylak
|
||||
jarred-sumner
|
||||
@@ -164,6 +176,7 @@ josephmart
|
||||
jparise
|
||||
juniqlim
|
||||
justonia
|
||||
jzillmann
|
||||
karesansui-u
|
||||
kataokatsuki
|
||||
kawarimidoll
|
||||
@@ -173,6 +186,7 @@ khipp
|
||||
kierancanter
|
||||
kirwiisp
|
||||
kjvdven
|
||||
kleshzz
|
||||
kloneets
|
||||
knu
|
||||
-kody-w
|
||||
@@ -201,6 +215,7 @@ marrocco-simone
|
||||
masterflitzer
|
||||
matkotiric
|
||||
mattn
|
||||
mgsloan
|
||||
micaeljarniac
|
||||
michielvk
|
||||
miguelelgallo
|
||||
@@ -235,6 +250,7 @@ nolinmcfarland
|
||||
nouritsu
|
||||
nwehg
|
||||
ocean6954
|
||||
ollioddi
|
||||
oshdubh
|
||||
otomn
|
||||
paaloeye
|
||||
@@ -251,8 +267,10 @@ pluiedev
|
||||
pouwerkerk
|
||||
poweruser64
|
||||
prakhar54-byte
|
||||
preiter93
|
||||
prihlop
|
||||
priyans-hu
|
||||
pssalman
|
||||
puzza007
|
||||
qappell
|
||||
quinnypig
|
||||
@@ -265,6 +283,7 @@ rgehan
|
||||
rhodes-b
|
||||
rightaditya
|
||||
rjwittams
|
||||
rkoten
|
||||
rmengelbrecht
|
||||
rmunn
|
||||
rockorager
|
||||
@@ -280,23 +299,32 @@ sanjj1
|
||||
secrus
|
||||
seruman
|
||||
seyoungjeong
|
||||
sghng
|
||||
shorsher
|
||||
silveirapf
|
||||
simonbcn
|
||||
slowdub
|
||||
slsrepo
|
||||
steven-tk
|
||||
sunshine-syz
|
||||
svector-anu
|
||||
svmhdvn
|
||||
tacherasasi
|
||||
-tangivis
|
||||
tasselx
|
||||
tbrundige
|
||||
tdgroot
|
||||
tdslot
|
||||
thirstycrow
|
||||
-thomas-trijindev No human in the loop
|
||||
thoutbeckers
|
||||
ticclick
|
||||
tnagatomi
|
||||
tothedarktowercame
|
||||
trag1c
|
||||
tristan957
|
||||
tsacha
|
||||
tuananh
|
||||
turbolent
|
||||
tweedbeetle
|
||||
uhojin
|
||||
@@ -304,6 +332,7 @@ unphased
|
||||
unsaltedscholar
|
||||
uzaaft
|
||||
vancluever
|
||||
vasilmytsyk
|
||||
vaughanandrews
|
||||
-vectorpeak Stupid bot cosplaying as a maintainer
|
||||
vegerot
|
||||
@@ -320,4 +349,5 @@ ydah
|
||||
zenangst
|
||||
zenyr
|
||||
zeshi09
|
||||
zlitus
|
||||
zubb
|
||||
|
||||
2
.github/workflows/clean-artifacts.yml
vendored
2
.github/workflows/clean-artifacts.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Remove old artifacts
|
||||
uses: c-hive/gha-remove-artifacts@44fc7acaf1b3d0987da0e8d4707a989d80e9554b # v1.4.0
|
||||
uses: c-hive/gha-remove-artifacts@62c2fbea931baa7dd4a6b73ea5a799984a818f61 # v1.8.0
|
||||
with:
|
||||
age: "1 week"
|
||||
skip-tags: true
|
||||
|
||||
2
.github/workflows/flatpak.yml
vendored
2
.github/workflows/flatpak.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
mkdir dist
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@79327416609af08178ad73b352877e51450790b3 # v6.8
|
||||
with:
|
||||
bundle: com.mitchellh.ghostty
|
||||
manifest-path: dist/flatpak/com.mitchellh.ghostty.yml
|
||||
|
||||
4
.github/workflows/milestone.yml
vendored
4
.github/workflows/milestone.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Milestone Update
|
||||
steps:
|
||||
- name: Set Milestone for PR
|
||||
uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1
|
||||
uses: hustcer/milestone-action@2f38355153344ccaaa44b5b5fcff9f604dff1b45 # v3.2
|
||||
if: github.event.pull_request.merged == true && !contains(github.event.pull_request.title, 'VOUCHED') && !startsWith(github.event.pull_request.title, 'ci:')
|
||||
with:
|
||||
action: bind-pr # `bind-pr` is the default action
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
# Bind milestone to closed issue that has a merged PR fix
|
||||
- name: Set Milestone for Issue
|
||||
uses: hustcer/milestone-action@ebed8d5daafd855a600d7e665c1b130f06d24130 # v3.1
|
||||
uses: hustcer/milestone-action@2f38355153344ccaaa44b5b5fcff9f604dff1b45 # v3.2
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
|
||||
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@@ -47,10 +47,10 @@ jobs:
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
2
.github/workflows/publish-tag.yml
vendored
2
.github/workflows/publish-tag.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
mkdir blob
|
||||
mv appcast.xml blob/appcast.xml
|
||||
- name: Upload Appcast to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
|
||||
|
||||
12
.github/workflows/release-tag.yml
vendored
12
.github/workflows/release-tag.yml
vendored
@@ -89,11 +89,11 @@ jobs:
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
SPARKLE_VERSION: 2.9.0
|
||||
SPARKLE_VERSION: 2.9.6
|
||||
run: |
|
||||
mkdir -p .action/sparkle
|
||||
cd .action/sparkle
|
||||
@@ -328,7 +328,7 @@ jobs:
|
||||
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
SPARKLE_VERSION: 2.9.0
|
||||
SPARKLE_VERSION: 2.9.6
|
||||
run: |
|
||||
mkdir -p .action/sparkle
|
||||
cd .action/sparkle
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
|
||||
mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
|
||||
|
||||
181
.github/workflows/release-tip.yml
vendored
181
.github/workflows/release-tip.yml
vendored
@@ -42,10 +42,10 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -219,10 +219,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -239,7 +239,7 @@ jobs:
|
||||
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
|
||||
|
||||
- name: Update Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -274,10 +274,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
nix develop -c minisign -S -m libghostty-vt-source.tar.gz -s minisign.key < minisign.password
|
||||
|
||||
- name: Update Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -310,7 +310,7 @@ jobs:
|
||||
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
|
||||
cp libghostty-vt-source.tar.gz blob/${GHOSTTY_COMMIT_LONG}/libghostty-vt-source.tar.gz
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
@@ -356,7 +356,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -379,7 +379,7 @@ jobs:
|
||||
nix develop -c minisign -S -m ghostty-vt.xcframework.zip -s minisign.key < minisign.password
|
||||
|
||||
- name: Update Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -395,7 +395,7 @@ jobs:
|
||||
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
|
||||
cp ghostty-vt.xcframework.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
@@ -409,6 +409,135 @@ jobs:
|
||||
echo "Release URLs:"
|
||||
echo " XCFramework: https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-vt.xcframework.zip"
|
||||
|
||||
build-lib-vt-wasm:
|
||||
needs: [setup]
|
||||
if: |
|
||||
needs.setup.outputs.should_skip != 'true' &&
|
||||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
)
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build ReleaseFast WASM
|
||||
run: |
|
||||
nix develop -c zig build \
|
||||
-Demit-lib-vt \
|
||||
-Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseFast
|
||||
|
||||
- name: Optimize ReleaseFast WASM
|
||||
run: |
|
||||
nix develop -c wasm-opt -O3 --strip-dwarf \
|
||||
--enable-simd \
|
||||
--enable-bulk-memory \
|
||||
--enable-sign-ext \
|
||||
--enable-nontrapping-float-to-int \
|
||||
--enable-multivalue \
|
||||
--enable-reference-types \
|
||||
zig-out/bin/ghostty-vt.wasm \
|
||||
-o ghostty-vt.wasm
|
||||
|
||||
- name: Build ReleaseSmall WASM
|
||||
run: |
|
||||
nix develop -c zig build \
|
||||
-Demit-lib-vt \
|
||||
-Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseSmall
|
||||
|
||||
- name: Optimize ReleaseSmall WASM
|
||||
run: |
|
||||
nix develop -c wasm-opt -O3 \
|
||||
--enable-simd \
|
||||
--enable-bulk-memory \
|
||||
--enable-sign-ext \
|
||||
--enable-nontrapping-float-to-int \
|
||||
--enable-multivalue \
|
||||
--enable-reference-types \
|
||||
zig-out/bin/ghostty-vt.wasm \
|
||||
-o ghostty-vt-small.wasm
|
||||
|
||||
- name: Verify WASM simd128
|
||||
run: |
|
||||
# wasm-tools names the simd128 proposal "simd". The artifact must
|
||||
# fail validation when that feature is disabled.
|
||||
for artifact in ghostty-vt.wasm ghostty-vt-small.wasm; do
|
||||
nix develop -c wasm-tools validate "${artifact}"
|
||||
if nix develop -c wasm-tools validate \
|
||||
--features=-simd "${artifact}"; then
|
||||
echo "${artifact} unexpectedly validates with simd128 disabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified ${artifact} requires simd128"
|
||||
done
|
||||
|
||||
- name: Sign WASM
|
||||
run: |
|
||||
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
|
||||
echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
|
||||
nix develop -c minisign -S -m ghostty-vt.wasm -s minisign.key < minisign.password
|
||||
nix develop -c minisign -S -m ghostty-vt-small.wasm -s minisign.key < minisign.password
|
||||
|
||||
- name: Update Release
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
tag_name: tip
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: |
|
||||
ghostty-vt.wasm
|
||||
ghostty-vt.wasm.minisig
|
||||
ghostty-vt-small.wasm
|
||||
ghostty-vt-small.wasm.minisig
|
||||
token: ${{ secrets.GH_RELEASE_TOKEN }}
|
||||
|
||||
- name: Prep R2 Storage
|
||||
run: |
|
||||
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
|
||||
cp ghostty-vt.wasm blob/${GHOSTTY_COMMIT_LONG}/ghostty-vt.wasm
|
||||
cp ghostty-vt-small.wasm blob/${GHOSTTY_COMMIT_LONG}/ghostty-vt-small.wasm
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
|
||||
r2-bucket: ghostty-tip
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
||||
- name: Echo Release URLs
|
||||
run: |
|
||||
echo "Release URLs:"
|
||||
echo " WASM (ReleaseFast): https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-vt.wasm"
|
||||
echo " WASM (ReleaseSmall): https://tip.files.ghostty.org/${GHOSTTY_COMMIT_LONG}/ghostty-vt-small.wasm"
|
||||
|
||||
build-macos:
|
||||
needs: [setup]
|
||||
if: |
|
||||
@@ -448,7 +577,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -462,7 +591,7 @@ jobs:
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
SPARKLE_VERSION: 2.9.0
|
||||
SPARKLE_VERSION: 2.9.6
|
||||
run: |
|
||||
mkdir -p .action/sparkle
|
||||
cd .action/sparkle
|
||||
@@ -581,7 +710,7 @@ jobs:
|
||||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -616,7 +745,7 @@ jobs:
|
||||
cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
|
||||
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
@@ -640,7 +769,7 @@ jobs:
|
||||
if: |
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
@@ -659,7 +788,7 @@ jobs:
|
||||
EOF
|
||||
|
||||
- name: Upload Release URLs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v6.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-urls-${{ inputs.pr || '0' }}
|
||||
path: release-urls.txt
|
||||
@@ -704,7 +833,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -718,7 +847,7 @@ jobs:
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
SPARKLE_VERSION: 2.9.0
|
||||
SPARKLE_VERSION: 2.9.6
|
||||
run: |
|
||||
mkdir -p .action/sparkle
|
||||
cd .action/sparkle
|
||||
@@ -830,7 +959,7 @@ jobs:
|
||||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -847,7 +976,7 @@ jobs:
|
||||
cp ghostty-macos-universal-debug-slow.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip
|
||||
cp ghostty-macos-universal-debug-slow-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
@@ -901,7 +1030,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -915,7 +1044,7 @@ jobs:
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
SPARKLE_VERSION: 2.9.0
|
||||
SPARKLE_VERSION: 2.9.6
|
||||
run: |
|
||||
mkdir -p .action/sparkle
|
||||
cd .action/sparkle
|
||||
@@ -1027,7 +1156,7 @@ jobs:
|
||||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
|
||||
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
@@ -1044,7 +1173,7 @@ jobs:
|
||||
cp ghostty-macos-universal-debug-fast.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip
|
||||
cp ghostty-macos-universal-debug-fast-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
|
||||
329
.github/workflows/test.yml
vendored
329
.github/workflows/test.yml
vendored
@@ -94,6 +94,9 @@ jobs:
|
||||
- build-cmake
|
||||
- build-flatpak
|
||||
- build-libghostty-vt
|
||||
- build-libghostty-vt-freestanding
|
||||
- build-libghostty-vt-wasm
|
||||
- build-libghostty-vt-features
|
||||
- build-libghostty-vt-android
|
||||
- build-libghostty-vt-macos
|
||||
- build-libghostty-vt-windows
|
||||
@@ -169,10 +172,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -227,10 +230,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -263,7 +266,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -295,10 +298,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -364,7 +367,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -401,10 +404,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -439,10 +442,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -556,10 +559,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -590,10 +593,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -616,7 +619,6 @@ jobs:
|
||||
x86_64-windows-gnu,
|
||||
# doesn't work yet, we need a way to find msvc libc/c++ headers
|
||||
# x86_64-windows-msvc
|
||||
wasm32-freestanding,
|
||||
]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
@@ -635,10 +637,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -648,6 +650,180 @@ jobs:
|
||||
nix develop -c zig build -Demit-lib-vt \
|
||||
-Dtarget=${{ matrix.target }}
|
||||
|
||||
build-libghostty-vt-freestanding:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: riscv32-freestanding-eabi
|
||||
cpu: baseline
|
||||
- target: thumb-freestanding-eabi
|
||||
cpu: cortex_m4
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix develop -c zig build -Demit-lib-vt \
|
||||
-Dtarget=${{ matrix.target }} \
|
||||
-Dcpu=${{ matrix.cpu }} \
|
||||
-Doptimize=ReleaseSafe
|
||||
|
||||
build-libghostty-vt-wasm:
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Build ReleaseFast WASM
|
||||
run: |
|
||||
nix develop -c zig build \
|
||||
-Demit-lib-vt \
|
||||
-Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseFast
|
||||
|
||||
- name: Optimize ReleaseFast WASM
|
||||
run: |
|
||||
nix develop -c wasm-opt -O3 --strip-dwarf \
|
||||
--enable-simd \
|
||||
--enable-bulk-memory \
|
||||
--enable-sign-ext \
|
||||
--enable-nontrapping-float-to-int \
|
||||
--enable-multivalue \
|
||||
--enable-reference-types \
|
||||
zig-out/bin/ghostty-vt.wasm \
|
||||
-o ghostty-vt.wasm
|
||||
|
||||
- name: Build ReleaseSmall WASM
|
||||
run: |
|
||||
nix develop -c zig build \
|
||||
-Demit-lib-vt \
|
||||
-Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseSmall
|
||||
|
||||
- name: Validate WASM ABI manifest
|
||||
run: |
|
||||
nix develop -c zig build test-lib-vt-schema \
|
||||
-Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseSmall
|
||||
|
||||
- name: Optimize ReleaseSmall WASM
|
||||
run: |
|
||||
nix develop -c wasm-opt -O3 \
|
||||
--enable-simd \
|
||||
--enable-bulk-memory \
|
||||
--enable-sign-ext \
|
||||
--enable-nontrapping-float-to-int \
|
||||
--enable-multivalue \
|
||||
--enable-reference-types \
|
||||
zig-out/bin/ghostty-vt.wasm \
|
||||
-o ghostty-vt-small.wasm
|
||||
|
||||
- name: Verify WASM simd128
|
||||
run: |
|
||||
# wasm-tools names the simd128 proposal "simd". The artifact must
|
||||
# fail validation when that feature is disabled.
|
||||
for artifact in ghostty-vt.wasm ghostty-vt-small.wasm; do
|
||||
nix develop -c wasm-tools validate "${artifact}"
|
||||
if nix develop -c wasm-tools validate \
|
||||
--features=-simd "${artifact}"; then
|
||||
echo "${artifact} unexpectedly validates with simd128 disabled" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified ${artifact} requires simd128"
|
||||
done
|
||||
|
||||
- name: Test WASM allocator
|
||||
run: |
|
||||
nix develop -c node test/wasm-alloc.mjs ghostty-vt.wasm
|
||||
nix develop -c node test/wasm-alloc.mjs ghostty-vt-small.wasm
|
||||
|
||||
# Compile-only checks for the -Dvt-features flags so that future changes
|
||||
# don't regress any feature combination.
|
||||
build-libghostty-vt-features:
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@c5f8dab7560444c4bf8dbc64f1b203431873c547 # v1.6.1
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Compile feature flag matrix
|
||||
run: |
|
||||
features="snapshot formatter selection render_state input_encode color grid_introspection glyph_protocol kitty_graphics"
|
||||
configs="-all"
|
||||
for f in $features; do configs="$configs -all,+$f"; done
|
||||
|
||||
for cfg in $configs; do
|
||||
echo "::group::-Dvt-features=$cfg"
|
||||
# The wasm lib build compiles the C API export block; the
|
||||
# test build compiles every module test without running any.
|
||||
nix develop -c zig build -Demit-lib-vt \
|
||||
"-Dvt-features=$cfg" \
|
||||
-Dtarget=wasm32-freestanding
|
||||
nix develop -c zig build test-lib-vt-build \
|
||||
"-Dvt-features=$cfg"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
# lib-vt requires macOS runner for macOS/iOS builds because it requires the `apple_sdk` path
|
||||
build-libghostty-vt-macos:
|
||||
strategy:
|
||||
@@ -690,7 +866,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -735,10 +911,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -816,10 +992,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -845,10 +1021,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -878,10 +1054,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -960,10 +1136,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1000,10 +1176,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1068,10 +1244,10 @@ jobs:
|
||||
# uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
# # Install Nix and use that to run our tests so our environment matches exactly.
|
||||
# - uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
# - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
# with:
|
||||
# nix_path: nixpkgs=channel:nixos-unstable
|
||||
# - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
# - uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
# with:
|
||||
# name: ghostty
|
||||
# authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1134,7 +1310,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1186,7 +1362,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1226,7 +1402,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1277,10 +1453,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1334,10 +1510,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1345,6 +1521,9 @@ jobs:
|
||||
- name: Test
|
||||
run: nix develop -c zig build test-lib-vt
|
||||
|
||||
- name: Validate native ABI manifest
|
||||
run: nix develop -c zig build test-lib-vt-schema
|
||||
|
||||
test-kaitai:
|
||||
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true'
|
||||
needs: skip
|
||||
@@ -1359,10 +1538,10 @@ jobs:
|
||||
path: /nix
|
||||
|
||||
# Install Nix so the verifier uses the pinned compiler and runtimes.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1394,10 +1573,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1442,10 +1621,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1477,10 +1656,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1511,7 +1690,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1567,10 +1746,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1598,10 +1777,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1640,10 +1819,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1671,10 +1850,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1701,10 +1880,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1733,7 +1912,7 @@ jobs:
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
determinate: true
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1759,10 +1938,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1787,10 +1966,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1815,10 +1994,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1848,10 +2027,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1876,10 +2055,10 @@ jobs:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1913,10 +2092,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -1975,10 +2154,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
6
.github/workflows/update-colorschemes.yml
vendored
6
.github/workflows/update-colorschemes.yml
vendored
@@ -29,10 +29,10 @@ jobs:
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@630ae543ea3a38a9a4166f03376c02c50f408342 # v31.11.0
|
||||
uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
- uses: cachix/cachix-action@38b082610b782e7e93e209c35fd730d399dee866 # v17
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
echo "filename=$FILENAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
uses: ryand56/r2-upload-action@33ebb7a494b3a8a3c5ed2ea6bbdc72e707090317 # v1.5
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_DEPS_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_DEPS_AWS_KEY }}
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
/po/be.po @ghostty-org/be_BY
|
||||
/po/bg.po @ghostty-org/bg_BG
|
||||
/po/ca.po @ghostty-org/ca_ES
|
||||
/po/da.po @ghostty-org/da_DK
|
||||
/po/de.po @ghostty-org/de_DE
|
||||
/po/es_AR.po @ghostty-org/es_AR
|
||||
/po/es_BO.po @ghostty-org/es_BO
|
||||
@@ -191,6 +192,8 @@
|
||||
/po/pl.po @ghostty-org/pl_PL
|
||||
/po/pt_BR.po @ghostty-org/pt_BR
|
||||
/po/ru.po @ghostty-org/ru_RU
|
||||
/po/sr.po @ghostty-org/sr_RS
|
||||
/po/sr@latin.po @ghostty-org/sr_RS
|
||||
/po/tr.po @ghostty-org/tr_TR
|
||||
/po/uk.po @ghostty-org/uk_UA
|
||||
/po/vi.po @ghostty-org/vi_VN
|
||||
|
||||
3
Doxyfile
3
Doxyfile
@@ -57,7 +57,8 @@ GENERATE_HTML = YES
|
||||
HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
||||
HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css
|
||||
HTML_EXTRA_FILES = dist/doxygen/favicon.png \
|
||||
dist/doxygen/mobile-nav.js
|
||||
dist/doxygen/mobile-nav.js \
|
||||
src/terminal/c/types.schema.json
|
||||
HTML_COLORSTYLE = DARK
|
||||
HTML_CODE_FOLDING = NO
|
||||
HTML_HEADER = dist/doxygen/header.html
|
||||
|
||||
53
PACKAGING.md
53
PACKAGING.md
@@ -122,3 +122,56 @@ relevant to package maintainers:
|
||||
often necessary for system packages to specify a specific minimum Linux
|
||||
version, glibc, etc. Run `zig targets` to a get a full list of available
|
||||
targets.
|
||||
|
||||
## WebAssembly (libghostty-vt)
|
||||
|
||||
libghostty-vt can be built for WebAssembly for use in browsers and other
|
||||
wasm runtimes:
|
||||
|
||||
```sh
|
||||
zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
|
||||
```
|
||||
|
||||
This produces `zig-out/bin/ghostty-vt.wasm`.
|
||||
|
||||
Some notes for packaging the wasm module:
|
||||
|
||||
- The build enables the `simd128` feature by default. Every browser engine
|
||||
has supported it for years (Chrome 91, Firefox 89, Safari 16.4) and it is
|
||||
a large performance win for VT parsing. If you target an unusual runtime
|
||||
without SIMD support, opt out with `-Dcpu=generic`.
|
||||
|
||||
- Optional feature areas can be compiled out with `-Dvt-features` to
|
||||
significantly reduce binary size. The flag takes comma-separated
|
||||
modifications applied to the default all-enabled feature set,
|
||||
`-Dcpu`-style: `+feature` (or bare `feature`) enables, `-feature`
|
||||
disables, and the special name `all` refers to every feature. Hyphens
|
||||
and underscores are interchangeable in feature names. For example, a
|
||||
read-only terminal viewer only needs the render state API:
|
||||
|
||||
```sh
|
||||
zig build -Demit-lib-vt -Dtarget=wasm32-freestanding \
|
||||
-Doptimize=ReleaseSmall -Dvt-features=-all,+render-state
|
||||
```
|
||||
|
||||
This roughly halves the compressed module size versus the default
|
||||
build. An interactive terminal typically wants
|
||||
`-Dvt-features=-all,+render-state,+input-encode,+selection,+color`.
|
||||
Disabled features drop both their C API exports and any escape
|
||||
sequence handling (the sequences are still consumed and safely
|
||||
ignored). See the `Features` struct in `src/terminal/build_options.zig`
|
||||
for the full list of features and what each one covers.
|
||||
|
||||
- `ReleaseSmall` is the recommended optimization mode for the web. Running
|
||||
the result through [Binaryen's](https://github.com/WebAssembly/binaryen)
|
||||
`wasm-opt -O3` shrinks it by roughly a further 10% without hurting
|
||||
performance.
|
||||
|
||||
- `ReleaseFast` measures 10-20% faster than `ReleaseSmall` on escape-heavy
|
||||
terminal workloads, but the artifact is dominated by DWARF debug info.
|
||||
If you want the speed, strip it: `wasm-opt -O3 --strip-dwarf` reduces a
|
||||
ReleaseFast build from over 5MB to roughly 1.1MB (versus roughly 0.8MB
|
||||
for ReleaseSmall). When invoking `wasm-opt`, pass the feature flags for
|
||||
what the module uses, e.g. `--enable-simd --enable-bulk-memory
|
||||
--enable-sign-ext --enable-nontrapping-float-to-int --enable-multivalue
|
||||
--enable-reference-types`.
|
||||
|
||||
41
build.zig
41
build.zig
@@ -69,6 +69,14 @@ pub fn build(b: *std.Build) !void {
|
||||
"test-lib-vt",
|
||||
"Run libghostty-vt tests",
|
||||
);
|
||||
const test_lib_vt_build_step = b.step(
|
||||
"test-lib-vt-build",
|
||||
"Build libghostty-vt tests without running them (compile check)",
|
||||
);
|
||||
const test_lib_vt_schema_step = b.step(
|
||||
"test-lib-vt-schema",
|
||||
"Validate the libghostty-vt ABI type manifest",
|
||||
);
|
||||
const test_valgrind_step = b.step(
|
||||
"test-valgrind",
|
||||
"Run tests under valgrind",
|
||||
@@ -117,20 +125,36 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
|
||||
// libghostty-vt
|
||||
const libghostty_vt_shared = shared: {
|
||||
const native_freestanding = config.target.result.os.tag == .freestanding and
|
||||
!config.target.result.cpu.arch.isWasm();
|
||||
const libghostty_vt_shared: ?buildpkg.GhosttyLibVt = shared: {
|
||||
if (config.target.result.cpu.arch.isWasm()) {
|
||||
break :shared try buildpkg.GhosttyLibVt.initWasm(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
}
|
||||
if (native_freestanding) break :shared null;
|
||||
|
||||
break :shared try buildpkg.GhosttyLibVt.initShared(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
};
|
||||
libghostty_vt_shared.install(b.getInstallStep());
|
||||
if (libghostty_vt_shared) |shared| {
|
||||
shared.install(b.getInstallStep());
|
||||
|
||||
const type_schema_test = b.addSystemCommand(&.{"python3"});
|
||||
type_schema_test.addFileArg(b.path("src/terminal/c/types-schema-verify.py"));
|
||||
type_schema_test.addFileArg(b.path("src/terminal/c/types.schema.json"));
|
||||
type_schema_test.addFileArg(shared.output);
|
||||
test_lib_vt_schema_step.dependOn(&type_schema_test.step);
|
||||
} else {
|
||||
try test_lib_vt_schema_step.addError(
|
||||
"cannot execute the ABI manifest for a native freestanding target",
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
// libghostty-vt static lib
|
||||
const libghostty_vt_static = try buildpkg.GhosttyLibVt.initStatic(
|
||||
@@ -153,6 +177,15 @@ pub fn build(b: *std.Build) !void {
|
||||
libghostty_vt_static.output,
|
||||
static_lib_name,
|
||||
).step);
|
||||
|
||||
if (native_freestanding) {
|
||||
b.getInstallStep().dependOn(&b.addInstallDirectory(.{
|
||||
.source_dir = b.path("include/ghostty"),
|
||||
.install_dir = .header,
|
||||
.install_subdir = "ghostty",
|
||||
.include_extensions = &.{".h"},
|
||||
}).step);
|
||||
}
|
||||
}
|
||||
|
||||
// libghostty-vt xcframework (Apple only, universal binary).
|
||||
@@ -312,6 +345,7 @@ pub fn build(b: *std.Build) !void {
|
||||
const run_cmd = b.addSystemCommand(&.{
|
||||
"valgrind",
|
||||
"--leak-check=full",
|
||||
"--error-exitcode=1",
|
||||
"--num-callers=50",
|
||||
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
|
||||
"--gen-suppressions=all",
|
||||
@@ -329,6 +363,7 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
|
||||
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
|
||||
test_lib_vt_build_step.dependOn(&mod_vt_test.step);
|
||||
|
||||
const mod_vt_c_test = b.addTest(.{
|
||||
.root_module = mod.vt_c,
|
||||
@@ -336,6 +371,7 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test);
|
||||
test_lib_vt_step.dependOn(&mod_vt_c_test_run.step);
|
||||
test_lib_vt_build_step.dependOn(&mod_vt_c_test.step);
|
||||
}
|
||||
|
||||
// Tests (skip when building libghostty-vt)
|
||||
@@ -376,6 +412,7 @@ pub fn build(b: *std.Build) !void {
|
||||
const valgrind_run = b.addSystemCommand(&.{
|
||||
"valgrind",
|
||||
"--leak-check=full",
|
||||
"--error-exitcode=1",
|
||||
"--num-callers=50",
|
||||
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
|
||||
"--gen-suppressions=all",
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
// Aro/translate-c itself.
|
||||
.translate_c = .{
|
||||
.lazy = true,
|
||||
.url = "https://deps.files.ghostty.org/translate_c-80f8b6e4f45a303268717d8e5f4f91d7837138bb.tar.gz",
|
||||
.hash = "translate_c-0.0.0-Q_BUWmU6BwB_9JKG2l2W7i_mhmYWeRseTGBEHi_YlV5f",
|
||||
.url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
|
||||
.hash = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
|
||||
},
|
||||
|
||||
// Zig libs
|
||||
@@ -66,7 +66,7 @@
|
||||
.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.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst",
|
||||
.url = "https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst",
|
||||
.hash = "gobject-0.3.2-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3",
|
||||
.lazy = true,
|
||||
},
|
||||
@@ -125,8 +125,8 @@
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.android_ndk = .{ .path = "./pkg/android-ndk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260803-155300-875a82f.tgz",
|
||||
.hash = "N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl",
|
||||
.url = "https://deps.files.ghostty.org/ghostty-themes-release-20260831-151010-752a9c0.tgz",
|
||||
.hash = "N-V-__8AAEFmBABuDGOKxAI6VMg41b9euMZ-z7HS9EcUdaor",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
||||
32
build.zig.zon.json
generated
32
build.zig.zon.json
generated
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"aro-0.0.0-JSD1Qk8rOQDnuVcD4jAwMpHitA6pADRKzQ7M7hKRwxvD": {
|
||||
"aro-0.0.0-JSD1Qk6lNgDdcDV4Vh7Sfy-34m2TluIVOdPzMmj_0BjX": {
|
||||
"name": "aro",
|
||||
"url": "https://github.com/vancluever/arocc/archive/ecbc5c799574e0da2758a961b12efa586007f03c.tar.gz",
|
||||
"hash": "sha256-rjNfhWjmA/1WR/xuHo4ls4fnDCbsI1VZSvb8SFRvwso="
|
||||
"url": "https://github.com/vancluever/arocc/archive/f97cdfc3779aec4b242299e2fc9a1c828c3547c6.tar.gz",
|
||||
"hash": "sha256-G/NNgk7KhJSLdy17ip1igIzpYhAUlzO7ef8r3/iCv/s="
|
||||
},
|
||||
"N-V-__8AANT61wB--nJ95Gj_ctmzAtcjloZ__hRqNw5lC1Kr": {
|
||||
"name": "bindings",
|
||||
@@ -14,10 +14,10 @@
|
||||
"url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz",
|
||||
"hash": "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="
|
||||
},
|
||||
"N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v": {
|
||||
"N-V-__8AAOgqbADacob-q2_DMQlmgaG4xKHRuW-6PJ4oJzMZ": {
|
||||
"name": "fontconfig",
|
||||
"url": "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz",
|
||||
"hash": "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="
|
||||
"url": "https://gitlab.freedesktop.org/api/v4/projects/890/packages/generic/fontconfig/2.18.3/fontconfig-2.18.3.tar.xz",
|
||||
"hash": "sha256-T3tVSjjN94wDP2ZsiHHzdJ4UoJT2Wgf2MMke0LQ9NeM="
|
||||
},
|
||||
"N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub": {
|
||||
"name": "freetype",
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"gobject-0.3.2-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3": {
|
||||
"name": "gobject",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst",
|
||||
"hash": "sha256-dyCfm2XjiAk30zccjD6AgKFBdE7IRsJuoqnscfvnWSQ="
|
||||
},
|
||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||
@@ -59,10 +59,10 @@
|
||||
"url": "https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz",
|
||||
"hash": "sha256-yBbCDox18+Fa6Gc1DnmSVQLRpqhZOLsac7iSfl8x+cs="
|
||||
},
|
||||
"N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl": {
|
||||
"N-V-__8AAEFmBABuDGOKxAI6VMg41b9euMZ-z7HS9EcUdaor": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260803-155300-875a82f.tgz",
|
||||
"hash": "sha256-/qxLa9S8bSg2NAbJzwEtZhGvOxaPTAVFNWnBCs/MFBs="
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260831-151010-752a9c0.tgz",
|
||||
"hash": "sha256-g2U+uDe661v8fVDJG31EdyeW4IwcJDE04Rxe32yNFcU="
|
||||
},
|
||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||
"name": "jetbrains_mono",
|
||||
@@ -114,10 +114,10 @@
|
||||
"url": "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz",
|
||||
"hash": "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="
|
||||
},
|
||||
"translate_c-0.0.0-Q_BUWmU6BwB_9JKG2l2W7i_mhmYWeRseTGBEHi_YlV5f": {
|
||||
"translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr": {
|
||||
"name": "translate_c",
|
||||
"url": "https://deps.files.ghostty.org/translate_c-80f8b6e4f45a303268717d8e5f4f91d7837138bb.tar.gz",
|
||||
"hash": "sha256-fB7OsZ2PIijMzVMYg8SzDBtTKX7IZHbEvPuBTdyGtWk="
|
||||
"url": "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
|
||||
"hash": "sha256-/sT7W8Kp+O11xaFBgpb/kDiWzfQ0MuXk/d/TuGq1Am8="
|
||||
},
|
||||
"uucode-0.2.0-ZZjBPlK5VADj7fdoq7G8LIHzD5o6FSkcBXXrRWr4jnrA": {
|
||||
"name": "uucode",
|
||||
@@ -149,10 +149,10 @@
|
||||
"url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
|
||||
"hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="
|
||||
},
|
||||
"N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs": {
|
||||
"N-V-__8AAP5JWgCGP_AD0teWpa4krRvE9VPZzvviGdbmN4jI": {
|
||||
"name": "wuffs",
|
||||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
||||
"url": "https://deps.files.ghostty.org/wuffs-7411f488fe2e2c205c3d3b3d28638b7356522930.tar.gz",
|
||||
"hash": "sha256-F4d9NG95iGUdbGLkWy47BchoCaZELF40YSN0sqlxmhw="
|
||||
},
|
||||
"z2d-0.12.1-j5P_Hsw8EQAKyZTQICCQnAH2xYkLDW8k9uefbsYdfPZ-": {
|
||||
"name": "z2d",
|
||||
|
||||
32
build.zig.zon.nix
generated
32
build.zig.zon.nix
generated
@@ -98,11 +98,11 @@
|
||||
in
|
||||
linkFarm name [
|
||||
{
|
||||
name = "aro-0.0.0-JSD1Qk8rOQDnuVcD4jAwMpHitA6pADRKzQ7M7hKRwxvD";
|
||||
name = "aro-0.0.0-JSD1Qk6lNgDdcDV4Vh7Sfy-34m2TluIVOdPzMmj_0BjX";
|
||||
path = fetchZigArtifact {
|
||||
name = "aro";
|
||||
url = "https://github.com/vancluever/arocc/archive/ecbc5c799574e0da2758a961b12efa586007f03c.tar.gz";
|
||||
hash = "sha256-rjNfhWjmA/1WR/xuHo4ls4fnDCbsI1VZSvb8SFRvwso=";
|
||||
url = "https://github.com/vancluever/arocc/archive/f97cdfc3779aec4b242299e2fc9a1c828c3547c6.tar.gz";
|
||||
hash = "sha256-G/NNgk7KhJSLdy17ip1igIzpYhAUlzO7ef8r3/iCv/s=";
|
||||
unpack = true;
|
||||
};
|
||||
}
|
||||
@@ -125,11 +125,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v";
|
||||
name = "N-V-__8AAOgqbADacob-q2_DMQlmgaG4xKHRuW-6PJ4oJzMZ";
|
||||
path = fetchZigArtifact {
|
||||
name = "fontconfig";
|
||||
url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz";
|
||||
hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU=";
|
||||
url = "https://gitlab.freedesktop.org/api/v4/projects/890/packages/generic/fontconfig/2.18.3/fontconfig-2.18.3.tar.xz";
|
||||
hash = "sha256-T3tVSjjN94wDP2ZsiHHzdJ4UoJT2Wgf2MMke0LQ9NeM=";
|
||||
unpack = false;
|
||||
};
|
||||
}
|
||||
@@ -164,7 +164,7 @@ in
|
||||
name = "gobject-0.3.2-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3";
|
||||
path = fetchZigArtifact {
|
||||
name = "gobject";
|
||||
url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst";
|
||||
url = "https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst";
|
||||
hash = "sha256-dyCfm2XjiAk30zccjD6AgKFBdE7IRsJuoqnscfvnWSQ=";
|
||||
unpack = true;
|
||||
};
|
||||
@@ -206,11 +206,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl";
|
||||
name = "N-V-__8AAEFmBABuDGOKxAI6VMg41b9euMZ-z7HS9EcUdaor";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260803-155300-875a82f.tgz";
|
||||
hash = "sha256-/qxLa9S8bSg2NAbJzwEtZhGvOxaPTAVFNWnBCs/MFBs=";
|
||||
url = "https://deps.files.ghostty.org/ghostty-themes-release-20260831-151010-752a9c0.tgz";
|
||||
hash = "sha256-g2U+uDe661v8fVDJG31EdyeW4IwcJDE04Rxe32yNFcU=";
|
||||
unpack = false;
|
||||
};
|
||||
}
|
||||
@@ -305,11 +305,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "translate_c-0.0.0-Q_BUWmU6BwB_9JKG2l2W7i_mhmYWeRseTGBEHi_YlV5f";
|
||||
name = "translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr";
|
||||
path = fetchZigArtifact {
|
||||
name = "translate_c";
|
||||
url = "https://deps.files.ghostty.org/translate_c-80f8b6e4f45a303268717d8e5f4f91d7837138bb.tar.gz";
|
||||
hash = "sha256-fB7OsZ2PIijMzVMYg8SzDBtTKX7IZHbEvPuBTdyGtWk=";
|
||||
url = "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz";
|
||||
hash = "sha256-/sT7W8Kp+O11xaFBgpb/kDiWzfQ0MuXk/d/TuGq1Am8=";
|
||||
unpack = true;
|
||||
};
|
||||
}
|
||||
@@ -368,11 +368,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs";
|
||||
name = "N-V-__8AAP5JWgCGP_AD0teWpa4krRvE9VPZzvviGdbmN4jI";
|
||||
path = fetchZigArtifact {
|
||||
name = "wuffs";
|
||||
url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz";
|
||||
hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=";
|
||||
url = "https://deps.files.ghostty.org/wuffs-7411f488fe2e2c205c3d3b3d28638b7356522930.tar.gz";
|
||||
hash = "sha256-F4d9NG95iGUdbGLkWy47BchoCaZELF40YSN0sqlxmhw=";
|
||||
unpack = false;
|
||||
};
|
||||
}
|
||||
|
||||
12
build.zig.zon.txt
generated
12
build.zig.zon.txt
generated
@@ -1,16 +1,17 @@
|
||||
git+https://github.com/rockorager/libvaxis.git#c1e1f23be38951c425cdf31af455ba23ef178940
|
||||
git+https://github.com/zigimg/zigimg#d695acd97c02e57bb151e8f659d1280f5cd6ca70
|
||||
https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz
|
||||
https://deps.files.ghostty.org/DearBindings_v0.17_ImGui_v1.92.5-docking.tar.gz
|
||||
https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz
|
||||
https://deps.files.ghostty.org/N-V-__8AAEbOfQBnvcFcCX2W5z7tDaN8vaNZGamEQtNOe0UI.tar.gz
|
||||
https://deps.files.ghostty.org/N-V-__8AAFdWDwA0ktbNUi9pFBHCRN4weXIgIfCrVjfGxqgA.tar.gz
|
||||
https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz
|
||||
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
|
||||
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-20260803-155300-875a82f.tgz
|
||||
https://deps.files.ghostty.org/ghostty-themes-release-20260831-151010-752a9c0.tgz
|
||||
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst
|
||||
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
|
||||
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
||||
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
|
||||
@@ -22,17 +23,16 @@ https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d8
|
||||
https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz
|
||||
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
|
||||
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
|
||||
https://deps.files.ghostty.org/translate_c-80f8b6e4f45a303268717d8e5f4f91d7837138bb.tar.gz
|
||||
https://deps.files.ghostty.org/uucode-2826a37a4562284fdacd8fa029d49509cc9bffcd.tar.gz
|
||||
https://deps.files.ghostty.org/vaxis-1dbbe575dff4586fe51e3217aa5c3fecdcbb6089.tar.gz
|
||||
https://deps.files.ghostty.org/wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX.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/wuffs-7411f488fe2e2c205c3d3b3d28638b7356522930.tar.gz
|
||||
https://deps.files.ghostty.org/z2d-7dbae85c81784dba9988320bf9543ed9a81350c8.tar.gz
|
||||
https://deps.files.ghostty.org/zf-c35c421f84895193246db06c40683c1a30e616ef.tar.gz
|
||||
https://deps.files.ghostty.org/zig_js-3c23860e47fdcdc5af805efb7fd0bdac5fd3e9bc.tar.gz
|
||||
https://deps.files.ghostty.org/zig_objc-c8de82ff80281215ad92900866dab7103a8efa8b.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ghostty-org/zig-gobject/releases/download/0.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst
|
||||
https://github.com/vancluever/arocc/archive/ecbc5c799574e0da2758a961b12efa586007f03c.tar.gz
|
||||
https://github.com/vancluever/arocc/archive/f97cdfc3779aec4b242299e2fc9a1c828c3547c6.tar.gz
|
||||
https://gitlab.freedesktop.org/api/v4/projects/890/packages/generic/fontconfig/2.18.3/fontconfig-2.18.3.tar.xz
|
||||
|
||||
@@ -40,13 +40,17 @@ void on_title_changed(GhosttyTerminal terminal, void* userdata) {
|
||||
//! [effects-title-changed]
|
||||
|
||||
//! [effects-clipboard-write]
|
||||
GhosttyClipboardWriteResult on_clipboard_write(
|
||||
void on_clipboard_write(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardWrite* write) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
|
||||
// The write is synchronous: a real embedder would ask the user for
|
||||
// permission here (unless write->granted) and the VT stream waits until
|
||||
// this callback returns. The replied result is sent to the program
|
||||
// (OSC 5522) through the write_pty callback.
|
||||
printf(" clipboard write (location=%d, contents=%zu)\n",
|
||||
(int)write->location, write->contents_len);
|
||||
if (write->contents_len == 0) {
|
||||
@@ -66,10 +70,55 @@ GhosttyClipboardWriteResult on_clipboard_write(
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
return GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS;
|
||||
GhosttyClipboardWriteReply reply = {
|
||||
.size = sizeof(reply),
|
||||
.result = GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS,
|
||||
.remember = false,
|
||||
};
|
||||
write->reply(write, &reply);
|
||||
}
|
||||
//! [effects-clipboard-write]
|
||||
|
||||
//! [effects-clipboard-read]
|
||||
void on_clipboard_read(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardRead* read) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
|
||||
// The read is synchronous: a real embedder would ask the user for
|
||||
// permission here (unless read->granted) and the VT stream waits until
|
||||
// this callback returns. The reply is sent to the program through the
|
||||
// write_pty callback.
|
||||
printf(" clipboard read (location=%d, mimes=%zu)\n",
|
||||
(int)read->location, read->mimes_len);
|
||||
for (size_t i = 0; i < read->mimes_len; i++) {
|
||||
printf(" ");
|
||||
fwrite(read->mimes[i].ptr, 1, read->mimes[i].len, stdout);
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
// Reply with every requested representation we have. This example only
|
||||
// has text.
|
||||
const char* text = "Hello from the clipboard";
|
||||
GhosttyClipboardContent content = {
|
||||
.mime = {.ptr = (const uint8_t*)"text/plain", .len = 10},
|
||||
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
|
||||
};
|
||||
GhosttyClipboardReadReply reply = {
|
||||
.size = sizeof(reply),
|
||||
.result = GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS,
|
||||
.contents = &content,
|
||||
.contents_len = 1,
|
||||
.available = NULL,
|
||||
.available_len = 0,
|
||||
.remember = false,
|
||||
};
|
||||
read->reply(read, &reply);
|
||||
}
|
||||
//! [effects-clipboard-read]
|
||||
|
||||
//! [effects-unknown-sequence]
|
||||
void on_unknown_sequence(
|
||||
GhosttyTerminal terminal,
|
||||
@@ -118,6 +167,8 @@ int main() {
|
||||
(const void *)on_title_changed);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE,
|
||||
(const void *)on_clipboard_write);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ,
|
||||
(const void *)on_clipboard_read);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE,
|
||||
(const void *)on_unknown_sequence);
|
||||
|
||||
@@ -154,13 +205,19 @@ int main() {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)clipboard_seq,
|
||||
strlen(clipboard_seq));
|
||||
|
||||
// 5. Unsupported APC sequence
|
||||
// 5. Clipboard read (OSC 52 ; c ; ? ST)
|
||||
printf("Sending clipboard read:\n");
|
||||
const char* clipboard_read_seq = "\x1B]52;c;?\x1B\\";
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)clipboard_read_seq,
|
||||
strlen(clipboard_read_seq));
|
||||
|
||||
// 6. Unsupported APC sequence
|
||||
printf("Sending unknown APC:\n");
|
||||
const char* unknown_apc = "\x1B_private-command;payload\x1B\\";
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)unknown_apc,
|
||||
strlen(unknown_apc));
|
||||
|
||||
// 6. Another bell to show the counter increments
|
||||
// 7. Another bell to show the counter increments
|
||||
printf("Sending another BEL:\n");
|
||||
ghostty_terminal_vt_write(terminal, &bel, 1);
|
||||
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
typedef struct {
|
||||
FILE *file;
|
||||
size_t written;
|
||||
} OutputWriter;
|
||||
|
||||
static bool write_output(void *userdata, const uint8_t *data, size_t len) {
|
||||
OutputWriter *output = userdata;
|
||||
size_t offset = 0;
|
||||
while (offset < len) {
|
||||
size_t written = fwrite(data + offset, 1, len - offset, output->file);
|
||||
output->written += written;
|
||||
offset += written;
|
||||
if (written == 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
// Create a terminal with a small grid
|
||||
GhosttyTerminal terminal;
|
||||
@@ -14,8 +32,8 @@ int main() {
|
||||
// cursor movement and styling sequences.
|
||||
const char *commands[] = {
|
||||
"Line 1: Hello World!\r\n", // Simple text on row 1
|
||||
"Line 2: \033[1mBold\033[0m and " // Bold text on row 2
|
||||
"\033[4mUnderline\033[0m\r\n",
|
||||
("Line 2: \033[1mBold\033[0m and " // Bold text on row 2
|
||||
"\033[4mUnderline\033[0m\r\n"),
|
||||
"Line 3: placeholder\r\n", // Will be overwritten below
|
||||
"\033[3;1H", // CUP: move cursor back to row 3, col 1
|
||||
"\033[2K", // EL: erase the entire line
|
||||
@@ -39,19 +57,16 @@ int main() {
|
||||
result = ghostty_formatter_terminal_new(NULL, &formatter, terminal, fmt_opts);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Format into an allocated buffer
|
||||
uint8_t *buf = NULL;
|
||||
size_t len = 0;
|
||||
result = ghostty_formatter_format_alloc(formatter, NULL, &buf, &len);
|
||||
// Stream the formatted output directly to stdout. The writer retains the
|
||||
// exact byte count and reports destination errors through its return value.
|
||||
OutputWriter output = {.file = stdout};
|
||||
GhosttyWriter writer = {.write = write_output, .userdata = &output};
|
||||
printf("Formatted output:\n");
|
||||
result = ghostty_formatter_format(formatter, writer);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Print the formatted output
|
||||
printf("Formatted output (%zu bytes):\n", len);
|
||||
fwrite(buf, 1, len, stdout);
|
||||
printf("\n");
|
||||
printf("\n(%zu bytes)\n", output.written);
|
||||
|
||||
// Clean up
|
||||
ghostty_free(NULL, buf, len);
|
||||
ghostty_formatter_free(formatter);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Example: `ghostty-vt` Paste Utilities
|
||||
# Example: `ghostty-vt` Paste
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` paste
|
||||
utilities to check if paste data is safe and encode it for terminal input.
|
||||
This contains a simple example of how to paste into a `ghostty-vt`
|
||||
terminal with `ghostty_terminal_paste`: plain and bracketed (mode 2004)
|
||||
text pastes, the unsafe-paste confirmation flow, and Kitty clipboard
|
||||
protocol paste events (mode 5522) including the program's follow-up
|
||||
clipboard read. The clipboard's data is produced on demand through a
|
||||
read callback, so only what is actually pasted is ever read, and the
|
||||
result streams to the pty in chunks. It also shows the terminal-free
|
||||
building blocks for checking paste safety and encoding paste data.
|
||||
|
||||
This uses a `build.zig` and `Zig` to build the C program so that we
|
||||
can reuse a lot of our build logic and depend directly on our source
|
||||
|
||||
@@ -1,7 +1,160 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
#define GS(s) ((GhosttyString){.ptr = (const uint8_t*)(s), .len = sizeof(s) - 1})
|
||||
|
||||
// Print bytes destined for the pty with control characters made visible.
|
||||
static void print_escaped(const uint8_t* data, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
switch (data[i]) {
|
||||
case 0x1b: printf("ESC"); break;
|
||||
case '\r': printf("\\r"); break;
|
||||
case '\n': printf("\\n"); break;
|
||||
default: putchar(data[i]); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The base64 password of the last paste event, captured from the OK
|
||||
// packet so the example can play the program's side of the protocol.
|
||||
static char event_pw[128];
|
||||
|
||||
// Everything the terminal writes to the running program: the pasted
|
||||
// text, or the paste event packets when mode 5522 is enabled.
|
||||
static void on_write_pty(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const uint8_t* data,
|
||||
size_t len) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
printf(" -> pty (%zu bytes): ", len);
|
||||
print_escaped(data, len);
|
||||
printf("\n");
|
||||
|
||||
// A paste event's OK packet: OSC 5522 ; type=read:status=OK:pw=<b64> ST
|
||||
const char* prefix = "\x1b]5522;type=read:status=OK:pw=";
|
||||
size_t prefix_len = strlen(prefix);
|
||||
if (len > prefix_len && memcmp(data, prefix, prefix_len) == 0) {
|
||||
size_t end = prefix_len;
|
||||
while (end < len && data[end] != 0x1b) end++;
|
||||
size_t pw_len = end - prefix_len;
|
||||
if (pw_len < sizeof(event_pw)) {
|
||||
memcpy(event_pw, data + prefix_len, pw_len);
|
||||
event_pw[pw_len] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Serves clipboard reads. After a paste event the program's read arrives
|
||||
// with `granted` set, because it carries the event's one-time password,
|
||||
// so the embedder skips its permission prompt.
|
||||
static void on_clipboard_read(GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardRead* read) {
|
||||
(void)terminal;
|
||||
(void)userdata;
|
||||
printf(" clipboard read: name=\"");
|
||||
fwrite(read->name.ptr, 1, read->name.len, stdout);
|
||||
printf("\" granted=%s\n", read->granted ? "yes (no prompt needed)" : "no");
|
||||
|
||||
const char* text = "hello from the clipboard";
|
||||
GhosttyClipboardContent content = {
|
||||
.mime = GS("text/plain"),
|
||||
.data = {.ptr = (const uint8_t*)text, .len = strlen(text)},
|
||||
};
|
||||
GhosttyClipboardReadReply reply = {
|
||||
.size = sizeof(reply),
|
||||
.result = GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS,
|
||||
.contents = &content,
|
||||
.contents_len = 1,
|
||||
.available = NULL,
|
||||
.available_len = 0,
|
||||
.remember = false,
|
||||
};
|
||||
read->reply(read, &reply);
|
||||
}
|
||||
|
||||
// A real embedder would show a dialog here.
|
||||
static bool confirm_with_user(void) {
|
||||
printf(" paste could inject commands; user confirmed\n");
|
||||
return true;
|
||||
}
|
||||
|
||||
//! [terminal-paste]
|
||||
// What the clipboard holds. A real embedder would keep a handle to the
|
||||
// pasteboard or its items here; the data is only produced on demand.
|
||||
typedef struct {
|
||||
const char* text;
|
||||
} clipboard_t;
|
||||
|
||||
// Produces the data of one representation when the terminal needs it.
|
||||
// Only the text is ever read: the image is listed on a paste event
|
||||
// but never requested, so a large image costs nothing to paste.
|
||||
// Nothing written to the writer is retained, so the data can be
|
||||
// streamed from anywhere in pieces of any size.
|
||||
static bool read_clipboard(void* userdata, GhosttyString mime, GhosttyWriter writer) {
|
||||
clipboard_t* clipboard = userdata;
|
||||
if (mime.len == strlen("text/plain") &&
|
||||
memcmp(mime.ptr, "text/plain", mime.len) == 0) {
|
||||
// Stream the text in small pieces just to show that it works.
|
||||
const uint8_t* data = (const uint8_t*)clipboard->text;
|
||||
size_t len = strlen(clipboard->text);
|
||||
for (size_t offset = 0; offset < len; offset += 4) {
|
||||
size_t n = len - offset < 4 ? len - offset : 4;
|
||||
if (!writer.write(writer.userdata, data + offset, n)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
printf(" image read requested, which never happens\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Paste whatever the clipboard holds. The terminal applies its own
|
||||
// state: bracketed paste framing (mode 2004) or a Kitty paste event
|
||||
// (mode 5522) instead of the text.
|
||||
static void paste_clipboard(GhosttyTerminal terminal, const char* text) {
|
||||
clipboard_t clipboard = {.text = text};
|
||||
GhosttyString mimes[] = {
|
||||
// The first text representation is what a text paste writes.
|
||||
GS("text/plain"),
|
||||
// Listed on a paste event, never read.
|
||||
GS("image/png"),
|
||||
};
|
||||
GhosttyPaste paste = {
|
||||
.size = sizeof(paste),
|
||||
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
|
||||
.source = GHOSTTY_PASTE_SOURCE_CLIPBOARD,
|
||||
.mimes = mimes,
|
||||
.mimes_len = sizeof(mimes) / sizeof(mimes[0]),
|
||||
.reader = {.read = read_clipboard, .userdata = &clipboard},
|
||||
.allow_unsafe = false,
|
||||
};
|
||||
|
||||
bool written = false;
|
||||
GhosttyResult result = ghostty_terminal_paste(terminal, &paste, &written);
|
||||
if (result == GHOSTTY_REJECTED) {
|
||||
// The text could inject commands (e.g. a newline outside of a
|
||||
// bracketed paste). Nothing was written; ask, then retry.
|
||||
if (!confirm_with_user()) return;
|
||||
paste.allow_unsafe = true;
|
||||
result = ghostty_terminal_paste(terminal, &paste, &written);
|
||||
}
|
||||
if (result != GHOSTTY_SUCCESS) {
|
||||
fprintf(stderr, "paste failed: %d\n", (int)result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Whether the pty got the text or a paste event depends on the
|
||||
// terminal's modes; either way it went through write_pty above, in
|
||||
// chunks as the text was read.
|
||||
printf(" %s\n", written ? "written" : "nothing to paste");
|
||||
}
|
||||
//! [terminal-paste]
|
||||
|
||||
//! [paste-safety]
|
||||
void safety_example() {
|
||||
const char* safe_data = "hello world";
|
||||
@@ -29,27 +182,87 @@ void encode_example() {
|
||||
|
||||
if (result == GHOSTTY_SUCCESS) {
|
||||
printf("Encoded %zu bytes: ", written);
|
||||
fwrite(buf, 1, written, stdout);
|
||||
print_escaped((const uint8_t*)buf, written);
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
//! [paste-encode]
|
||||
|
||||
static void vt_write(GhosttyTerminal terminal, const char* seq) {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t*)seq, strlen(seq));
|
||||
}
|
||||
|
||||
int main() {
|
||||
GhosttyTerminal terminal = NULL;
|
||||
if (ghostty_terminal_new(NULL, &terminal, 80, 24) != GHOSTTY_SUCCESS) {
|
||||
fprintf(stderr, "Failed to create terminal\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pasted bytes and paste events go to write_pty. Serving clipboard
|
||||
// reads is what lets the terminal send paste events at all: without
|
||||
// this callback the program could never read the clipboard, so pastes
|
||||
// stay text even when mode 5522 is enabled.
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_WRITE_PTY,
|
||||
(const void*)on_write_pty);
|
||||
ghostty_terminal_set(terminal, GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ,
|
||||
(const void*)on_clipboard_read);
|
||||
|
||||
printf("Plain paste:\n");
|
||||
paste_clipboard(terminal, "hello world");
|
||||
|
||||
printf("Paste with a newline (refused, then confirmed):\n");
|
||||
paste_clipboard(terminal, "echo hi\n");
|
||||
|
||||
// The program enables bracketed paste: newlines are safe inside the
|
||||
// frame and are preserved.
|
||||
printf("Bracketed paste (mode 2004):\n");
|
||||
vt_write(terminal, "\x1b[?2004h");
|
||||
paste_clipboard(terminal, "line one\nline two");
|
||||
|
||||
// The program enables paste events: the clipboard's MIME types are
|
||||
// listed with a one-time password instead of writing the data.
|
||||
printf("Paste event (mode 5522):\n");
|
||||
vt_write(terminal, "\x1b[?5522h");
|
||||
paste_clipboard(terminal, "hello world");
|
||||
|
||||
// Play the program's side: read the clipboard with the password from
|
||||
// the event. The read arrives granted and the data is served through
|
||||
// write_pty as base64 without any permission prompt.
|
||||
if (event_pw[0] != 0) {
|
||||
printf("Program reads with the event password:\n");
|
||||
char read_seq[256];
|
||||
snprintf(read_seq, sizeof(read_seq),
|
||||
"\x1b]5522;type=read:pw=%s:name=UGFzdGUgZXZlbnQ=;dGV4dC9wbGFpbg==\x1b\\",
|
||||
event_pw);
|
||||
vt_write(terminal, read_seq);
|
||||
}
|
||||
|
||||
// Text inserted by other means (IME, drag and drop) is never an event.
|
||||
printf("IME text with mode 5522 enabled:\n");
|
||||
{
|
||||
clipboard_t clipboard = {.text = "committed"};
|
||||
GhosttyString mime = GS("text/plain");
|
||||
GhosttyPaste paste = {
|
||||
.size = sizeof(paste),
|
||||
.location = GHOSTTY_CLIPBOARD_LOCATION_STANDARD,
|
||||
.source = GHOSTTY_PASTE_SOURCE_TEXT,
|
||||
.mimes = &mime,
|
||||
.mimes_len = 1,
|
||||
.reader = {.read = read_clipboard, .userdata = &clipboard},
|
||||
.allow_unsafe = true,
|
||||
};
|
||||
bool written = false;
|
||||
if (ghostty_terminal_paste(terminal, &paste, &written) == GHOSTTY_SUCCESS &&
|
||||
written) {
|
||||
printf(" written\n");
|
||||
}
|
||||
}
|
||||
|
||||
ghostty_terminal_free(terminal);
|
||||
|
||||
printf("\nTerminal-free building blocks:\n");
|
||||
safety_example();
|
||||
|
||||
// Test unsafe paste data with bracketed paste end sequence
|
||||
const char *unsafe_escape = "evil\x1b[201~code";
|
||||
if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) {
|
||||
printf("Data with escape sequence is UNSAFE\n");
|
||||
}
|
||||
|
||||
// Test empty data
|
||||
const char *empty_data = "";
|
||||
if (ghostty_paste_is_safe(empty_data, 0)) {
|
||||
printf("Empty data is safe\n");
|
||||
}
|
||||
|
||||
encode_example();
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -97,7 +97,8 @@ int main(void) {
|
||||
// state. These are needed to resolve palette-indexed cell colors.
|
||||
GhosttyRenderStateColors colors =
|
||||
GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
|
||||
result = ghostty_render_state_colors_get(render_state, &colors);
|
||||
result = ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_COLORS, &colors);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
printf("Background: #%02x%02x%02x\n",
|
||||
@@ -107,31 +108,16 @@ int main(void) {
|
||||
//! [render-colors]
|
||||
|
||||
//! [render-cursor]
|
||||
// Read cursor position and visual style from the render state.
|
||||
bool cursor_visible = false;
|
||||
ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISIBLE,
|
||||
&cursor_visible);
|
||||
|
||||
bool cursor_in_viewport = false;
|
||||
ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_HAS_VALUE,
|
||||
&cursor_in_viewport);
|
||||
|
||||
if (cursor_visible && cursor_in_viewport) {
|
||||
uint16_t cx, cy;
|
||||
ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_X, &cx);
|
||||
ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_Y, &cy);
|
||||
|
||||
GhosttyRenderStateCursorVisualStyle style;
|
||||
ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR_VISUAL_STYLE,
|
||||
&style);
|
||||
// Read all cursor state in one call.
|
||||
GhosttyRenderStateCursor cursor =
|
||||
GHOSTTY_INIT_SIZED(GhosttyRenderStateCursor);
|
||||
result = ghostty_render_state_get(
|
||||
render_state, GHOSTTY_RENDER_STATE_DATA_CURSOR, &cursor);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
if (cursor.visible && cursor.viewport_has_value) {
|
||||
const char* style_name = "unknown";
|
||||
switch (style) {
|
||||
switch (cursor.visual_style) {
|
||||
case GHOSTTY_RENDER_STATE_CURSOR_VISUAL_STYLE_BAR:
|
||||
style_name = "bar";
|
||||
break;
|
||||
@@ -145,7 +131,8 @@ int main(void) {
|
||||
style_name = "hollow";
|
||||
break;
|
||||
}
|
||||
printf("Cursor at (%u, %u), style: %s\n", cx, cy, style_name);
|
||||
printf("Cursor at (%u, %u), style: %s\n",
|
||||
cursor.viewport_x, cursor.viewport_y, style_name);
|
||||
}
|
||||
//! [render-cursor]
|
||||
|
||||
@@ -165,15 +152,9 @@ int main(void) {
|
||||
result = ghostty_render_state_row_cells_new(NULL, &cells);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
int row_index = 0;
|
||||
while (ghostty_render_state_row_iterator_next(row_iter)) {
|
||||
// Check per-row dirty state; a real renderer would skip clean rows.
|
||||
bool row_dirty = false;
|
||||
ghostty_render_state_row_get(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_DIRTY, &row_dirty);
|
||||
|
||||
printf("Row %2d [%s]: ", row_index,
|
||||
row_dirty ? "dirty" : "clean");
|
||||
uint16_t row_y = 0;
|
||||
while (ghostty_render_state_row_iterator_next_dirty(row_iter, &row_y)) {
|
||||
printf("Row %2u [dirty]: ", row_y);
|
||||
|
||||
// Query the row-local selection range. Rows without a selection return
|
||||
// GHOSTTY_NO_VALUE; selected rows return inclusive start/end columns.
|
||||
@@ -239,22 +220,13 @@ int main(void) {
|
||||
}
|
||||
|
||||
printf("\n");
|
||||
|
||||
// Clear per-row dirty flag after "rendering" it.
|
||||
bool clean = false;
|
||||
ghostty_render_state_row_set(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_OPTION_DIRTY, &clean);
|
||||
|
||||
row_index++;
|
||||
}
|
||||
//! [render-row-iterate]
|
||||
|
||||
//! [render-dirty-reset]
|
||||
// After finishing the frame, reset the global dirty state so the next
|
||||
// update can report changes accurately.
|
||||
GhosttyRenderStateDirty clean_state = GHOSTTY_RENDER_STATE_DIRTY_FALSE;
|
||||
result = ghostty_render_state_set(
|
||||
render_state, GHOSTTY_RENDER_STATE_OPTION_DIRTY, &clean_state);
|
||||
// After successfully rendering the complete frame, clear both the global
|
||||
// and per-row dirty state in one call.
|
||||
result = ghostty_render_state_clean(render_state);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
//! [render-dirty-reset]
|
||||
|
||||
|
||||
19
example/c-vt-search/README.md
Normal file
19
example/c-vt-search/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Example: `ghostty-vt` Terminal Search
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` search
|
||||
API from C. It writes content into a terminal, searches it for a
|
||||
string, navigates between the matches like a find bar, and reads the
|
||||
viewport matches an embedder would use to draw highlights.
|
||||
|
||||
This uses a `build.zig` and `Zig` to build the C program so that we
|
||||
can reuse a lot of our build logic and depend directly on our source
|
||||
tree, but Ghostty emits a standard C library that can be used with any
|
||||
C tooling.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the program:
|
||||
|
||||
```shell-session
|
||||
zig build run
|
||||
```
|
||||
42
example/c-vt-search/build.zig
Normal file
42
example/c-vt-search/build.zig
Normal file
@@ -0,0 +1,42 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
|
||||
const exe_mod = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe_mod.addCSourceFiles(.{
|
||||
.root = b.path("src"),
|
||||
.files = &.{"main.c"},
|
||||
});
|
||||
|
||||
// You'll want to use a lazy dependency here so that ghostty is only
|
||||
// downloaded if you actually need it.
|
||||
if (b.lazyDependency("ghostty", .{
|
||||
// Setting simd to false will force a pure static build that
|
||||
// doesn't even require libc, but it has a significant performance
|
||||
// penalty. If your embedding app requires libc anyway, you should
|
||||
// always keep simd enabled.
|
||||
// .simd = false,
|
||||
})) |dep| {
|
||||
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
|
||||
}
|
||||
|
||||
// Exe
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "c_vt_search",
|
||||
.root_module = exe_mod,
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Run
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
||||
24
example/c-vt-search/build.zig.zon
Normal file
24
example/c-vt-search/build.zig.zon
Normal file
@@ -0,0 +1,24 @@
|
||||
.{
|
||||
.name = .c_vt_search,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x91a25a7f6cd3ff58,
|
||||
.minimum_zig_version = "0.15.1",
|
||||
.dependencies = .{
|
||||
// Ghostty dependency. In reality, you'd probably use a URL-based
|
||||
// dependency like the one showed (and commented out) below this one.
|
||||
// We use a path dependency here for simplicity and to ensure our
|
||||
// examples always test against the source they're bundled with.
|
||||
.ghostty = .{ .path = "../../" },
|
||||
|
||||
// Example of what a URL-based dependency looks like:
|
||||
// .ghostty = .{
|
||||
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
|
||||
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
|
||||
// },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
117
example/c-vt-search/src/main.c
Normal file
117
example/c-vt-search/src/main.c
Normal file
@@ -0,0 +1,117 @@
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [search-main]
|
||||
int main() {
|
||||
// Create a terminal and fill it with some content to search.
|
||||
GhosttyTerminal terminal;
|
||||
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
const char *lines[] = {
|
||||
"$ make test\r\n",
|
||||
"compiling module A... ok\r\n",
|
||||
"compiling module B... error: missing semicolon\r\n",
|
||||
"linking... error: undefined symbol\r\n",
|
||||
"$ grep -n ERROR build.log\r\n",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(lines) / sizeof(lines[0]); i++) {
|
||||
ghostty_terminal_vt_write(terminal, (const uint8_t *)lines[i],
|
||||
strlen(lines[i]));
|
||||
}
|
||||
|
||||
// The user opened the find bar, so create a search bound to the
|
||||
// terminal. It starts idle until it has a needle.
|
||||
GhosttySearch search;
|
||||
result = ghostty_search_new(NULL, &search, terminal);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// The user typed a query. Matching is byte-exact except ASCII
|
||||
// letters, which compare case-insensitively, so "error" also finds
|
||||
// "ERROR". Retyping just sets the needle again: a changed needle
|
||||
// restarts the search and an unchanged one keeps its results.
|
||||
GhosttyString needle = { (const uint8_t *)"error", 5 };
|
||||
result = ghostty_search_set(search, GHOSTTY_SEARCH_OPT_NEEDLE, &needle);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Drive the search. Interactive embedders interleave
|
||||
// ghostty_search_tick() and ghostty_search_feed() with their event
|
||||
// loop, but for a one-shot search we can just run it to completion.
|
||||
result = ghostty_search_run(search);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// The total match count, for find bar text like "1 of 3".
|
||||
size_t total = 0;
|
||||
result = ghostty_search_get(search, GHOSTTY_SEARCH_DATA_TOTAL_MATCHES,
|
||||
&total);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
printf("%zu matches for \"error\"\n", total);
|
||||
|
||||
// The user pressed Enter, so select the next match. Selection starts
|
||||
// at the newest match, moves toward older content, and wraps around.
|
||||
// This scrolls the viewport to the match if it isn't visible, per
|
||||
// the GHOSTTY_SEARCH_OPT_SELECT_SCROLL policy.
|
||||
while (true) {
|
||||
result = ghostty_search_set(search, GHOSTTY_SEARCH_OPT_SELECT_NEXT, NULL);
|
||||
if (result != GHOSTTY_SUCCESS) break;
|
||||
|
||||
// Read the selection state in one call. Index 0 is the newest
|
||||
// match, so a "k of n" find bar renders index + 1.
|
||||
size_t idx = 0;
|
||||
GhosttySelection match = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
const GhosttySearchData keys[] = {
|
||||
GHOSTTY_SEARCH_DATA_SELECTED_INDEX,
|
||||
GHOSTTY_SEARCH_DATA_SELECTED_MATCH,
|
||||
};
|
||||
void *values[] = { &idx, &match };
|
||||
result = ghostty_search_get_multi(
|
||||
search, sizeof(keys) / sizeof(keys[0]), keys, values, NULL);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
printf("selected %zu of %zu\n", idx + 1, total);
|
||||
|
||||
// Wrapped back around to the first match: stop.
|
||||
if (idx + 1 == total) break;
|
||||
}
|
||||
|
||||
// Each frame while the find bar is open, feed to catch up with any
|
||||
// terminal changes and then read the viewport matches to draw
|
||||
// highlights. The list can include matches just past the viewport
|
||||
// when they share a page with it, so convert each endpoint to
|
||||
// viewport coordinates and skip matches outside the visible rows.
|
||||
result = ghostty_search_feed(search);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttySelection viewport_storage[64];
|
||||
GhosttySelectionBuffer viewport = {
|
||||
.ptr = viewport_storage,
|
||||
.cap = sizeof(viewport_storage) / sizeof(viewport_storage[0]),
|
||||
};
|
||||
result = ghostty_search_get(search, GHOSTTY_SEARCH_DATA_VIEWPORT_MATCHES,
|
||||
&viewport);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
for (size_t i = 0; i < viewport.len; i++) {
|
||||
GhosttyPointCoordinate start, end;
|
||||
if (ghostty_terminal_point_from_grid_ref(
|
||||
terminal, &viewport_storage[i].start, GHOSTTY_POINT_TAG_VIEWPORT,
|
||||
&start) != GHOSTTY_SUCCESS) continue;
|
||||
if (ghostty_terminal_point_from_grid_ref(
|
||||
terminal, &viewport_storage[i].end, GHOSTTY_POINT_TAG_VIEWPORT,
|
||||
&end) != GHOSTTY_SUCCESS) continue;
|
||||
if (start.y >= 24 || end.y >= 24) continue;
|
||||
|
||||
// A real embedder draws a highlight rect from start to end here.
|
||||
printf("highlight rows %u-%u, cols %u-%u\n",
|
||||
(unsigned)start.y, (unsigned)end.y,
|
||||
(unsigned)start.x, (unsigned)end.x);
|
||||
}
|
||||
|
||||
// Closing the find bar. The search borrows the terminal, but the
|
||||
// two can be freed in either order.
|
||||
ghostty_search_free(search);
|
||||
ghostty_terminal_free(terminal);
|
||||
return 0;
|
||||
}
|
||||
//! [search-main]
|
||||
@@ -163,6 +163,7 @@
|
||||
let wasmMemory = null;
|
||||
let encoderPtr = null;
|
||||
let lastKeyEvent = null;
|
||||
let typeLayout = null;
|
||||
|
||||
async function loadWasm() {
|
||||
try {
|
||||
@@ -184,6 +185,12 @@
|
||||
|
||||
wasmInstance = wasmModule.instance;
|
||||
wasmMemory = wasmInstance.exports.memory;
|
||||
|
||||
const jsonPtr = wasmInstance.exports.ghostty_type_json();
|
||||
const jsonStr = new TextDecoder().decode(
|
||||
new Uint8Array(wasmMemory.buffer, jsonPtr, wasmMemory.buffer.byteLength - jsonPtr)
|
||||
).split('\0')[0];
|
||||
typeLayout = JSON.parse(jsonStr);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -199,6 +206,15 @@
|
||||
return wasmMemory.buffer;
|
||||
}
|
||||
|
||||
function readUsize(ptr) {
|
||||
const view = new DataView(getBuffer());
|
||||
switch (typeLayout.abi.usize_size) {
|
||||
case 4: return view.getUint32(ptr, true);
|
||||
case 8: return Number(view.getBigUint64(ptr, true));
|
||||
default: throw new Error('unsupported size_t width');
|
||||
}
|
||||
}
|
||||
|
||||
function formatHex(bytes) {
|
||||
return Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
@@ -415,16 +431,28 @@
|
||||
function encodeKeyEvent(event) {
|
||||
if (!encoderPtr) return null;
|
||||
|
||||
const usizeSize = typeLayout.abi.usize_size;
|
||||
let eventPtrPtr = 0;
|
||||
let eventPtr = 0;
|
||||
let utf8Ptr = 0;
|
||||
let utf8Length = 0;
|
||||
let requiredPtr = 0;
|
||||
let required = 0;
|
||||
let bufPtr = 0;
|
||||
let writtenPtr = 0;
|
||||
|
||||
try {
|
||||
// Create key event
|
||||
const eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
const result = wasmInstance.exports.ghostty_key_event_new(0, eventPtrPtr);
|
||||
|
||||
if (result !== 0) {
|
||||
throw new Error(`ghostty_key_event_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
const eventPtr = new DataView(getBuffer()).getUint32(eventPtrPtr, true);
|
||||
eventPtr = wasmInstance.exports.ghostty_wasm_take_opaque(eventPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(eventPtrPtr);
|
||||
eventPtrPtr = 0;
|
||||
|
||||
// Get action from radio buttons
|
||||
const actionRadio = document.querySelector('input[name="action"]:checked');
|
||||
@@ -458,9 +486,10 @@
|
||||
// Set UTF-8 text from the key event (the actual character produced)
|
||||
if (event.key.length === 1) {
|
||||
const utf8Bytes = new TextEncoder().encode(event.key);
|
||||
const utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(utf8Bytes.length);
|
||||
utf8Length = utf8Bytes.length;
|
||||
utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc(utf8Length);
|
||||
new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr);
|
||||
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length);
|
||||
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Length);
|
||||
}
|
||||
|
||||
// Set unshifted codepoint
|
||||
@@ -470,15 +499,15 @@
|
||||
}
|
||||
|
||||
// Encode the key event
|
||||
const requiredPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
||||
requiredPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
|
||||
wasmInstance.exports.ghostty_key_encoder_encode(
|
||||
encoderPtr, eventPtr, 0, 0, requiredPtr
|
||||
);
|
||||
|
||||
const required = new DataView(getBuffer()).getUint32(requiredPtr, true);
|
||||
required = readUsize(requiredPtr);
|
||||
|
||||
const bufPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(required);
|
||||
const writtenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
||||
bufPtr = wasmInstance.exports.ghostty_wasm_alloc(required);
|
||||
writtenPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
|
||||
const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode(
|
||||
encoderPtr, eventPtr, bufPtr, required, writtenPtr
|
||||
);
|
||||
@@ -487,7 +516,7 @@
|
||||
return null; // No encoding for this key
|
||||
}
|
||||
|
||||
const written = new DataView(getBuffer()).getUint32(writtenPtr, true);
|
||||
const written = readUsize(writtenPtr);
|
||||
const encoded = new Uint8Array(getBuffer()).slice(bufPtr, bufPtr + written);
|
||||
|
||||
return {
|
||||
@@ -498,6 +527,13 @@
|
||||
} catch (e) {
|
||||
console.error('Encoding error:', e);
|
||||
return null;
|
||||
} finally {
|
||||
wasmInstance.exports.ghostty_wasm_free(writtenPtr, usizeSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(bufPtr, required);
|
||||
wasmInstance.exports.ghostty_wasm_free(requiredPtr, usizeSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(utf8Ptr, utf8Length);
|
||||
wasmInstance.exports.ghostty_key_event_free(eventPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(eventPtrPtr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,13 +591,14 @@
|
||||
if (!encoderPtr) return;
|
||||
|
||||
const flags = getKittyFlags();
|
||||
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
new DataView(getBuffer()).setUint8(flagsPtr, flags);
|
||||
wasmInstance.exports.ghostty_key_encoder_setopt(
|
||||
encoderPtr,
|
||||
5, // GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS
|
||||
flagsPtr
|
||||
);
|
||||
wasmInstance.exports.ghostty_wasm_free(flagsPtr, 1);
|
||||
|
||||
// Re-encode last key with new flags
|
||||
reencodeLastKey();
|
||||
@@ -649,7 +686,8 @@
|
||||
throw new Error(`ghostty_key_encoder_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
encoderPtr = new DataView(getBuffer()).getUint32(encoderPtrPtr, true);
|
||||
encoderPtr = wasmInstance.exports.ghostty_wasm_take_opaque(encoderPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(encoderPtrPtr);
|
||||
|
||||
// Set kitty flags based on checkboxes
|
||||
updateEncoderFlags();
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
<script>
|
||||
let wasmInstance = null;
|
||||
let wasmMemory = null;
|
||||
let typeLayout = null;
|
||||
|
||||
async function loadWasm() {
|
||||
try {
|
||||
@@ -124,6 +125,12 @@
|
||||
|
||||
wasmInstance = wasmModule.instance;
|
||||
wasmMemory = wasmInstance.exports.memory;
|
||||
|
||||
const jsonPtr = wasmInstance.exports.ghostty_type_json();
|
||||
const jsonStr = new TextDecoder().decode(
|
||||
new Uint8Array(wasmMemory.buffer, jsonPtr, wasmMemory.buffer.byteLength - jsonPtr)
|
||||
).split('\0')[0];
|
||||
typeLayout = JSON.parse(jsonStr);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -252,18 +259,22 @@
|
||||
throw new Error(`ghostty_sgr_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
const parserPtr = new DataView(getBuffer()).getUint32(parserPtrPtr, true);
|
||||
const parserPtr = wasmInstance.exports.ghostty_wasm_take_opaque(parserPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(parserPtrPtr);
|
||||
|
||||
// Allocate and set parameters
|
||||
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc_u16_array(params.length);
|
||||
const paramsByteLength = params.length * Uint16Array.BYTES_PER_ELEMENT;
|
||||
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc(paramsByteLength);
|
||||
const paramsView = new Uint16Array(getBuffer(), paramsPtr, params.length);
|
||||
params.forEach((p, i) => paramsView[i] = p);
|
||||
|
||||
// Allocate and set separators (or use null if empty)
|
||||
let sepsPtr = 0;
|
||||
const sepsByteLength = separators.length > 0 ? params.length : 0;
|
||||
if (separators.length > 0) {
|
||||
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(separators.length);
|
||||
const sepsView = new Uint8Array(getBuffer(), sepsPtr, separators.length);
|
||||
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc(sepsByteLength);
|
||||
const sepsView = new Uint8Array(getBuffer(), sepsPtr, sepsByteLength);
|
||||
sepsView.fill(0);
|
||||
separators.forEach((s, i) => sepsView[i] = s.charCodeAt(0));
|
||||
}
|
||||
|
||||
@@ -289,7 +300,8 @@
|
||||
output += 'm\n\n';
|
||||
|
||||
// Iterate through attributes
|
||||
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc_sgr_attribute();
|
||||
const attrSize = typeLayout.types.GhosttySgrAttribute.size;
|
||||
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc(attrSize);
|
||||
let count = 0;
|
||||
|
||||
while (wasmInstance.exports.ghostty_sgr_next(parserPtr, attrPtr)) {
|
||||
@@ -313,9 +325,9 @@
|
||||
|
||||
case SGR_ATTR_TAGS.DIRECT_COLOR_FG: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -325,17 +337,17 @@
|
||||
|
||||
output += `Foreground RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case SGR_ATTR_TAGS.DIRECT_COLOR_BG: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -345,17 +357,17 @@
|
||||
|
||||
output += `Background RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case SGR_ATTR_TAGS.UNDERLINE_COLOR: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -365,9 +377,9 @@
|
||||
|
||||
output += `Underline color RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -415,7 +427,9 @@
|
||||
outputDiv.textContent = output;
|
||||
|
||||
// Cleanup
|
||||
wasmInstance.exports.ghostty_wasm_free_sgr_attribute(attrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(attrPtr, attrSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(paramsPtr, paramsByteLength);
|
||||
wasmInstance.exports.ghostty_wasm_free(sepsPtr, sepsByteLength);
|
||||
wasmInstance.exports.ghostty_sgr_free(parserPtr);
|
||||
|
||||
} catch (e) {
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
|
||||
// Look up a field's offset and DataView setter from the type layout JSON.
|
||||
function fieldInfo(structName, fieldName) {
|
||||
const field = typeLayout[structName].fields[fieldName];
|
||||
const field = typeLayout.types[structName].fields[fieldName];
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -166,9 +166,16 @@
|
||||
switch (field.type) {
|
||||
case 'u8': case 'bool': view.setUint8(field.offset, value); break;
|
||||
case 'u16': view.setUint16(field.offset, value, true); break;
|
||||
case 'u32': case 'enum': view.setUint32(field.offset, value, true); break;
|
||||
case 'u32': view.setUint32(field.offset, value, true); break;
|
||||
case 'u64': view.setBigUint64(field.offset, BigInt(value), true); break;
|
||||
default: throw new Error(`Unsupported field type: ${field.type}`);
|
||||
default: {
|
||||
const type = typeLayout.types[field.type];
|
||||
if (type?.kind === 'enum' && type.underlying === 'i32') {
|
||||
view.setInt32(field.offset, value, true);
|
||||
break;
|
||||
}
|
||||
throw new Error(`Unsupported field type: ${field.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +183,15 @@
|
||||
return wasmMemory.buffer;
|
||||
}
|
||||
|
||||
function readUsize(ptr) {
|
||||
const view = new DataView(getBuffer());
|
||||
switch (typeLayout.abi.usize_size) {
|
||||
case 4: return view.getUint32(ptr, true);
|
||||
case 8: return Number(view.getBigUint64(ptr, true));
|
||||
default: throw new Error('unsupported size_t width');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse escape sequences in the input string (e.g. \x1b, \r, \n)
|
||||
function parseEscapes(str) {
|
||||
return str
|
||||
@@ -186,18 +202,18 @@
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
// GHOSTTY_FORMATTER_FORMAT_PLAIN = 0
|
||||
const GHOSTTY_FORMATTER_FORMAT_PLAIN = 0;
|
||||
// GHOSTTY_SUCCESS = 0
|
||||
const GHOSTTY_SUCCESS = 0;
|
||||
|
||||
function run() {
|
||||
const outputDiv = document.getElementById('output');
|
||||
|
||||
try {
|
||||
const GHOSTTY_FORMATTER_FORMAT_PLAIN =
|
||||
typeLayout.types.GhosttyFormatterFormat.values.PLAIN;
|
||||
const GHOSTTY_SUCCESS =
|
||||
typeLayout.types.GhosttyResult.values.SUCCESS;
|
||||
const cols = parseInt(document.getElementById('cols').value, 10);
|
||||
const rows = parseInt(document.getElementById('rows').value, 10);
|
||||
const vtText = parseEscapes(document.getElementById('vtInput').value);
|
||||
const usizeSize = typeLayout.abi.usize_size;
|
||||
|
||||
// Allocate pointer to receive the terminal handle
|
||||
const termPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
@@ -213,19 +229,19 @@
|
||||
throw new Error(`ghostty_terminal_new failed with result ${newResult}`);
|
||||
}
|
||||
|
||||
const termPtr = new DataView(getBuffer()).getUint32(termPtrPtr, true);
|
||||
const termPtr = wasmInstance.exports.ghostty_wasm_take_opaque(termPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(termPtrPtr);
|
||||
|
||||
// Write VT data to the terminal
|
||||
const vtBytes = new TextEncoder().encode(vtText);
|
||||
const dataPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(vtBytes.length);
|
||||
const dataPtr = wasmInstance.exports.ghostty_wasm_alloc(vtBytes.length);
|
||||
new Uint8Array(getBuffer()).set(vtBytes, dataPtr);
|
||||
wasmInstance.exports.ghostty_terminal_vt_write(termPtr, dataPtr, vtBytes.length);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length);
|
||||
wasmInstance.exports.ghostty_wasm_free(dataPtr, vtBytes.length);
|
||||
|
||||
// Create a plain-text formatter
|
||||
const FMT_OPTS_SIZE = typeLayout['GhosttyFormatterTerminalOptions'].size;
|
||||
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE);
|
||||
const FMT_OPTS_SIZE = typeLayout.types.GhosttyFormatterTerminalOptions.size;
|
||||
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc(FMT_OPTS_SIZE);
|
||||
new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0);
|
||||
const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE);
|
||||
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'size', FMT_OPTS_SIZE);
|
||||
@@ -235,12 +251,12 @@
|
||||
|
||||
// Set the nested sized-struct `size` fields for extra and extra.screen
|
||||
const extraOffset = fieldInfo('GhosttyFormatterTerminalOptions', 'extra').offset;
|
||||
const extraSize = typeLayout['GhosttyFormatterTerminalExtra'].size;
|
||||
const extraSize = typeLayout.types.GhosttyFormatterTerminalExtra.size;
|
||||
const extraSizeField = fieldInfo('GhosttyFormatterTerminalExtra', 'size');
|
||||
fmtOptsView.setUint32(extraOffset + extraSizeField.offset, extraSize, true);
|
||||
|
||||
const screenOffset = fieldInfo('GhosttyFormatterTerminalExtra', 'screen').offset;
|
||||
const screenSize = typeLayout['GhosttyFormatterScreenExtra'].size;
|
||||
const screenSize = typeLayout.types.GhosttyFormatterScreenExtra.size;
|
||||
const screenSizeField = fieldInfo('GhosttyFormatterScreenExtra', 'size');
|
||||
fmtOptsView.setUint32(extraOffset + screenOffset + screenSizeField.offset, screenSize, true);
|
||||
|
||||
@@ -248,19 +264,19 @@
|
||||
const fmtResult = wasmInstance.exports.ghostty_formatter_terminal_new(
|
||||
0, fmtPtrPtr, termPtr, fmtOptsPtr
|
||||
);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8_array(fmtOptsPtr, FMT_OPTS_SIZE);
|
||||
wasmInstance.exports.ghostty_wasm_free(fmtOptsPtr, FMT_OPTS_SIZE);
|
||||
|
||||
if (fmtResult !== GHOSTTY_SUCCESS) {
|
||||
wasmInstance.exports.ghostty_terminal_free(termPtr);
|
||||
throw new Error(`ghostty_formatter_terminal_new failed with result ${fmtResult}`);
|
||||
}
|
||||
|
||||
const fmtPtr = new DataView(getBuffer()).getUint32(fmtPtrPtr, true);
|
||||
const fmtPtr = wasmInstance.exports.ghostty_wasm_take_opaque(fmtPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(fmtPtrPtr);
|
||||
|
||||
// Format with alloc
|
||||
const outPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
const outLenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
||||
const outLenPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
|
||||
const formatResult = wasmInstance.exports.ghostty_formatter_format_alloc(
|
||||
fmtPtr, 0, outPtrPtr, outLenPtr
|
||||
);
|
||||
@@ -271,8 +287,8 @@
|
||||
throw new Error(`ghostty_formatter_format_alloc failed with result ${formatResult}`);
|
||||
}
|
||||
|
||||
const outPtr = new DataView(getBuffer()).getUint32(outPtrPtr, true);
|
||||
const outLen = new DataView(getBuffer()).getUint32(outLenPtr, true);
|
||||
const outPtr = wasmInstance.exports.ghostty_wasm_take_opaque(outPtrPtr);
|
||||
const outLen = readUsize(outLenPtr);
|
||||
|
||||
const outBytes = new Uint8Array(getBuffer(), outPtr, outLen);
|
||||
const outText = new TextDecoder().decode(outBytes);
|
||||
@@ -288,7 +304,7 @@
|
||||
// Clean up
|
||||
wasmInstance.exports.ghostty_free(0, outPtr, outLen);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(outPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_usize(outLenPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(outLenPtr, usizeSize);
|
||||
wasmInstance.exports.ghostty_formatter_free(fmtPtr);
|
||||
wasmInstance.exports.ghostty_terminal_free(termPtr);
|
||||
|
||||
|
||||
26
flake.lock
generated
26
flake.lock
generated
@@ -23,11 +23,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782657028,
|
||||
"narHash": "sha256-PHTCpYZCMzJYS3phhywqRAZphKVr2zjvlGYa+H20ZZ4=",
|
||||
"lastModified": 1787661347,
|
||||
"narHash": "sha256-THmnBAdAIV+jT348f3r/vyoeb6ilhcVLwVXkBU7Pai8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "4ad9aaae70c9aaab504127f926c0fa9cfbc2b365",
|
||||
"rev": "52c3a5881c821ad7367bbde075e52b76cdb378ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -38,15 +38,15 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1782545840,
|
||||
"narHash": "sha256-CAi8oAZaE6pTkcYQBnnOlvmfMgG/p1AO0FohKKN3J7I=",
|
||||
"rev": "3d46470bb3030020f7e1361f33514854f5bfa86d",
|
||||
"lastModified": 1787424095,
|
||||
"narHash": "sha256-SpMiSe9OSfWseGAupsdcED3He9mTYTbGMtWb7EVt6pE=",
|
||||
"rev": "174eb786fb68e3a13e4e535a3deea479a0c07a6a",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1023445.3d46470bb303/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1059855.174eb786fb68/nixexprs.tar.zst"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.zst"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
@@ -88,11 +88,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782609341,
|
||||
"narHash": "sha256-OfCPJFS/2Z2ZyjE4adR7PL+ZEqvDQFi2XNI1L8s6wKU=",
|
||||
"lastModified": 1787617283,
|
||||
"narHash": "sha256-jiM1glpDL5ASrqvcWyacI+c1wZo3g6AUhdo6M09AoiY=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "5386fea92c45b154bbeb52d14ef5504f82eda86c",
|
||||
"rev": "da55b9fe9260fb3eb1456c3ee6064b8f738e4ba9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -108,11 +108,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784117571,
|
||||
"lastModified": 1784080626,
|
||||
"narHash": "sha256-Hvnmnuu0VpHiZl+8z+UkMoxC7JZJvRYBqu2Asb0ZJOE=",
|
||||
"owner": "jcollie",
|
||||
"repo": "zon2nix",
|
||||
"rev": "0ce628fb78309cdb13d098ae9c6fdbf87c75d25b",
|
||||
"rev": "776b5f6864a607c141d7966421b433fa1657ba9a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
30
flake.nix
30
flake.nix
@@ -6,10 +6,10 @@
|
||||
# glibc versions used by our dependencies from Nix are compatible with the
|
||||
# system glibc that the user is building for.
|
||||
#
|
||||
# We are currently on nixpkgs-unstable to get Zig 0.15 for our package.nix and
|
||||
# Gnome 49/Gtk 4.20.
|
||||
# We are currently on nixpkgs-unstable to get Zig 0.16 for our package.nix,
|
||||
# Gnome 50/Gtk 4.22, and fontconfig 2.18.
|
||||
#
|
||||
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.zst";
|
||||
|
||||
# Used for shell.nix
|
||||
flake-compat = {
|
||||
@@ -76,25 +76,7 @@
|
||||
devShells = forAllPlatforms (pkgs: {
|
||||
default =
|
||||
pkgs.callPackage ./nix/devShell.nix
|
||||
(let
|
||||
libfyaml =
|
||||
if pkgs.stdenv.hostPlatform.isDarwin
|
||||
then
|
||||
pkgs.libfyaml.overrideAttrs (prev: {
|
||||
# Manually fix libfyaml.pc until NixOS/nixpkgs#515614 is available
|
||||
postInstall =
|
||||
(prev.postInstall or "")
|
||||
+ ''
|
||||
substituteInPlace "$dev/lib/pkgconfig/libfyaml.pc" \
|
||||
--replace-fail " none required" ""
|
||||
'';
|
||||
})
|
||||
else pkgs.libfyaml;
|
||||
|
||||
appstream = pkgs.appstream.override {libfyaml = libfyaml;};
|
||||
libadwaita = pkgs.libadwaita.override {appstream = appstream;};
|
||||
blueprint-compiler = pkgs.blueprint-compiler.override {libadwaita = libadwaita;};
|
||||
in {
|
||||
{
|
||||
zig = zig.packages.${pkgs.stdenv.hostPlatform.system}."0.16.0";
|
||||
wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
|
||||
zon2nix = zon2nix;
|
||||
@@ -107,9 +89,7 @@
|
||||
wcwidth = pyfinal.callPackage ./nix/pkgs/wcwidth.nix {};
|
||||
};
|
||||
};
|
||||
|
||||
inherit appstream libadwaita blueprint-compiler;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
packages =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/vancluever/arocc/archive/ecbc5c799574e0da2758a961b12efa586007f03c.tar.gz",
|
||||
"dest": "vendor/p/aro-0.0.0-JSD1Qk8rOQDnuVcD4jAwMpHitA6pADRKzQ7M7hKRwxvD",
|
||||
"sha256": "aa54487bd727e8fa36744ff0a824dedb2f4ca45f4c2d4d9d2437cf5fd3d0ad1a"
|
||||
"url": "https://github.com/vancluever/arocc/archive/f97cdfc3779aec4b242299e2fc9a1c828c3547c6.tar.gz",
|
||||
"dest": "vendor/p/aro-0.0.0-JSD1Qk6lNgDdcDV4Vh7Sfy-34m2TluIVOdPzMmj_0BjX",
|
||||
"sha256": "42c8cab9135b12518e6af7dc5e461f055c78b863adca8ed7774f2fb7fe9a7784"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@@ -19,9 +19,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v",
|
||||
"sha256": "3ba2dd92158718acec5caaf1a716043b5aa055c27b081d914af3ccb40dce8a55"
|
||||
"url": "https://gitlab.freedesktop.org/api/v4/projects/890/packages/generic/fontconfig/2.18.3/fontconfig-2.18.3.tar.xz",
|
||||
"dest": "vendor/p/N-V-__8AAOgqbADacob-q2_DMQlmgaG4xKHRuW-6PJ4oJzMZ",
|
||||
"sha256": "4f7b554a38cdf78c033f666c8871f3749e14a094f65a07f630c91ed0b43d35e3"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@@ -43,7 +43,7 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2026-07-28-36-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.2-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3",
|
||||
"sha256": "9014db570db06e3fbfcfadb7b32aa8addacc6969e701650683c1c74428adba6c"
|
||||
},
|
||||
@@ -73,9 +73,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260803-155300-875a82f.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAPpcBAD4_75xLBbLiYqdojOwQP74eoWmpL3jPrBl",
|
||||
"sha256": "feac4b6bd4bc6d28363406c9cf012d6611af3b168f4c05453569c10acfcc141b"
|
||||
"url": "https://deps.files.ghostty.org/ghostty-themes-release-20260831-151010-752a9c0.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAEFmBABuDGOKxAI6VMg41b9euMZ-z7HS9EcUdaor",
|
||||
"sha256": "83653eb837baeb5bfc7d50c91b7d44772796e08c1c243134e11c5edf6c8d15c5"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@@ -139,9 +139,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/translate_c-80f8b6e4f45a303268717d8e5f4f91d7837138bb.tar.gz",
|
||||
"dest": "vendor/p/translate_c-0.0.0-Q_BUWmU6BwB_9JKG2l2W7i_mhmYWeRseTGBEHi_YlV5f",
|
||||
"sha256": "38c9e59d58f36e3481f6fa38f11fe59c63c12492fccf399ca56f3dfcce17b53e"
|
||||
"url": "https://codeberg.org/vancluever/translate-c/archive/4e879eb8aba615de112eabd1231ea6e01920cead.tar.gz",
|
||||
"dest": "vendor/p/translate_c-0.0.0-Q_BUWhVNBwDOEcIqub4VFPJPB6D9dgwzUMHTX5KWr8Xr",
|
||||
"sha256": "8fb0f5c6a3e8b3bf0e35225d3abc1230bcf6f1fdf162467f1f747ec2fa5eda07"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@@ -181,9 +181,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs",
|
||||
"sha256": "9e4cd20abe96e6c4c6ede9c3057108860126e7be2e2c3e35515476c250be1c13"
|
||||
"url": "https://deps.files.ghostty.org/wuffs-7411f488fe2e2c205c3d3b3d28638b7356522930.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAP5JWgCGP_AD0teWpa4krRvE9VPZzvviGdbmN4jI",
|
||||
"sha256": "17877d346f7988651d6c62e45b2e3b05c86809a6442c5e34612374b2a9719a1c"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
||||
@@ -75,19 +75,56 @@ typedef enum {
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_STANDARD,
|
||||
GHOSTTY_CLIPBOARD_SELECTION,
|
||||
GHOSTTY_CLIPBOARD_PRIMARY,
|
||||
} ghostty_clipboard_e;
|
||||
|
||||
// One representation of clipboard contents. The data is binary-safe with
|
||||
// an explicit length; it is not necessarily null-terminated.
|
||||
typedef struct {
|
||||
const char *mime;
|
||||
const char *data;
|
||||
size_t len;
|
||||
} ghostty_clipboard_content_s;
|
||||
|
||||
// The payload for completing a clipboard read request. See
|
||||
// ghostty_surface_complete_clipboard_request.
|
||||
typedef struct {
|
||||
const ghostty_clipboard_content_s *contents;
|
||||
size_t contents_len;
|
||||
const char *const *available;
|
||||
size_t available_len;
|
||||
bool confirmed;
|
||||
bool remember;
|
||||
} ghostty_clipboard_complete_s;
|
||||
|
||||
// The payload of a clipboard read confirmation request: the would-be
|
||||
// completion contents plus the information shown in the permission
|
||||
// prompt. See ghostty_runtime_confirm_read_clipboard_cb.
|
||||
typedef struct {
|
||||
const ghostty_clipboard_content_s *contents;
|
||||
size_t contents_len;
|
||||
const char *const *available;
|
||||
size_t available_len;
|
||||
const char *name;
|
||||
bool can_remember;
|
||||
} ghostty_clipboard_confirm_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_KITTY_READ,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_KITTY_WRITE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_LIST,
|
||||
} ghostty_clipboard_request_e;
|
||||
|
||||
// apprt.ClipboardReadResult
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_READ_STARTED,
|
||||
GHOSTTY_CLIPBOARD_READ_UNAVAILABLE,
|
||||
GHOSTTY_CLIPBOARD_READ_UNSUPPORTED,
|
||||
} ghostty_clipboard_read_result_e;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_MOUSE_RELEASE,
|
||||
GHOSTTY_MOUSE_PRESS,
|
||||
@@ -1023,12 +1060,16 @@ typedef struct {
|
||||
} ghostty_action_s;
|
||||
|
||||
typedef void (*ghostty_runtime_wakeup_cb)(void*);
|
||||
typedef bool (*ghostty_runtime_read_clipboard_cb)(void*,
|
||||
ghostty_clipboard_e,
|
||||
void*);
|
||||
typedef ghostty_clipboard_read_result_e (*ghostty_runtime_read_clipboard_cb)(
|
||||
void*,
|
||||
ghostty_clipboard_e,
|
||||
void*,
|
||||
const char* const*,
|
||||
size_t,
|
||||
bool);
|
||||
typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
|
||||
void*,
|
||||
const char*,
|
||||
const ghostty_clipboard_confirm_s*,
|
||||
void*,
|
||||
ghostty_clipboard_request_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
|
||||
@@ -1178,10 +1219,12 @@ GHOSTTY_API void ghostty_surface_split_resize(ghostty_surface_t,
|
||||
uint16_t);
|
||||
GHOSTTY_API void ghostty_surface_split_equalize(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_binding_action(ghostty_surface_t, const char*, uintptr_t);
|
||||
GHOSTTY_API void ghostty_surface_complete_clipboard_request(ghostty_surface_t,
|
||||
const char*,
|
||||
void*,
|
||||
bool);
|
||||
GHOSTTY_API void ghostty_surface_complete_clipboard_request(
|
||||
ghostty_surface_t,
|
||||
const ghostty_clipboard_complete_s*,
|
||||
void*);
|
||||
GHOSTTY_API void ghostty_surface_deny_clipboard_request(ghostty_surface_t,
|
||||
void*);
|
||||
GHOSTTY_API bool ghostty_surface_has_selection(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*);
|
||||
GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t,
|
||||
|
||||
@@ -32,9 +32,10 @@
|
||||
* - @ref render "Render State" - Incremental render state updates for custom renderers
|
||||
* - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
|
||||
* - @ref snapshot "Terminal Snapshot" - Encode and incrementally restore terminal state
|
||||
* - @ref search "Search" - Search terminal contents, including scrollback
|
||||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
|
||||
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
|
||||
* - @ref paste "Paste Utilities" - Validate paste data safety
|
||||
* - @ref paste "Paste" - Paste into a terminal, validate and encode paste data
|
||||
* - @ref unicode "Unicode Utilities" - Codepoint properties for text layout
|
||||
* - @ref build_info "Build Info" - Query compile-time build configuration
|
||||
* - @ref allocator "Memory Management" - Memory management and custom allocators
|
||||
@@ -53,12 +54,13 @@
|
||||
* - @ref c-vt/src/main.c - OSC parser example
|
||||
* - @ref c-vt-encode-key/src/main.c - Key encoding example
|
||||
* - @ref c-vt-encode-mouse/src/main.c - Mouse encoding example
|
||||
* - @ref c-vt-paste/src/main.c - Paste safety check example
|
||||
* - @ref c-vt-paste/src/main.c - Paste example
|
||||
* - @ref c-vt-sgr/src/main.c - SGR parser example
|
||||
* - @ref c-vt-formatter/src/main.c - Terminal formatter example
|
||||
* - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs
|
||||
* - @ref c-vt-grid-ref-tracked/src/main.c - Tracked grid ref example
|
||||
* - @ref c-vt-compression/src/main.c - Idle scrollback compression example
|
||||
* - @ref c-vt-search/src/main.c - Terminal search example
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -83,8 +85,10 @@
|
||||
*/
|
||||
|
||||
/** @example c-vt-paste/src/main.c
|
||||
* This example demonstrates how to use the paste utilities to check if
|
||||
* paste data is safe before sending it to the terminal.
|
||||
* This example demonstrates how to paste into a terminal, including the
|
||||
* unsafe-paste confirmation flow and Kitty clipboard protocol paste events
|
||||
* (mode 5522), as well as the terminal-free paste safety and encoding
|
||||
* utilities.
|
||||
*/
|
||||
|
||||
/** @example c-vt-sgr/src/main.c
|
||||
@@ -123,6 +127,12 @@
|
||||
* PNG decoder callback and send a Kitty Graphics Protocol image.
|
||||
*/
|
||||
|
||||
/** @example c-vt-search/src/main.c
|
||||
* This example demonstrates how to search terminal contents for a
|
||||
* string, navigate between the matches like a find bar, and read the
|
||||
* viewport matches used to draw highlights.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_H
|
||||
#define GHOSTTY_VT_H
|
||||
|
||||
@@ -154,6 +164,7 @@ extern "C" {
|
||||
#include <ghostty/vt/paste.h>
|
||||
#include <ghostty/vt/point.h>
|
||||
#include <ghostty/vt/screen.h>
|
||||
#include <ghostty/vt/search.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/size_report.h>
|
||||
#include <ghostty/vt/snapshot.h>
|
||||
|
||||
@@ -31,14 +31,17 @@
|
||||
* function that accepts one,** and libghostty will use a default allocator.
|
||||
* The default allocator will be libc malloc/free if libc is linked.
|
||||
* Otherwise, a custom allocator is used (currently Zig's SMP allocator)
|
||||
* that doesn't require any external dependencies.
|
||||
* that doesn't require any external dependencies. On native freestanding
|
||||
* targets, the default allocator always fails instead.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* For simple use cases, you can ignore this interface entirely by passing NULL
|
||||
* as the allocator parameter to functions that accept one. This will use the
|
||||
* default allocator (typically libc malloc/free, if libc is linked, but
|
||||
* we provide our own default allocator if libc isn't linked).
|
||||
* we provide our own default allocator if libc isn't linked). Native
|
||||
* freestanding builds must provide a custom allocator for operations that
|
||||
* allocate memory.
|
||||
*
|
||||
* To use a custom allocator:
|
||||
* 1. Implement the GhosttyAllocatorVtable function pointers
|
||||
@@ -76,7 +79,8 @@
|
||||
*
|
||||
* If you're not going to use a custom allocator, you can ignore all of
|
||||
* this. All functions that take an allocator pointer allow NULL to use a
|
||||
* default allocator.
|
||||
* default allocator. Native freestanding builds must provide an allocator
|
||||
* for operations that allocate memory.
|
||||
*
|
||||
* The interface is based on the Zig allocator interface. I'll say up front
|
||||
* that it is easy to look at this interface and think "wow, this is really
|
||||
@@ -183,7 +187,8 @@ typedef struct {
|
||||
* For functions that take an allocator pointer, a NULL pointer indicates
|
||||
* that the default allocator should be used. The default allocator will
|
||||
* be libc malloc/free if we're linking to libc. If libc isn't linked,
|
||||
* a custom allocator is used (currently Zig's SMP allocator).
|
||||
* a custom allocator is used (currently Zig's SMP allocator). On native
|
||||
* freestanding targets, the default allocator always fails instead.
|
||||
*
|
||||
* @ingroup allocator
|
||||
*
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
/* DA1 conformance levels (Pp parameter). */
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT100 1
|
||||
#define GHOSTTY_DA_CONFORMANCE_VT101 1
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/io.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
@@ -137,6 +138,30 @@ GHOSTTY_API GhosttyResult ghostty_formatter_terminal_new(
|
||||
GhosttyTerminal terminal,
|
||||
GhosttyFormatterTerminalOptions options);
|
||||
|
||||
/**
|
||||
* Run the formatter and stream output to a writer.
|
||||
*
|
||||
* Each call formats the current terminal state and invokes the writer
|
||||
* synchronously as output becomes available. The callback may be called more
|
||||
* than once and must not call formatter or terminal APIs using the same
|
||||
* formatter or its terminal.
|
||||
*
|
||||
* If an error occurs, the writer may already contain a partial formatted
|
||||
* output. The operation cannot be resumed from that partial output. This
|
||||
* function does not flush or make the caller's destination durable.
|
||||
*
|
||||
* @param formatter The formatter handle (must not be NULL)
|
||||
* @param writer Destination writer whose write callback must not be NULL
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_IO_ERROR if the writer rejects
|
||||
* output, GHOSTTY_LIMIT_EXCEEDED if output accounting overflows, or
|
||||
* GHOSTTY_INVALID_VALUE if an argument is invalid
|
||||
*
|
||||
* @ingroup formatter
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_formatter_format(
|
||||
GhosttyFormatter formatter,
|
||||
GhosttyWriter writer);
|
||||
|
||||
/**
|
||||
* Run the formatter and produce output into the caller-provided buffer.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
/** @defgroup io I/O
|
||||
*
|
||||
@@ -98,6 +99,49 @@ typedef struct {
|
||||
void* userdata;
|
||||
} GhosttyWriter;
|
||||
|
||||
/**
|
||||
* Read one MIME-typed representation of some content, streaming its
|
||||
* bytes to a writer.
|
||||
*
|
||||
* The library calls this with the MIME type of the representation it
|
||||
* needs. The callback writes all of that representation's data to
|
||||
* @p writer, in as many calls to `writer.write(writer.userdata, data,
|
||||
* len)` as is convenient (one call with everything or many small
|
||||
* pieces both work), and returns true. Nothing written is retained
|
||||
* beyond each write call, so the data may be borrowed from anywhere:
|
||||
* a pasteboard item, a file being read, a stream.
|
||||
*
|
||||
* Returning false reports that the data could not be read. If the
|
||||
* writer refuses a write (returns false), stop and return false
|
||||
* without writing more.
|
||||
*
|
||||
* All pointer arguments, the mime, and the writer are borrowed and
|
||||
* valid only for the duration of the callback. The callback is
|
||||
* invoked synchronously on the calling thread. The API receiving the
|
||||
* GhosttyMimeReader defines which MIME types are requested, how many
|
||||
* times, and any consistency requirements across repeated reads.
|
||||
*
|
||||
* @param userdata Opaque userdata from GhosttyMimeReader
|
||||
* @param mime The MIME type of the representation to read
|
||||
* @param writer Where to write the data; valid only during this call
|
||||
* @return true once all the data was written, false if it could not
|
||||
* be read or the writer refused a write
|
||||
*/
|
||||
typedef bool (*GhosttyMimeReaderFn)(
|
||||
void* userdata,
|
||||
GhosttyString mime,
|
||||
GhosttyWriter writer);
|
||||
|
||||
/**
|
||||
* A MIME-typed content source callback and its opaque context.
|
||||
*
|
||||
* The struct is passed by value. @p read must be non-NULL.
|
||||
*/
|
||||
typedef struct {
|
||||
GhosttyMimeReaderFn read;
|
||||
void* userdata;
|
||||
} GhosttyMimeReader;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -399,6 +399,12 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* time, before the image is stored. Consumers can upload this
|
||||
* directly to the GPU without any decode step.
|
||||
*
|
||||
* For an animated image (Kitty graphics animation, actions a=f/a=a)
|
||||
* this is the pixel data of the current animation frame. The
|
||||
* image's GHOSTTY_KITTY_IMAGE_DATA_GENERATION changes whenever the
|
||||
* current frame changes, so generation-keyed caches remain
|
||||
* coherent.
|
||||
*
|
||||
* Output type: const uint8_t **
|
||||
*/
|
||||
GHOSTTY_KITTY_IMAGE_DATA_DATA_PTR = 7,
|
||||
|
||||
@@ -94,6 +94,7 @@ extern "C" {
|
||||
#define GHOSTTY_MODE_COLOR_SCHEME_REPORT (ghostty_mode_new(2031, false)) /**< Report color scheme */
|
||||
#define GHOSTTY_MODE_VISIBILITY_REPORT (ghostty_mode_new(2033, false)) /**< Report terminal visibility */
|
||||
#define GHOSTTY_MODE_IN_BAND_RESIZE (ghostty_mode_new(2048, false)) /**< In-band size reports */
|
||||
#define GHOSTTY_MODE_PASTE_EVENTS (ghostty_mode_new(5522, false)) /**< Kitty clipboard protocol paste events */
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
|
||||
@@ -63,6 +63,10 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_CLIPBOARD_PROTOCOL = 23,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_DND_PROTOCOL = 24,
|
||||
GHOSTTY_OSC_COMMAND_CONTEXT_SIGNAL = 25,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_DESKTOP_NOTIFICATION = 26,
|
||||
GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyOscCommandType;
|
||||
|
||||
|
||||
@@ -1,26 +1,67 @@
|
||||
/**
|
||||
* @file paste.h
|
||||
*
|
||||
* Paste utilities - validate and encode paste data for terminal input.
|
||||
* Paste - paste into a terminal, and validate and encode paste data.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_PASTE_H
|
||||
#define GHOSTTY_VT_PASTE_H
|
||||
|
||||
/** @defgroup paste Paste Utilities
|
||||
/** @defgroup paste Paste
|
||||
*
|
||||
* Utilities for validating and encoding paste data for terminal input.
|
||||
* Pasting into a terminal, plus the terminal-free utilities for
|
||||
* validating and encoding paste data.
|
||||
*
|
||||
* ## Basic Usage
|
||||
* ## Pasting into a Terminal
|
||||
*
|
||||
* Use ghostty_paste_is_safe() to check if paste data contains potentially
|
||||
* dangerous sequences before sending it to the terminal.
|
||||
* What a paste writes to the pty depends on the terminal's state, so
|
||||
* the recommended way to paste is ghostty_terminal_paste(). The embedder
|
||||
* hands over the MIME types the clipboard holds (just `text/plain` for
|
||||
* an ordinary paste), a GhosttyMimeReader that produces the data of
|
||||
* any one of them, and where the paste came from, and the terminal
|
||||
* decides how its current modes apply:
|
||||
*
|
||||
* Use ghostty_paste_encode() to encode paste data for writing to the pty,
|
||||
* - If Kitty clipboard protocol paste events (mode 5522,
|
||||
* GHOSTTY_MODE_PASTE_EVENTS) are enabled, the paste was user-initiated
|
||||
* (GHOSTTY_PASTE_SOURCE_CLIPBOARD), and a clipboard_read callback is
|
||||
* installed, the terminal sends the program a paste event listing the
|
||||
* clipboard's MIME types with a one-time password instead of the data.
|
||||
* The program then reads what it wants through the clipboard_read
|
||||
* callback, which arrives with `granted` set so no permission prompt
|
||||
* is needed. No data is read for the event.
|
||||
* - Otherwise the first text representation is written: unsafe control
|
||||
* bytes are replaced with spaces, and it is wrapped in bracketed paste
|
||||
* sequences if mode 2004 (GHOSTTY_MODE_BRACKETED_PASTE) is enabled, or
|
||||
* has its newlines converted to carriage returns if not.
|
||||
*
|
||||
* The data is pulled through GhosttyPaste::reader only when a
|
||||
* representation is actually pasted (so a clipboard holding a large
|
||||
* image next to some text costs nothing), and the encoded bytes
|
||||
* stream to the write_pty callback (GHOSTTY_TERMINAL_OPT_WRITE_PTY)
|
||||
* in chunks as they are produced, never in one piece. The callback
|
||||
* may be invoked several times for a single paste; the pieces must be
|
||||
* written to the pty in order.
|
||||
*
|
||||
* Text that could inject commands (a newline when unbracketed, or the
|
||||
* bracketed paste terminator when bracketed) is refused with
|
||||
* GHOSTTY_REJECTED and nothing written unless GhosttyPaste::allow_unsafe
|
||||
* is set. The usual flow is to call once, confirm with the user on
|
||||
* GHOSTTY_REJECTED, and call again with `allow_unsafe` set. Each call
|
||||
* reads the text at most once and buffers it whole while the rule is
|
||||
* applied, so the source needs no stability across reads (the
|
||||
* confirmed retry simply pastes whatever the source holds then) and a
|
||||
* refused or failed paste writes nothing at all.
|
||||
*
|
||||
* @snippet c-vt-paste/src/main.c terminal-paste
|
||||
*
|
||||
* ## Building Blocks
|
||||
*
|
||||
* For embedders that encode without a terminal, ghostty_paste_is_safe()
|
||||
* checks if paste data contains potentially dangerous sequences
|
||||
* (conservatively, regardless of terminal state) and
|
||||
* ghostty_paste_encode() encodes paste data for writing to the pty,
|
||||
* including bracketed paste wrapping and unsafe byte stripping.
|
||||
*
|
||||
* ## Examples
|
||||
*
|
||||
* ### Safety Check
|
||||
*
|
||||
* @snippet c-vt-paste/src/main.c paste-safety
|
||||
@@ -35,11 +76,119 @@
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
#include <ghostty/vt/io.h>
|
||||
#include <ghostty/vt/terminal.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Why a paste happened.
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** The user pasted from a clipboard: keybind, menu, middle click. */
|
||||
GHOSTTY_PASTE_SOURCE_CLIPBOARD = 0,
|
||||
|
||||
/**
|
||||
* Text inserted some other way: IME commit, drag and drop, scripted
|
||||
* input. Always written as text, never as a paste event, matching
|
||||
* kitty. This is not a way to opt out of paste events; an embedder
|
||||
* that doesn't want them doesn't install a clipboard_read callback.
|
||||
*/
|
||||
GHOSTTY_PASTE_SOURCE_TEXT = 1,
|
||||
GHOSTTY_PASTE_SOURCE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyPasteSource;
|
||||
|
||||
/**
|
||||
* A paste of clipboard contents into the terminal.
|
||||
*
|
||||
* This is a sized struct; set `size` to `sizeof(GhosttyPaste)`. The
|
||||
* MIME type array and the strings it points to are borrowed only for
|
||||
* the duration of the ghostty_terminal_paste() call, as is everything
|
||||
* the reader produces.
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/**
|
||||
* The clipboard the contents came from. Reported to the program on a
|
||||
* paste event (the selection and primary locations are both reported
|
||||
* as the primary selection, the protocol knows only two); no effect
|
||||
* on a text paste.
|
||||
*/
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Why this paste happened. */
|
||||
GhosttyPasteSource source;
|
||||
|
||||
/**
|
||||
* Borrowed array of the MIME types of the representations available,
|
||||
* in preferred order. A text paste reads and writes the first entry
|
||||
* with a text MIME type such as "text/plain" and ignores the rest. A
|
||||
* paste event lists every entry and reads none. May be NULL when
|
||||
* mimes_len is zero, which is nothing to paste.
|
||||
*/
|
||||
const GhosttyString* mimes;
|
||||
|
||||
/** Number of entries in mimes. */
|
||||
size_t mimes_len;
|
||||
|
||||
/**
|
||||
* Produces the data of a representation on demand. Required when
|
||||
* mimes_len is nonzero.
|
||||
*
|
||||
* Called at most once per ghostty_terminal_paste() call: for the
|
||||
* text representation being pasted, never for anything else and
|
||||
* never for a paste event. The MIME type requested is always an
|
||||
* entry of `mimes`, passed through exactly as given there (the same
|
||||
* pointer and length), so the callback may identify the
|
||||
* representation by pointer or by content. A false return fails the
|
||||
* paste with GHOSTTY_IO_ERROR.
|
||||
*/
|
||||
GhosttyMimeReader reader;
|
||||
|
||||
/**
|
||||
* Write text that could inject commands. Call with false, confirm
|
||||
* with the user on GHOSTTY_REJECTED, and call again with true.
|
||||
*/
|
||||
bool allow_unsafe;
|
||||
} GhosttyPaste;
|
||||
|
||||
/**
|
||||
* Paste into the terminal according to its current state: a Kitty
|
||||
* clipboard protocol paste event if mode 5522 is enabled and a
|
||||
* clipboard_read callback is installed, otherwise the text framed per
|
||||
* mode 2004. See the group documentation for the full behavior. Output
|
||||
* streams through the write_pty callback in chunks. The viewport is not
|
||||
* scrolled; that is up to the embedder, as for key input.
|
||||
*
|
||||
* A paste event records a session grant for its one-time password only
|
||||
* once the event is written; a failed call never leaves a grant for an
|
||||
* event that was never sent.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param paste The paste request, borrowed for the duration of the call
|
||||
* @param[out] out_written On success, whether anything was written to
|
||||
* the pty (the encoded text or a paste event). False means
|
||||
* there was nothing to paste: no non-empty text
|
||||
* representation. May be NULL.
|
||||
* @return GHOSTTY_SUCCESS on success (see @p out_written);
|
||||
* GHOSTTY_REJECTED if the text could inject commands and
|
||||
* GhosttyPaste::allow_unsafe is false (nothing was written);
|
||||
* GHOSTTY_INVALID_VALUE for a NULL terminal or paste, MIME
|
||||
* types without a reader, or when no write_pty callback is
|
||||
* installed; GHOSTTY_OUT_OF_MEMORY; GHOSTTY_IO_ERROR if the
|
||||
* reader failed or there is no secure entropy source to mint a
|
||||
* paste event password (wasm32-freestanding without
|
||||
* GHOSTTY_SYS_OPT_RANDOM_SECURE set). Errors write nothing.
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_terminal_paste(
|
||||
GhosttyTerminal terminal,
|
||||
const GhosttyPaste* paste,
|
||||
bool* out_written);
|
||||
|
||||
/**
|
||||
* Check if paste data is safe to paste into the terminal.
|
||||
*
|
||||
@@ -49,7 +198,9 @@ extern "C" {
|
||||
* to exit bracketed paste mode and inject commands
|
||||
*
|
||||
* This check is conservative and considers data unsafe regardless of
|
||||
* current terminal state.
|
||||
* current terminal state. ghostty_terminal_paste() applies the
|
||||
* terminal-state-aware rule itself (newlines are safe inside a
|
||||
* bracketed paste); use this to apply the stricter rule on top.
|
||||
*
|
||||
* @param data The paste data to check (must not be NULL)
|
||||
* @param len The length of the data in bytes
|
||||
@@ -74,6 +225,9 @@ GHOSTTY_API bool ghostty_paste_is_safe(const char* data, size_t len);
|
||||
* GHOSTTY_OUT_OF_SPACE and sets the required size in @p out_written.
|
||||
* The caller can then retry with a sufficiently sized buffer.
|
||||
*
|
||||
* This is the encoder ghostty_terminal_paste() uses for a text paste;
|
||||
* use it directly when there is no terminal to paste into.
|
||||
*
|
||||
* @param data The paste data to encode (modified in place, may be NULL)
|
||||
* @param data_len The length of the input data in bytes
|
||||
* @param bracketed Whether bracketed paste mode is active
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
@@ -63,7 +63,10 @@ extern "C" {
|
||||
* tracking which rows in a partially dirty frame have changed.
|
||||
*
|
||||
* The user of the render state API is expected to unset both of these.
|
||||
* The `update` call does not unset dirty state, it only updates it.
|
||||
* The `update` call does not unset dirty state, it only updates it. After
|
||||
* successfully rendering a complete frame, use ghostty_render_state_clean()
|
||||
* to unset both layers in one call. The granular setters remain available
|
||||
* for callers that only consume part of a frame.
|
||||
*
|
||||
* An extremely important detail: setting one dirty state doesn't unset
|
||||
* the other. For example, setting the global dirty state to false does not
|
||||
@@ -199,6 +202,14 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Whether the cursor is on the tail of a wide character (bool).
|
||||
* Only valid when CURSOR_VIEWPORT_HAS_VALUE is true. */
|
||||
GHOSTTY_RENDER_STATE_DATA_CURSOR_VIEWPORT_WIDE_TAIL = 17,
|
||||
|
||||
/** All cursor state in one sized struct (GhosttyRenderStateCursor).
|
||||
* Initialize the output with GHOSTTY_INIT_SIZED before querying. */
|
||||
GHOSTTY_RENDER_STATE_DATA_CURSOR = 18,
|
||||
|
||||
/** All render-state colors in one sized struct (GhosttyRenderStateColors).
|
||||
* Initialize the output with GHOSTTY_INIT_SIZED before querying. */
|
||||
GHOSTTY_RENDER_STATE_DATA_COLORS = 19,
|
||||
GHOSTTY_RENDER_STATE_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateData;
|
||||
|
||||
@@ -236,6 +247,22 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
|
||||
/** Row-local selected cell range (GhosttyRenderStateRowSelection). */
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION = 4,
|
||||
|
||||
/** A borrowed view of the raw cell values for the current row
|
||||
* (GhosttyCellsView). One value per column, identical to querying
|
||||
* GHOSTTY_RENDER_STATE_ROW_CELLS_DATA_RAW for each cell. The view
|
||||
* is only valid as long as the underlying render state is not
|
||||
* updated; it is unsafe to use after updating the render state.
|
||||
*
|
||||
* This is the bulk alternative to iterating cells one at a time.
|
||||
* It lets callers with expensive call boundaries (e.g. WebAssembly
|
||||
* embedders) read an entire row with a single call.
|
||||
*
|
||||
* Bit positions aren't protected by ABI, so callers should parse them
|
||||
* out of the manifest from `ghostty_type_json`. Callers with access
|
||||
* to the C header or without high FFI costs should use `ghostty_cell_get`.
|
||||
*/
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS_RAW = 5,
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateRowData;
|
||||
|
||||
@@ -273,17 +300,59 @@ typedef struct {
|
||||
uint16_t end_x;
|
||||
} GhosttyRenderStateRowSelection;
|
||||
|
||||
/**
|
||||
* Render-state cursor information.
|
||||
*
|
||||
* This struct uses the sized-struct ABI pattern. Initialize with
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateCursor) before querying
|
||||
* GHOSTTY_RENDER_STATE_DATA_CURSOR.
|
||||
*
|
||||
* When viewport_has_value is false, viewport_x, viewport_y, and wide_tail
|
||||
* contain undefined data and must not be read.
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateCursor). */
|
||||
size_t size;
|
||||
|
||||
/** Whether the cursor is visible within the viewport. */
|
||||
bool viewport_has_value;
|
||||
|
||||
/** Cursor viewport x position in cells. */
|
||||
uint16_t viewport_x;
|
||||
|
||||
/** Cursor viewport y position in cells. */
|
||||
uint16_t viewport_y;
|
||||
|
||||
/** Whether the cursor is on the tail of a wide character. */
|
||||
bool wide_tail;
|
||||
|
||||
/** Whether the cursor is visible based on terminal modes. */
|
||||
bool visible;
|
||||
|
||||
/** Whether the cursor should blink based on terminal modes. */
|
||||
bool blinking;
|
||||
|
||||
/** Whether the cursor is at a password input field. */
|
||||
bool password_input;
|
||||
|
||||
/** The visual style of the cursor. */
|
||||
GhosttyRenderStateCursorVisualStyle visual_style;
|
||||
} GhosttyRenderStateCursor;
|
||||
|
||||
/**
|
||||
* Render-state color information.
|
||||
*
|
||||
* This struct uses the sized-struct ABI pattern. Initialize with
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before calling
|
||||
* ghostty_render_state_colors_get().
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors) before querying
|
||||
* GHOSTTY_RENDER_STATE_DATA_COLORS.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* GhosttyRenderStateColors colors = GHOSTTY_INIT_SIZED(GhosttyRenderStateColors);
|
||||
* GhosttyResult result = ghostty_render_state_colors_get(state, &colors);
|
||||
* GhosttyResult result = ghostty_render_state_get(
|
||||
* state, GHOSTTY_RENDER_STATE_DATA_COLORS, &colors);
|
||||
* @endcode
|
||||
*
|
||||
* @ingroup render
|
||||
@@ -408,6 +477,23 @@ GHOSTTY_API GhosttyResult ghostty_render_state_begin_update(GhosttyRenderState s
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_render_state_end_update(GhosttyRenderState state);
|
||||
|
||||
/**
|
||||
* Mark all dirty render-state data as consumed.
|
||||
*
|
||||
* This sets the global dirty state to GHOSTTY_RENDER_STATE_DIRTY_FALSE and
|
||||
* clears every per-row dirty flag. It is idempotent and does not modify cell
|
||||
* contents or dirty state owned by the terminal. Call this only after a
|
||||
* complete frame has been rendered successfully; partial consumers should
|
||||
* use ghostty_render_state_set() and ghostty_render_state_row_set() instead.
|
||||
*
|
||||
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
|
||||
* NULL
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_render_state_clean(GhosttyRenderState state);
|
||||
|
||||
/**
|
||||
* Get a value from a render state.
|
||||
*
|
||||
@@ -417,8 +503,9 @@ GHOSTTY_API GhosttyResult ghostty_render_state_end_update(GhosttyRenderState sta
|
||||
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param data The data kind to query
|
||||
* @param[out] out Pointer to receive the queried value
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
|
||||
* NULL or `data` is not a recognized enum value
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
|
||||
* `out` is NULL, `data` is not a recognized enum value, or a sized
|
||||
* output struct is smaller than `sizeof(size_t)`
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
@@ -473,24 +560,6 @@ GHOSTTY_API GhosttyResult ghostty_render_state_set(GhosttyRenderState state,
|
||||
GhosttyRenderStateOption option,
|
||||
const void* value);
|
||||
|
||||
/**
|
||||
* Get the current color information from a render state.
|
||||
*
|
||||
* This writes as many fields as fit in the caller-provided sized struct.
|
||||
* `out_colors->size` must be set by the caller (typically via
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateColors)).
|
||||
*
|
||||
* @param state The render state handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param[out] out_colors Sized output struct to receive render-state colors
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` or
|
||||
* `out_colors` is NULL, or if `out_colors->size` is smaller than
|
||||
* `sizeof(size_t)`
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_render_state_colors_get(GhosttyRenderState state,
|
||||
GhosttyRenderStateColors* out_colors);
|
||||
|
||||
/**
|
||||
* Create a new row iterator instance.
|
||||
*
|
||||
@@ -521,7 +590,8 @@ GHOSTTY_API void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIte
|
||||
/**
|
||||
* Move a render-state row iterator to the next row.
|
||||
*
|
||||
* Returns true if the iterator moved successfully and row data is
|
||||
* Rows are visited contiguously in ascending viewport order, starting at
|
||||
* y = 0. Returns true if the iterator moved successfully and row data is
|
||||
* available to read at the new position.
|
||||
*
|
||||
* @param iterator The iterator handle to advance (may be NULL)
|
||||
@@ -532,12 +602,36 @@ GHOSTTY_API void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIte
|
||||
*/
|
||||
GHOSTTY_API bool ghostty_render_state_row_iterator_next(GhosttyRenderStateRowIterator iterator);
|
||||
|
||||
/**
|
||||
* Move a render-state row iterator to the next row requiring a redraw.
|
||||
*
|
||||
* If the global dirty state is GHOSTTY_RENDER_STATE_DIRTY_FALSE, this returns
|
||||
* false. If it is GHOSTTY_RENDER_STATE_DIRTY_PARTIAL, clean rows are skipped.
|
||||
* If it is GHOSTTY_RENDER_STATE_DIRTY_FULL, every remaining row is returned
|
||||
* regardless of its per-row dirty flag. Rows are returned in ascending
|
||||
* viewport order. This function does not clear any dirty state.
|
||||
*
|
||||
* @param iterator The iterator handle to advance (NULL returns false)
|
||||
* @param[out] out_y Receives the viewport y coordinate when true is returned
|
||||
* (NULL returns false); it is not modified when false is
|
||||
* returned
|
||||
* @return true if advanced to a row requiring a redraw, false if an argument
|
||||
* is NULL or the iterator has reached the end of the effective dirty
|
||||
* rows
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
GHOSTTY_API bool ghostty_render_state_row_iterator_next_dirty(
|
||||
GhosttyRenderStateRowIterator iterator,
|
||||
uint16_t* out_y);
|
||||
|
||||
/**
|
||||
* Get a value from the current row in a render-state row iterator.
|
||||
*
|
||||
* The `out` pointer must point to a value of the type corresponding to the
|
||||
* requested data kind (see GhosttyRenderStateRowData).
|
||||
* Call ghostty_render_state_row_iterator_next() at least once before
|
||||
* Call ghostty_render_state_row_iterator_next() or
|
||||
* ghostty_render_state_row_iterator_next_dirty() at least once before
|
||||
* calling this function.
|
||||
*
|
||||
* @param iterator The iterator handle to query (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
@@ -586,7 +680,8 @@ GHOSTTY_API GhosttyResult ghostty_render_state_row_get_multi(
|
||||
*
|
||||
* The `value` pointer must point to a value of the type corresponding to the
|
||||
* requested option kind (see GhosttyRenderStateRowOption).
|
||||
* Call ghostty_render_state_row_iterator_next() at least once before
|
||||
* Call ghostty_render_state_row_iterator_next() or
|
||||
* ghostty_render_state_row_iterator_next_dirty() at least once before
|
||||
* calling this function.
|
||||
*
|
||||
* @param iterator The iterator handle to update (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
|
||||
@@ -21,19 +21,21 @@ extern "C" {
|
||||
* Terminal screen cell and row types.
|
||||
*
|
||||
* These types represent the contents of a terminal screen. A GhosttyCell
|
||||
* is a single grid cell and a GhosttyRow is a single row. Both are opaque
|
||||
* values whose fields are accessed via ghostty_cell_get() and
|
||||
* ghostty_row_get() respectively.
|
||||
* is a single grid cell and a GhosttyRow is a single row. Cell fields can
|
||||
* be accessed via ghostty_cell_get() or decoded from the packed layout in
|
||||
* ghostty_type_json(). Rows are opaque and accessed via ghostty_row_get().
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Opaque cell value.
|
||||
* Packed cell value.
|
||||
*
|
||||
* Represents a single terminal cell. The internal layout is opaque and
|
||||
* must be queried via ghostty_cell_get(). Obtain cell values from
|
||||
* terminal query APIs.
|
||||
* Represents a single terminal cell. Portable callers can query fields via
|
||||
* ghostty_cell_get(). Boundary-sensitive callers can decode the packed value
|
||||
* using the GhosttyCell descriptor returned by ghostty_type_json(). The
|
||||
* manifest is authoritative for the linked build; hardcoding bit positions
|
||||
* is unsupported.
|
||||
*
|
||||
* @ingroup screen
|
||||
*/
|
||||
@@ -50,6 +52,24 @@ typedef uint64_t GhosttyCell;
|
||||
*/
|
||||
typedef uint64_t GhosttyRow;
|
||||
|
||||
/**
|
||||
* A borrowed view of contiguous raw cell values.
|
||||
*
|
||||
* The memory is not owned by this struct. The pointer is only valid
|
||||
* for the lifetime documented by the API that produces it. Each value
|
||||
* can be queried via ghostty_cell_get() or decoded using the GhosttyCell
|
||||
* packed descriptor returned by ghostty_type_json().
|
||||
*
|
||||
* @ingroup screen
|
||||
*/
|
||||
typedef struct {
|
||||
/** Pointer to len contiguous cell values. */
|
||||
const GhosttyCell* ptr;
|
||||
|
||||
/** Number of cells. */
|
||||
size_t len;
|
||||
} GhosttyCellsView;
|
||||
|
||||
/**
|
||||
* Cell content tag.
|
||||
*
|
||||
|
||||
501
include/ghostty/vt/search.h
Normal file
501
include/ghostty/vt/search.h
Normal file
@@ -0,0 +1,501 @@
|
||||
/**
|
||||
* @file search.h
|
||||
*
|
||||
* Search terminal contents, including scrollback, for a string.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_SEARCH_H
|
||||
#define GHOSTTY_VT_SEARCH_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/selection.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** @defgroup search Search
|
||||
*
|
||||
* Search a terminal for a string, covering the active area and
|
||||
* scrollback of both the primary and alternate screens.
|
||||
*
|
||||
* A GhosttySearch searches the terminal it was created with for a
|
||||
* needle set with GHOSTTY_SEARCH_OPT_NEEDLE. It handles the hard
|
||||
* parts of terminal search internally: results stay in sync with the
|
||||
* live screens, survive primary/alternate screen switches (entering
|
||||
* and leaving a fullscreen app such as vim does not restart a
|
||||
* scrollback search), and recover from resize, reflow, resets, and
|
||||
* scrollback pruning.
|
||||
*
|
||||
* A search starts idle. Setting the needle starts the search,
|
||||
* changing it restarts the search from scratch, and clearing it
|
||||
* returns the search to idle. Matching is byte-exact except ASCII
|
||||
* letters, which compare case-insensitively.
|
||||
*
|
||||
* ## Driving a search
|
||||
*
|
||||
* Searching a large scrollback takes time, so the work is split into
|
||||
* small steps the caller drives so that the caller can control
|
||||
* performance more directly:
|
||||
*
|
||||
* - ghostty_search_tick() makes a bounded amount of progress on data
|
||||
* the search has already copied. It never touches the terminal,
|
||||
* meaning it can be safely called from a thread.
|
||||
* - ghostty_search_feed() reads the terminal to copy in more data and
|
||||
* pick up terminal changes. Feeding is the only way the search
|
||||
* learns that the terminal changed, so keep feeding periodically
|
||||
* while the search is in use. This requires exclusive terminal access.
|
||||
* - ghostty_search_run() is a blocking convenience that feeds and
|
||||
* ticks until the search is caught up.
|
||||
*
|
||||
* GHOSTTY_SEARCH_STATUS_COMPLETE means the search is caught up with
|
||||
* the terminal as of the last feed. It never means finished forever,
|
||||
* since later terminal writes require another feed to be seen.
|
||||
*
|
||||
* ## Matches are selections
|
||||
*
|
||||
* Every match is returned as a GhosttySelection snapshot with
|
||||
* rectangle set to false, so the existing selection APIs all work on
|
||||
* matches: ghostty_terminal_selection_format_buf() to copy the
|
||||
* matched text, ghostty_terminal_point_from_grid_ref() with
|
||||
* GHOSTTY_POINT_TAG_VIEWPORT to position highlight rectangles,
|
||||
* ghostty_terminal_selection_contains() for hit testing, and
|
||||
* ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to make
|
||||
* a match the terminal's selection.
|
||||
*
|
||||
* Returned matches follow the usual snapshot lifetime rules: they are
|
||||
* only valid until the next operation that modifies the terminal,
|
||||
* including ghostty_terminal_vt_write(), resize, reset, and free.
|
||||
* Read matches after a feed, use them before the terminal changes
|
||||
* again, and re-read them rather than caching them. The selected
|
||||
* match is kept accurate internally across terminal changes, so the
|
||||
* safe way to follow a match is to re-read
|
||||
* GHOSTTY_SEARCH_DATA_SELECTED_MATCH after each feed.
|
||||
*
|
||||
* ## Lifetime
|
||||
*
|
||||
* The search borrows the terminal it was created with and never frees
|
||||
* it. Any number of searches, alongside other terminal readers such
|
||||
* as formatters and render states, may share one terminal.
|
||||
*
|
||||
* The search and its terminal can be freed in either order. Freeing
|
||||
* the search first releases tracked state it holds within the
|
||||
* terminal. If the terminal is freed first, the search detects this:
|
||||
* calls that need the terminal return GHOSTTY_INVALID_VALUE, reads
|
||||
* return whatever the search last saw, and ghostty_search_free()
|
||||
* releases only search-owned memory. A search cannot be rebound, so
|
||||
* searching another terminal means creating a new search.
|
||||
*
|
||||
* ## Threading
|
||||
*
|
||||
* The library creates no threads. Calls on one GhosttySearch are not
|
||||
* safe to make concurrently with each other, so the caller must
|
||||
* serialize them.
|
||||
*
|
||||
* Functions that touch the terminal (ghostty_search_new(),
|
||||
* ghostty_search_feed(), ghostty_search_run(), ghostty_search_set()
|
||||
* with the needle and select options, and ghostty_search_free()) must
|
||||
* also be serialized with all other access to the same terminal.
|
||||
*
|
||||
* Everything else (ghostty_search_tick(), ghostty_search_get(), and
|
||||
* ghostty_search_get_multi()) only touches memory owned by the search
|
||||
* and is safe to call while another thread modifies the terminal.
|
||||
* This split is how Ghostty runs search on a background thread: tick
|
||||
* freely, and take the terminal lock only to feed. Reading returned
|
||||
* match values is always safe, but passing them to APIs that take the
|
||||
* terminal follows the terminal serialization rule above.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @snippet c-vt-search/src/main.c search-main
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Progress state of a search.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/**
|
||||
* ghostty_search_tick() can make progress without terminal access.
|
||||
*/
|
||||
GHOSTTY_SEARCH_STATUS_RUNNING = 0,
|
||||
|
||||
/**
|
||||
* Blocked until ghostty_search_feed(). This is also the state right
|
||||
* after a needle is set, since the search has not yet seen the
|
||||
* terminal.
|
||||
*/
|
||||
GHOSTTY_SEARCH_STATUS_FEED_REQUIRED = 1,
|
||||
|
||||
/**
|
||||
* Caught up with the terminal state as of the last feed. This never
|
||||
* means finished forever, since later terminal writes require
|
||||
* another feed to be seen. A search with no needle set also reports
|
||||
* complete, since there is nothing to look for.
|
||||
*/
|
||||
GHOSTTY_SEARCH_STATUS_COMPLETE = 2,
|
||||
|
||||
GHOSTTY_SEARCH_STATUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySearchStatus;
|
||||
|
||||
/**
|
||||
* Scroll policy applied when a match becomes selected via
|
||||
* GHOSTTY_SEARCH_OPT_SELECT_NEXT or GHOSTTY_SEARCH_OPT_SELECT_PREV.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Scroll the viewport so the match is visible, only if it is not
|
||||
* already visible. This is the default. */
|
||||
GHOSTTY_SEARCH_SCROLL_IF_NEEDED = 0,
|
||||
|
||||
/** Never scroll the viewport. */
|
||||
GHOSTTY_SEARCH_SCROLL_NONE = 1,
|
||||
|
||||
GHOSTTY_SEARCH_SCROLL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySearchScroll;
|
||||
|
||||
/**
|
||||
* Data fields readable with ghostty_search_get(). The output value
|
||||
* type is documented per field.
|
||||
*
|
||||
* All reads reflect the terminal's active screen as of the last feed.
|
||||
* When the running application switches to the alternate screen, the
|
||||
* next feed switches counts, matches, and selection to that screen's
|
||||
* results. Primary screen results, including completed scrollback
|
||||
* searches, are retained and restored on the way back.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** Current search status: GhosttySearchStatus*. */
|
||||
GHOSTTY_SEARCH_DATA_STATUS = 0,
|
||||
|
||||
/**
|
||||
* The needle this search is looking for: GhosttyString*. The bytes
|
||||
* are borrowed from the search and remain valid until the needle is
|
||||
* changed or the search is freed. Returns GHOSTTY_NO_VALUE when no
|
||||
* needle is set.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_NEEDLE = 1,
|
||||
|
||||
/**
|
||||
* Total matches found so far on the active screen: size_t*. Zero
|
||||
* until the first feed.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_TOTAL_MATCHES = 2,
|
||||
|
||||
/**
|
||||
* Index of the selected match: size_t*. This indexes the newest to
|
||||
* oldest ordering of GHOSTTY_SEARCH_DATA_MATCHES, where 0 is the
|
||||
* newest match, so a "k of n" find bar renders index + 1 of
|
||||
* GHOSTTY_SEARCH_DATA_TOTAL_MATCHES. Returns GHOSTTY_NO_VALUE when
|
||||
* nothing is selected.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_SELECTED_INDEX = 3,
|
||||
|
||||
/**
|
||||
* The selected match: GhosttySelection*. This is an untracked
|
||||
* snapshot with standard GhosttySelection lifetime rules. Returns
|
||||
* GHOSTTY_NO_VALUE when nothing is selected.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_SELECTED_MATCH = 4,
|
||||
|
||||
/**
|
||||
* All matches on the active screen, ordered newest to oldest, from
|
||||
* the bottom of the active area up through scrollback:
|
||||
* GhosttySelectionBuffer*. Set ptr to NULL with cap 0 to query the
|
||||
* required capacity. An undersized buffer returns
|
||||
* GHOSTTY_OUT_OF_SPACE with the required capacity in len.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_MATCHES = 5,
|
||||
|
||||
/**
|
||||
* Matches on the pages covering the viewport, for drawing highlight
|
||||
* rectangles: GhosttySelectionBuffer*. The list is computed during
|
||||
* feeds and cached, so it reflects the viewport as of the last
|
||||
* feed.
|
||||
*
|
||||
* Matches are found a page at a time, so the list can include
|
||||
* matches slightly outside the visible viewport when they share a
|
||||
* page with it. Ghostty's own renderer behaves the same way.
|
||||
* Converting each match to viewport coordinates with
|
||||
* ghostty_terminal_point_from_grid_ref() clips this naturally: skip
|
||||
* matches that fail the conversion or whose row is beyond the
|
||||
* visible row count.
|
||||
*/
|
||||
GHOSTTY_SEARCH_DATA_VIEWPORT_MATCHES = 6,
|
||||
|
||||
/** Current scroll policy: GhosttySearchScroll*. */
|
||||
GHOSTTY_SEARCH_DATA_SELECT_SCROLL = 7,
|
||||
|
||||
GHOSTTY_SEARCH_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySearchData;
|
||||
|
||||
/**
|
||||
* Options writable with ghostty_search_set(). The value type, and
|
||||
* what a NULL value means, is documented per option.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/**
|
||||
* Set the needle to search for: const GhosttyString*. The bytes are
|
||||
* copied, so the caller's memory does not need to outlive the call.
|
||||
* Matching is byte-exact except ASCII letters, which compare
|
||||
* case-insensitively.
|
||||
*
|
||||
* Changing the needle restarts the search from scratch and drops
|
||||
* all results. As an exception, setting a needle equal to the current
|
||||
* one (compared the same way as matching) keeps existing results,
|
||||
* so find bars can resubmit freely. A NULL or empty value clears
|
||||
* the needle and returns the search to idle.
|
||||
*
|
||||
* Replacing or clearing a needle releases tracked state held
|
||||
* within the terminal, so the caller must serialize this with all
|
||||
* other access to the same terminal. Returns GHOSTTY_INVALID_VALUE
|
||||
* after the terminal was freed.
|
||||
*/
|
||||
GHOSTTY_SEARCH_OPT_NEEDLE = 0,
|
||||
|
||||
/**
|
||||
* Select the next match, moving toward older content: from the
|
||||
* bottom of the screen upward into history, the direction a search
|
||||
* from the prompt usually wants. Wraps around past the oldest
|
||||
* match.
|
||||
*
|
||||
* The value must be NULL. It is reserved for future use.
|
||||
*
|
||||
* This catches up with the terminal first, so it is safe to call at
|
||||
* any time relative to feeds. The viewport scrolls to the newly
|
||||
* selected match according to GHOSTTY_SEARCH_OPT_SELECT_SCROLL.
|
||||
* This reads the terminal, so the caller must serialize it with all
|
||||
* other access to the same terminal. Returns GHOSTTY_NO_VALUE when
|
||||
* there are no matches.
|
||||
*/
|
||||
GHOSTTY_SEARCH_OPT_SELECT_NEXT = 1,
|
||||
|
||||
/**
|
||||
* Select the previous match, moving toward newer content, wrapping
|
||||
* around past the newest match. Otherwise identical to
|
||||
* GHOSTTY_SEARCH_OPT_SELECT_NEXT.
|
||||
*/
|
||||
GHOSTTY_SEARCH_OPT_SELECT_PREV = 2,
|
||||
|
||||
/**
|
||||
* Set the scroll policy applied by the select options: const
|
||||
* GhosttySearchScroll*. The policy persists until changed. A NULL
|
||||
* value resets it to GHOSTTY_SEARCH_SCROLL_IF_NEEDED. This only
|
||||
* modifies search-owned state and never reads the terminal.
|
||||
*/
|
||||
GHOSTTY_SEARCH_OPT_SELECT_SCROLL = 3,
|
||||
|
||||
GHOSTTY_SEARCH_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySearchOption;
|
||||
|
||||
/**
|
||||
* Create a search bound to a terminal.
|
||||
*
|
||||
* The search borrows the terminal and never frees it. The search and
|
||||
* the terminal can be freed in either order; see ghostty_search_free().
|
||||
*
|
||||
* The search starts idle with no needle: it reports
|
||||
* GHOSTTY_SEARCH_STATUS_COMPLETE and finds nothing. Set
|
||||
* GHOSTTY_SEARCH_OPT_NEEDLE to start searching.
|
||||
*
|
||||
* Creation is cheap and does not read terminal contents, but it
|
||||
* registers the search with the terminal so the two can be freed in
|
||||
* any order. The caller must serialize this call with all other
|
||||
* access to the same terminal.
|
||||
*
|
||||
* @param allocator Allocator, or NULL for the default allocator
|
||||
* @param out_search Receives the created search handle
|
||||
* @param terminal Terminal to bind the search to
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
|
||||
* out_search or terminal is invalid, or GHOSTTY_OUT_OF_MEMORY
|
||||
* if allocation fails
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_new(
|
||||
const GhosttyAllocator* allocator,
|
||||
GhosttySearch* out_search,
|
||||
GhosttyTerminal terminal);
|
||||
|
||||
/**
|
||||
* Free a search.
|
||||
*
|
||||
* If the bound terminal is still alive, this releases tracked state
|
||||
* the search holds within it, so the caller must serialize this call
|
||||
* with all other access to the same terminal. If the terminal was
|
||||
* already freed, the search has been detached and this releases only
|
||||
* search-owned memory. Passing NULL is allowed and is a no-op.
|
||||
*
|
||||
* @param search Search handle to free
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API void ghostty_search_free(GhosttySearch search);
|
||||
|
||||
/**
|
||||
* Make a bounded amount of search progress.
|
||||
*
|
||||
* This only works on data the search has already copied and never
|
||||
* reads the terminal, so it is safe to call while another thread
|
||||
* modifies the terminal. Call it in a loop while the status is
|
||||
* GHOSTTY_SEARCH_STATUS_RUNNING. When the status becomes
|
||||
* GHOSTTY_SEARCH_STATUS_FEED_REQUIRED, call ghostty_search_feed() to
|
||||
* unblock it.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param[out] out_status Receives the status after the tick (may be NULL)
|
||||
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
|
||||
* search is NULL
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_tick(
|
||||
GhosttySearch search,
|
||||
GhosttySearchStatus* out_status);
|
||||
|
||||
/**
|
||||
* Read the terminal to update the search.
|
||||
*
|
||||
* Each feed catches the search up with the terminal: it reconciles
|
||||
* the tracked screens against the live ones, re-scans the active
|
||||
* area, refreshes the viewport match list, gives the scrollback
|
||||
* searcher its next chunk of data, and prunes results that scrollback
|
||||
* eviction invalidated. Feeding is also the only way the search
|
||||
* learns about terminal changes, so keep feeding periodically while
|
||||
* the search is in use, even after it reports complete.
|
||||
*
|
||||
* This reads the terminal, so the caller must serialize it with all
|
||||
* other access to the same terminal. Each call does a bounded amount
|
||||
* of work so that any caller-held terminal lock is held only briefly.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
|
||||
* search is NULL or the terminal was freed
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_feed(GhosttySearch search);
|
||||
|
||||
/**
|
||||
* Feed and tick until the search is caught up with the terminal.
|
||||
*
|
||||
* This is a blocking convenience for one-shot and single-threaded
|
||||
* embedders. It always performs at least one feed, so it also picks
|
||||
* up any terminal changes since the last feed, then loops until the
|
||||
* status is GHOSTTY_SEARCH_STATUS_COMPLETE. Searching a large
|
||||
* scrollback can take a while, so interactive embedders should drive
|
||||
* ghostty_search_tick() and ghostty_search_feed() themselves.
|
||||
*
|
||||
* This reads the terminal for the entire call, so the caller must
|
||||
* serialize it with all other access to the same terminal.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
|
||||
* search is NULL or the terminal was freed
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_run(GhosttySearch search);
|
||||
|
||||
/**
|
||||
* Write an option to a search.
|
||||
*
|
||||
* The value type, and what a NULL value means, depends on the option
|
||||
* and is documented by GhosttySearchOption. The needle and select
|
||||
* options touch the terminal, so the caller must serialize those
|
||||
* calls with all other access to the same terminal.
|
||||
* GHOSTTY_SEARCH_OPT_SELECT_SCROLL only modifies search-owned state.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param option Option to write
|
||||
* @param value Pointer to the input value for the option. The meaning
|
||||
* of NULL is documented per option.
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if a select
|
||||
* option found no matches, GHOSTTY_OUT_OF_MEMORY if
|
||||
* allocation fails, or GHOSTTY_INVALID_VALUE if search,
|
||||
* option, or value is invalid or the option needs a terminal
|
||||
* that was already freed
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_set(
|
||||
GhosttySearch search,
|
||||
GhosttySearchOption option,
|
||||
const void* value);
|
||||
|
||||
/**
|
||||
* Read a data field from a search.
|
||||
*
|
||||
* The output value type depends on data and is documented by
|
||||
* GhosttySearchData. This never reads the terminal, so it is safe to
|
||||
* call while another thread modifies the terminal. Returned
|
||||
* selections are untracked snapshots with standard GhosttySelection
|
||||
* lifetime rules.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param data Data field to read
|
||||
* @param value Output pointer whose type depends on data
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the
|
||||
* requested data has no value, GHOSTTY_OUT_OF_SPACE if a
|
||||
* provided GhosttySelectionBuffer is too small (required
|
||||
* capacity in its len), GHOSTTY_OUT_OF_MEMORY if collecting
|
||||
* viewport matches fails, or GHOSTTY_INVALID_VALUE if search,
|
||||
* data, or value is invalid
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_get(
|
||||
GhosttySearch search,
|
||||
GhosttySearchData data,
|
||||
void* value);
|
||||
|
||||
/**
|
||||
* Read multiple data fields from a search in a single call.
|
||||
*
|
||||
* This is an optimization over calling ghostty_search_get() multiple
|
||||
* times. Each entry in values must point to storage of the type
|
||||
* documented by the corresponding GhosttySearchData key.
|
||||
*
|
||||
* If any individual read fails, the function returns that error and
|
||||
* writes the index of the failing key to out_written when out_written
|
||||
* is non-NULL. Earlier keys have already been written. On success,
|
||||
* out_written receives count when non-NULL. A too-small
|
||||
* GhosttySelectionBuffer stops the batch with GHOSTTY_OUT_OF_SPACE at
|
||||
* that key's index with the required capacity in its len, so order
|
||||
* buffer-valued keys after scalar keys.
|
||||
*
|
||||
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
|
||||
* @param count Number of data fields to read
|
||||
* @param keys Data fields to read (must not be NULL)
|
||||
* @param values Output pointers corresponding to keys (must not be NULL)
|
||||
* @param out_written Optional number of fields read, or failing index
|
||||
* on error
|
||||
* @return GHOSTTY_SUCCESS on success, or the first failing read's
|
||||
* result
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
GHOSTTY_API GhosttyResult ghostty_search_get_multi(
|
||||
GhosttySearch search,
|
||||
size_t count,
|
||||
const GhosttySearchData* keys,
|
||||
void** values,
|
||||
size_t* out_written);
|
||||
|
||||
/** @} */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* GHOSTTY_VT_SEARCH_H */
|
||||
@@ -116,6 +116,29 @@ typedef struct {
|
||||
bool rectangle;
|
||||
} GhosttySelection;
|
||||
|
||||
/**
|
||||
* A caller-provided buffer of selections.
|
||||
*
|
||||
* This follows the same conventions as GhosttyBuffer: ptr may be NULL with
|
||||
* cap 0 to query the required capacity. APIs that fill this type set len to
|
||||
* the number of entries written on GHOSTTY_SUCCESS, or to the required entry
|
||||
* capacity on GHOSTTY_OUT_OF_SPACE.
|
||||
*
|
||||
* @ingroup selection
|
||||
*/
|
||||
typedef struct {
|
||||
/** Destination buffer for selections. May be NULL when cap is 0 to query
|
||||
* the required capacity. */
|
||||
GhosttySelection* ptr;
|
||||
|
||||
/** Capacity of ptr in entries. */
|
||||
size_t cap;
|
||||
|
||||
/** Entries written on success, or required entry capacity on
|
||||
* GHOSTTY_OUT_OF_SPACE. */
|
||||
size_t len;
|
||||
} GhosttySelectionBuffer;
|
||||
|
||||
/**
|
||||
* Options for deriving a word selection from a terminal grid reference.
|
||||
*
|
||||
|
||||
@@ -319,31 +319,6 @@ GHOSTTY_API GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute
|
||||
GHOSTTY_API GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
|
||||
GhosttySgrAttribute* attr);
|
||||
|
||||
#ifdef __wasm__
|
||||
/**
|
||||
* Allocate memory for an SGR attribute (WebAssembly only).
|
||||
*
|
||||
* This is a convenience function for WebAssembly environments to allocate
|
||||
* memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
|
||||
*
|
||||
* @return Pointer to the allocated attribute structure
|
||||
*
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
|
||||
|
||||
/**
|
||||
* Free memory for an SGR attribute (WebAssembly only).
|
||||
*
|
||||
* Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
|
||||
*
|
||||
* @param attr Pointer to the attribute structure to free
|
||||
*
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef GHOSTTY_VT_SNAPSHOT_H
|
||||
#define GHOSTTY_VT_SNAPSHOT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -131,13 +132,34 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* state. The decoder default matches the largest built-in APC protocol
|
||||
* buffer limit, currently 65 MiB.
|
||||
*
|
||||
* This is an input validation limit only. It does not configure continuation
|
||||
* tracking on a terminal returned by the decoder.
|
||||
* This is primarily an input validation limit. When
|
||||
* GHOSTTY_SNAPSHOT_DECODER_OPT_RETAIN_CONTINUATION is true, the same value
|
||||
* also becomes the continuation tracking limit on the returned terminal.
|
||||
*
|
||||
* Input type: size_t *
|
||||
*/
|
||||
GHOSTTY_SNAPSHOT_DECODER_OPT_MAX_CONTINUATION_BYTES = 0,
|
||||
|
||||
/**
|
||||
* Retain the decoded continuation on the returned terminal.
|
||||
*
|
||||
* When true, terminals returned by ghostty_snapshot_decoder_ready() and
|
||||
* ghostty_snapshot_decoder_decode() use
|
||||
* GHOSTTY_SNAPSHOT_DECODER_OPT_MAX_CONTINUATION_BYTES as their continuation
|
||||
* tracking limit. The existing ghostty_terminal_continuation_* APIs can then
|
||||
* export the exact unfinished VT or UTF-8 input restored from the snapshot.
|
||||
*
|
||||
* This is false by default. A maximum continuation size of zero leaves
|
||||
* tracking disabled. With a nonzero maximum, tracking remains enabled even
|
||||
* when the decoded continuation is empty. Exporting an empty continuation
|
||||
* does not disable it. Callers that do not need ongoing tracking must still
|
||||
* set GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES to zero after export and
|
||||
* before writing post-snapshot input.
|
||||
*
|
||||
* Input type: bool *
|
||||
*/
|
||||
GHOSTTY_SNAPSHOT_DECODER_OPT_RETAIN_CONTINUATION = 1,
|
||||
|
||||
GHOSTTY_SNAPSHOT_DECODER_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySnapshotDecoderOption;
|
||||
|
||||
@@ -222,6 +244,15 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
*/
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_REMAINING = 7,
|
||||
|
||||
/**
|
||||
* Whether decoded continuation tracking is retained on returned terminals.
|
||||
*
|
||||
* This value is available in every non-failed decoder state.
|
||||
*
|
||||
* Output type: bool *
|
||||
*/
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_RETAIN_CONTINUATION = 8,
|
||||
|
||||
GHOSTTY_SNAPSHOT_DECODER_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySnapshotDecoderData;
|
||||
|
||||
@@ -397,10 +428,16 @@ GHOSTTY_API GhosttyResult ghostty_snapshot_decoder_set(
|
||||
* immediately usable for rendering and live input. Older scrollback remains
|
||||
* to be restored with ghostty_snapshot_decoder_next().
|
||||
*
|
||||
* The restored parser state may be unfinished, but terminal continuation
|
||||
* tracking is disabled; GHOSTTY_TERMINAL_DATA_CONTINUATION_MAX_BYTES returns
|
||||
* zero. The decoder's continuation option is an input limit, not terminal
|
||||
* runtime policy.
|
||||
* The restored parser state may be unfinished. By default, terminal
|
||||
* continuation tracking is disabled and
|
||||
* GHOSTTY_TERMINAL_DATA_CONTINUATION_MAX_BYTES returns zero. When
|
||||
* GHOSTTY_SNAPSHOT_DECODER_OPT_RETAIN_CONTINUATION is true, the decoder's
|
||||
* maximum continuation size is applied to the terminal, and the terminal
|
||||
* continuation APIs export the exact current continuation when that limit is
|
||||
* nonzero. Tracking remains enabled even if the exported continuation is
|
||||
* empty. Callers that do not need ongoing tracking must set
|
||||
* GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES to zero after export and before
|
||||
* writing any post-snapshot bytes, because later input may change it.
|
||||
*
|
||||
* The caller must keep the returned terminal alive until FINISH validates or
|
||||
* the decoder is freed. The decoder borrows this terminal handle while it
|
||||
@@ -455,8 +492,15 @@ GHOSTTY_API GhosttyResult ghostty_snapshot_decoder_next(
|
||||
* FINISH. It may only be called before decoding starts. Bytes following FINISH
|
||||
* are left unread. On success terminal receives a caller-owned terminal with
|
||||
* its persistent VT stream restored. Continuation tracking on the returned
|
||||
* terminal is disabled and GHOSTTY_TERMINAL_DATA_CONTINUATION_MAX_BYTES
|
||||
* returns zero. terminal is set to NULL on every error.
|
||||
* terminal is disabled by default. When
|
||||
* GHOSTTY_SNAPSHOT_DECODER_OPT_RETAIN_CONTINUATION is true, the decoder's
|
||||
* maximum continuation size is applied to the terminal, and the terminal
|
||||
* continuation APIs export the exact current continuation when that limit is
|
||||
* nonzero. Tracking remains enabled even if the exported continuation is
|
||||
* empty. Callers that do not need ongoing tracking must set
|
||||
* GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES to zero after export and before
|
||||
* writing any post-snapshot bytes, because later input may change it.
|
||||
* terminal is set to NULL on every error.
|
||||
* A decoding, I/O, or allocation error after input consumption begins poisons
|
||||
* the decoder, after which it must be freed. An invalid argument or
|
||||
* lifecycle error detected before the operation consumes input does not
|
||||
|
||||
@@ -124,6 +124,24 @@ typedef bool (*GhosttySysDecodePngFn)(
|
||||
size_t data_len,
|
||||
GhosttySysImage* out);
|
||||
|
||||
/**
|
||||
* Callback type for secure random bytes.
|
||||
*
|
||||
* Fills @p buf with @p len cryptographically secure random bytes. The
|
||||
* library uses this for secrets, so it must be a real CSPRNG (getrandom,
|
||||
* arc4random_buf, BCryptGenRandom, crypto.getRandomValues, ...); a
|
||||
* predictable source is a security hole.
|
||||
*
|
||||
* @param userdata The userdata pointer set via GHOSTTY_SYS_OPT_USERDATA
|
||||
* @param buf Buffer to fill
|
||||
* @param len Number of bytes to fill
|
||||
* @return true if the buffer was filled, false if no entropy is available
|
||||
*/
|
||||
typedef bool (*GhosttySysRandomSecureFn)(
|
||||
void* userdata,
|
||||
uint8_t* buf,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* System option identifiers for ghostty_sys_set().
|
||||
*/
|
||||
@@ -165,6 +183,21 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: GhosttySysLogFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_LOG = 2,
|
||||
|
||||
/**
|
||||
* Override the secure random source.
|
||||
*
|
||||
* By default the library draws secure random bytes from the
|
||||
* platform (getrandom or arc4random_buf on POSIX, CNG on Windows).
|
||||
* Targets without one, such as wasm32-freestanding, have no default
|
||||
* and operations that need entropy fail with GHOSTTY_IO_ERROR until
|
||||
* this is set. When set,
|
||||
* it is used instead of the platform source on every target. When
|
||||
* cleared (NULL value), the platform default is restored.
|
||||
*
|
||||
* Input type: GhosttySysRandomSecureFn (function pointer, or NULL)
|
||||
*/
|
||||
GHOSTTY_SYS_OPT_RANDOM_SECURE = 3,
|
||||
GHOSTTY_SYS_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttySysOption;
|
||||
|
||||
|
||||
@@ -86,16 +86,17 @@ extern "C" {
|
||||
*
|
||||
* | Option | Callback Type | Trigger |
|
||||
* |-----------------------------------------|-----------------------------------|-------------------------------------------|
|
||||
* | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | Query responses written back to the pty |
|
||||
* | `GHOSTTY_TERMINAL_OPT_WRITE_PTY` | `GhosttyTerminalWritePtyFn` | VT query and mode reports written back to the PTY |
|
||||
* | `GHOSTTY_TERMINAL_OPT_BELL` | `GhosttyTerminalBellFn` | BEL character (0x07) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_TITLE_CHANGED` | `GhosttyTerminalTitleChangedFn` | Title change via OSC 0 / OSC 2 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_PWD_CHANGED` | `GhosttyTerminalPwdChangedFn` | Pwd change via OSC 7 / OSC 9 / OSC 1337 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_ENQUIRY` | `GhosttyTerminalEnquiryFn` | ENQ character (0x05) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_XTVERSION` | `GhosttyTerminalXtversionFn` | XTVERSION query (CSI > q) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS size query (CSI 14/16/18 t) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_SIZE` | `GhosttyTerminalSizeFn` | XTWINOPS query (CSI 14/16/18 t) or mode 2048 enable |
|
||||
* | `GHOSTTY_TERMINAL_OPT_COLOR_SCHEME` | `GhosttyTerminalColorSchemeFn` | Color scheme query (CSI ? 996 n) |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DEVICE_ATTRIBUTES`| `GhosttyTerminalDeviceAttributesFn`| Device attributes query (CSI c / > c / = c)|
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE` | `GhosttyTerminalClipboardWriteFn` | Clipboard write via OSC 52 / OSC 1337 / OSC 5522 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ` | `GhosttyTerminalClipboardReadFn` | Clipboard read via OSC 52 "?" / OSC 5522 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_DESKTOP_NOTIFICATION`| `GhosttyTerminalDesktopNotificationFn` | Desktop notification via OSC 9 / OSC 777 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_PROGRESS_REPORT` | `GhosttyTerminalProgressReportFn` | Progress report via OSC 9;4 |
|
||||
* | `GHOSTTY_TERMINAL_OPT_UNKNOWN_SEQUENCE` | `GhosttyTerminalUnknownSequenceFn` | Unsupported sequence identifier |
|
||||
@@ -112,6 +113,9 @@ extern "C" {
|
||||
* ### Defining a clipboard_write callback
|
||||
* @snippet c-vt-effects/src/main.c effects-clipboard-write
|
||||
*
|
||||
* ### Defining a clipboard_read callback
|
||||
* @snippet c-vt-effects/src/main.c effects-clipboard-read
|
||||
*
|
||||
* ### Defining an unknown_sequence callback
|
||||
* @snippet c-vt-effects/src/main.c effects-unknown-sequence
|
||||
*
|
||||
@@ -459,38 +463,7 @@ typedef struct {
|
||||
} GhosttyClipboardContent;
|
||||
|
||||
/**
|
||||
* A semantic, atomic clipboard write.
|
||||
*
|
||||
* This is a sized struct. The callback must only access fields present in the
|
||||
* size reported by `size`. The request, contents array, MIME strings, and
|
||||
* data strings are all borrowed and valid only for the callback duration.
|
||||
*
|
||||
* All entries in `contents` are representations of the same logical value
|
||||
* and must be committed atomically. A `contents_len` of zero requests that
|
||||
* the destination be cleared. This is distinct from a content entry whose data
|
||||
* has zero length.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Clipboard destination. */
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Borrowed array of MIME representations. */
|
||||
const GhosttyClipboardContent* contents;
|
||||
|
||||
/** Number of entries in contents; zero means clear the destination. */
|
||||
size_t contents_len;
|
||||
} GhosttyClipboardWrite;
|
||||
|
||||
/**
|
||||
* Result of a clipboard write callback.
|
||||
*
|
||||
* Protocols without write acknowledgements, including OSC 52 and iTerm2
|
||||
* OSC 1337 Copy, ignore this result.
|
||||
* Result of a clipboard write reply.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
@@ -515,28 +488,335 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_CLIPBOARD_WRITE_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyClipboardWriteResult;
|
||||
|
||||
/**
|
||||
* The reply to a clipboard write request.
|
||||
*
|
||||
* This is a sized struct; set `size` to `sizeof(GhosttyClipboardWriteReply)`.
|
||||
* The reply is borrowed only for the duration of the reply call and may be
|
||||
* freed as soon as it returns.
|
||||
*
|
||||
* The result answers the program with the matching protocol status for
|
||||
* protocols with a write acknowledgement (OSC 5522: DONE, EPERM, ENOSYS,
|
||||
* EBUSY, EINVAL, EIO); protocols without one (OSC 52, OSC 1337 Copy)
|
||||
* discard the reply. `remember` is ignored on any result other than
|
||||
* GHOSTTY_CLIPBOARD_WRITE_RESULT_SUCCESS.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Outcome of the write. */
|
||||
GhosttyClipboardWriteResult result;
|
||||
|
||||
/**
|
||||
* Record a session grant so future requests from the same program skip
|
||||
* the permission prompt. Only honored on success when
|
||||
* GhosttyClipboardWrite::can_remember is set.
|
||||
*/
|
||||
bool remember;
|
||||
} GhosttyClipboardWriteReply;
|
||||
|
||||
typedef struct GhosttyClipboardWrite GhosttyClipboardWrite;
|
||||
|
||||
/**
|
||||
* Function type used to answer a clipboard write request. Obtained from
|
||||
* GhosttyClipboardWrite::reply; see that struct for the contract.
|
||||
*
|
||||
* @param write The request being answered
|
||||
* @param reply The reply, borrowed only for the duration of this call
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyClipboardWriteReplyFn)(
|
||||
const GhosttyClipboardWrite* write,
|
||||
const GhosttyClipboardWriteReply* reply);
|
||||
|
||||
/**
|
||||
* A synchronous request to write clipboard contents.
|
||||
*
|
||||
* This is a sized struct. The callback must only access fields present in the
|
||||
* size reported by `size`. The request, contents array, MIME strings, and
|
||||
* data strings are all borrowed and valid only for the callback duration.
|
||||
*
|
||||
* All entries in `contents` are representations of the same logical value
|
||||
* and must be committed atomically. A `contents_len` of zero requests that
|
||||
* the destination be cleared. This is distinct from a content entry whose data
|
||||
* has zero length.
|
||||
*
|
||||
* The write is answered by calling `reply` with this request and a
|
||||
* GhosttyClipboardWriteReply. This must happen within the clipboard write
|
||||
* request callback. This struct is only valid during that time. Calling
|
||||
* `reply` more than once is safely ignored. Returning without replying
|
||||
* denies the write.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
struct GhosttyClipboardWrite {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Clipboard destination. */
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/** Borrowed array of MIME representations. */
|
||||
const GhosttyClipboardContent* contents;
|
||||
|
||||
/** Number of entries in contents; zero means clear the destination. */
|
||||
size_t contents_len;
|
||||
|
||||
/**
|
||||
* Name of the writing program for permission prompts, if the protocol
|
||||
* carries one. Empty otherwise.
|
||||
*/
|
||||
GhosttyString name;
|
||||
|
||||
/**
|
||||
* True if the terminal already holds a session grant for this request
|
||||
* The embedder should skip any permission prompt and perform the write.
|
||||
*/
|
||||
bool granted;
|
||||
|
||||
/**
|
||||
* True if the program supplied a session password, so the embedder may
|
||||
* offer to remember the user's decision through
|
||||
* GhosttyClipboardWriteReply::remember. When false, remember is ignored.
|
||||
*/
|
||||
bool can_remember;
|
||||
|
||||
/** Terminal-owned reply state. Do not access. */
|
||||
const void* ctx;
|
||||
|
||||
/** Answer the write; see the struct documentation. */
|
||||
GhosttyClipboardWriteReplyFn reply;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function type for clipboard_write.
|
||||
*
|
||||
* Called synchronously for a complete logical clipboard write. Protocol
|
||||
* details such as OSC 52 selectors, base64 encoding, multipart chunks,
|
||||
* aliases, and terminators are normalized before this callback is invoked.
|
||||
* OSC 52 and iTerm2 OSC 1337 Copy writes therefore use the same callback
|
||||
* shape. OSC 52 clipboard read requests ("?") are always ignored and never
|
||||
* forwarded to this callback.
|
||||
* The embedder may ask for permission to write or perform the write
|
||||
* async, but the callback itself is synchronous and the reply function
|
||||
* must be called during the lifetime of this function. While this callback
|
||||
* is active the VT stream is paused.
|
||||
*
|
||||
* Answer by calling `write->reply(write, &reply)` before returning. See
|
||||
* GhosttyClipboardWrite for the full contract.
|
||||
*
|
||||
* The request may carry an optional program name requesting the write
|
||||
* and the state of prior permission granted. If `can_remember` is set
|
||||
* the response may set the `remember` flag and future requests from this
|
||||
* same program will be "granted" and the embedder can skip permission
|
||||
* requests.
|
||||
*
|
||||
* Clipboard read requests (OSC 52 "?" and OSC 5522 reads) are delivered
|
||||
* to GhosttyTerminalClipboardReadFn instead.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param write Borrowed atomic clipboard write request
|
||||
* @return The result of attempting the clipboard write
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef GhosttyClipboardWriteResult (*GhosttyTerminalClipboardWriteFn)(
|
||||
typedef void (*GhosttyTerminalClipboardWriteFn)(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardWrite* write);
|
||||
|
||||
/**
|
||||
* Result of a clipboard read reply.
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef enum GHOSTTY_ENUM_TYPED {
|
||||
/** The clipboard was read; the reply carries its contents. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS = 0,
|
||||
|
||||
/** The clipboard read was denied by policy or the user. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_DENIED = 1,
|
||||
|
||||
/** The embedder cannot read this clipboard. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_UNSUPPORTED = 2,
|
||||
|
||||
/** The clipboard is temporarily unavailable. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_BUSY = 3,
|
||||
|
||||
/** Reading the clipboard failed due to an I/O error. */
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_IO_ERROR = 4,
|
||||
GHOSTTY_CLIPBOARD_READ_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyClipboardReadResult;
|
||||
|
||||
/**
|
||||
* The reply to a clipboard read request.
|
||||
*
|
||||
* This is a sized struct; set `size` to `sizeof(GhosttyClipboardReadReply)`.
|
||||
* All arrays and the strings they point to are borrowed only for the
|
||||
* duration of the reply call and may be freed as soon as it returns.
|
||||
*
|
||||
* Any result other than GHOSTTY_CLIPBOARD_READ_RESULT_SUCCESS answers the
|
||||
* program with an empty clipboard (OSC 52) or the matching protocol status
|
||||
* (OSC 5522: EPERM, ENOSYS, EBUSY, EIO); the other fields are ignored in
|
||||
* that case. On success, `contents` should carry one representation per
|
||||
* requested MIME type (GhosttyClipboardRead::mimes) that the clipboard
|
||||
* has; unrequested representations are ignored. Protocols that carry a
|
||||
* single text value (OSC 52) use the first entry with a text MIME type
|
||||
* such as "text/plain".
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Outcome of the read. */
|
||||
GhosttyClipboardReadResult result;
|
||||
|
||||
/** Borrowed array of MIME representations of the clipboard contents. */
|
||||
const GhosttyClipboardContent* contents;
|
||||
|
||||
/** Number of entries in contents. */
|
||||
size_t contents_len;
|
||||
|
||||
/**
|
||||
* Borrowed array of all MIME types available on the clipboard. Only
|
||||
* used when GhosttyClipboardRead::list is set; may be NULL otherwise.
|
||||
*/
|
||||
const GhosttyString* available;
|
||||
|
||||
/** Number of entries in available. */
|
||||
size_t available_len;
|
||||
|
||||
/**
|
||||
* Record a session grant so future requests from the same program skip
|
||||
* the permission prompt. Only honored on success when
|
||||
* GhosttyClipboardRead::can_remember is set.
|
||||
*/
|
||||
bool remember;
|
||||
} GhosttyClipboardReadReply;
|
||||
|
||||
typedef struct GhosttyClipboardRead GhosttyClipboardRead;
|
||||
|
||||
/**
|
||||
* Function type used to answer a clipboard read request. Obtained from
|
||||
* GhosttyClipboardRead::reply; see that struct for the contract.
|
||||
*
|
||||
* @param read The request being answered
|
||||
* @param reply The reply, borrowed only for the duration of this call
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyClipboardReadReplyFn)(
|
||||
const GhosttyClipboardRead* read,
|
||||
const GhosttyClipboardReadReply* reply);
|
||||
|
||||
/**
|
||||
* A synchronous request to read clipboard contents.
|
||||
*
|
||||
* This is a sized struct. The callback must only access fields present in the
|
||||
* size reported by `size`. The request is borrowed and valid only for the
|
||||
* callback duration.
|
||||
*
|
||||
* The read is answered by calling `reply` with this request and a
|
||||
* GhosttyClipboardReadReply. This must happen before the callback returns;
|
||||
* the request is invalid afterwards. Calling `reply` more than once is
|
||||
* ignored. Returning without replying answers the program with an empty
|
||||
* clipboard (OSC 52) or EPERM (OSC 5522).
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
struct GhosttyClipboardRead {
|
||||
/** Size of this struct in bytes. */
|
||||
size_t size;
|
||||
|
||||
/** Clipboard to read. */
|
||||
GhosttyClipboardLocation location;
|
||||
|
||||
/**
|
||||
* Borrowed array of the MIME types the program wants, in order of
|
||||
* preference. Protocols that only carry text (OSC 52) request
|
||||
* "text/plain". NULL when mimes_len is zero.
|
||||
*/
|
||||
const GhosttyString* mimes;
|
||||
|
||||
/** Number of entries in mimes. */
|
||||
size_t mimes_len;
|
||||
|
||||
/**
|
||||
* True if the program also wants the list of MIME types available on the
|
||||
* clipboard, delivered through GhosttyClipboardReadReply::available.
|
||||
*/
|
||||
bool list;
|
||||
|
||||
/**
|
||||
* Name of the requesting program for permission prompts, if the protocol
|
||||
* carries one. Empty otherwise.
|
||||
*/
|
||||
GhosttyString name;
|
||||
|
||||
/**
|
||||
* True if the terminal already holds a session grant for this request
|
||||
* (kitty clipboard protocol passwords). The embedder should skip any
|
||||
* permission prompt and serve the read.
|
||||
*
|
||||
* Always false when mimes_len is zero: such a request is served
|
||||
* without a prompt (see the callback docs), so the terminal never
|
||||
* consults grants for it and a one-time password is preserved for
|
||||
* the follow-up data read.
|
||||
*/
|
||||
bool granted;
|
||||
|
||||
/**
|
||||
* True if the program supplied a session password, so the embedder may
|
||||
* offer to remember the user's decision through
|
||||
* GhosttyClipboardReadReply::remember. When false, remember is ignored.
|
||||
*/
|
||||
bool can_remember;
|
||||
|
||||
/** Terminal-owned reply state. Do not access. */
|
||||
const void* ctx;
|
||||
|
||||
/** Answer the read; see the struct documentation. */
|
||||
GhosttyClipboardReadReplyFn reply;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback function type for clipboard_read.
|
||||
*
|
||||
* Called synchronously when the running program requests clipboard contents
|
||||
* via OSC 52 with a "?" payload or a Kitty clipboard (OSC 5522) read.
|
||||
* Answering lets the program read the user's clipboard, so the embedder is
|
||||
* expected to mediate consent. Because the read is synchronous, an embedder
|
||||
* that needs to ask the user must block (for example by running a modal
|
||||
* prompt) until it has an answer; the VT stream waits until the callback
|
||||
* returns.
|
||||
*
|
||||
* Answer by calling `read->reply(read, &reply)` before returning. See
|
||||
* GhosttyClipboardRead for the full contract.
|
||||
*
|
||||
* OSC 5522 requests carry the program's MIME list, name, and password grant
|
||||
* state; a reply that sets `remember` records a session grant so later
|
||||
* requests with the same password arrive with `granted` set. Kitty itself
|
||||
* serves a request for only the targets listing (`list` with no `mimes`)
|
||||
* without prompting, and embedders are expected to do the same; the
|
||||
* terminal never consults grants for such requests (`granted` is false
|
||||
* and one-time passwords are not consumed).
|
||||
*
|
||||
* Installing this callback also enables Kitty paste events (mode 5522):
|
||||
* ghostty_terminal_paste() sends the program an event instead of the text,
|
||||
* and the program's follow-up read arrives here with `granted` set since
|
||||
* the user already pasted. See ghostty_terminal_paste().
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param read Borrowed clipboard read request
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
typedef void (*GhosttyTerminalClipboardReadFn)(
|
||||
GhosttyTerminal terminal,
|
||||
void* userdata,
|
||||
const GhosttyClipboardRead* read);
|
||||
|
||||
/**
|
||||
* A request to show a desktop notification.
|
||||
*
|
||||
@@ -688,16 +968,18 @@ typedef GhosttyString (*GhosttyTerminalEnquiryFn)(GhosttyTerminal terminal,
|
||||
void* userdata);
|
||||
|
||||
/**
|
||||
* Callback function type for size queries (XTWINOPS).
|
||||
* Callback function type for terminal size reports.
|
||||
*
|
||||
* Called in response to XTWINOPS size queries (CSI 14/16/18 t).
|
||||
* Called in response to XTWINOPS size queries (CSI 14/16/18 t) and when VT
|
||||
* input enables in-band size reports (mode 2048).
|
||||
* Return true and fill *out_size with the current terminal geometry,
|
||||
* or return false to silently ignore the query.
|
||||
* or return false to suppress the report.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
* @param[out] out_size Pointer to store the terminal size information
|
||||
* @return true if size was filled, false to ignore the query
|
||||
* @return true if size was filled, false to suppress the XTWINOPS response or
|
||||
* mode 2048 report
|
||||
*
|
||||
* @ingroup terminal
|
||||
*/
|
||||
@@ -749,9 +1031,9 @@ typedef void (*GhosttyTerminalPwdChangedFn)(GhosttyTerminal terminal,
|
||||
* Callback function type for write_pty.
|
||||
*
|
||||
* Called when the terminal needs to write data back to the pty, for
|
||||
* example in response to a device status report or mode query. The
|
||||
* data is only valid for the duration of the call; callers must copy
|
||||
* it if it needs to persist.
|
||||
* example in response to a device status report, mode query, or VT-driven
|
||||
* mode 2048 enable. The data is only valid for the duration of the call;
|
||||
* callers must copy it if it needs to persist.
|
||||
*
|
||||
* @param terminal The terminal handle
|
||||
* @param userdata The userdata pointer set via GHOSTTY_TERMINAL_OPT_USERDATA
|
||||
@@ -818,8 +1100,9 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
|
||||
/**
|
||||
* Callback invoked when the terminal needs to write data back
|
||||
* to the pty (e.g. in response to a DECRQM query or device
|
||||
* status report). Set to NULL to ignore such sequences.
|
||||
* to the pty (e.g. in response to a DECRQM query, device status
|
||||
* report, or VT-driven mode 2048 enable). Set to NULL to ignore such
|
||||
* sequences.
|
||||
*
|
||||
* Input type: GhosttyTerminalWritePtyFn
|
||||
*/
|
||||
@@ -1062,10 +1345,11 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
|
||||
/**
|
||||
* Callback invoked when the running program performs a clipboard write.
|
||||
* OSC 52 and iTerm2 OSC 1337 Copy writes are normalized to an atomic set
|
||||
* of decoded MIME representations. Set to NULL to ignore clipboard writes.
|
||||
* Clipboard read requests are always ignored; see
|
||||
* GhosttyTerminalClipboardWriteFn.
|
||||
* OSC 52, iTerm2 OSC 1337 Copy, and Kitty clipboard (OSC 5522) writes
|
||||
* are normalized to an atomic set of decoded MIME representations. Set
|
||||
* to NULL to ignore clipboard writes (Kitty clipboard writes are then
|
||||
* refused with ENOSYS). Clipboard read requests are delivered to
|
||||
* GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ instead.
|
||||
*
|
||||
* Input type: GhosttyTerminalClipboardWriteFn
|
||||
*/
|
||||
@@ -1227,6 +1511,40 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Input type: GhosttyString*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_TERMINFO_NAME = 37,
|
||||
|
||||
/**
|
||||
* Callback invoked when the running program requests clipboard contents
|
||||
* via OSC 52 with a "?" payload or a Kitty clipboard (OSC 5522) read. The
|
||||
* read is synchronous and must be answered before the callback returns.
|
||||
* Set to NULL (the default) to ignore OSC 52 read requests and refuse
|
||||
* OSC 5522 reads with EPERM.
|
||||
*
|
||||
* Input type: GhosttyTerminalClipboardReadFn
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_CLIPBOARD_READ = 38,
|
||||
|
||||
/**
|
||||
* Set the maximum total decoded bytes a single Kitty clipboard protocol
|
||||
* (OSC 5522) write transaction may accumulate. The limit is captured
|
||||
* when a transaction begins; an in-flight transaction keeps the limit
|
||||
* it started with.
|
||||
*
|
||||
* Data beyond the limit fails the whole transaction with EFBIG. The
|
||||
* transaction is discarded, later write-related packets are ignored
|
||||
* until a new write begins, and nothing reaches the clipboard write
|
||||
* callback.
|
||||
*
|
||||
* Transactions are buffered in memory, so this limit bounds how much
|
||||
* memory a single write can make the terminal allocate. Pass SIZE_MAX
|
||||
* to remove the limit. A NULL value pointer reverts to the built-in
|
||||
* default of 64MiB, the minimum required by the protocol.
|
||||
*
|
||||
* This limit doesn't apply to OSC 52 writes, which are bounded by the
|
||||
* maximum length of an escape sequence instead.
|
||||
*
|
||||
* Input type: size_t*
|
||||
*/
|
||||
GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE_MAX_BYTES = 39,
|
||||
GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalOption;
|
||||
|
||||
@@ -1624,6 +1942,15 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* Output type: bool *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CURSOR_AT_PROMPT = 39,
|
||||
|
||||
/**
|
||||
* The configured maximum decoded bytes per Kitty clipboard protocol
|
||||
* (OSC 5522) write transaction. See
|
||||
* GHOSTTY_TERMINAL_OPT_CLIPBOARD_WRITE_MAX_BYTES.
|
||||
*
|
||||
* Output type: size_t *
|
||||
*/
|
||||
GHOSTTY_TERMINAL_DATA_CLIPBOARD_WRITE_MAX_BYTES = 40,
|
||||
GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyTerminalData;
|
||||
|
||||
|
||||
@@ -39,18 +39,21 @@
|
||||
* The Zig side backs all C enums with c_int, so the C declarations
|
||||
* must use int as their underlying type to maintain ABI compatibility.
|
||||
*
|
||||
* C23 (detected via __STDC_VERSION__ >= 202311L) supports explicit
|
||||
* enum underlying types with `enum : int { ... }`. For pre-C23
|
||||
* compilers, which are free to choose any type that can represent
|
||||
* all values (C11 §6.7.2.2), we add an INT_MAX sentinel as the last
|
||||
* entry to force the compiler to use int.
|
||||
* C++11 and C23 support explicit enum underlying types with
|
||||
* `enum : int { ... }`. Clang and GCC 13+ also support this syntax as
|
||||
* an extension in older C language modes, so use it when available.
|
||||
*
|
||||
* Other pre-C23 C compilers are free to choose any type that can
|
||||
* represent all values (C11 §6.7.2.2). For those compilers, we add an
|
||||
* INT_MAX sentinel as the last entry so the compatible type must be
|
||||
* able to represent INT_MAX. The exact compatible type and its
|
||||
* signedness remain implementation-defined in this fallback.
|
||||
*
|
||||
* INT_MAX is used rather than a fixed constant like 0xFFFFFFFF
|
||||
* because enum constants must have type int (which is signed).
|
||||
* Values above INT_MAX overflow signed int and are a constraint
|
||||
* violation in standard C; compilers that accept them interpret them
|
||||
* as negative values via two's complement, which can collide with
|
||||
* legitimate negative enum values.
|
||||
* because enum constants must have type int in pre-C23 C. Values above
|
||||
* INT_MAX are a constraint violation there; compilers that accept them
|
||||
* may interpret them as negative values via two's complement, which can
|
||||
* collide with legitimate negative enum values.
|
||||
*
|
||||
* Usage:
|
||||
* @code
|
||||
@@ -61,7 +64,18 @@
|
||||
* } Foo;
|
||||
* @endcode
|
||||
*/
|
||||
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
#if defined(__cplusplus) && \
|
||||
(__cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1700))
|
||||
#define GHOSTTY_ENUM_TYPED : int
|
||||
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
|
||||
#define GHOSTTY_ENUM_TYPED : int
|
||||
#elif defined(__clang__)
|
||||
#if __has_extension(c_fixed_enum)
|
||||
#define GHOSTTY_ENUM_TYPED : int
|
||||
#else
|
||||
#define GHOSTTY_ENUM_TYPED
|
||||
#endif
|
||||
#elif defined(__GNUC__) && __GNUC__ >= 13
|
||||
#define GHOSTTY_ENUM_TYPED : int
|
||||
#else
|
||||
#define GHOSTTY_ENUM_TYPED
|
||||
@@ -86,6 +100,12 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_IO_ERROR = -5,
|
||||
/** Operation failed because encoded input exceeded a configured limit */
|
||||
GHOSTTY_LIMIT_EXCEEDED = -6,
|
||||
/**
|
||||
* Operation was rejected by a safety check (e.g. pasted text that could
|
||||
* inject commands). Nothing was done. Confirm with the user and retry
|
||||
* with the operation's allow flag set.
|
||||
*/
|
||||
GHOSTTY_REJECTED = -7,
|
||||
GHOSTTY_RESULT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyResult;
|
||||
|
||||
@@ -168,6 +188,19 @@ typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
|
||||
*/
|
||||
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
|
||||
|
||||
/**
|
||||
* Opaque handle to a terminal search.
|
||||
*
|
||||
* A search is bound to the terminal it was created with. It borrows the
|
||||
* terminal, so it never frees it, and the search must be freed with
|
||||
* ghostty_search_free(). If the terminal is freed first, the search
|
||||
* detects this: calls that need the terminal fail cleanly and the
|
||||
* search can still be freed.
|
||||
*
|
||||
* @ingroup search
|
||||
*/
|
||||
typedef struct GhosttySearchImpl* GhosttySearch;
|
||||
|
||||
/**
|
||||
* Opaque handle to an SGR parser instance.
|
||||
*
|
||||
@@ -305,33 +338,64 @@ typedef struct {
|
||||
* opts.trim = true;
|
||||
* @endcode
|
||||
*/
|
||||
#ifdef __cplusplus
|
||||
#define GHOSTTY_INIT_SIZED(type) \
|
||||
([]() noexcept { \
|
||||
type value{}; \
|
||||
value.size = sizeof(value); \
|
||||
return value; \
|
||||
}())
|
||||
#else
|
||||
#define GHOSTTY_INIT_SIZED(type) \
|
||||
((type){ .size = sizeof(type) })
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Return a pointer to a null-terminated JSON string describing the
|
||||
* layout of every C API struct for the current target.
|
||||
* Return the versioned libghostty-vt C type manifest for the current target.
|
||||
*
|
||||
* This is primarily useful for language bindings that can't easily
|
||||
* set C struct fields and need to do so via byte offsets. For example,
|
||||
* WebAssembly modules can't share struct definitions with the host.
|
||||
* The manifest defines all the public types available in the linked
|
||||
* build. The types contain their layouts, enum values, union fields, and more.
|
||||
*
|
||||
* Language bindings, such as WebAssembly hosts, should obtain offsets,
|
||||
* sizes, alignments, array shapes, enum constants, and tagged-union arms from
|
||||
* this manifest rather than hardcoding them. Consumers should reject unknown
|
||||
* schema versions and verify the descriptors they require at initialization.
|
||||
*
|
||||
* Packed type descriptors define fields using `lsb` and `width`. `lsb` is
|
||||
* relative to bit zero of the containing numerical value; for nested packed
|
||||
* layouts it is relative to the immediate containing field. Tagged packed
|
||||
* unions select an inline arm layout using the named tag field. These layouts
|
||||
* describe the current linked build and are not a cross-version stability
|
||||
* promise.
|
||||
*
|
||||
* The formal format is defined by the
|
||||
* <a href="types.schema.json">libghostty-vt ABI manifest JSON Schema</a>.
|
||||
*
|
||||
* Example (abbreviated):
|
||||
* @code{.json}
|
||||
* {
|
||||
* "GhosttyMouseEncoderSize": {
|
||||
* "size": 40,
|
||||
* "align": 8,
|
||||
* "fields": {
|
||||
* "size": { "offset": 0, "size": 8, "type": "u64" },
|
||||
* "screen_width": { "offset": 8, "size": 4, "type": "u32" },
|
||||
* "screen_height": { "offset": 12, "size": 4, "type": "u32" },
|
||||
* "cell_width": { "offset": 16, "size": 4, "type": "u32" },
|
||||
* "cell_height": { "offset": 20, "size": 4, "type": "u32" },
|
||||
* "padding_top": { "offset": 24, "size": 4, "type": "u32" },
|
||||
* "padding_bottom": { "offset": 28, "size": 4, "type": "u32" },
|
||||
* "padding_right": { "offset": 32, "size": 4, "type": "u32" },
|
||||
* "padding_left": { "offset": 36, "size": 4, "type": "u32" }
|
||||
* "schema": 1,
|
||||
* "abi": {
|
||||
* "target": "wasm32", "os": "freestanding", "environment": "none",
|
||||
* "pointer_size": 4, "usize_size": 4, "max_alignment": 16,
|
||||
* "endian": "little"
|
||||
* },
|
||||
* "types": {
|
||||
* "GhosttyRenderStateData": {
|
||||
* "kind": "enum", "size": 4, "align": 4,
|
||||
* "underlying": "i32", "prefix": "GHOSTTY_RENDER_STATE_DATA_",
|
||||
* "values": { "INVALID": 0, "DIRTY": 3, "MAX_VALUE": 2147483647 }
|
||||
* },
|
||||
* "GhosttyStyleColor": {
|
||||
* "kind": "struct", "size": 16, "align": 8,
|
||||
* "fields": {
|
||||
* "tag": { "offset": 0, "size": 4,
|
||||
* "type": "GhosttyStyleColorTag" },
|
||||
* "value": { "offset": 8, "size": 8,
|
||||
* "type": "GhosttyStyleColorValue", "tag": "tag",
|
||||
* "arms": { "NONE": null, "PALETTE": "palette",
|
||||
* "RGB": "rgb" } }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
#ifdef __wasm__
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/types.h>
|
||||
|
||||
/** @defgroup wasm WebAssembly Utilities
|
||||
*
|
||||
* Convenience functions for allocating various types in WebAssembly builds.
|
||||
* Convenience functions for working with the low-level C ABI in WebAssembly
|
||||
* builds.
|
||||
* **These are only available the libghostty-vt wasm module.**
|
||||
*
|
||||
* Ghostty relies on pointers to various types for ABI compatibility, and
|
||||
@@ -28,37 +28,64 @@
|
||||
* your custom allocator. This is a very rare use case in the WebAssembly
|
||||
* world so these are optimized for simplicity.
|
||||
*
|
||||
* Use ghostty_wasm_alloc() and ghostty_wasm_free() for host-owned scratch
|
||||
* buffers and ABI values. Dynamic-language hosts can use ghostty_type_json()
|
||||
* to discover pointer and size_t widths, maximum alignment, byte order, and
|
||||
* the size and alignment of public C structs. Do not mix allocation families:
|
||||
* buffers returned by libghostty-vt allocating APIs must still be released
|
||||
* with ghostty_free(), and opaque handles must be released with their
|
||||
* type-specific destructor.
|
||||
*
|
||||
* ## Memory growth
|
||||
*
|
||||
* An exported function may grow Wasm linear memory when it allocates. Numeric
|
||||
* pointers and handles remain valid, but JavaScript ArrayBuffer, DataView, and
|
||||
* typed-array objects created before the growth may no longer cover the live
|
||||
* memory. Reacquire `exports.memory.buffer` immediately before every host-side
|
||||
* memory access. A host that caches views should recreate them whenever either
|
||||
* the buffer identity or its byte length changes.
|
||||
*
|
||||
* ## Example Usage
|
||||
*
|
||||
* Here's a simple example of using the Wasm utilities with the key encoder:
|
||||
* Here's a simple example that creates a terminal, writes bytes, and safely
|
||||
* handles memory growth:
|
||||
*
|
||||
* @code
|
||||
* const { exports } = wasmInstance;
|
||||
* const view = new DataView(wasmMemory.buffer);
|
||||
* const memory = exports.memory;
|
||||
* let cachedBuffer = null;
|
||||
* let cachedLength = 0;
|
||||
* let cachedBytes = null;
|
||||
*
|
||||
* // Create key encoder
|
||||
* const encoderPtr = exports.ghostty_wasm_alloc_opaque();
|
||||
* exports.ghostty_key_encoder_new(null, encoderPtr);
|
||||
* const encoder = view.getUint32(encoder, true);
|
||||
* function bytes() {
|
||||
* const buffer = memory.buffer;
|
||||
* if (buffer !== cachedBuffer || buffer.byteLength !== cachedLength) {
|
||||
* cachedBuffer = buffer;
|
||||
* cachedLength = buffer.byteLength;
|
||||
* cachedBytes = new Uint8Array(buffer);
|
||||
* }
|
||||
* return cachedBytes;
|
||||
* }
|
||||
*
|
||||
* // Configure encoder with Kitty protocol flags
|
||||
* const flagsPtr = exports.ghostty_wasm_alloc_u8();
|
||||
* view.setUint8(flagsPtr, 0x1F);
|
||||
* exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
|
||||
* function check(result) {
|
||||
* if (result !== 0) throw new Error(`libghostty-vt error: ${result}`);
|
||||
* }
|
||||
*
|
||||
* // Allocate output buffer and size pointer
|
||||
* const bufferSize = 32;
|
||||
* const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
|
||||
* const writtenPtr = exports.ghostty_wasm_alloc_usize();
|
||||
* // One slot can be reused for every opaque-handle constructor.
|
||||
* const slot = exports.ghostty_wasm_alloc_opaque();
|
||||
* if (slot === 0) throw new Error("out of memory");
|
||||
* check(exports.ghostty_terminal_new(0, slot, 80, 24));
|
||||
* const terminal = exports.ghostty_wasm_take_opaque(slot);
|
||||
*
|
||||
* // Encode the key event
|
||||
* exports.ghostty_key_encoder_encode(
|
||||
* encoder, eventPtr, bufPtr, bufferSize, writtenPtr
|
||||
* );
|
||||
* const input = new TextEncoder().encode("Hello, world!");
|
||||
* const inputPtr = exports.ghostty_wasm_alloc(input.length);
|
||||
* if (inputPtr === 0) throw new Error("out of memory");
|
||||
* bytes().set(input, inputPtr); // Acquires the current memory after alloc.
|
||||
* exports.ghostty_terminal_vt_write(terminal, inputPtr, input.length);
|
||||
*
|
||||
* // Read encoded output
|
||||
* const bytesWritten = view.getUint32(writtenPtr, true);
|
||||
* const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
|
||||
* exports.ghostty_wasm_free(inputPtr, input.length);
|
||||
* exports.ghostty_terminal_free(terminal);
|
||||
* exports.ghostty_wasm_free_opaque(slot);
|
||||
* @endcode
|
||||
*
|
||||
* @remark The code above is pretty ugly! This is the lowest level interface
|
||||
@@ -68,9 +95,36 @@
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allocate caller-owned storage for a Wasm ABI value or scratch buffer.
|
||||
*
|
||||
* The returned address is aligned to the target's maximum C ABI alignment,
|
||||
* reported as `abi.max_alignment` by ghostty_type_json(). The memory is
|
||||
* uninitialized. A zero-length request returns NULL.
|
||||
*
|
||||
* The returned pointer must be released with ghostty_wasm_free() using the
|
||||
* exact same length.
|
||||
*
|
||||
* @param len Number of bytes to allocate
|
||||
* @return Pointer to allocated storage, or NULL if len is zero or allocation
|
||||
* failed
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void* ghostty_wasm_alloc(size_t len);
|
||||
|
||||
/**
|
||||
* Free storage allocated by ghostty_wasm_alloc().
|
||||
*
|
||||
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
|
||||
* @param len Original allocation length passed to ghostty_wasm_alloc()
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free(void *ptr, size_t len);
|
||||
|
||||
/**
|
||||
* Allocate an opaque pointer. This can be used for any opaque pointer
|
||||
* types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
|
||||
* types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. The allocated slot
|
||||
* is initialized to NULL and may be reused across constructors.
|
||||
*
|
||||
* @return Pointer to allocated opaque pointer, or NULL if allocation failed
|
||||
* @ingroup wasm
|
||||
@@ -86,72 +140,18 @@ GHOSTTY_API void** ghostty_wasm_alloc_opaque(void);
|
||||
GHOSTTY_API void ghostty_wasm_free_opaque(void **ptr);
|
||||
|
||||
/**
|
||||
* Allocate an array of uint8_t values.
|
||||
* Take an opaque handle from an out-parameter slot.
|
||||
*
|
||||
* @param len Number of uint8_t elements to allocate
|
||||
* @return Pointer to allocated array, or NULL if allocation failed
|
||||
* Returns the handle currently stored in @p slot and resets the slot to NULL.
|
||||
* This function does not allocate, free the returned handle, or free the slot.
|
||||
* Always check the GhosttyResult returned by the function that populated the
|
||||
* slot before calling this function.
|
||||
*
|
||||
* @param slot Pointer to an opaque out-parameter slot, or NULL
|
||||
* @return Stored opaque handle, or NULL if slot or its value is NULL
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
|
||||
|
||||
/**
|
||||
* Free an array allocated by ghostty_wasm_alloc_u8_array().
|
||||
*
|
||||
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
|
||||
* @param len Length of the array (must match the length passed to alloc)
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
|
||||
|
||||
/**
|
||||
* Allocate an array of uint16_t values.
|
||||
*
|
||||
* @param len Number of uint16_t elements to allocate
|
||||
* @return Pointer to allocated array, or NULL if allocation failed
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
|
||||
|
||||
/**
|
||||
* Free an array allocated by ghostty_wasm_alloc_u16_array().
|
||||
*
|
||||
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
|
||||
* @param len Length of the array (must match the length passed to alloc)
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
|
||||
|
||||
/**
|
||||
* Allocate a single uint8_t value.
|
||||
*
|
||||
* @return Pointer to allocated uint8_t, or NULL if allocation failed
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8(void);
|
||||
|
||||
/**
|
||||
* Free a uint8_t allocated by ghostty_wasm_alloc_u8().
|
||||
*
|
||||
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free_u8(uint8_t *ptr);
|
||||
|
||||
/**
|
||||
* Allocate a single size_t value.
|
||||
*
|
||||
* @return Pointer to allocated size_t, or NULL if allocation failed
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API size_t* ghostty_wasm_alloc_usize(void);
|
||||
|
||||
/**
|
||||
* Free a size_t allocated by ghostty_wasm_alloc_usize().
|
||||
*
|
||||
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
|
||||
* @ingroup wasm
|
||||
*/
|
||||
GHOSTTY_API void ghostty_wasm_free_usize(size_t *ptr);
|
||||
GHOSTTY_API void* ghostty_wasm_take_opaque(void **slot);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<command name="send key" code="GhstSKey" description="Send a keyboard event to a terminal.">
|
||||
<command name="send key" code="GhstSKey" description="Send a keyboard event to a terminal. Keys are translated once through the current keyboard layout. Dead keys do not compose with subsequent keys.">
|
||||
<cocoa class="GhosttyScriptKeyEventCommand"/>
|
||||
<direct-parameter type="text" description="The key name (e.g. "enter", "a", "space")."/>
|
||||
<parameter name="action" code="GKeA" type="input action" optional="yes" description="Press or release (default: press).">
|
||||
|
||||
@@ -1099,8 +1099,8 @@
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/sparkle-project/Sparkle";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 2.5.1;
|
||||
kind = exactVersion;
|
||||
version = 2.9.6;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2",
|
||||
"version" : "2.9.0"
|
||||
"revision" : "ac2def288cbff5cfc7df3ffef6abdf45b72bcb0a",
|
||||
"version" : "2.9.6"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -263,6 +263,12 @@ class AppDelegate: NSObject,
|
||||
name: .ghosttyConfigDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(keyboardSelectionDidChange(_:)),
|
||||
name: NSTextInputContext.keyboardSelectionDidChangeNotification,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
@@ -649,6 +655,11 @@ class AppDelegate: NSObject,
|
||||
ghosttyConfigDidChange(config: config)
|
||||
}
|
||||
|
||||
@MainActor @objc private func keyboardSelectionDidChange(_ notification: Notification) {
|
||||
syncMenuShortcuts(ghostty.config)
|
||||
TerminalController.all.forEach { $0.relabelTabs() }
|
||||
}
|
||||
|
||||
@objc private func ghosttyBellDidRing(_ notification: Notification) {
|
||||
if ghostty.config.bellFeatures.contains(.system) {
|
||||
NSSound.beep()
|
||||
@@ -1335,13 +1346,9 @@ extension AppDelegate {
|
||||
|
||||
return .terminateLater
|
||||
} else {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "You have \(controllersNeedConfirmation.count) windows with running processes. Do you want to review these windows before quitting?"
|
||||
alert.informativeText = "If you don't review your windows, any running processes will be terminated"
|
||||
alert.addButton(withTitle: "Review Windows...")
|
||||
alert.addButton(withTitle: "Terminate Processes")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
let alert = NSAlert.reviewWindowsAlert(
|
||||
messageText: "You have \(controllersNeedConfirmation.count) windows with running processes. Do you want to review these windows before quitting?"
|
||||
)
|
||||
|
||||
switch alert.runModal() {
|
||||
case .alertFirstButtonReturn:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24765" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="25095" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24765"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="25095"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@@ -99,6 +99,7 @@
|
||||
<action selector="openConfig:" target="bbz-4X-AYv" id="X65-fg-iWU"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="GOd-qX-exa"/>
|
||||
<menuItem title="Reload Configuration" id="KKH-XX-5py">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
|
||||
@@ -39,6 +39,6 @@ struct CyclingIconView: View {
|
||||
|
||||
iconImage
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ struct NewTerminalIntent: AppIntent {
|
||||
// If we were given a working directory then open that directory
|
||||
if let url = workingDirectory?.fileURL {
|
||||
let dir = url.hasDirectoryPath ? url : url.deletingLastPathComponent()
|
||||
config.workingDirectory = dir.path(percentEncoded: false)
|
||||
config.workingDirectory = dir.pathWithoutTrailingSlash
|
||||
}
|
||||
|
||||
// Parse environment variables from KEY=VALUE format
|
||||
|
||||
@@ -11,12 +11,6 @@ final class ScriptKeyEventCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard NSApp.validateScript(command: self) else { return nil }
|
||||
|
||||
guard let keyName = directParameter as? String else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing key name."
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing terminal target."
|
||||
@@ -35,10 +29,63 @@ final class ScriptKeyEventCommand: NSScriptCommand {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let key = Ghostty.Input.Key(rawValue: keyName) else {
|
||||
let keyEvent: Ghostty.Input.KeyEvent
|
||||
do {
|
||||
keyEvent = try Self.parse(
|
||||
directParameter: directParameter,
|
||||
evaluatedArguments: evaluatedArguments,
|
||||
translationMods: surface.keyTranslationMods,
|
||||
)
|
||||
} catch ArgumentError.missingKey {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing key name."
|
||||
return nil
|
||||
} catch let ArgumentError.unknownKey(keyName) {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown key name: \(keyName)"
|
||||
return nil
|
||||
} catch let ArgumentError.unknownModifiers(modsString) {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown modifier in: \(modsString)"
|
||||
return nil
|
||||
} catch {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Invalid key event."
|
||||
return nil
|
||||
}
|
||||
|
||||
surface.sendKeyEvent(keyEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ScriptKeyEventCommand {
|
||||
enum ArgumentError: Error, Equatable {
|
||||
case missingKey
|
||||
case unknownKey(String)
|
||||
case unknownModifiers(String)
|
||||
}
|
||||
|
||||
/// Parse the scripting arguments for `send key` into the key event to
|
||||
/// deliver to the surface.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - directParameter: The command's direct parameter (the key name).
|
||||
/// - evaluatedArguments: The command's evaluated arguments.
|
||||
/// - translationMods: Maps the event's modifiers to the subset that
|
||||
/// participates in text translation for the target surface.
|
||||
static func parse(
|
||||
directParameter: Any?,
|
||||
evaluatedArguments: [String: Any]?,
|
||||
translationMods: (Ghostty.Input.Mods) -> Ghostty.Input.Mods = { $0 },
|
||||
) throws -> Ghostty.Input.KeyEvent {
|
||||
guard let keyName = directParameter as? String else {
|
||||
throw ArgumentError.missingKey
|
||||
}
|
||||
|
||||
guard let key = Ghostty.Input.Key(rawValue: keyName) else {
|
||||
throw ArgumentError.unknownKey(keyName)
|
||||
}
|
||||
|
||||
let action: Ghostty.Input.Action
|
||||
@@ -55,22 +102,18 @@ final class ScriptKeyEventCommand: NSScriptCommand {
|
||||
let mods: Ghostty.Input.Mods
|
||||
if let modsString = evaluatedArguments?["modifiers"] as? String {
|
||||
guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown modifier in: \(modsString)"
|
||||
return nil
|
||||
throw ArgumentError.unknownModifiers(modsString)
|
||||
}
|
||||
mods = parsed
|
||||
} else {
|
||||
mods = []
|
||||
}
|
||||
|
||||
let keyEvent = Ghostty.Input.KeyEvent(
|
||||
key: key,
|
||||
return Ghostty.Input.KeyEvent(
|
||||
synthesizing: key,
|
||||
action: action,
|
||||
mods: mods
|
||||
mods: mods,
|
||||
translationMods: translationMods(mods),
|
||||
)
|
||||
surface.sendKeyEvent(keyEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,16 @@ class ClipboardConfirmationController: NSWindowController {
|
||||
switch confirmation.kind {
|
||||
case .paste:
|
||||
window.title = "Warning: Potentially Unsafe Paste"
|
||||
case .osc_52_read, .osc_52_write:
|
||||
case .osc_52_read, .osc_52_write, .kitty_read, .kitty_write:
|
||||
window.title = "Authorize Clipboard Access"
|
||||
}
|
||||
|
||||
window.contentView = NSHostingView(rootView: ClipboardConfirmationView(
|
||||
contents: confirmation.contents,
|
||||
request: confirmation.kind,
|
||||
programName: confirmation.programName,
|
||||
canRemember: confirmation.canRemember,
|
||||
previewImage: confirmation.previewImage,
|
||||
delegate: delegate
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import SwiftUI
|
||||
|
||||
/// This delegate is notified of the completion result of the clipboard confirmation dialog.
|
||||
protocol ClipboardConfirmationViewDelegate: AnyObject {
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action)
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, remember: Bool)
|
||||
}
|
||||
|
||||
/// The SwiftUI view for showing a clipboard confirmation dialog.
|
||||
@@ -15,11 +15,13 @@ struct ClipboardConfirmationView: View {
|
||||
switch (action, reason) {
|
||||
case (.cancel, .paste):
|
||||
return "Cancel"
|
||||
case (.cancel, .osc_52_read), (.cancel, .osc_52_write):
|
||||
case (.cancel, .osc_52_read), (.cancel, .osc_52_write),
|
||||
(.cancel, .kitty_read), (.cancel, .kitty_write):
|
||||
return "Deny"
|
||||
case (.confirm, .paste):
|
||||
return "Paste"
|
||||
case (.confirm, .osc_52_read), (.confirm, .osc_52_write):
|
||||
case (.confirm, .osc_52_read), (.confirm, .osc_52_write),
|
||||
(.confirm, .kitty_read), (.confirm, .kitty_write):
|
||||
return "Allow"
|
||||
}
|
||||
}
|
||||
@@ -31,9 +33,24 @@ struct ClipboardConfirmationView: View {
|
||||
/// The type of the clipboard request
|
||||
let request: Ghostty.ClipboardRequest
|
||||
|
||||
/// The human friendly name of the requesting program, when the
|
||||
/// protocol carries one.
|
||||
var programName: String?
|
||||
|
||||
/// True when the user's decision may be remembered as a session
|
||||
/// grant, showing the remember toggle.
|
||||
var canRemember: Bool = false
|
||||
|
||||
/// An image decoded from the request contents, shown scaled in
|
||||
/// place of most of the text area when present.
|
||||
var previewImage: NSImage?
|
||||
|
||||
/// Optional delegate to get results. If this is nil, then this view will never close on its own.
|
||||
weak var delegate: ClipboardConfirmationViewDelegate?
|
||||
|
||||
/// Whether the user's decision should be remembered for the session.
|
||||
@State private var remember: Bool = false
|
||||
|
||||
/// Used to track if we should rehide on disappear
|
||||
@State private var cursorHiddenCount: UInt = 0
|
||||
|
||||
@@ -46,14 +63,27 @@ struct ClipboardConfirmationView: View {
|
||||
.padding()
|
||||
.frame(alignment: .center)
|
||||
|
||||
Text(request.text())
|
||||
Text(request.text(name: programName))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
|
||||
TextEditor(text: .constant(contents))
|
||||
.focusable(false)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
if let previewImage {
|
||||
Image(nsImage: previewImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
TextEditor(text: .constant(contents))
|
||||
.focusable(false)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
}
|
||||
|
||||
if canRemember {
|
||||
Toggle("Remember this choice for the session", isOn: $remember)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
@@ -87,10 +117,10 @@ struct ClipboardConfirmationView: View {
|
||||
}
|
||||
|
||||
private func onCancel() {
|
||||
delegate?.clipboardConfirmationComplete(.cancel)
|
||||
delegate?.clipboardConfirmationComplete(.cancel, remember: false)
|
||||
}
|
||||
|
||||
private func onPaste() {
|
||||
delegate?.clipboardConfirmationComplete(.confirm)
|
||||
delegate?.clipboardConfirmationComplete(.confirm, remember: remember)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ struct CommandOption: Identifiable, Hashable {
|
||||
/// Whether to visually emphasize this option.
|
||||
let emphasis: Bool
|
||||
/// Sort key for stable ordering when titles are equal.
|
||||
let sortKey: AnySortKey?
|
||||
let sortKey: ObjectIdentifier?
|
||||
/// The action to perform when this option is selected.
|
||||
let action: () -> Void
|
||||
|
||||
@@ -33,7 +33,7 @@ struct CommandOption: Identifiable, Hashable {
|
||||
leadingColor: Color? = nil,
|
||||
badge: String? = nil,
|
||||
emphasis: Bool = false,
|
||||
sortKey: AnySortKey? = nil,
|
||||
sortKey: ObjectIdentifier? = nil,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
func sortedTerminalPaletteOptions(_ options: [CommandOption]) -> [CommandOption] {
|
||||
options.sorted { lhs, rhs in
|
||||
let lhsTitle = lhs.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let rhsTitle = rhs.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let comparison = lhsTitle.localizedCaseInsensitiveCompare(rhsTitle)
|
||||
if comparison != .orderedSame {
|
||||
return comparison == .orderedAscending
|
||||
}
|
||||
if let lhsKey = lhs.sortKey, let rhsKey = rhs.sortKey {
|
||||
return lhsKey < rhsKey
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalCommandPaletteView: View {
|
||||
/// The surface that this command palette represents.
|
||||
let surfaceView: Ghostty.SurfaceView
|
||||
@@ -64,19 +79,7 @@ struct TerminalCommandPaletteView: View {
|
||||
// Sort the rest. We replace ":" with a character that sorts before space
|
||||
// so that "Foo:" sorts before "Foo Bar:". Use sortKey as a tie-breaker
|
||||
// for stable ordering when titles are equal.
|
||||
options.append(contentsOf: (jumpOptions + terminalOptions).sorted { a, b in
|
||||
let aNormalized = a.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let bNormalized = b.title.replacingOccurrences(of: ":", with: "\t")
|
||||
let comparison = aNormalized.localizedCaseInsensitiveCompare(bNormalized)
|
||||
if comparison != .orderedSame {
|
||||
return comparison == .orderedAscending
|
||||
}
|
||||
// Tie-breaker: use sortKey if both have one
|
||||
if let aSortKey = a.sortKey, let bSortKey = b.sortKey {
|
||||
return aSortKey < bSortKey
|
||||
}
|
||||
return false
|
||||
})
|
||||
options.append(contentsOf: sortedTerminalPaletteOptions(jumpOptions + terminalOptions))
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -168,7 +171,7 @@ struct TerminalCommandPaletteView: View {
|
||||
subtitle: subtitle,
|
||||
leadingIcon: "rectangle.on.rectangle",
|
||||
leadingColor: displayColor?.displayColor.map { Color($0) },
|
||||
sortKey: AnySortKey(ObjectIdentifier(surface))
|
||||
sortKey: ObjectIdentifier(surface)
|
||||
) {
|
||||
NotificationCenter.default.post(
|
||||
name: Ghostty.Notification.ghosttyPresentTerminal,
|
||||
|
||||
@@ -159,8 +159,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// applies if we can be seen.
|
||||
guard visible else { return }
|
||||
|
||||
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true)
|
||||
|
||||
// Re-hide the dock if we were hiding it before.
|
||||
hiddenDock?.hide()
|
||||
}
|
||||
@@ -174,8 +172,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// ensures we don't run logic twice.
|
||||
guard visible else { return }
|
||||
|
||||
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false)
|
||||
|
||||
// We don't animate out if there is a modal sheet being shown currently.
|
||||
// This lets us show alerts without causing the window to disappear.
|
||||
guard window?.attachedSheet == nil else { return }
|
||||
|
||||
@@ -15,7 +15,7 @@ struct SecureInputOverlay: View {
|
||||
|
||||
Image(systemName: "lock.shield.fill")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.scaledToFit()
|
||||
.frame(width: 25, height: 25)
|
||||
.foregroundColor(.primary)
|
||||
.padding(5)
|
||||
|
||||
@@ -59,7 +59,7 @@ class ServiceProvider: NSObject {
|
||||
|
||||
for url in directoryURLs {
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
config.workingDirectory = url.path(percentEncoded: false)
|
||||
config.workingDirectory = url.pathWithoutTrailingSlash
|
||||
|
||||
switch target {
|
||||
case .window:
|
||||
|
||||
@@ -1462,7 +1462,7 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
@IBAction func findPrevious(_ sender: Any) {
|
||||
focusedSurface?.findNext(sender)
|
||||
focusedSurface?.findPrevious(sender)
|
||||
}
|
||||
|
||||
@IBAction func findHide(_ sender: Any) {
|
||||
@@ -1665,7 +1665,7 @@ extension BaseTerminalController {
|
||||
target.pendingClipboardConfirmation = nil
|
||||
}
|
||||
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action) {
|
||||
func clipboardConfirmationComplete(_ action: ClipboardConfirmationView.Action, remember: Bool) {
|
||||
// End our clipboard confirmation no matter what
|
||||
guard let cc = self.clipboardConfirmation else { return }
|
||||
dismissClipboardConfirmation(cc)
|
||||
@@ -1674,7 +1674,7 @@ extension BaseTerminalController {
|
||||
case .cancel:
|
||||
cc.confirmation.cancel()
|
||||
case .confirm:
|
||||
cc.confirmation.complete()
|
||||
cc.confirmation.complete(remember: remember)
|
||||
}
|
||||
|
||||
// Clear only if this is still the surface's current request. Completing
|
||||
|
||||
@@ -48,7 +48,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
|
||||
/// The initial window presentation is deferred by one runloop turn in a few places so
|
||||
/// AppKit can settle tab/window state first. Close actions must cancel it to avoid
|
||||
/// re-showing a tab that was already closed.
|
||||
/// re-showing a tab/window that was already closed.
|
||||
private var pendingInitialPresentation: DispatchWorkItem?
|
||||
|
||||
/// This is set to false by init if the window managed by this controller should not be restorable.
|
||||
@@ -283,17 +283,25 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
}
|
||||
}
|
||||
|
||||
// We're dispatching this async because otherwise the lastCascadePoint doesn't
|
||||
// take effect. Our best theory is there is some next-event-loop-tick logic
|
||||
// that Cocoa is doing that we need to be after.
|
||||
c.scheduleInitialPresentation {
|
||||
c.showWindow(self)
|
||||
// We're dispatching this async because in some cases AppKit will tab this window,
|
||||
// although we have a check in `windowDidLoad` and it works in most cases, but not for AppIntent
|
||||
//
|
||||
// That weird tabbing behavior only happens in the following cases at the point of writing.
|
||||
// - Creating a window via the Shortcuts app for now.
|
||||
// - Creating a window via `New Ghostty Window Here` service.
|
||||
c.showWindowSafely(self)
|
||||
|
||||
// Only cascade if we aren't fullscreen.
|
||||
if let window = c.window {
|
||||
if !window.styleMask.contains(.fullScreen) {
|
||||
let hasFixedPos = c.derivedConfig.windowPositionX != nil && c.derivedConfig.windowPositionY != nil
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
// We're dispatching this async because otherwise the lastCascadePoint doesn't
|
||||
// take effect after positioning in `showWindow`. Our best theory is there is
|
||||
// some next-event-loop-tick logic that Cocoa is doing that we need to be after.
|
||||
DispatchQueue.main.async {
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,8 +361,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
c.isBackgroundOpaque = inheritBackgroundOpacity
|
||||
}
|
||||
|
||||
// Showing window in current event loop works so far with dragging surface into
|
||||
// a new window, but remember to defer the cascade when you move it inside
|
||||
// `scheduleInitialPresentation` to solve other issues in the future.
|
||||
c.showWindowSafely(self)
|
||||
c.scheduleInitialPresentation {
|
||||
c.showWindow(self)
|
||||
if let window = c.window {
|
||||
// If we have a tree size, resize the window's content to match
|
||||
if let treeSize, treeSize.width > 0, treeSize.height > 0 {
|
||||
@@ -453,26 +464,44 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
|
||||
// If we don't allow tabs then we create a new window instead.
|
||||
if window.tabbingMode != .disallowed {
|
||||
let tabCreated: Bool
|
||||
// Add the window to the tab group and show it.
|
||||
switch ghostty.config.windowNewTabPosition {
|
||||
case "end":
|
||||
// If we already have a tab group and we want the new tab to open at the end,
|
||||
// then we use the last window in the tab group as the parent.
|
||||
if let last = parent.tabGroup?.windows.last {
|
||||
last.addTabbedWindowSafely(window, ordered: .above)
|
||||
tabCreated = last.addTabbedWindowSafely(window, ordered: .above)
|
||||
} else {
|
||||
fallthrough
|
||||
}
|
||||
|
||||
case "current": fallthrough
|
||||
default:
|
||||
parent.addTabbedWindowSafely(window, ordered: .above)
|
||||
tabCreated = parent.addTabbedWindowSafely(window, ordered: .above)
|
||||
}
|
||||
if tabCreated {
|
||||
// We set the selectedWindow early here because we want the next window
|
||||
// to become first responder as quickly as possible. Usually this is
|
||||
// set while `-[NSWindowController showWindow:]` is called, but we're
|
||||
// dispatching it to resolve other issues.
|
||||
parent.tabGroup?.selectedWindow = window
|
||||
}
|
||||
}
|
||||
|
||||
// showWindow makes regular windows key and ordered front. AppKit can
|
||||
// throw while selecting a tab if its fullscreen stack is inconsistent,
|
||||
// so this must cross the Objective-C exception bridge.
|
||||
// We don't need to dispatch this because `tabbingMode = .disallowed`
|
||||
// for HiddenTitlebarTerminalWindow.
|
||||
controller.showWindowSafely(self)
|
||||
|
||||
// Windows with `macos-titlebar-style = hidden` create new windows when the
|
||||
// new tab binding is pressed, we should cascade those windows as well.
|
||||
|
||||
// We're dispatching this async because otherwise the lastCascadePoint doesn't
|
||||
// take effect. Our best theory is there is some next-event-loop-tick logic
|
||||
// that Cocoa is doing that we need to be after.
|
||||
// take effect after position in `showWindow`. Our best theory is there is some
|
||||
// next-event-loop-tick logic that Cocoa is doing that we need to be after.
|
||||
controller.scheduleInitialPresentation {
|
||||
// Only cascade if we aren't fullscreen and are alone in the tab group.
|
||||
if !window.styleMask.contains(.fullScreen) &&
|
||||
@@ -481,11 +510,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
Self.applyCascade(to: window, hasFixedPos: hasFixedPos)
|
||||
}
|
||||
|
||||
// showWindow makes regular windows key and ordered front. AppKit can
|
||||
// throw while selecting a tab if its fullscreen stack is inconsistent,
|
||||
// so this must cross the Objective-C exception bridge.
|
||||
controller.showWindowSafely(self)
|
||||
|
||||
// We also activate our app so that it becomes front. This may be
|
||||
// necessary for the dock menu.
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
@@ -1239,12 +1263,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
super.windowDidBecomeKey(notification)
|
||||
self.relabelTabs()
|
||||
self.fixTabBar()
|
||||
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: true)
|
||||
}
|
||||
|
||||
override func windowDidResignKey(_ notification: Notification) {
|
||||
super.windowDidResignKey(notification)
|
||||
terminalViewContainer?.updateGlassTintOverlay(isKeyWindow: false)
|
||||
}
|
||||
|
||||
override func windowDidMove(_ notification: Notification) {
|
||||
@@ -1384,21 +1402,58 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
// if we're closing the window. If we don't have a tabgroup for any
|
||||
// reason we check ourselves.
|
||||
let windows: [NSWindow] = window.tabGroup?.windows ?? [window]
|
||||
guard let confirmController = windows
|
||||
let confirmControllers = windows
|
||||
.compactMap({ $0.windowController as? TerminalController })
|
||||
.first(where: { $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })
|
||||
.filter({ $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })
|
||||
guard
|
||||
!confirmControllers.isEmpty
|
||||
else {
|
||||
closeWindowImmediately()
|
||||
return
|
||||
}
|
||||
if confirmControllers.count == 1 {
|
||||
// We call confirmClose on the proper controller so the alert is
|
||||
// attached to the window that needs confirmation.
|
||||
confirmControllers[0].confirmClose(
|
||||
messageText: "Close Window?",
|
||||
informativeText: "All terminal sessions in this window will be terminated.",
|
||||
) {
|
||||
self.closeWindowImmediately()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// We call confirmClose on the proper controller so the alert is
|
||||
// attached to the window that needs confirmation.
|
||||
confirmController.confirmClose(
|
||||
messageText: "Close Window?",
|
||||
informativeText: "All terminal sessions in this window will be terminated.",
|
||||
) {
|
||||
self.closeWindowImmediately()
|
||||
Task {
|
||||
let alert = NSAlert.reviewWindowsAlert(
|
||||
messageText: "You have \(confirmControllers.count) windows with running processes. Do you want to review these windows before closing?",
|
||||
terminateNowButtonTitle: "Close"
|
||||
)
|
||||
switch await alert.beginSheetModal(for: window) {
|
||||
case .alertFirstButtonReturn:
|
||||
await reviewWindows(confirmControllers, window: window)
|
||||
case .alertSecondButtonReturn:
|
||||
closeWindowImmediately()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reviewWindows(_ controllers: [TerminalController], window: NSWindow) async {
|
||||
for controller in controllers {
|
||||
let response = await controller.confirmCloseAsync(
|
||||
messageText: "Close Window?",
|
||||
informativeText: "All terminal sessions in this window will be terminated.",
|
||||
)
|
||||
|
||||
if [.OK, .alertFirstButtonReturn].contains(response) {
|
||||
// Close this tab
|
||||
controller.closeTabImmediately()
|
||||
continue
|
||||
} else {
|
||||
// Cancel the review
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import SwiftUI
|
||||
class TerminalViewContainer: NSView {
|
||||
private let terminalView: NSView
|
||||
|
||||
/// Combined glass effect and inactive tint overlay view
|
||||
/// Background color applied with glass effect
|
||||
private(set) var glassEffectView: NSView?
|
||||
private var derivedConfig: DerivedConfig?
|
||||
|
||||
@@ -78,7 +78,14 @@ class TerminalViewContainer: NSView {
|
||||
let newValue = DerivedConfig(config: config, preferredBackgroundColor: preferredBackgroundColor, cornerRadius: windowCornerRadius)
|
||||
guard newValue != derivedConfig else { return }
|
||||
derivedConfig = newValue
|
||||
DispatchQueue.main.async(execute: updateGlassEffectIfNeeded)
|
||||
|
||||
// Attach the glass effect synchronously if missing to prevent flicker when a new tab appears.
|
||||
// Existing updates remain deferred, as they can occur during SwiftUI rendering.
|
||||
if glassEffectView == nil {
|
||||
updateGlassEffectIfNeeded()
|
||||
} else {
|
||||
DispatchQueue.main.async(execute: updateGlassEffectIfNeeded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,14 +103,41 @@ extension BaseTerminalController {
|
||||
/// an inactive-window tint overlay.
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
private class TerminalGlassView: NSView {
|
||||
private let glassEffectView: NSGlassEffectView
|
||||
private class TerminalGlassView: NSView, ObservableObject {
|
||||
/// We use this to apply glass effect to background colors
|
||||
///
|
||||
struct GlassBackground: View {
|
||||
@ObservedObject var model: GlassViewModel
|
||||
|
||||
var body: some View {
|
||||
model.color
|
||||
.glassEffect(
|
||||
model.glass,
|
||||
in: RoundedRectangle(cornerRadius: model.cornerRadius)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class GlassViewModel: ObservableObject {
|
||||
@Published var backgroundColor: Color = .clear
|
||||
@Published var backgroundOpacity: Double = 0
|
||||
@Published var cornerRadius: CGFloat = 0
|
||||
@Published var glass: Glass = .identity
|
||||
|
||||
/// backgroundColor applied with backgroundOpacity
|
||||
var color: Color {
|
||||
backgroundColor.opacity(backgroundOpacity)
|
||||
}
|
||||
}
|
||||
|
||||
private let glassEffectView: NSView
|
||||
private var topConstraint: NSLayoutConstraint!
|
||||
private let tintOverlay: NSView
|
||||
private let glassViewModel: GlassViewModel
|
||||
|
||||
init(topOffset: CGFloat) {
|
||||
self.glassEffectView = NSGlassEffectView()
|
||||
self.tintOverlay = NSView()
|
||||
let viewModel = GlassViewModel()
|
||||
self.glassEffectView = NSHostingView(rootView: GlassBackground(model: viewModel))
|
||||
self.glassViewModel = viewModel
|
||||
super.init(frame: .zero)
|
||||
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -121,19 +155,6 @@ private class TerminalGlassView: NSView {
|
||||
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)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
tintOverlay.topAnchor.constraint(equalTo: glassEffectView.topAnchor),
|
||||
tintOverlay.leadingAnchor.constraint(equalTo: glassEffectView.leadingAnchor),
|
||||
tintOverlay.bottomAnchor.constraint(equalTo: glassEffectView.bottomAnchor),
|
||||
tintOverlay.trailingAnchor.constraint(equalTo: glassEffectView.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@@ -141,19 +162,17 @@ private class TerminalGlassView: NSView {
|
||||
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.
|
||||
/// Configures the glass, tint color, corner radius.
|
||||
func configure(
|
||||
style: NSGlassEffectView.Style,
|
||||
glass: Glass,
|
||||
backgroundColor: NSColor,
|
||||
backgroundOpacity: Double,
|
||||
cornerRadius: CGFloat?,
|
||||
isKeyWindow: Bool
|
||||
) {
|
||||
glassEffectView.style = style
|
||||
glassEffectView.tintColor = backgroundColor.withAlphaComponent(backgroundOpacity)
|
||||
glassEffectView.cornerRadius = cornerRadius ?? 0
|
||||
updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor)
|
||||
glassViewModel.backgroundColor = Color(backgroundColor)
|
||||
glassViewModel.backgroundOpacity = backgroundOpacity
|
||||
glassViewModel.cornerRadius = cornerRadius ?? 0
|
||||
glassViewModel.glass = glass
|
||||
}
|
||||
|
||||
/// Updates the top inset offset for both the glass effect and tint overlay.
|
||||
@@ -161,21 +180,6 @@ private class TerminalGlassView: NSView {
|
||||
func updateTopInset(_ offset: CGFloat) {
|
||||
topConstraint.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)
|
||||
|
||||
@@ -215,11 +219,10 @@ extension TerminalViewContainer {
|
||||
}
|
||||
|
||||
effectView.configure(
|
||||
style: derivedConfig.style.official,
|
||||
glass: derivedConfig.glass.official,
|
||||
backgroundColor: derivedConfig.backgroundColor,
|
||||
backgroundOpacity: derivedConfig.backgroundOpacity,
|
||||
cornerRadius: derivedConfig.cornerRadius,
|
||||
isKeyWindow: window?.isKeyWindow ?? true
|
||||
)
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
@@ -237,21 +240,8 @@ extension TerminalViewContainer {
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
func updateGlassTintOverlay(isKeyWindow: Bool) {
|
||||
#if compiler(>=6.2)
|
||||
guard
|
||||
#available(macOS 26.0, *),
|
||||
let effectView = glassEffectView as? TerminalGlassView,
|
||||
let derivedConfig
|
||||
else {
|
||||
return
|
||||
}
|
||||
effectView.updateKeyStatus(isKeyWindow, backgroundColor: derivedConfig.backgroundColor)
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
struct DerivedConfig: Equatable {
|
||||
let style: BackportNSGlassStyle
|
||||
let glass: BackportGlass
|
||||
let backgroundColor: NSColor
|
||||
let backgroundOpacity: Double
|
||||
let cornerRadius: CGFloat?
|
||||
@@ -259,9 +249,9 @@ extension TerminalViewContainer {
|
||||
init?(config: Ghostty.Config, preferredBackgroundColor: NSColor?, cornerRadius: CGFloat?) {
|
||||
switch config.backgroundBlur {
|
||||
case .macosGlassRegular:
|
||||
style = .regular
|
||||
glass = .regular
|
||||
case .macosGlassClear:
|
||||
style = .clear
|
||||
glass = .clear
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class TerminalWindow: NSWindow {
|
||||
|
||||
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
||||
// where buttons were not clickable.
|
||||
// where buttons were not clickable on macOS 15.
|
||||
tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor)
|
||||
|
||||
let stackView = NSStackView()
|
||||
|
||||
@@ -57,7 +57,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
super.becomeKey()
|
||||
|
||||
updateNewTabButtonOpacity()
|
||||
resetZoomToolbarButton.contentTintColor = .controlAccentColor
|
||||
tab.attributedTitle = attributedTitle
|
||||
}
|
||||
|
||||
@@ -65,7 +64,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
super.resignKey()
|
||||
|
||||
updateNewTabButtonOpacity()
|
||||
resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor
|
||||
tab.attributedTitle = attributedTitle
|
||||
}
|
||||
|
||||
@@ -236,42 +234,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Split Zoom Button
|
||||
|
||||
private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton()
|
||||
|
||||
private func generateResetZoomButton() -> NSButton {
|
||||
let button = NSButton()
|
||||
button.target = nil
|
||||
button.action = #selector(TerminalController.splitZoom(_:))
|
||||
button.isBordered = false
|
||||
button.allowsExpansionToolTips = true
|
||||
button.toolTip = "Reset Zoom"
|
||||
button.contentTintColor = .controlAccentColor
|
||||
button.state = .on
|
||||
button.image = NSImage(named: "ResetZoom")
|
||||
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.widthAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
button.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func selectTabAndZoom(_ sender: NSButton) {
|
||||
guard let tabGroup else { return }
|
||||
|
||||
guard let associatedWindow = tabGroup.windows.first(where: {
|
||||
guard let accessoryView = $0.tab.accessoryView else { return false }
|
||||
return accessoryView.subviews.contains(sender)
|
||||
}),
|
||||
let windowController = associatedWindow.windowController as? TerminalController
|
||||
else { return }
|
||||
|
||||
tabGroup.selectedWindow = associatedWindow
|
||||
windowController.splitZoom(self)
|
||||
}
|
||||
|
||||
// MARK: - Titlebar Font
|
||||
|
||||
// Used to set the titlebar font.
|
||||
@@ -321,12 +283,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
|
||||
toolbar = terminalToolbar
|
||||
toolbarStyle = .unifiedCompact
|
||||
if let resetZoomItem = terminalToolbar.items.first(where: { $0.itemIdentifier == .resetZoom }) {
|
||||
resetZoomItem.view = resetZoomToolbarButton
|
||||
resetZoomItem.view!.removeConstraints(resetZoomItem.view!.constraints)
|
||||
resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true
|
||||
resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
// For titlebar tabs, we want to hide the separator view so that we get rid
|
||||
@@ -648,8 +604,6 @@ private class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
||||
])
|
||||
|
||||
item.isEnabled = true
|
||||
case .resetZoom:
|
||||
item = NSToolbarItem(itemIdentifier: .resetZoom)
|
||||
default:
|
||||
item = NSToolbarItem(itemIdentifier: itemIdentifier)
|
||||
}
|
||||
@@ -658,7 +612,7 @@ private class TerminalToolbar: NSToolbar, NSToolbarDelegate {
|
||||
}
|
||||
|
||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return [.titleText, .flexibleSpace, .space, .resetZoom]
|
||||
return [.titleText, .flexibleSpace]
|
||||
}
|
||||
|
||||
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
@@ -713,6 +667,5 @@ private class CenteredDynamicLabel: NSTextField {
|
||||
}
|
||||
|
||||
extension NSToolbarItem.Identifier {
|
||||
static let resetZoom = NSToolbarItem.Identifier("ResetZoom")
|
||||
static let titleText = NSToolbarItem.Identifier("TitleText")
|
||||
}
|
||||
|
||||
@@ -60,8 +60,20 @@ extension Ghostty {
|
||||
supports_selection_clipboard: true,
|
||||
wakeup_cb: { userdata in App.wakeup(userdata) },
|
||||
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
|
||||
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
||||
confirm_read_clipboard_cb: { userdata, str, state, request in App.confirmReadClipboard(userdata, string: str, state: state, request: request ) },
|
||||
read_clipboard_cb: { userdata, loc, state, mimes, mimesLen, list in
|
||||
App.readClipboard(
|
||||
userdata,
|
||||
location: loc,
|
||||
state: state,
|
||||
mimes: mimes,
|
||||
mimesLen: mimesLen,
|
||||
list: list) },
|
||||
confirm_read_clipboard_cb: { userdata, confirm, state, request in
|
||||
App.confirmReadClipboard(
|
||||
userdata,
|
||||
confirm: confirm,
|
||||
state: state,
|
||||
request: request) },
|
||||
write_clipboard_cb: { userdata, loc, content, len, confirm in
|
||||
App.writeClipboard(userdata, location: loc, content: content, len: len, confirm: confirm) },
|
||||
close_surface_cb: { userdata, processAlive in App.closeSurface(userdata, processAlive: processAlive) }
|
||||
@@ -286,60 +298,186 @@ extension Ghostty {
|
||||
static func readClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
location: ghostty_clipboard_e,
|
||||
state: UnsafeMutableRawPointer?
|
||||
) -> Bool {
|
||||
state: UnsafeMutableRawPointer?,
|
||||
mimes: UnsafePointer<UnsafePointer<CChar>?>?,
|
||||
mimesLen: Int,
|
||||
list: Bool
|
||||
) -> ghostty_clipboard_read_result_e {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard let surface = surfaceView.surface else { return false }
|
||||
guard let surface = surfaceView.surface else {
|
||||
return GHOSTTY_CLIPBOARD_READ_UNSUPPORTED
|
||||
}
|
||||
|
||||
// Get our pasteboard
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else { return false }
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else {
|
||||
return GHOSTTY_CLIPBOARD_READ_UNSUPPORTED
|
||||
}
|
||||
|
||||
// Return false if there is no text-like clipboard content so
|
||||
// performable paste bindings can pass through to the terminal.
|
||||
guard let str = pasteboard.getOpinionatedStringContents() else { return false }
|
||||
// Gather the representation for each requested MIME type that
|
||||
// the pasteboard can serve. We only ever read the requested
|
||||
// representations so unrelated (potentially large) clipboard
|
||||
// contents are never loaded.
|
||||
var contents: [Ghostty.ClipboardContent] = []
|
||||
var seen = Set<String>()
|
||||
if let mimes {
|
||||
for i in 0..<mimesLen {
|
||||
guard let ptr = mimes[i] else { continue }
|
||||
let mime = String(cString: ptr)
|
||||
guard !seen.contains(mime) else { continue }
|
||||
seen.insert(mime)
|
||||
guard let data = pasteboard.ghosttyData(forMime: mime) else { continue }
|
||||
contents.append(.init(mime: mime, data: data))
|
||||
}
|
||||
}
|
||||
|
||||
completeClipboardRequest(surface, data: str, state: state)
|
||||
return true
|
||||
// The listing of available types, only gathered when requested.
|
||||
let available: [String] = list ? pasteboard.ghosttyAvailableMimes() : []
|
||||
|
||||
// With nothing to serve and no listing requested there is
|
||||
// nothing to complete the read with.
|
||||
if contents.isEmpty && !list {
|
||||
return GHOSTTY_CLIPBOARD_READ_UNAVAILABLE
|
||||
}
|
||||
|
||||
completeClipboardRequest(
|
||||
surface,
|
||||
contents: contents,
|
||||
available: available,
|
||||
state: state)
|
||||
return GHOSTTY_CLIPBOARD_READ_STARTED
|
||||
}
|
||||
|
||||
static func confirmReadClipboard(
|
||||
_ userdata: UnsafeMutableRawPointer?,
|
||||
string: UnsafePointer<CChar>?,
|
||||
confirm: UnsafePointer<ghostty_clipboard_confirm_s>?,
|
||||
state: UnsafeMutableRawPointer?,
|
||||
request: ghostty_clipboard_request_e
|
||||
) {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard surfaceView.surface != nil,
|
||||
let string,
|
||||
let valueStr = String(cString: string, encoding: .utf8),
|
||||
let kind = Ghostty.ClipboardRequest.from(request: request) else { return }
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
guard let confirm,
|
||||
let kind = Ghostty.ClipboardRequest.from(request: request) else {
|
||||
ghostty_surface_deny_clipboard_request(surface, state)
|
||||
return
|
||||
}
|
||||
let c = confirm.pointee
|
||||
|
||||
// Copy the borrowed C representations: the confirmation is
|
||||
// asynchronous and completes with exactly what the user
|
||||
// approved, so the clipboard is never re-read.
|
||||
var reps: [Ghostty.ClipboardContent] = []
|
||||
if let contents = c.contents {
|
||||
for i in 0..<c.contents_len {
|
||||
let content = contents[i]
|
||||
let data: Data = if content.len > 0 {
|
||||
Data(bytes: content.data, count: content.len)
|
||||
} else {
|
||||
Data()
|
||||
}
|
||||
reps.append(.init(mime: String(cString: content.mime), data: data))
|
||||
}
|
||||
}
|
||||
var avail: [String] = []
|
||||
if let available = c.available {
|
||||
for i in 0..<c.available_len {
|
||||
guard let ptr = available[i] else { continue }
|
||||
avail.append(String(cString: ptr))
|
||||
}
|
||||
}
|
||||
|
||||
// The dialog can only display text: show the text
|
||||
// representation when there is one and summarize the rest.
|
||||
let display = reps.first(where: { $0.mime == "text/plain" })
|
||||
.flatMap { String(data: $0.data, encoding: .utf8) }
|
||||
?? reps.map { "\($0.mime) (\($0.data.count) bytes)" }.joined(separator: "\n")
|
||||
|
||||
// Decode an image representation so the dialog can preview
|
||||
// exactly what would be disclosed rather than a byte count.
|
||||
let previewImage: NSImage? = reps.lazy
|
||||
.filter { $0.mime.hasPrefix("image/") }
|
||||
.compactMap { NSImage(data: $0.data) }
|
||||
.first
|
||||
|
||||
// libghostty reaches this callback only when the request attempted
|
||||
// by readClipboard requires confirmation. Reads allowed by policy
|
||||
// complete immediately and never become pending Swift state.
|
||||
let request = Ghostty.ClipboardConfirmationRequest(
|
||||
surface: surfaceView,
|
||||
contents: valueStr,
|
||||
kind: kind
|
||||
) { surfaceView, contents in
|
||||
contents: display,
|
||||
kind: kind,
|
||||
programName: c.name.map { String(cString: $0) },
|
||||
canRemember: c.can_remember,
|
||||
previewImage: previewImage
|
||||
) { surfaceView, confirmed, remember in
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
completeClipboardRequest(
|
||||
surface,
|
||||
data: contents ?? "",
|
||||
state: state,
|
||||
confirmed: true)
|
||||
if confirmed {
|
||||
completeClipboardRequest(
|
||||
surface,
|
||||
contents: reps,
|
||||
available: avail,
|
||||
state: state,
|
||||
confirmed: true,
|
||||
remember: remember)
|
||||
} else {
|
||||
ghostty_surface_deny_clipboard_request(surface, state)
|
||||
}
|
||||
}
|
||||
surfaceView.pendingClipboardConfirmation = request
|
||||
}
|
||||
|
||||
private static func completeClipboardRequest(
|
||||
_ surface: ghostty_surface_t,
|
||||
data: String,
|
||||
contents: [Ghostty.ClipboardContent],
|
||||
available: [String],
|
||||
state: UnsafeMutableRawPointer?,
|
||||
confirmed: Bool = false
|
||||
confirmed: Bool = false,
|
||||
remember: Bool = false
|
||||
) {
|
||||
data.withCString { ptr in
|
||||
ghostty_surface_complete_clipboard_request(surface, ptr, state, confirmed)
|
||||
// Copy everything into C memory for the duration of the call.
|
||||
var cStrings: [UnsafeMutablePointer<CChar>] = []
|
||||
var cDatas: [UnsafeMutableRawPointer] = []
|
||||
defer {
|
||||
cStrings.forEach { free($0) }
|
||||
cDatas.forEach { $0.deallocate() }
|
||||
}
|
||||
|
||||
var cContents: [ghostty_clipboard_content_s] = []
|
||||
for entry in contents {
|
||||
guard let mime = strdup(entry.mime) else { continue }
|
||||
cStrings.append(mime)
|
||||
let buf = UnsafeMutableRawPointer.allocate(
|
||||
byteCount: max(entry.data.count, 1),
|
||||
alignment: 1)
|
||||
cDatas.append(buf)
|
||||
entry.data.withUnsafeBytes { src in
|
||||
if let base = src.baseAddress {
|
||||
buf.copyMemory(from: base, byteCount: src.count)
|
||||
}
|
||||
}
|
||||
cContents.append(ghostty_clipboard_content_s(
|
||||
mime: mime,
|
||||
data: buf.assumingMemoryBound(to: CChar.self),
|
||||
len: entry.data.count))
|
||||
}
|
||||
|
||||
var cAvailable: [UnsafePointer<CChar>?] = []
|
||||
for mime in available {
|
||||
guard let str = strdup(mime) else { continue }
|
||||
cStrings.append(str)
|
||||
cAvailable.append(UnsafePointer(str))
|
||||
}
|
||||
|
||||
cContents.withUnsafeBufferPointer { contentsBuf in
|
||||
cAvailable.withUnsafeBufferPointer { availableBuf in
|
||||
var complete = ghostty_clipboard_complete_s(
|
||||
contents: contentsBuf.baseAddress,
|
||||
contents_len: contentsBuf.count,
|
||||
available: availableBuf.baseAddress,
|
||||
available_len: availableBuf.count,
|
||||
confirmed: confirmed,
|
||||
remember: remember)
|
||||
ghostty_surface_complete_clipboard_request(surface, &complete, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,24 +514,25 @@ extension Ghostty {
|
||||
// Set data for each type
|
||||
for item in contentArray {
|
||||
guard let type = NSPasteboard.PasteboardType(mimeType: item.mime) else { continue }
|
||||
pasteboard.setString(item.data, forType: type)
|
||||
pasteboard.setData(item.data, forType: type)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For confirmation, use the text/plain content if it exists
|
||||
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }) else {
|
||||
guard let textPlainContent = contentArray.first(where: { $0.mime == "text/plain" }),
|
||||
let textPlainString = textPlainContent.string else {
|
||||
return
|
||||
}
|
||||
|
||||
let request = Ghostty.ClipboardConfirmationRequest(
|
||||
surface: surfaceView,
|
||||
contents: textPlainContent.data,
|
||||
contents: textPlainString,
|
||||
kind: .osc_52_write
|
||||
) { _, contents in
|
||||
guard let contents else { return }
|
||||
) { _, confirmed, _ in
|
||||
guard confirmed else { return }
|
||||
pasteboard.declareTypes([.string], owner: nil)
|
||||
pasteboard.setString(contents, forType: .string)
|
||||
pasteboard.setString(textPlainString, forType: .string)
|
||||
}
|
||||
surfaceView.pendingClipboardConfirmation = request
|
||||
}
|
||||
@@ -1985,19 +2124,21 @@ extension Ghostty {
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
if v.active {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didContinueKeySequence,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any
|
||||
]
|
||||
)
|
||||
} else {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didEndKeySequence,
|
||||
object: surfaceView
|
||||
)
|
||||
DispatchQueue.main.async {
|
||||
if v.active {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didContinueKeySequence,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any
|
||||
]
|
||||
)
|
||||
} else {
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.didEndKeySequence,
|
||||
object: surfaceView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
@@ -13,21 +14,32 @@ extension Ghostty {
|
||||
/// An application is attempting to write to the clipboard using OSC 52.
|
||||
case osc_52_write
|
||||
|
||||
/// The text to show in the clipboard confirmation prompt for this request.
|
||||
func text() -> String {
|
||||
/// An application is attempting to read from the clipboard using
|
||||
/// the Kitty clipboard protocol (OSC 5522).
|
||||
case kitty_read
|
||||
|
||||
/// An application is attempting to write to the clipboard using
|
||||
/// the Kitty clipboard protocol (OSC 5522).
|
||||
case kitty_write
|
||||
|
||||
/// The text to show in the clipboard confirmation prompt for this
|
||||
/// request. The name is the requesting program's human friendly
|
||||
/// name, when the protocol carries one.
|
||||
func text(name: String? = nil) -> String {
|
||||
let program = name.map { "\"\($0)\"" } ?? "An application"
|
||||
switch self {
|
||||
case .paste:
|
||||
return """
|
||||
Pasting this text to the terminal may be dangerous as it looks like some commands may be executed.
|
||||
"""
|
||||
case .osc_52_read:
|
||||
case .osc_52_read, .kitty_read:
|
||||
return """
|
||||
An application is attempting to read from the clipboard.
|
||||
\(program) is attempting to read from the clipboard.
|
||||
The current clipboard contents are shown below.
|
||||
"""
|
||||
case .osc_52_write:
|
||||
case .osc_52_write, .kitty_write:
|
||||
return """
|
||||
An application is attempting to write to the clipboard.
|
||||
\(program) is attempting to write to the clipboard.
|
||||
The content to write is shown below.
|
||||
"""
|
||||
}
|
||||
@@ -41,6 +53,10 @@ extension Ghostty {
|
||||
return .osc_52_read
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
|
||||
return .osc_52_write
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_KITTY_READ:
|
||||
return .kitty_read
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_KITTY_WRITE:
|
||||
return .kitty_write
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -56,20 +72,46 @@ extension Ghostty {
|
||||
/// occurs from inside the libghostty callback that created the request.
|
||||
final class ClipboardConfirmationRequest {
|
||||
private(set) weak var surface: SurfaceView?
|
||||
|
||||
/// The textual preview of the clipboard contents shown in the
|
||||
/// confirmation dialog. The actual representations served on
|
||||
/// confirmation are held by the completion.
|
||||
let contents: String
|
||||
|
||||
let kind: ClipboardRequest
|
||||
|
||||
private var completion: ((SurfaceView, String?) -> Void)?
|
||||
/// The human friendly name of the requesting program to show in
|
||||
/// the prompt, when the protocol carries one.
|
||||
let programName: String?
|
||||
|
||||
/// True when the user's decision may be remembered as a session
|
||||
/// grant, showing a remember option in the prompt.
|
||||
let canRemember: Bool
|
||||
|
||||
/// An image decoded from the request contents, previewed scaled
|
||||
/// in the dialog when the request carries an image
|
||||
/// representation.
|
||||
let previewImage: NSImage?
|
||||
|
||||
/// Called exactly once with whether the user confirmed the
|
||||
/// request and whether their decision should be remembered.
|
||||
private var completion: ((SurfaceView, Bool, Bool) -> Void)?
|
||||
|
||||
init(
|
||||
surface: SurfaceView,
|
||||
contents: String,
|
||||
kind: ClipboardRequest,
|
||||
completion: @escaping (SurfaceView, String?) -> Void
|
||||
programName: String? = nil,
|
||||
canRemember: Bool = false,
|
||||
previewImage: NSImage? = nil,
|
||||
completion: @escaping (SurfaceView, Bool, Bool) -> Void
|
||||
) {
|
||||
self.surface = surface
|
||||
self.contents = contents
|
||||
self.kind = kind
|
||||
self.programName = programName
|
||||
self.canRemember = canRemember
|
||||
self.previewImage = previewImage
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
@@ -77,29 +119,30 @@ extension Ghostty {
|
||||
guard let surface, let completion else { return }
|
||||
self.completion = nil
|
||||
DispatchQueue.main.async {
|
||||
completion(surface, nil)
|
||||
completion(surface, false, false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the request using the displayed clipboard contents.
|
||||
func complete() {
|
||||
finish(contents)
|
||||
/// Complete the request with the displayed clipboard contents.
|
||||
func complete(remember: Bool = false) {
|
||||
finish(true, remember: remember)
|
||||
}
|
||||
|
||||
/// Cancel the request without using the displayed clipboard contents.
|
||||
/// Cancel the request, denying access to the clipboard contents.
|
||||
func cancel() {
|
||||
finish(nil)
|
||||
finish(false)
|
||||
}
|
||||
|
||||
/// Cancel using the owning surface explicitly. SurfaceView uses this
|
||||
/// for replacement and teardown because its weak reference is already
|
||||
/// nil during the owner's deinitialization.
|
||||
func cancel(from surface: SurfaceView) {
|
||||
finish(nil, on: surface)
|
||||
finish(false, on: surface)
|
||||
}
|
||||
|
||||
private func finish(
|
||||
_ contents: String?,
|
||||
_ confirmed: Bool,
|
||||
remember: Bool = false,
|
||||
on explicitSurface: SurfaceView? = nil
|
||||
) {
|
||||
guard let surface = explicitSurface ?? self.surface,
|
||||
@@ -108,7 +151,7 @@ extension Ghostty {
|
||||
return
|
||||
}
|
||||
self.completion = nil
|
||||
completion(surface, contents)
|
||||
completion(surface, confirmed, remember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,13 +111,16 @@ extension Ghostty {
|
||||
/// configuration would be "quit" action.
|
||||
///
|
||||
/// Returns nil if there is no key equivalent for the given action.
|
||||
func keyboardShortcut(for action: String) -> KeyboardShortcut? {
|
||||
guard let cfg = self.config else { return nil }
|
||||
|
||||
let trigger = ghostty_config_trigger(cfg, action, UInt(action.lengthOfBytes(using: .utf8)))
|
||||
@MainActor func keyboardShortcut(for action: String) -> KeyboardShortcut? {
|
||||
guard let trigger = keybindTrigger(for: action) else { return nil }
|
||||
return Ghostty.keyboardShortcut(for: trigger)
|
||||
}
|
||||
|
||||
func keybindTrigger(for action: String) -> ghostty_input_trigger_s? {
|
||||
guard let config else { return nil }
|
||||
return ghostty_config_trigger(config, action, UInt(action.lengthOfBytes(using: .utf8)))
|
||||
}
|
||||
|
||||
// MARK: - Configuration Values
|
||||
|
||||
/// For all of the configuration values below, see the associated Ghostty documentation for
|
||||
@@ -125,10 +128,10 @@ extension Ghostty {
|
||||
/// due to the embedded library and Swift.
|
||||
|
||||
var bellFeatures: BellFeatures {
|
||||
guard let config = self.config else { return .init() }
|
||||
guard let config = self.config else { return .defaultValue }
|
||||
var v: CUnsignedInt = 0
|
||||
let key = "bell-features"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .init() }
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .defaultValue }
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
@@ -807,6 +810,8 @@ extension Ghostty.Config {
|
||||
static let attention = BellFeatures(rawValue: 1 << 2)
|
||||
static let title = BellFeatures(rawValue: 1 << 3)
|
||||
static let border = BellFeatures(rawValue: 1 << 4)
|
||||
|
||||
static let defaultValue = BellFeatures([.attention, .title])
|
||||
}
|
||||
|
||||
struct SplitPreserveZoom: OptionSet {
|
||||
|
||||
@@ -16,16 +16,28 @@ extension Ghostty {
|
||||
/// (F1, F2, ...) with a KeyboardShortcut. This doesn't represent a practical issue because input
|
||||
/// handling for Ghostty is handled at a lower level (usually). This function should generally only
|
||||
/// be used for things like NSMenu that only support keyboard shortcuts anyways.
|
||||
static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? {
|
||||
@MainActor static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? {
|
||||
let modifierFlags = Self.eventModifierFlags(mods: trigger.mods)
|
||||
let key: KeyEquivalent
|
||||
switch trigger.tag {
|
||||
case GHOSTTY_TRIGGER_PHYSICAL:
|
||||
// Only functional keys can be converted to a KeyboardShortcut. Other physical
|
||||
// mappings cannot because KeyboardShortcut in Swift is inherently layout-dependent.
|
||||
if let equiv = Self.keyToEquivalent[trigger.key.physical] {
|
||||
key = equiv
|
||||
let physical = trigger.key.physical
|
||||
if let equivalent = Self.keyToEquivalent[physical] {
|
||||
key = equivalent
|
||||
} else {
|
||||
return nil
|
||||
guard
|
||||
Self.writingSystemKeyRange.contains(physical.rawValue),
|
||||
let inputKey = Input.Key(cKey: physical),
|
||||
let keyCode = inputKey.keyCode,
|
||||
// Command can select a distinct layout table. Other modifiers remain
|
||||
// separate in the menu's modifier mask and must not affect this character.
|
||||
let character = KeyboardLayout.character(
|
||||
for: keyCode,
|
||||
modifiers: modifierFlags.intersection(.command))
|
||||
else { return nil }
|
||||
|
||||
// Printable physical keys must be translated through the current layout.
|
||||
key = KeyEquivalent(character)
|
||||
}
|
||||
|
||||
case GHOSTTY_TRIGGER_UNICODE:
|
||||
@@ -45,7 +57,7 @@ extension Ghostty {
|
||||
|
||||
return KeyboardShortcut(
|
||||
key,
|
||||
modifiers: EventModifiers(nsFlags: Ghostty.eventModifierFlags(mods: trigger.mods)))
|
||||
modifiers: EventModifiers(nsFlags: modifierFlags))
|
||||
}
|
||||
|
||||
// MARK: Mods
|
||||
@@ -101,6 +113,10 @@ extension Ghostty {
|
||||
GHOSTTY_KEY_BACKSPACE: .delete,
|
||||
GHOSTTY_KEY_SPACE: .space,
|
||||
]
|
||||
|
||||
/// The contiguous W3C "Writing System Keys" § 3.1.1 key range.
|
||||
private static let writingSystemKeyRange =
|
||||
GHOSTTY_KEY_BACKQUOTE.rawValue...GHOSTTY_KEY_SLASH.rawValue
|
||||
}
|
||||
|
||||
// MARK: Ghostty.Input.BindingFlags
|
||||
@@ -224,6 +240,66 @@ extension Ghostty.Input {
|
||||
}
|
||||
}
|
||||
|
||||
extension Ghostty.Input.KeyEvent {
|
||||
/// Create a translated key event for programmatic input (e.g. AppleScript).
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - key: The key being pressed or released.
|
||||
/// - action: The key action.
|
||||
/// - mods: The full set of modifiers for the event.
|
||||
/// - translationMods: The subset of `mods` that participates in text
|
||||
/// translation. Use `Surface.keyTranslationMods(_:)` so that
|
||||
/// configuration such as `macos-option-as-alt` is honored.
|
||||
///
|
||||
/// - Note: Translation is a single stateless pass through the keyboard layout.
|
||||
/// A key that starts a dead-key sequence (e.g. option+E on a US layout)
|
||||
/// produces its standalone character or nothing.
|
||||
@MainActor
|
||||
init(
|
||||
synthesizing key: Ghostty.Input.Key,
|
||||
action: Ghostty.Input.Action,
|
||||
mods: Ghostty.Input.Mods,
|
||||
translationMods: Ghostty.Input.Mods
|
||||
) {
|
||||
let keyCode = key.keyCode
|
||||
|
||||
// Control never contributes to the translation of text,
|
||||
// matching `NSEvent.ghosttyCharacters`.
|
||||
let text: String?
|
||||
if action == .release {
|
||||
// We don't need to attach text to a release key event,
|
||||
// as real NSEvents don't carry them in most cases.
|
||||
text = nil
|
||||
} else {
|
||||
text = keyCode
|
||||
.flatMap {
|
||||
KeyboardLayout.character(
|
||||
for: $0,
|
||||
modifiers: translationMods.nsFlags.subtracting(.control))
|
||||
}
|
||||
.flatMap { String($0).keyEventText }
|
||||
}
|
||||
|
||||
// The unshifted codepoint ignores all modifiers. Control characters are
|
||||
// reported as no codepoint (0) so that Ghostty encodes such keys from
|
||||
// the key enum instead.
|
||||
let unshiftedCodepoint = keyCode
|
||||
.flatMap { KeyboardLayout.character(for: $0, modifiers: []) }
|
||||
.flatMap { String($0).keyEventText }?
|
||||
.unicodeScalars.first?.value ?? 0
|
||||
|
||||
self.init(
|
||||
key: key,
|
||||
action: action,
|
||||
text: text,
|
||||
mods: mods,
|
||||
// Same as `NSEvent.ghosttyKeyEvent`
|
||||
consumedMods: translationMods.subtracting([.ctrl, .super]),
|
||||
unshiftedCodepoint: unshiftedCodepoint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Ghostty.Input.Action
|
||||
|
||||
extension Ghostty.Input {
|
||||
@@ -771,14 +847,15 @@ extension Ghostty.Input {
|
||||
case cut
|
||||
case paste
|
||||
|
||||
init?(cKey: ghostty_input_key_e) {
|
||||
guard let key = Key.allCases.first(where: { $0.cKey == cKey }) else { return nil }
|
||||
self = key
|
||||
}
|
||||
|
||||
/// Get a key from a keycode
|
||||
init?(keyCode: UInt16) {
|
||||
if let key = Key.allCases.first(where: { $0.keyCode == keyCode }) {
|
||||
self = key
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
guard let key = Key.allCases.first(where: { $0.keyCode == keyCode }) else { return nil }
|
||||
self = key
|
||||
}
|
||||
|
||||
var cKey: ghostty_input_key_e {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// The manager that's responsible for updating shortcuts of Ghostty's app menu
|
||||
@@ -26,6 +27,8 @@ extension Ghostty {
|
||||
if !updateMenuShortcut(config, action: action, menuItem: menu) {
|
||||
menu.keyEquivalent = ""
|
||||
menu.keyEquivalentModifierMask = []
|
||||
menu.allowsAutomaticKeyEquivalentLocalization = true
|
||||
menu.allowsAutomaticKeyEquivalentMirroring = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +37,23 @@ extension Ghostty {
|
||||
/// bindings through the menu so they flash but also lets our surface override macOS built-ins
|
||||
/// like Cmd+H.
|
||||
func performGhosttyBindingMenuKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
// Convert this event into the same normalized lookup key we use when
|
||||
// syncing menu shortcuts from configuration.
|
||||
guard let key = MenuShortcutKey(event: event) else {
|
||||
return false
|
||||
// Physical bindings take precedence over Unicode bindings in the core.
|
||||
let physicalKey = MenuShortcutKey(
|
||||
physicalKeyCode: event.keyCode,
|
||||
modifiers: event.modifierFlags)
|
||||
if let result = performMenuItem(for: physicalKey) {
|
||||
return result
|
||||
}
|
||||
|
||||
guard let key = MenuShortcutKey(event: event) else { return false }
|
||||
return performMenuItem(for: key) ?? false
|
||||
}
|
||||
|
||||
private func performMenuItem(for key: MenuShortcutKey) -> Bool? {
|
||||
// If we don't have an entry for this key combo, no Ghostty-owned
|
||||
// menu shortcut exists for this event.
|
||||
guard let weakItem = menuItemsByShortcut[key] else {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
// Weak references can be nil if a menu item was deallocated after sync.
|
||||
@@ -81,16 +91,23 @@ private extension Ghostty.MenuShortcutManager {
|
||||
func updateMenuShortcut(_ config: Ghostty.Config, action: String?, menuItem menu: NSMenuItem) -> Bool {
|
||||
guard
|
||||
let action,
|
||||
let shortcut = config.keyboardShortcut(for: action),
|
||||
// Build a direct lookup for key-equivalent dispatch so we don't need to
|
||||
// linearly walk the full menu hierarchy at event time.
|
||||
let key = MenuShortcutKey(shortcut)
|
||||
else {
|
||||
let trigger = config.keybindTrigger(for: action),
|
||||
let shortcut = Ghostty.keyboardShortcut(for: trigger)
|
||||
else { return false }
|
||||
|
||||
let isPhysical = trigger.tag == GHOSTTY_TRIGGER_PHYSICAL
|
||||
let physicalKeyCode = isPhysical ? Ghostty.Input.Key(cKey: trigger.key.physical)?.keyCode : nil
|
||||
// Build a direct lookup for key-equivalent dispatch so we don't need to
|
||||
// linearly walk the full menu hierarchy at event time.
|
||||
guard let key = MenuShortcutKey(shortcut, physicalKeyCode: physicalKeyCode) else {
|
||||
return false
|
||||
}
|
||||
|
||||
menu.keyEquivalent = key.keyEquivalent
|
||||
menu.keyEquivalent = shortcut.key.character.description
|
||||
menu.keyEquivalentModifierMask = key.modifierFlags
|
||||
// The key equivalent was already localized from the physical keycode.
|
||||
menu.allowsAutomaticKeyEquivalentLocalization = !isPhysical
|
||||
menu.allowsAutomaticKeyEquivalentMirroring = !isPhysical
|
||||
|
||||
// Later registrations intentionally override earlier ones for the same key.
|
||||
menuItemsByShortcut[key] = .init(menu)
|
||||
@@ -101,10 +118,14 @@ private extension Ghostty.MenuShortcutManager {
|
||||
extension Ghostty.MenuShortcutManager {
|
||||
/// Hashable key for a menu shortcut match, normalized for quick lookup.
|
||||
struct MenuShortcutKey: Hashable {
|
||||
private enum Identity: Hashable {
|
||||
case keyEquivalent(String)
|
||||
case physicalKeyCode(UInt16)
|
||||
}
|
||||
|
||||
private static let shortcutModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command]
|
||||
|
||||
let keyEquivalent: String
|
||||
// Make it Hashable
|
||||
private let identity: Identity
|
||||
private let modifiersRawValue: UInt
|
||||
|
||||
var modifierFlags: NSEvent.ModifierFlags {
|
||||
@@ -122,40 +143,30 @@ extension Ghostty.MenuShortcutManager {
|
||||
// it's originally uppercased, then we need to add `shift` to the modifiers
|
||||
mods.insert(.shift)
|
||||
}
|
||||
self.keyEquivalent = normalized
|
||||
self.identity = .keyEquivalent(normalized)
|
||||
self.modifiersRawValue = mods.rawValue
|
||||
}
|
||||
|
||||
init(physicalKeyCode: UInt16, modifiers: NSEvent.ModifierFlags) {
|
||||
self.identity = .physicalKeyCode(physicalKeyCode)
|
||||
self.modifiersRawValue = modifiers.intersection(Self.shortcutModifiers).rawValue
|
||||
}
|
||||
|
||||
init?(event: NSEvent) {
|
||||
guard let keyEquivalent = event.charactersIgnoringModifiers else { return nil }
|
||||
self.init(keyEquivalent: keyEquivalent, modifiers: event.modifierFlags)
|
||||
}
|
||||
|
||||
/// Create from a `NSMenuItem`
|
||||
///
|
||||
/// - Important: This will check whether the `keyEquivalent` is uppercased by `.shift` modifier.
|
||||
init?(_ menuItem: NSMenuItem) {
|
||||
self.init(
|
||||
keyEquivalent: menuItem.keyEquivalent,
|
||||
modifiers: menuItem.keyEquivalentModifierMask,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create from a swiftUI `KeyboardShortcut`
|
||||
init?(_ shortcut: KeyboardShortcut) {
|
||||
// Ghostty configured shortcuts are already normalized
|
||||
// in `Ghostty.keyboardShortcut(for:)`, see also gh-#12039
|
||||
let keyEquivalent = shortcut.key.character.description
|
||||
let modifierMask = NSEvent.ModifierFlags(swiftUIFlags: shortcut.modifiers)
|
||||
self.init(keyEquivalent: keyEquivalent, modifiers: modifierMask)
|
||||
}
|
||||
|
||||
var swiftUIShortcut: KeyboardShortcut? {
|
||||
guard let character = keyEquivalent.first else { return nil }
|
||||
return KeyboardShortcut(
|
||||
KeyEquivalent(character),
|
||||
modifiers: .init(nsFlags: modifierFlags)
|
||||
)
|
||||
/// Create from a SwiftUI `KeyboardShortcut`.
|
||||
init?(_ shortcut: KeyboardShortcut, physicalKeyCode: UInt16? = nil) {
|
||||
let modifiers = NSEvent.ModifierFlags(swiftUIFlags: shortcut.modifiers)
|
||||
if let physicalKeyCode {
|
||||
self.init(physicalKeyCode: physicalKeyCode, modifiers: modifiers)
|
||||
} else {
|
||||
self.init(
|
||||
keyEquivalent: shortcut.key.character.description,
|
||||
modifiers: modifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,9 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send text to the terminal as if it was typed. This doesn't send the key events so keyboard
|
||||
/// shortcuts and other encodings do not take effect.
|
||||
/// Send text to the terminal using paste semantics. This doesn't send key events, so keyboard
|
||||
/// shortcuts and other encodings do not take effect. Bracketed paste framing is applied when
|
||||
/// the terminal has enabled it.
|
||||
@MainActor
|
||||
func sendText(_ text: String) {
|
||||
let len = text.utf8CString.count
|
||||
@@ -57,6 +58,17 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the modifiers that participate in text translation for key
|
||||
/// events on this surface. This honors configuration such as
|
||||
/// `macos-option-as-alt`, which may exclude option from translation.
|
||||
///
|
||||
/// - Parameter mods: The full set of modifiers for the key event.
|
||||
/// - Returns: The subset of `mods` to use for keyboard layout translation.
|
||||
@MainActor
|
||||
func keyTranslationMods(_ mods: Input.Mods) -> Input.Mods {
|
||||
Input.Mods(cMods: ghostty_surface_key_translation_mods(surface, mods.cMods))
|
||||
}
|
||||
|
||||
/// Send a key event to the terminal.
|
||||
///
|
||||
/// This sends the full key event including modifiers, action type, and text to the terminal.
|
||||
|
||||
@@ -237,9 +237,14 @@ extension Ghostty.SplitFocusDirection {
|
||||
}
|
||||
|
||||
extension Ghostty {
|
||||
/// One representation of clipboard contents. The data is binary-safe;
|
||||
/// textual consumers use `string`.
|
||||
struct ClipboardContent {
|
||||
let mime: String
|
||||
let data: String
|
||||
let data: Data
|
||||
|
||||
/// The data as text, if it is valid UTF-8.
|
||||
var string: String? { String(data: data, encoding: .utf8) }
|
||||
|
||||
static func from(content: ghostty_clipboard_content_s) -> ClipboardContent? {
|
||||
guard let mimePtr = content.mime,
|
||||
@@ -247,9 +252,15 @@ extension Ghostty {
|
||||
return nil
|
||||
}
|
||||
|
||||
let data: Data = if content.len > 0 {
|
||||
Data(bytes: dataPtr, count: content.len)
|
||||
} else {
|
||||
Data()
|
||||
}
|
||||
|
||||
return ClipboardContent(
|
||||
mime: String(cString: mimePtr),
|
||||
data: String(cString: dataPtr)
|
||||
data: data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1212,7 +1212,7 @@ extension Ghostty {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = committedPreeditTextAction(action, text: text)
|
||||
_ = committedTextAction(action, text: text)
|
||||
}
|
||||
|
||||
if shouldReplayCommittedPreeditKey(translationEvent) {
|
||||
@@ -1484,11 +1484,7 @@ extension Ghostty {
|
||||
var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags)
|
||||
key_ev.composing = composing
|
||||
|
||||
// For text, we only encode UTF8 if we don't have a single control
|
||||
// character. Control characters are encoded by Ghostty itself.
|
||||
// Without this, `ctrl+enter` does the wrong thing.
|
||||
if let text, text.count > 0,
|
||||
let codepoint = text.utf8.first, codepoint >= 0x20 {
|
||||
if let text = text?.keyEventText {
|
||||
return text.withCString { ptr in
|
||||
key_ev.text = ptr
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
@@ -1512,7 +1508,7 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private func committedPreeditTextAction(
|
||||
private func committedTextAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
text: String
|
||||
) -> Bool {
|
||||
@@ -1787,20 +1783,31 @@ extension Ghostty {
|
||||
|
||||
// Note the callback may be executed on a background thread as documented
|
||||
// so we need @MainActor since we're reading/writing view state.
|
||||
Task { @MainActor in
|
||||
// We use [weak self] here because we don't want to extend the surface's
|
||||
// lifetime when a notification is triggered right before the surface closes.
|
||||
Task { @MainActor [weak self] in
|
||||
do {
|
||||
try await UNUserNotificationCenter.current().add(request)
|
||||
|
||||
guard let focused = self?.focused else {
|
||||
// We remove the notification if the surface is deallocated.
|
||||
UNUserNotificationCenter.current()
|
||||
.removeDeliveredNotifications(withIdentifiers: [uuid])
|
||||
return
|
||||
}
|
||||
|
||||
// We need to keep track of this notification so we can remove it
|
||||
// under certain circumstances
|
||||
notificationIdentifiers.insert(uuid)
|
||||
self?.notificationIdentifiers.insert(uuid)
|
||||
|
||||
// If we're focused then we schedule to remove the notification
|
||||
// after a few seconds. If we gain focus we automatically remove it
|
||||
// in focusDidChange.
|
||||
if self.focused {
|
||||
try await Task.sleep(for: .seconds(3))
|
||||
notificationIdentifiers.remove(uuid)
|
||||
if focused {
|
||||
// If the suspension is failed, we remove the notification anyway.
|
||||
try? await Task.sleep(for: .seconds(3))
|
||||
self?.notificationIdentifiers.remove(uuid)
|
||||
// We remove the notification if the surface is deallocated while we wait.
|
||||
UNUserNotificationCenter.current()
|
||||
.removeDeliveredNotifications(withIdentifiers: [uuid])
|
||||
}
|
||||
@@ -2063,7 +2070,6 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
func insertText(_ string: Any, replacementRange: NSRange) {
|
||||
// We must have an associated event
|
||||
guard NSApp.currentEvent != nil else { return }
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// We want the string view of the any value
|
||||
var chars = ""
|
||||
@@ -2089,8 +2095,6 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
return
|
||||
}
|
||||
|
||||
let hadMarkedText = hasMarkedText()
|
||||
|
||||
// If insertText is called, our preedit must be over.
|
||||
unmarkText()
|
||||
|
||||
@@ -2102,14 +2106,11 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
return
|
||||
}
|
||||
|
||||
if hadMarkedText, !chars.isEmpty {
|
||||
// Send preedit commits as key events instead of raw text for
|
||||
// keybind interpretation by programs.
|
||||
_ = committedPreeditTextAction(GHOSTTY_ACTION_PRESS, text: chars)
|
||||
return
|
||||
// All committed text (IME, dictation, etc.) must be sent as key
|
||||
// events so programs treat it as typed input, never as a paste.
|
||||
if !chars.isEmpty {
|
||||
_ = committedTextAction(GHOSTTY_ACTION_PRESS, text: chars)
|
||||
}
|
||||
|
||||
surfaceModel.sendText(chars)
|
||||
}
|
||||
|
||||
/// This function needs to exist for two reasons:
|
||||
@@ -2299,10 +2300,7 @@ extension Ghostty.SurfaceView {
|
||||
|
||||
if let content {
|
||||
DispatchQueue.main.async {
|
||||
self.insertText(
|
||||
content,
|
||||
replacementRange: NSRange(location: 0, length: 0)
|
||||
)
|
||||
self.surfaceModel?.sendText(content)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
/// Type-erased wrapper for any Comparable type to use as a sort key.
|
||||
struct AnySortKey: Comparable {
|
||||
private let value: Any
|
||||
private let comparator: (Any, Any) -> ComparisonResult
|
||||
|
||||
init<T: Comparable>(_ value: T) {
|
||||
self.value = value
|
||||
self.comparator = { lhs, rhs in
|
||||
guard let l = lhs as? T, let r = rhs as? T else { return .orderedSame }
|
||||
if l < r { return .orderedAscending }
|
||||
if l > r { return .orderedDescending }
|
||||
return .orderedSame
|
||||
}
|
||||
}
|
||||
|
||||
static func < (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
|
||||
lhs.comparator(lhs.value, rhs.value) == .orderedAscending
|
||||
}
|
||||
|
||||
static func == (lhs: AnySortKey, rhs: AnySortKey) -> Bool {
|
||||
lhs.comparator(lhs.value, rhs.value) == .orderedSame
|
||||
}
|
||||
}
|
||||
@@ -104,11 +104,11 @@ enum BackportPointerStyle {
|
||||
}
|
||||
}
|
||||
|
||||
enum BackportNSGlassStyle {
|
||||
enum BackportGlass {
|
||||
case regular, clear
|
||||
|
||||
@available(macOS 26, *)
|
||||
var official: NSGlassEffectView.Style {
|
||||
var official: Glass {
|
||||
switch self {
|
||||
case .regular: return .regular
|
||||
case .clear: return .clear
|
||||
|
||||
19
macos/Sources/Helpers/Extensions/NSAlert+Extension.swift
Normal file
19
macos/Sources/Helpers/Extensions/NSAlert+Extension.swift
Normal file
@@ -0,0 +1,19 @@
|
||||
import AppKit
|
||||
|
||||
extension NSAlert {
|
||||
static func reviewWindowsAlert(
|
||||
messageText: String,
|
||||
informativeText: String = "If you don't review your windows, any running processes will be terminated",
|
||||
terminateNowButtonTitle: String = "Terminate Processes"
|
||||
) -> NSAlert {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: "Review Windows...")
|
||||
alert.addButton(withTitle: terminateNowButtonTitle)
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
|
||||
return alert
|
||||
}
|
||||
}
|
||||
@@ -24,14 +24,6 @@ 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),
|
||||
|
||||
@@ -7,10 +7,5 @@ extension NSMenuItem {
|
||||
if #available(macOS 26, *) {
|
||||
image = NSImage(systemSymbolName: symbol, accessibilityDescription: title)
|
||||
}
|
||||
#if compiler(>=6.4)
|
||||
if #available(macOS 27.0, *) {
|
||||
preferredImageVisibility = .automatic
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,86 @@ extension NSPasteboard {
|
||||
return strings.joined(separator: " ")
|
||||
}
|
||||
|
||||
/// The pasteboard for the Ghostty enum type.
|
||||
/// The file URLs on the pasteboard, e.g. files copied in Finder.
|
||||
private var ghosttyFileURLs: [URL] {
|
||||
(pasteboardItems ?? []).compactMap { item in
|
||||
guard let plist = item.propertyList(forType: .fileURL),
|
||||
let url = NSURL(pasteboardPropertyList: plist, ofType: .fileURL) as URL?,
|
||||
url.isFileURL else { return nil }
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/// The data for the given MIME type, if the pasteboard can serve it.
|
||||
///
|
||||
/// The canonical "text/plain" type uses the opinionated string
|
||||
/// contents so that e.g. copying a file yields its escaped path;
|
||||
/// this matches what pasting into the terminal produces. Copied
|
||||
/// files are additionally served as "text/uri-list" (RFC 2483, the
|
||||
/// type X11/Wayland clipboards carry file copies under). All other
|
||||
/// types are mapped through UTType.
|
||||
func ghosttyData(forMime mime: String) -> Data? {
|
||||
switch mime {
|
||||
case "text/plain":
|
||||
guard let str = getOpinionatedStringContents() else { return nil }
|
||||
return Data(str.utf8)
|
||||
|
||||
case "text/uri-list":
|
||||
let urls = ghosttyFileURLs
|
||||
guard !urls.isEmpty else { return nil }
|
||||
return Data(urls.map { $0.absoluteString + "\r\n" }.joined().utf8)
|
||||
|
||||
default:
|
||||
guard let type = NSPasteboard.PasteboardType(mimeType: mime) else { return nil }
|
||||
return data(forType: type)
|
||||
}
|
||||
}
|
||||
|
||||
/// The MIME types available on the pasteboard, best-effort mapped
|
||||
/// from the pasteboard types. Types without a MIME mapping are not
|
||||
/// reported. This only inspects declared types and never reads data,
|
||||
/// since mode 5522 paste events must remain metadata-only.
|
||||
func ghosttyAvailableMimes() -> [String] {
|
||||
var result: [String] = []
|
||||
var seen = Set<String>()
|
||||
let availableTypes = types ?? []
|
||||
let mimeType: (NSPasteboard.PasteboardType) -> String? = { type in
|
||||
guard let mime = UTType(type.rawValue)?.preferredMIMEType else { return nil }
|
||||
return mime == "text/plain;charset=utf-8" ? "text/plain" : mime
|
||||
}
|
||||
|
||||
// Plain text and copied files can both be served as the canonical
|
||||
// text representation. Infer this from declared types so lazy
|
||||
// pasteboard providers are not asked for their contents.
|
||||
let hasFileURL = availableTypes.contains(.fileURL)
|
||||
let hasPlainText = hasFileURL || availableTypes.contains { type in
|
||||
mimeType(type) == "text/plain"
|
||||
}
|
||||
if hasPlainText {
|
||||
result.append("text/plain")
|
||||
seen.insert("text/plain")
|
||||
}
|
||||
|
||||
// Copied files are additionally served as a URI list. The
|
||||
// generic mapping below never reports this since file URL
|
||||
// pasteboard types have no MIME type.
|
||||
if hasFileURL {
|
||||
result.append("text/uri-list")
|
||||
seen.insert("text/uri-list")
|
||||
}
|
||||
|
||||
for type in availableTypes {
|
||||
guard let mime = mimeType(type),
|
||||
!seen.contains(mime) else { continue }
|
||||
seen.insert(mime)
|
||||
result.append(mime)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/// The pasteboard for the Ghostty enum type. Returns nil for locations
|
||||
/// macOS can't serve; callers report those as unsupported.
|
||||
static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? {
|
||||
switch clipboard {
|
||||
case GHOSTTY_CLIPBOARD_STANDARD:
|
||||
@@ -63,6 +142,10 @@ extension NSPasteboard {
|
||||
case GHOSTTY_CLIPBOARD_SELECTION:
|
||||
return Self.ghosttySelection
|
||||
|
||||
case GHOSTTY_CLIPBOARD_PRIMARY:
|
||||
// macOS has no primary selection.
|
||||
return nil
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
extension String {
|
||||
/// True when the first scalar is an ASCII control character (C0 or DEL).
|
||||
var startsWithASCIIControlCharacter: Bool {
|
||||
guard let scalar = unicodeScalars.first else { return false }
|
||||
return scalar.value < 0x20 || scalar.value == 0x7F
|
||||
}
|
||||
|
||||
/// The string as the text of a terminal key event, or nil when it is empty
|
||||
/// or begins with an ASCII control character.
|
||||
///
|
||||
/// - Note: Control characters are encoded by Ghostty itself so that the
|
||||
/// physical key and its modifiers remain available to protocols
|
||||
/// such as the Kitty keyboard protocol.
|
||||
var keyEventText: String? {
|
||||
guard !isEmpty, !startsWithASCIIControlCharacter else { return nil }
|
||||
return self
|
||||
}
|
||||
|
||||
func truncate(length: Int, trailing: String = "…") -> String {
|
||||
let maxLength = length - trailing.count
|
||||
guard maxLength > 0, !self.isEmpty, self.count > length else {
|
||||
|
||||
9
macos/Sources/Helpers/Extensions/URL+Extension.swift
Normal file
9
macos/Sources/Helpers/Extensions/URL+Extension.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import System
|
||||
|
||||
extension URL {
|
||||
/// The decoded path with trailing separators removed, except for the root path.
|
||||
var pathWithoutTrailingSlash: String {
|
||||
FilePath(path(percentEncoded: false)).string
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HostingWindowKey: EnvironmentKey {
|
||||
typealias Value = () -> NSWindow? // needed for weak link
|
||||
static let defaultValue: Self.Value = { nil }
|
||||
}
|
||||
|
||||
extension EnvironmentValues {
|
||||
/// This can be used to set the hosting NSWindow to a NSHostingView
|
||||
var hostingWindow: HostingWindowKey.Value {
|
||||
get { return self[HostingWindowKey.self] }
|
||||
set { self[HostingWindowKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import Carbon
|
||||
|
||||
class KeyboardLayout {
|
||||
@@ -11,4 +12,32 @@ class KeyboardLayout {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Translate a physical keycode for use as a menu key equivalent.
|
||||
///
|
||||
/// AppKit retranslates against the current input source without changing its dead key state.
|
||||
///
|
||||
/// - Important: Must be called on the main thread because underlying Text Input Sources APIs are not thread-safe.
|
||||
@MainActor static func character(
|
||||
for keyCode: UInt16,
|
||||
modifiers: NSEvent.ModifierFlags
|
||||
) -> Character? {
|
||||
guard
|
||||
let event = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: .zero,
|
||||
modifierFlags: [],
|
||||
timestamp: 0,
|
||||
windowNumber: 0,
|
||||
context: nil,
|
||||
characters: "",
|
||||
charactersIgnoringModifiers: "",
|
||||
isARepeat: false,
|
||||
keyCode: keyCode),
|
||||
let result = event.characters(byApplyingModifiers: modifiers),
|
||||
result.count == 1
|
||||
else { return nil }
|
||||
|
||||
return result.first
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,15 @@ struct CommandPaletteFilterTests {
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
description: String? = nil,
|
||||
leadingColor: Color? = nil
|
||||
leadingColor: Color? = nil,
|
||||
sortKey: ObjectIdentifier? = nil
|
||||
) -> CommandOption {
|
||||
CommandOption(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
leadingColor: leadingColor
|
||||
leadingColor: leadingColor,
|
||||
sortKey: sortKey
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -46,4 +48,24 @@ struct CommandPaletteFilterTests {
|
||||
#expect([first, second].filteredAndSorted(query: "new") == [first, second])
|
||||
#expect([second, first].filteredAndSorted(query: "new") == [second, first])
|
||||
}
|
||||
|
||||
/// Equal titles use their sort keys independent of input order.
|
||||
@Test func equalTitlesUseSortKey() {
|
||||
let firstKey = NSObject()
|
||||
let secondKey = NSObject()
|
||||
let first = option(
|
||||
title: "Focus: Shell",
|
||||
subtitle: "/tmp",
|
||||
sortKey: ObjectIdentifier(firstKey)
|
||||
)
|
||||
let second = option(
|
||||
title: "Focus: Shell",
|
||||
subtitle: "/tmp",
|
||||
sortKey: ObjectIdentifier(secondKey)
|
||||
)
|
||||
|
||||
let forward = sortedTerminalPaletteOptions([first, second])
|
||||
let reverse = sortedTerminalPaletteOptions([second, first])
|
||||
#expect(forward == reverse)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import AppKit
|
||||
import Testing
|
||||
|
||||
@testable import Ghostty
|
||||
|
||||
/// Tests for `ScriptKeyEventCommand.parse`, which turns the scripting
|
||||
/// arguments of the `send key` AppleScript command into the
|
||||
/// `Ghostty.Input.KeyEvent` delivered to the surface.
|
||||
///
|
||||
/// Printable keys must carry the text and codepoints a real NSEvent-based key
|
||||
/// press would, derived from the current keyboard layout; otherwise the key
|
||||
/// produces no terminal output. Expected characters are computed through
|
||||
/// `KeyboardLayout` rather than hardcoded so the tests hold on any layout.
|
||||
@MainActor
|
||||
struct ScriptKeyEventCommandTests {
|
||||
private let keyCodeA: UInt16 = 0x00 // W3C KeyA
|
||||
|
||||
/// Parse a `send key` command the way the scripting runtime delivers it.
|
||||
private func parse(
|
||||
_ keyName: Any?,
|
||||
modifiers: String? = nil,
|
||||
action: UInt32? = nil,
|
||||
translationMods: (Ghostty.Input.Mods) -> Ghostty.Input.Mods = { $0 },
|
||||
) throws -> Ghostty.Input.KeyEvent {
|
||||
var arguments: [String: Any] = [:]
|
||||
if let modifiers { arguments["modifiers"] = modifiers }
|
||||
if let action { arguments["action"] = action }
|
||||
return try ScriptKeyEventCommand.parse(
|
||||
directParameter: keyName,
|
||||
evaluatedArguments: arguments.isEmpty ? nil : arguments,
|
||||
translationMods: translationMods,
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Argument parsing
|
||||
|
||||
@Test func defaultsToPressWithNoModifiers() throws {
|
||||
let event = try parse("a")
|
||||
#expect(event.key == .a)
|
||||
#expect(event.action == .press)
|
||||
#expect(event.mods == [])
|
||||
}
|
||||
|
||||
@Test func parsesActionCodes() throws {
|
||||
try #expect(
|
||||
parse("enter", action: "GIpr".fourCharCode).action == .press
|
||||
)
|
||||
try #expect(
|
||||
parse("enter", action: "GIrl".fourCharCode).action == .release
|
||||
)
|
||||
}
|
||||
|
||||
@Test func unknownActionCodeFallsBackToPress() throws {
|
||||
try #expect(parse("a", action: 0).action == .press)
|
||||
}
|
||||
|
||||
@Test(
|
||||
arguments: [
|
||||
("shift", Ghostty.Input.Mods.shift),
|
||||
("control", .ctrl),
|
||||
("option", .alt),
|
||||
("command", .super),
|
||||
("shift, command", [.shift, .super]),
|
||||
("SHIFT,Option", [.shift, .alt]),
|
||||
(" control , shift ", [.ctrl, .shift]),
|
||||
("", []),
|
||||
] as [(String, Ghostty.Input.Mods)]
|
||||
)
|
||||
func parsesModifiers(string: String, expected: Ghostty.Input.Mods) throws {
|
||||
try #expect(parse("a", modifiers: string).mods == expected)
|
||||
}
|
||||
|
||||
@Test(arguments: [nil, 42, NSNull()] as [Any?])
|
||||
func missingOrNonStringKeyThrows(directParameter: Any?) {
|
||||
#expect(throws: ScriptKeyEventCommand.ArgumentError.missingKey) {
|
||||
try parse(directParameter)
|
||||
}
|
||||
}
|
||||
|
||||
@Test func unknownKeyNameThrows() {
|
||||
#expect(
|
||||
throws: ScriptKeyEventCommand.ArgumentError.unknownKey("banana")
|
||||
) {
|
||||
try parse("banana")
|
||||
}
|
||||
}
|
||||
|
||||
@Test func unknownModifierThrows() {
|
||||
#expect(
|
||||
throws: ScriptKeyEventCommand.ArgumentError.unknownModifiers(
|
||||
"shift, hyper"
|
||||
)
|
||||
) {
|
||||
try parse("a", modifiers: "shift, hyper")
|
||||
}
|
||||
}
|
||||
|
||||
/// The sdef's documented key name examples must all resolve.
|
||||
@Test(arguments: ["enter", "a", "space"])
|
||||
func documentedKeyNamesResolve(name: String) throws {
|
||||
_ = try parse(name)
|
||||
}
|
||||
|
||||
// MARK: Produced key event
|
||||
|
||||
// These record known issues until `send key` derives text and codepoints
|
||||
// from the keyboard layout the way real NSEvent-based input does.
|
||||
// Uncomment the expectations and remove the Issue.record calls once the
|
||||
// fix lands.
|
||||
|
||||
@Test func pressCarriesLayoutText() throws {
|
||||
let event = try parse("a")
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.unshiftedCodepoint == expected.unicodeScalars.first?.value)
|
||||
#expect(event.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func shiftShiftsTextAndIsConsumed() throws {
|
||||
let event = try parse("a", modifiers: "shift")
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: .shift))
|
||||
let unshifted = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.consumedMods == .shift)
|
||||
#expect(event.unshiftedCodepoint == unshifted.unicodeScalars.first?.value)
|
||||
}
|
||||
|
||||
/// The original bug scenario: `send key "c" with modifiers "control"`
|
||||
/// must produce the base character as text ("c", not 0x03) with control
|
||||
/// unconsumed, so core can encode the control sequence itself.
|
||||
@Test func controlKeepsBaseTextAndIsNotConsumed() throws {
|
||||
let event = try parse("c", modifiers: "control")
|
||||
let expected = try #require(KeyboardLayout.character(
|
||||
for: 0x08, // W3C KeyC
|
||||
modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.mods == .ctrl)
|
||||
#expect(event.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func optionIncludedInTranslationIsConsumed() throws {
|
||||
// macos-option-as-alt=false: option participates in translation.
|
||||
let event = try parse("a", modifiers: "option")
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: .option))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.consumedMods == .alt)
|
||||
}
|
||||
|
||||
@Test func optionExcludedFromTranslationIsNotConsumed() throws {
|
||||
// macos-option-as-alt=true: the surface's translation mods exclude
|
||||
// option, so it stays unconsumed and core can encode it (e.g. ESC
|
||||
// prefix).
|
||||
let event = try parse("a", modifiers: "option") { $0.subtracting(.alt) }
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func releaseCarriesNoText() throws {
|
||||
let event = try parse(
|
||||
"a",
|
||||
modifiers: "shift",
|
||||
action: "GIrl".fourCharCode
|
||||
)
|
||||
let unshifted = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == nil)
|
||||
#expect(event.unshiftedCodepoint == unshifted.unicodeScalars.first?.value)
|
||||
}
|
||||
|
||||
/// Keys whose layout translation is a control character (or a PUA
|
||||
/// function-key character) must carry no text and no unshifted codepoint;
|
||||
/// core encodes them from the key enum.
|
||||
@Test(arguments: [
|
||||
Ghostty.Input.Key.enter, .escape, .tab, .backspace, .arrowUp, .f1,
|
||||
.home, .delete,
|
||||
])
|
||||
func functionalKeysCarryNoTextOrCodepoint(key: Ghostty.Input.Key) throws {
|
||||
let event = try parse(key.rawValue)
|
||||
#expect(event.text == nil)
|
||||
#expect(event.unshiftedCodepoint == 0)
|
||||
}
|
||||
|
||||
@Test func modsPassThroughUnchanged() throws {
|
||||
let event = try parse("a", modifiers: "control, shift")
|
||||
#expect(event.mods == [.ctrl, .shift])
|
||||
}
|
||||
}
|
||||
@@ -223,8 +223,8 @@ struct ConfigTests {
|
||||
|
||||
// MARK: - Keybind
|
||||
|
||||
@Test
|
||||
func uppercasedLetterShouldBeNormalized() async throws {
|
||||
@MainActor @Test
|
||||
func uppercasedLetterShouldBeNormalized() throws {
|
||||
let config = try TemporaryConfig("""
|
||||
keybind=cmd+L=goto_split:left
|
||||
""")
|
||||
@@ -238,8 +238,8 @@ struct ConfigTests {
|
||||
#expect(shortcut2 == .init("ä", modifiers: [.command]))
|
||||
}
|
||||
|
||||
@Test
|
||||
func emptyConfigShouldBeHaveDefaultShortcut() async throws {
|
||||
@MainActor @Test
|
||||
func emptyConfigShouldBeHaveDefaultShortcut() throws {
|
||||
let config = try TemporaryConfig("")
|
||||
let newWindow = try #require(config.keyboardShortcut(for: "new_window"))
|
||||
#expect(newWindow == .init("n", modifiers: [.command]))
|
||||
|
||||
115
macos/Tests/Ghostty/KeyEventSynthesizeTests.swift
Normal file
115
macos/Tests/Ghostty/KeyEventSynthesizeTests.swift
Normal file
@@ -0,0 +1,115 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
import GhosttyKit
|
||||
|
||||
/// Tests for `Ghostty.Input.KeyEvent.init(synthesizing:...)`, the derivation
|
||||
/// used for programmatic key input such as AppleScript's `send key`.
|
||||
///
|
||||
/// Expected characters are computed through `KeyboardLayout` rather than
|
||||
/// hardcoded so the tests hold on any keyboard layout; what's under test is
|
||||
/// the text/consumed/unshifted derivation, not the layout itself.
|
||||
@MainActor
|
||||
struct KeyEventSynthesizeTests {
|
||||
private let keyCodeA: UInt16 = 0x00 // W3C KeyA
|
||||
|
||||
@Test func pressHasLayoutText() throws {
|
||||
let event = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .press, mods: [], translationMods: [])
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.unshiftedCodepoint == expected.unicodeScalars.first?.value)
|
||||
#expect(event.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func shiftAppliesToTextAndIsConsumed() throws {
|
||||
let event = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .press, mods: .shift, translationMods: .shift)
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: .shift))
|
||||
let unshifted = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.consumedMods == .shift)
|
||||
#expect(event.unshiftedCodepoint == unshifted.unicodeScalars.first?.value)
|
||||
}
|
||||
|
||||
@Test func controlIsNeverConsumedAndDoesNotAffectText() throws {
|
||||
// Core passes ctrl through translation mods; the event must still
|
||||
// produce the base character ("a", not 0x01) and not consume ctrl.
|
||||
let event = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .press, mods: .ctrl, translationMods: .ctrl)
|
||||
let expected = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == String(expected))
|
||||
#expect(event.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func optionFollowsTranslationMods() throws {
|
||||
// macos-option-as-alt=false: option participates in translation and
|
||||
// is consumed.
|
||||
let translated = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .press, mods: .alt, translationMods: .alt)
|
||||
let optionChar = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: .option))
|
||||
#expect(translated.text == String(optionChar))
|
||||
#expect(translated.consumedMods == .alt)
|
||||
|
||||
// macos-option-as-alt=true: option is excluded from translation and
|
||||
// remains unconsumed, so core can encode it (e.g. ESC prefix).
|
||||
let asAlt = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .press, mods: .alt, translationMods: [])
|
||||
let baseChar = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(asAlt.text == String(baseChar))
|
||||
#expect(asAlt.consumedMods == [])
|
||||
}
|
||||
|
||||
@Test func releaseHasNoText() throws {
|
||||
let event = Ghostty.Input.KeyEvent(
|
||||
synthesizing: .a, action: .release, mods: .shift, translationMods: .shift)
|
||||
let unshifted = try #require(KeyboardLayout.character(for: keyCodeA, modifiers: []))
|
||||
#expect(event.text == nil)
|
||||
#expect(event.unshiftedCodepoint == unshifted.unicodeScalars.first?.value)
|
||||
#expect(event.consumedMods == .shift)
|
||||
}
|
||||
|
||||
/// Every functional key with a Mac keycode. Their layout translations are
|
||||
/// control characters (or nothing), which must never be attached as text
|
||||
/// or reported as an unshifted codepoint; core encodes them from the key
|
||||
/// enum. This also guards against a translation leaking through in some
|
||||
/// other form, such as a PUA function-key character.
|
||||
@Test(arguments: [
|
||||
Ghostty.Input.Key.enter, .numpadEnter, .escape, .tab, .backspace,
|
||||
.delete, .insert, .home, .end, .pageUp, .pageDown,
|
||||
.arrowUp, .arrowDown, .arrowLeft, .arrowRight,
|
||||
.contextMenu, .numLock,
|
||||
.f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10,
|
||||
.f11, .f12, .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20,
|
||||
])
|
||||
func controlKeyTranslationsProduceNoTextOrCodepoint(key: Ghostty.Input.Key) {
|
||||
let event = Ghostty.Input.KeyEvent(
|
||||
synthesizing: key, action: .press, mods: [], translationMods: [])
|
||||
#expect(event.text == nil)
|
||||
#expect(event.unshiftedCodepoint == 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu-shortcut path must translate the key equivalent with only the
|
||||
/// command modifier applied: command can select a distinct layout table, while
|
||||
/// the other modifiers live in the shortcut's modifier mask.
|
||||
@MainActor
|
||||
struct KeyboardShortcutTranslationTests {
|
||||
@Test func keyEquivalentIgnoresNonCommandModifiers() throws {
|
||||
var trigger = ghostty_input_trigger_s()
|
||||
trigger.tag = GHOSTTY_TRIGGER_PHYSICAL
|
||||
trigger.key.physical = GHOSTTY_KEY_BACKQUOTE
|
||||
trigger.mods = ghostty_input_mods_e(
|
||||
GHOSTTY_MODS_SUPER.rawValue | GHOSTTY_MODS_SHIFT.rawValue | GHOSTTY_MODS_ALT.rawValue)
|
||||
|
||||
let shortcut = try #require(Ghostty.keyboardShortcut(for: trigger))
|
||||
let expected = try #require(KeyboardLayout.character(
|
||||
for: 0x32, // W3C Backquote
|
||||
modifiers: .command))
|
||||
#expect(shortcut.key.character == expected)
|
||||
#expect(shortcut.modifiers.contains(.shift))
|
||||
#expect(shortcut.modifiers.contains(.option))
|
||||
#expect(shortcut.modifiers.contains(.command))
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,21 @@ struct MenuShortcutManagerTests {
|
||||
#expect(item.keyEquivalentModifierMask == .command)
|
||||
}
|
||||
|
||||
@MainActor @Test func physicalBackquoteUsesCurrentKeyboardLayout() throws {
|
||||
let config = try TemporaryConfig("keybind=super+backquote=toggle_quick_terminal")
|
||||
let expected = try #require(KeyboardLayout.character(for: 0x32, modifiers: .command))
|
||||
let item = NSMenuItem(title: "Quick Terminal", action: nil, keyEquivalent: "")
|
||||
let manager = Ghostty.MenuShortcutManager()
|
||||
|
||||
manager.reset()
|
||||
manager.syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: item)
|
||||
|
||||
#expect(item.keyEquivalent == String(expected))
|
||||
#expect(item.keyEquivalentModifierMask == .command)
|
||||
#expect(!item.allowsAutomaticKeyEquivalentLocalization)
|
||||
#expect(!item.allowsAutomaticKeyEquivalentMirroring)
|
||||
}
|
||||
|
||||
@Test(.bug("https://github.com/ghostty-org/ghostty/issues/11396", id: 11396))
|
||||
func overrideDefault() async throws {
|
||||
let config = try TemporaryConfig("keybind=super+h=goto_split:left")
|
||||
|
||||
@@ -13,11 +13,6 @@ struct NormalizedMenuShortcutKeyTests {
|
||||
#expect(key == nil)
|
||||
}
|
||||
|
||||
@Test func lowercasesKeyEquivalent() {
|
||||
let key = Key(keyEquivalent: "A", modifiers: .command)
|
||||
#expect(key?.keyEquivalent == "a")
|
||||
}
|
||||
|
||||
@Test func stripsNonShortcutModifiers() {
|
||||
// .capsLock and .function should be stripped
|
||||
let key = Key(keyEquivalent: "c", modifiers: [.command, .capsLock, .function])
|
||||
@@ -74,6 +69,17 @@ struct NormalizedMenuShortcutKeyTests {
|
||||
#expect(a != b)
|
||||
}
|
||||
|
||||
@Test func physicalKeysUseKeyCodeIdentity() {
|
||||
let physical = Key(physicalKeyCode: 0x32, modifiers: .command)
|
||||
let same = Key(physicalKeyCode: 0x32, modifiers: .command)
|
||||
let different = Key(physicalKeyCode: 0x31, modifiers: .command)
|
||||
let unicode = Key(keyEquivalent: "`", modifiers: .command)
|
||||
|
||||
#expect(physical == same)
|
||||
#expect(physical != different)
|
||||
#expect(physical != unicode)
|
||||
}
|
||||
|
||||
@Test func differentModifiersAreNotEqual() {
|
||||
let a = Key(keyEquivalent: "c", modifiers: .command)
|
||||
let b = Key(keyEquivalent: "c", modifiers: .option)
|
||||
|
||||
27
macos/Tests/Helpers/KeyboardLayoutTests.swift
Normal file
27
macos/Tests/Helpers/KeyboardLayoutTests.swift
Normal file
@@ -0,0 +1,27 @@
|
||||
import AppKit
|
||||
import Testing
|
||||
@testable import Ghostty
|
||||
|
||||
@MainActor
|
||||
struct KeyboardLayoutTests {
|
||||
@Test(arguments: [
|
||||
0x00, // W3C KeyA
|
||||
0x12, // W3C Digit1
|
||||
0x32, // W3C Backquote
|
||||
])
|
||||
func characterHandlesKeyCode(keyCode: UInt16) {
|
||||
#expect(KeyboardLayout.character(for: keyCode, modifiers: []) != nil)
|
||||
}
|
||||
|
||||
@Test func characterRejectsInvalidKeyCode() {
|
||||
#expect(KeyboardLayout.character(for: UInt16.max, modifiers: []) == nil)
|
||||
}
|
||||
|
||||
@Test func characterAppliesModifiers() throws {
|
||||
let keyCode: UInt16 = 0x00 // W3C KeyA
|
||||
let unmodified = try #require(KeyboardLayout.character(for: keyCode, modifiers: []))
|
||||
let shifted = try #require(KeyboardLayout.character(for: keyCode, modifiers: .shift))
|
||||
#expect(shifted != unmodified)
|
||||
#expect(String(shifted).lowercased() == String(unmodified).lowercased())
|
||||
}
|
||||
}
|
||||
@@ -60,44 +60,4 @@ struct TerminalViewContainerTests {
|
||||
#expect(view.glassEffectView == nil)
|
||||
}
|
||||
}
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@Test func configChangeUpdatesGlass() async throws {
|
||||
guard #available(macOS 26.0, *) else { return }
|
||||
let view = await MockTerminalViewContainer {
|
||||
EmptyView()
|
||||
}
|
||||
let config1 = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1)
|
||||
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: nil)
|
||||
let glassEffectView = await view.descendants(withClassName: "NSGlassEffectView").first as? NSGlassEffectView
|
||||
let effectView = try #require(glassEffectView)
|
||||
try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed
|
||||
#expect(effectView.tintColor?.hexString == NSColor.clear.hexString)
|
||||
|
||||
// Test with same config but with different preferredBackgroundColor
|
||||
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red)
|
||||
#expect(effectView.tintColor?.hexString == NSColor.red.hexString)
|
||||
|
||||
// MARK: - Corner Radius
|
||||
|
||||
#expect(effectView.cornerRadius == 0)
|
||||
await MainActor.run { view._windowCornerRadius = 10 }
|
||||
|
||||
// This won't change, unless ghosttyConfigDidChange is called
|
||||
#expect(effectView.cornerRadius == 0)
|
||||
|
||||
await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red)
|
||||
#expect(effectView.cornerRadius == 10)
|
||||
|
||||
// MARK: - Glass Style
|
||||
|
||||
#expect(effectView.style == .regular)
|
||||
|
||||
let config2 = MockConfig(backgroundBlur: .macosGlassClear, backgroundColor: .clear, backgroundOpacity: 1)
|
||||
await view.ghosttyConfigDidChange(config2, preferredBackgroundColor: .red)
|
||||
|
||||
#expect(effectView.style == .clear)
|
||||
|
||||
}
|
||||
#endif // compiler(>=6.2)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user