Merge branch 'main' into vi_VN
commit
fc0a6e28ec
|
|
@ -9,4 +9,6 @@ pkg/glfw/wayland-headers/** linguist-vendored
|
|||
pkg/libintl/config.h linguist-generated=true
|
||||
pkg/libintl/libintl.h linguist-generated=true
|
||||
pkg/simdutf/vendor/** linguist-vendored
|
||||
src/font/nerd_font_attributes.zig linguist-generated=true
|
||||
src/font/nerd_font_codepoint_tables.py linguist-generated=true
|
||||
src/terminal/res/** linguist-vendored
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
name: Milestone Update
|
||||
steps:
|
||||
- name: Set Milestone for PR
|
||||
uses: hustcer/milestone-action@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9
|
||||
uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11
|
||||
if: github.event.pull_request.merged == true
|
||||
with:
|
||||
action: bind-pr # `bind-pr` is the default action
|
||||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
|
||||
# Bind milestone to closed issue that has a merged PR fix
|
||||
- name: Set Milestone for Issue
|
||||
uses: hustcer/milestone-action@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9
|
||||
uses: hustcer/milestone-action@bff2091b54a91cf1491564659c554742b285442f # v2.11
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
echo "Version is valid: ${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Exract the Version
|
||||
- name: Extract the Version
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
|
|
|
|||
|
|
@ -83,13 +83,13 @@ jobs:
|
|||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ jobs:
|
|||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -34,7 +33,7 @@ jobs:
|
|||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -151,7 +150,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -163,12 +161,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -188,7 +186,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@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -206,7 +204,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -359,7 +356,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -373,7 +370,6 @@ jobs:
|
|||
# Create our appcast for Sparkle
|
||||
- name: Generate Appcast
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
env:
|
||||
|
|
@ -408,7 +404,6 @@ jobs:
|
|||
# gets out of sync with the binaries.
|
||||
- name: Prep R2 Storage for Appcast
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
run: |
|
||||
|
|
@ -418,7 +413,6 @@ jobs:
|
|||
|
||||
- name: Upload Appcast to R2
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
|
|
@ -444,7 +438,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -590,7 +583,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -629,7 +622,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -775,7 +767,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ jobs:
|
|||
- build-dist
|
||||
- build-examples
|
||||
- build-flatpak
|
||||
- build-freebsd
|
||||
- build-libghostty-vt
|
||||
- build-linux
|
||||
- build-linux-libghostty
|
||||
|
|
@ -73,14 +72,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -95,7 +94,7 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
dir: [c-vt, zig-vt]
|
||||
dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt]
|
||||
name: Example ${{ matrix.dir }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
@ -107,14 +106,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -140,14 +139,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -174,14 +173,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -205,6 +204,7 @@ jobs:
|
|||
aarch64-linux,
|
||||
x86_64-linux,
|
||||
x86_64-windows,
|
||||
wasm32-freestanding,
|
||||
]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
@ -216,14 +216,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -252,14 +252,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -281,14 +281,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -314,14 +314,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -333,7 +333,7 @@ jobs:
|
|||
run: nix build .#ghostty-releasefast
|
||||
|
||||
- name: Check version
|
||||
run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.ReleaseFast'
|
||||
run: result/bin/ghostty +version | grep -q '.ReleaseFast'
|
||||
|
||||
- name: Check to see if the binary has been stripped
|
||||
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols'
|
||||
|
|
@ -342,7 +342,7 @@ jobs:
|
|||
run: nix build .#ghostty-debug
|
||||
|
||||
- name: Check version
|
||||
run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.Debug'
|
||||
run: result/bin/ghostty +version | grep -q '.Debug'
|
||||
|
||||
- name: Check to see if the binary has not been stripped
|
||||
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main'
|
||||
|
|
@ -360,14 +360,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -509,11 +509,11 @@ jobs:
|
|||
- name: Install zig
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Get the zig version from build.zig so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig" -Raw
|
||||
$pattern = 'buildpkg\.requireZig\("(.*?)"\);'
|
||||
# Get the zig version from build.zig.zon so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig.zon" -Raw
|
||||
$pattern = 'minimum_zig_version\s*=\s*"([^"]+)"'
|
||||
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
||||
$version = "zig-windows-x86_64-$zigVersion"
|
||||
$version = "zig-x86_64-windows-$zigVersion"
|
||||
Write-Output $version
|
||||
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
|
||||
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
|
||||
|
|
@ -564,6 +564,8 @@ jobs:
|
|||
test:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-md
|
||||
outputs:
|
||||
zig_version: ${{ steps.zig.outputs.version }}
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
|
|
@ -571,15 +573,20 @@ jobs:
|
|||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Get required Zig version
|
||||
id: zig
|
||||
run: |
|
||||
echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -614,14 +621,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -662,14 +669,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -697,14 +704,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -761,14 +768,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -790,12 +797,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -818,12 +825,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -845,12 +852,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -872,12 +879,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -899,12 +906,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -926,12 +933,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -960,12 +967,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -987,12 +994,12 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1022,14 +1029,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1051,7 +1058,7 @@ jobs:
|
|||
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
||||
|
||||
- name: Configure Namespace powered Buildx
|
||||
uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18
|
||||
uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20
|
||||
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
|
|
@ -1089,7 +1096,7 @@ jobs:
|
|||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
|
||||
with:
|
||||
bundle: com.mitchellh.ghostty
|
||||
manifest-path: flatpak/com.mitchellh.ghostty.yml
|
||||
|
|
@ -1110,14 +1117,14 @@ jobs:
|
|||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1142,7 +1149,8 @@ jobs:
|
|||
matrix:
|
||||
release:
|
||||
- "14.3"
|
||||
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
|
||||
- "15.0"
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout Ghostty
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
|
@ -1163,14 +1171,19 @@ jobs:
|
|||
devel/gettext \
|
||||
devel/git \
|
||||
devel/pkgconf \
|
||||
ftp/curl \
|
||||
graphics/wayland \
|
||||
lang/zig \
|
||||
security/ca_root_nss \
|
||||
textproc/hs-pandoc \
|
||||
x11-fonts/jetbrains-mono \
|
||||
x11-toolkits/libadwaita \
|
||||
x11-toolkits/gtk40 \
|
||||
x11-toolkits/gtk4-layer-shell
|
||||
curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/${{ needs.test.outputs.zig_version }}/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}.tar.xz" && \
|
||||
mkdir /opt && \
|
||||
tar -xf /tmp/zig.tar.xz -C /opt && \
|
||||
rm /tmp/zig.tar.xz && \
|
||||
ln -s "/opt/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}/zig" /usr/local/bin/zig
|
||||
|
||||
run: |
|
||||
zig env
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ jobs:
|
|||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
11
AGENTS.md
11
AGENTS.md
|
|
@ -13,11 +13,20 @@ A file for [guiding coding agents](https://agents.md/).
|
|||
## Directory Structure
|
||||
|
||||
- Shared Zig core: `src/`
|
||||
- C API: `include/ghostty.h`
|
||||
- C API: `include`
|
||||
- macOS app: `macos/`
|
||||
- GTK (Linux and FreeBSD) app: `src/apprt/gtk`
|
||||
|
||||
## libghostty-vt
|
||||
|
||||
- Build: `zig build lib-vt`
|
||||
- Test: `zig build test-lib-vt`
|
||||
- Test filter: `zig build test-lib-vt -Dtest-filter=<test name>`
|
||||
- When working on libghostty-vt, do not build the full app.
|
||||
- For C only changes, don't run the Zig tests. Build all the examples.
|
||||
|
||||
## macOS App
|
||||
|
||||
- Do not use `xcodebuild`
|
||||
- Use `zig build` to build the macOS app and any shared Zig code
|
||||
- Run Xcode tests using `zig build test`
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@
|
|||
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR
|
||||
/po/he_IL.UTF-8.po @ghostty-org/he_IL
|
||||
/po/it_IT.UTF-8.po @ghostty-org/it_IT
|
||||
/po/lt_LT.UTF-8.po @ghostty-org/lt_LT
|
||||
/po/zh_TW.UTF-8.po @ghostty-org/zh_TW
|
||||
/po/hr_HR.UTF-8.po @ghostty-org/hr_HR
|
||||
/po/vi_VN.UTF-8.po @ghostty-org/vi_VN
|
||||
|
|
|
|||
58
Doxyfile
58
Doxyfile
|
|
@ -2,9 +2,42 @@
|
|||
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = "libghostty"
|
||||
INPUT = include/ghostty/vt.h
|
||||
PROJECT_LOGO = images/gnome/64.png
|
||||
INPUT = include/ghostty
|
||||
INPUT_ENCODING = UTF-8
|
||||
RECURSIVE = NO
|
||||
RECURSIVE = YES
|
||||
FILE_PATTERNS = *.h
|
||||
EXAMPLE_PATH = example
|
||||
EXAMPLE_RECURSIVE = YES
|
||||
EXAMPLE_PATTERNS = *
|
||||
FULL_PATH_NAMES = NO
|
||||
STRIP_FROM_INC_PATH = include
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# C API Optimization
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
# Optimize output for C API documentation
|
||||
OPTIMIZE_OUTPUT_FOR_C = YES
|
||||
TYPEDEF_HIDES_STRUCT = YES
|
||||
HIDE_SCOPE_NAMES = YES
|
||||
|
||||
# Clean path names
|
||||
FULL_PATH_NAMES = NO
|
||||
STRIP_FROM_PATH = .
|
||||
STRIP_FROM_INC_PATH = include
|
||||
|
||||
# Hide undocumented and internal APIs
|
||||
HIDE_UNDOC_MEMBERS = YES
|
||||
HIDE_UNDOC_CLASSES = YES
|
||||
EXTRACT_ALL = NO
|
||||
INTERNAL_DOCS = NO
|
||||
EXTRACT_PRIVATE = NO
|
||||
EXTRACT_LOCAL_CLASSES = NO
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# HTML Output
|
||||
|
|
@ -12,6 +45,26 @@ RECURSIVE = NO
|
|||
|
||||
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
|
||||
HTML_COLORSTYLE = DARK
|
||||
HTML_CODE_FOLDING = NO
|
||||
HTML_HEADER = dist/doxygen/header.html
|
||||
LAYOUT_FILE = DoxygenLayout.xml
|
||||
GENERATE_TREEVIEW = YES
|
||||
HTML_DYNAMIC_SECTIONS = YES
|
||||
SEARCHENGINE = YES
|
||||
ALPHABETICAL_INDEX = YES
|
||||
HTML_TIMESTAMP = NO
|
||||
DISABLE_INDEX = NO
|
||||
FULL_SIDEBAR = NO
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Graphs and Diagrams
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
HAVE_DOT = NO
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Man Output
|
||||
|
|
@ -20,6 +73,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
|||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = zig-out/share/man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = YES
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Other Output
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doxygenlayout version="2.0">
|
||||
<!-- Generated by doxygen 1.14.0 -->
|
||||
<!-- Navigation index tabs for HTML output -->
|
||||
<navindex>
|
||||
<tab type="mainpage" visible="yes" title=""/>
|
||||
<tab type="pages" visible="yes" title="" intro=""/>
|
||||
<tab type="topics" visible="yes" title="" intro=""/>
|
||||
<tab type="modules" visible="yes" title="API Groups" intro="">
|
||||
<tab type="modulelist" visible="yes" title="" intro=""/>
|
||||
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="structs" visible="yes" title="Data Types" intro="">
|
||||
<tab type="structlist" visible="yes" title="" intro=""/>
|
||||
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||
</tab>
|
||||
<tab type="files" visible="yes" title="C Headers" intro="">
|
||||
<tab type="filelist" visible="yes" title="" intro=""/>
|
||||
<tab type="globals" visible="yes" title="" intro=""/>
|
||||
</tab>
|
||||
<tab type="examples" visible="yes" title="" intro=""/>
|
||||
</navindex>
|
||||
|
||||
<!-- Layout definition for a class page -->
|
||||
<class>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<inheritancegraph visible="yes"/>
|
||||
<collaborationgraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestedclasses visible="yes" title=""/>
|
||||
<publictypes visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicmethods visible="yes" title=""/>
|
||||
<publicstaticmethods visible="yes" title=""/>
|
||||
<publicattributes visible="yes" title=""/>
|
||||
<publicstaticattributes visible="yes" title=""/>
|
||||
<protectedtypes visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<protectedmethods visible="yes" title=""/>
|
||||
<protectedstaticmethods visible="yes" title=""/>
|
||||
<protectedattributes visible="yes" title=""/>
|
||||
<protectedstaticattributes visible="yes" title=""/>
|
||||
<packagetypes visible="yes" title=""/>
|
||||
<packagemethods visible="yes" title=""/>
|
||||
<packagestaticmethods visible="yes" title=""/>
|
||||
<packageattributes visible="yes" title=""/>
|
||||
<packagestaticattributes visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<privatetypes visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<privatemethods visible="yes" title=""/>
|
||||
<privatestaticmethods visible="yes" title=""/>
|
||||
<privateattributes visible="yes" title=""/>
|
||||
<privatestaticattributes visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<related visible="yes" title="" subtitle=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<services visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<constructors visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<related visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<allmemberslink visible="yes"/>
|
||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
||||
<authorsection visible="yes"/>
|
||||
</class>
|
||||
|
||||
<!-- Layout definition for a namespace page -->
|
||||
<namespace>
|
||||
<briefdescription visible="yes"/>
|
||||
<memberdecl>
|
||||
<nestednamespaces visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection visible="yes"/>
|
||||
</namespace>
|
||||
|
||||
<!-- Layout definition for a concept page -->
|
||||
<concept>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_HEADERFILE"/>
|
||||
<definition visible="yes" title=""/>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<authorsection visible="yes"/>
|
||||
</concept>
|
||||
|
||||
<!-- Layout definition for a file page -->
|
||||
<file>
|
||||
<briefdescription visible="yes"/>
|
||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
||||
<includegraph visible="yes"/>
|
||||
<includedbygraph visible="yes"/>
|
||||
<sourcelink visible="yes"/>
|
||||
<memberdecl>
|
||||
<interfaces visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<structs visible="yes" title=""/>
|
||||
<exceptions visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<constantgroups visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdef>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<authorsection/>
|
||||
</file>
|
||||
|
||||
<!-- Layout definition for a group page -->
|
||||
<group>
|
||||
<briefdescription visible="yes"/>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdecl>
|
||||
<nestedgroups visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="no" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<modules visible="yes" title=""/>
|
||||
<dirs visible="yes" title=""/>
|
||||
<files visible="yes" title=""/>
|
||||
<namespaces visible="yes" title=""/>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
<membergroups visible="yes"/>
|
||||
</memberdecl>
|
||||
<memberdef>
|
||||
<pagedocs/>
|
||||
<inlineclasses visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<enumvalues visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<defines visible="yes" title=""/>
|
||||
<sequences visible="yes" title=""/>
|
||||
<dictionaries visible="yes" title=""/>
|
||||
<signals visible="yes" title=""/>
|
||||
<publicslots visible="yes" title=""/>
|
||||
<protectedslots visible="yes" title=""/>
|
||||
<privateslots visible="yes" title=""/>
|
||||
<events visible="yes" title=""/>
|
||||
<properties visible="yes" title=""/>
|
||||
<friends visible="yes" title=""/>
|
||||
</memberdef>
|
||||
<groupgraph visible="yes"/>
|
||||
<authorsection visible="yes"/>
|
||||
</group>
|
||||
|
||||
<!-- Layout definition for a C++20 module page -->
|
||||
<module>
|
||||
<briefdescription visible="yes"/>
|
||||
<exportedmodules visible="yes"/>
|
||||
<memberdecl>
|
||||
<concepts visible="yes" title=""/>
|
||||
<classes visible="yes" title=""/>
|
||||
<enums visible="yes" title=""/>
|
||||
<typedefs visible="yes" title=""/>
|
||||
<functions visible="yes" title=""/>
|
||||
<variables visible="yes" title=""/>
|
||||
<membergroups visible="yes" title=""/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
<memberdecl>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
</module>
|
||||
|
||||
<!-- Layout definition for a directory page -->
|
||||
<directory>
|
||||
<briefdescription visible="yes"/>
|
||||
<directorygraph visible="yes"/>
|
||||
<memberdecl>
|
||||
<dirs visible="yes"/>
|
||||
<files visible="yes"/>
|
||||
</memberdecl>
|
||||
<detaileddescription visible="yes" title=""/>
|
||||
</directory>
|
||||
</doxygenlayout>
|
||||
10
HACKING.md
10
HACKING.md
|
|
@ -50,24 +50,22 @@ macOS users don't require any additional dependencies.
|
|||
## Xcode Version and SDKs
|
||||
|
||||
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
|
||||
and the iOS SDK are all installed.
|
||||
the iOS SDK, and Metal Toolchain are all installed.
|
||||
|
||||
A common issue is that the incorrect version of Xcode is either
|
||||
installed or selected. Use the `xcode-select` command to
|
||||
ensure that the correct version of Xcode is selected:
|
||||
|
||||
```shell-session
|
||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
||||
sudo xcode-select --switch /Applications/Xcode.app
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Main branch development of Ghostty is preparing for the next major
|
||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
||||
> **Xcode 26 and the macOS 26 SDK**.
|
||||
> Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**.
|
||||
>
|
||||
> You do not need to be running on macOS 26 to build Ghostty, you can
|
||||
> still use Xcode 26 beta on macOS 15 stable.
|
||||
> still use Xcode 26 on macOS 15 stable.
|
||||
|
||||
## AI and Agents
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h
|
|||
|
||||
clean:
|
||||
rm -rf \
|
||||
zig-out zig-cache \
|
||||
zig-out .zig-cache \
|
||||
macos/build \
|
||||
macos/GhosttyKit.xcframework
|
||||
.PHONY: clean
|
||||
|
|
|
|||
|
|
@ -193,4 +193,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
|
|||
> purposely contain sensitive information, but it does contain the full
|
||||
> stack memory of each thread at the time of the crash. This information
|
||||
> is used to rebuild the stack trace but can also contain sensitive data
|
||||
> depending when the crash occurred.
|
||||
> depending on when the crash occurred.
|
||||
|
|
|
|||
37
build.zig
37
build.zig
|
|
@ -2,16 +2,19 @@ const std = @import("std");
|
|||
const assert = std.debug.assert;
|
||||
const builtin = @import("builtin");
|
||||
const buildpkg = @import("src/build/main.zig");
|
||||
const appVersion = @import("build.zig.zon").version;
|
||||
const minimumZigVersion = @import("build.zig.zon").minimum_zig_version;
|
||||
|
||||
comptime {
|
||||
buildpkg.requireZig("0.14.0");
|
||||
buildpkg.requireZig(minimumZigVersion);
|
||||
}
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
// This defines all the available build options (e.g. `-D`). If you
|
||||
// want to know what options are available, you can run `--help` or
|
||||
// you can read `src/build/Config.zig`.
|
||||
const config = try buildpkg.Config.init(b);
|
||||
|
||||
const config = try buildpkg.Config.init(b, appVersion);
|
||||
const test_filters = b.option(
|
||||
[][]const u8,
|
||||
"test-filter",
|
||||
|
|
@ -98,10 +101,19 @@ pub fn build(b: *std.Build) !void {
|
|||
);
|
||||
|
||||
// libghostty-vt
|
||||
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
const libghostty_vt_shared = shared: {
|
||||
if (config.target.result.cpu.arch.isWasm()) {
|
||||
break :shared try buildpkg.GhosttyLibVt.initWasm(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
}
|
||||
|
||||
break :shared try buildpkg.GhosttyLibVt.initShared(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
};
|
||||
libghostty_vt_shared.install(libvt_step);
|
||||
libghostty_vt_shared.install(b.getInstallStep());
|
||||
|
||||
|
|
@ -245,12 +257,17 @@ pub fn build(b: *std.Build) !void {
|
|||
{
|
||||
const mod_vt_test = b.addTest(.{
|
||||
.root_module = mod.vt,
|
||||
.target = config.target,
|
||||
.optimize = config.optimize,
|
||||
.filters = test_filters,
|
||||
});
|
||||
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
|
||||
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
|
||||
|
||||
const mod_vt_c_test = b.addTest(.{
|
||||
.root_module = mod.vt_c,
|
||||
.filters = test_filters,
|
||||
});
|
||||
const mod_vt_c_test_run = b.addRunArtifact(mod_vt_c_test);
|
||||
test_lib_vt_step.dependOn(&mod_vt_c_test_run.step);
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
|
@ -267,6 +284,8 @@ pub fn build(b: *std.Build) !void {
|
|||
.omit_frame_pointer = false,
|
||||
.unwind_tables = .sync,
|
||||
}),
|
||||
// Crash on x86_64 without this
|
||||
.use_llvm = true,
|
||||
});
|
||||
if (config.emit_test_exe) b.installArtifact(test_exe);
|
||||
_ = try deps.add(test_exe);
|
||||
|
|
@ -276,7 +295,7 @@ pub fn build(b: *std.Build) !void {
|
|||
test_step.dependOn(&test_run.step);
|
||||
|
||||
// Normal tests always test our libghostty modules
|
||||
test_step.dependOn(test_lib_vt_step);
|
||||
//test_step.dependOn(test_lib_vt_step);
|
||||
|
||||
// Valgrind test running
|
||||
const valgrind_run = b.addSystemCommand(&.{
|
||||
|
|
|
|||
|
|
@ -1,62 +1,63 @@
|
|||
.{
|
||||
.name = .ghostty,
|
||||
.version = "1.2.1",
|
||||
.version = "1.3.0-dev",
|
||||
.paths = .{""},
|
||||
.fingerprint = 0x64407a2a0b4147e5,
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
|
||||
.libxev = .{
|
||||
// mitchellh/libxev
|
||||
.url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q",
|
||||
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
// rockorager/libvaxis
|
||||
.url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
||||
.hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn",
|
||||
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
.lazy = true,
|
||||
},
|
||||
.z2d = .{
|
||||
// vancluever/z2d
|
||||
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
||||
.hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
|
||||
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
// mitchellh/zig-objc
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
.url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_js = .{
|
||||
// mitchellh/zig-js
|
||||
.url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
||||
.hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ",
|
||||
.url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||
.hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
|
||||
.lazy = true,
|
||||
},
|
||||
.ziglyph = .{
|
||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf",
|
||||
.lazy = true,
|
||||
.uucode = .{
|
||||
// TODO: currently the use-llvm branch because its broken on self-hosted
|
||||
.url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
.hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
// codeberg ifreund/zig-wayland
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
||||
.hash = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy",
|
||||
.url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||
.hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||
.lazy = true,
|
||||
},
|
||||
.zf = .{
|
||||
// natecraddock/zf
|
||||
.url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9",
|
||||
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
.lazy = true,
|
||||
},
|
||||
.gobject = .{
|
||||
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
|
||||
// Temporary until we generate them at build time automatically.
|
||||
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst",
|
||||
.url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
.lazy = true,
|
||||
},
|
||||
|
|
@ -115,8 +116,8 @@
|
|||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
||||
.hash = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||
.hash = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
},
|
||||
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
||||
"name": "gobject",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="
|
||||
},
|
||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||
|
|
@ -49,10 +49,10 @@
|
|||
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
||||
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
||||
},
|
||||
"N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3": {
|
||||
"N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
||||
"hash": "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw="
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||
"hash": "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k="
|
||||
},
|
||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||
"name": "jetbrains_mono",
|
||||
|
|
@ -64,10 +64,10 @@
|
|||
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
|
||||
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
|
||||
},
|
||||
"libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q": {
|
||||
"libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs": {
|
||||
"name": "libxev",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
||||
"hash": "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc="
|
||||
"url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||
"hash": "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc="
|
||||
},
|
||||
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
|
||||
"name": "libxml2",
|
||||
|
|
@ -109,10 +109,20 @@
|
|||
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
|
||||
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
|
||||
},
|
||||
"vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn": {
|
||||
"uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM": {
|
||||
"name": "uucode",
|
||||
"url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
|
||||
"hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="
|
||||
},
|
||||
"uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": {
|
||||
"name": "uucode",
|
||||
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
"hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="
|
||||
},
|
||||
"vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
|
||||
"name": "vaxis",
|
||||
"url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
||||
"hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="
|
||||
},
|
||||
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
|
||||
"name": "wayland",
|
||||
|
|
@ -129,45 +139,35 @@
|
|||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
||||
},
|
||||
"z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": {
|
||||
"z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": {
|
||||
"name": "z2d",
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
||||
"hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
"hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="
|
||||
},
|
||||
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
|
||||
"zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {
|
||||
"name": "zf",
|
||||
"url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
||||
"hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="
|
||||
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="
|
||||
},
|
||||
"zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": {
|
||||
"name": "zg",
|
||||
"url": "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
||||
"hash": "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="
|
||||
},
|
||||
"N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ": {
|
||||
"zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": {
|
||||
"name": "zig_js",
|
||||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
||||
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
|
||||
"url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||
"hash": "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M="
|
||||
},
|
||||
"zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
|
||||
"zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK": {
|
||||
"name": "zig_objc",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
|
||||
"url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||
"hash": "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw="
|
||||
},
|
||||
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
|
||||
"wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe": {
|
||||
"name": "zig_wayland",
|
||||
"url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
||||
"hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="
|
||||
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||
"hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="
|
||||
},
|
||||
"zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": {
|
||||
"zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": {
|
||||
"name": "zigimg",
|
||||
"url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d",
|
||||
"hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="
|
||||
},
|
||||
"ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": {
|
||||
"name": "ziglyph",
|
||||
"url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
"hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="
|
||||
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||
"hash": "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM="
|
||||
},
|
||||
"N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": {
|
||||
"name": "zlib",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
fetchurl,
|
||||
fetchgit,
|
||||
runCommandLocal,
|
||||
zig_0_14,
|
||||
zig_0_15,
|
||||
name ? "zig-packages",
|
||||
}: let
|
||||
unpackZigArtifact = {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
}:
|
||||
runCommandLocal name
|
||||
{
|
||||
nativeBuildInputs = [zig_0_14];
|
||||
nativeBuildInputs = [zig_0_15];
|
||||
}
|
||||
''
|
||||
hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
|
||||
|
|
@ -126,7 +126,7 @@ in
|
|||
name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV";
|
||||
path = fetchZigArtifact {
|
||||
name = "gobject";
|
||||
url = "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst";
|
||||
url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst";
|
||||
hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=";
|
||||
};
|
||||
}
|
||||
|
|
@ -163,11 +163,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3";
|
||||
name = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz";
|
||||
hash = "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw=";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz";
|
||||
hash = "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -187,11 +187,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q";
|
||||
name = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs";
|
||||
path = fetchZigArtifact {
|
||||
name = "libxev";
|
||||
url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz";
|
||||
hash = "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc=";
|
||||
url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz";
|
||||
hash = "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -259,11 +259,27 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn";
|
||||
name = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM";
|
||||
path = fetchZigArtifact {
|
||||
name = "uucode";
|
||||
url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732";
|
||||
hash = "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT";
|
||||
path = fetchZigArtifact {
|
||||
name = "uucode";
|
||||
url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz";
|
||||
hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS";
|
||||
path = fetchZigArtifact {
|
||||
name = "vaxis";
|
||||
url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23";
|
||||
hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY=";
|
||||
url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
|
||||
hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -291,67 +307,51 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP";
|
||||
name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T";
|
||||
path = fetchZigArtifact {
|
||||
name = "z2d";
|
||||
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz";
|
||||
hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=";
|
||||
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz";
|
||||
hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9";
|
||||
name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh";
|
||||
path = fetchZigArtifact {
|
||||
name = "zf";
|
||||
url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz";
|
||||
hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I=";
|
||||
url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
|
||||
hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM";
|
||||
path = fetchZigArtifact {
|
||||
name = "zg";
|
||||
url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc";
|
||||
hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ";
|
||||
name = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi";
|
||||
path = fetchZigArtifact {
|
||||
name = "zig_js";
|
||||
url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz";
|
||||
hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=";
|
||||
url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz";
|
||||
hash = "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk";
|
||||
name = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK";
|
||||
path = fetchZigArtifact {
|
||||
name = "zig_objc";
|
||||
url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
|
||||
hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
|
||||
url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz";
|
||||
hash = "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy";
|
||||
name = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe";
|
||||
path = fetchZigArtifact {
|
||||
name = "zig_wayland";
|
||||
url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz";
|
||||
hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=";
|
||||
url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz";
|
||||
hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj";
|
||||
name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms";
|
||||
path = fetchZigArtifact {
|
||||
name = "zigimg";
|
||||
url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d";
|
||||
hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf";
|
||||
path = fetchZigArtifact {
|
||||
name = "ziglyph";
|
||||
url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz";
|
||||
hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=";
|
||||
url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz";
|
||||
hash = "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc
|
||||
git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d
|
||||
git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23
|
||||
https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz
|
||||
git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732
|
||||
https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz
|
||||
https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz
|
||||
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
|
||||
|
|
@ -9,11 +6,13 @@ 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/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-2025-09-20-20-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
|
||||
https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz
|
||||
https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz
|
||||
https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz
|
||||
https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz
|
||||
https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
|
||||
https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz
|
||||
|
|
@ -21,15 +20,16 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e
|
|||
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
|
||||
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
|
||||
https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
|
||||
https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.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/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
|
||||
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
|
||||
https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz
|
||||
https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
|
||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz
|
||||
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
|
||||
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
|
||||
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz
|
||||
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz
|
||||
https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
|
||||
https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,18 @@
|
|||
<!-- HTML footer for doxygen 1.14.0-->
|
||||
<!-- start footer part -->
|
||||
<!--BEGIN GENERATE_TREEVIEW-->
|
||||
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
|
||||
<ul>
|
||||
$navpath
|
||||
<li class="footer">$generatedby <a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
|
||||
</ul>
|
||||
</div>
|
||||
<!--END GENERATE_TREEVIEW-->
|
||||
<!--BEGIN !GENERATE_TREEVIEW-->
|
||||
<hr class="footer"/><address class="footer"><small>
|
||||
$generatedby <a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
|
||||
</small></address>
|
||||
</div><!-- doc-content -->
|
||||
<!--END !GENERATE_TREEVIEW-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
/**
|
||||
* Ghostty Doxygen Custom Stylesheet
|
||||
* Minimal branding customizations for Ghostty colors
|
||||
*/
|
||||
|
||||
/* Ghostty brand color for links and accents - high contrast for dark bg */
|
||||
a,
|
||||
a:link {
|
||||
color: #99b3ff;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #99b3ff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #c2d4ff;
|
||||
}
|
||||
|
||||
/* High contrast text colors */
|
||||
body,
|
||||
div.contents,
|
||||
div.header,
|
||||
.title,
|
||||
.summary,
|
||||
td,
|
||||
th,
|
||||
p,
|
||||
li {
|
||||
color: #e8e8e8 !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.groupheader {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.memtitle,
|
||||
.memname {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.memdoc {
|
||||
color: #e8e8e8 !important;
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
::selection {
|
||||
background: rgba(53, 81, 243, 0.6);
|
||||
}
|
||||
|
||||
/* Modern scrollbar styling for WebKit browsers (Safari, Chrome) */
|
||||
::-webkit-scrollbar {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1a1f2e;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #4a5260;
|
||||
border-radius: 8px;
|
||||
border: 3px solid #1a1f2e;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #5a6270;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:active {
|
||||
background: #6a7280;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background: #1a1f2e;
|
||||
}
|
||||
|
||||
/* Firefox scrollbar styling */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #404754 #1a1f2e;
|
||||
}
|
||||
|
||||
/* Tree view selected item */
|
||||
#nav-tree .selected {
|
||||
background-color: #3551f3 !important;
|
||||
}
|
||||
|
||||
/* Custom syntax highlighting optimized for dark backgrounds with high contrast */
|
||||
.fragment,
|
||||
div.line {
|
||||
color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
/* Keywords (int, void, const, static, etc.) */
|
||||
.keyword,
|
||||
.keywordtype {
|
||||
color: #ff8be6 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Control flow (if, else, return, for, while, etc.) */
|
||||
.keywordflow {
|
||||
color: #ff8be6 !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Comments */
|
||||
.comment {
|
||||
color: #8bc34a !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Preprocessor directives (#include, #define, etc.) */
|
||||
.preprocessor {
|
||||
color: #ffcc66 !important;
|
||||
}
|
||||
|
||||
/* String and character literals */
|
||||
.stringliteral,
|
||||
.charliteral {
|
||||
color: #b8e986 !important;
|
||||
}
|
||||
|
||||
/* Numbers */
|
||||
span.charliteral {
|
||||
color: #d4a5ff !important;
|
||||
}
|
||||
|
||||
/* Function names */
|
||||
.functionname {
|
||||
color: #6fe87c !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Line numbers */
|
||||
span.lineno {
|
||||
color: #8a8a8a !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
span.lineno a {
|
||||
color: #8a8a8a !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Desktop: ensure page-nav maintains default width */
|
||||
@media screen and (min-width: 768px) {
|
||||
#page-nav-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#page-nav {
|
||||
position: relative !important;
|
||||
width: 250px !important;
|
||||
height: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile-friendly responsive styles */
|
||||
@media screen and (max-width: 767px) {
|
||||
body {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Make navigation tree collapsible on mobile */
|
||||
#side-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#doc-content {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
/* Make right sidebar (page-nav) overlay on mobile */
|
||||
#page-nav {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
right: -280px !important;
|
||||
width: 280px !important;
|
||||
height: 100vh !important;
|
||||
z-index: 10000 !important;
|
||||
background: #101826 !important;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5) !important;
|
||||
transition: right 0.3s ease !important;
|
||||
overflow-y: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
#page-nav.mobile-open {
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
/* Hamburger menu button for page nav */
|
||||
#page-nav-toggle {
|
||||
display: block !important;
|
||||
position: fixed !important;
|
||||
top: 10px !important;
|
||||
right: 15px !important;
|
||||
z-index: 10001 !important;
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
background: rgba(53, 81, 243, 0.9) !important;
|
||||
border: none !important;
|
||||
border-radius: 5px !important;
|
||||
cursor: pointer !important;
|
||||
padding: 8px !important;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
#page-nav-toggle span {
|
||||
display: block !important;
|
||||
width: 24px !important;
|
||||
height: 3px !important;
|
||||
background: #fff !important;
|
||||
margin: 4px 0 !important;
|
||||
border-radius: 2px !important;
|
||||
transition: 0.3s !important;
|
||||
}
|
||||
|
||||
/* Mobile overlay backdrop */
|
||||
#page-nav-backdrop {
|
||||
display: none !important;
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
#page-nav-backdrop.active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Improve header and navigation */
|
||||
#top {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#titlearea {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
#projectname {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
#projectbrief,
|
||||
#projectnumber {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Make tabs stack better on mobile */
|
||||
#navrow1,
|
||||
#navrow2,
|
||||
#navrow3,
|
||||
#navrow4 {
|
||||
overflow-x: auto !important;
|
||||
white-space: nowrap !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
.tablist li {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Content adjustments */
|
||||
.contents {
|
||||
padding: 10px !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.fragment {
|
||||
font-size: 12px !important;
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
|
||||
div.line {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
display: block !important;
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.memberdecls table,
|
||||
.fieldtable {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.memtitle {
|
||||
font-size: 14px !important;
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
.memname {
|
||||
font-size: 13px !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
.memitem {
|
||||
margin: 5px 0 !important;
|
||||
}
|
||||
|
||||
/* Search box */
|
||||
#MSearchBox {
|
||||
width: 100% !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
|
||||
/* Reduce padding and margins */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 10px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
h4 {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
/* Directory/file listings */
|
||||
.directory .levels span {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.directory .arrow {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
/* Treeview adjustments */
|
||||
#nav-tree {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tablet adjustments */
|
||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||
.contents {
|
||||
padding: 15px !important;
|
||||
}
|
||||
|
||||
#side-nav {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
#doc-content {
|
||||
margin-left: 200px !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!-- HTML header for doxygen 1.14.0-->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="$langISO">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
|
||||
<meta name="generator" content="Doxygen $doxygenversion"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
|
||||
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
|
||||
<!--BEGIN PROJECT_ICON-->
|
||||
<link rel="icon" href="$relpath^$projecticon" type="image/x-icon" />
|
||||
<!--END PROJECT_ICON-->
|
||||
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
|
||||
<!--BEGIN FULL_SIDEBAR-->
|
||||
<script type="text/javascript">var page_layout=1;</script>
|
||||
<!--END FULL_SIDEBAR-->
|
||||
<script type="text/javascript" src="$relpath^jquery.js"></script>
|
||||
<script type="text/javascript" src="$relpath^dynsections.js"></script>
|
||||
<!--BEGIN COPY_CLIPBOARD-->
|
||||
<script type="text/javascript" src="$relpath^clipboard.js"></script>
|
||||
<!--END COPY_CLIPBOARD-->
|
||||
$treeview
|
||||
$search
|
||||
$mathjax
|
||||
$darkmode
|
||||
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
|
||||
$extrastylesheet
|
||||
<script type="text/javascript" src="$relpath^mobile-nav.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<!--BEGIN FULL_SIDEBAR-->
|
||||
<div id="side-nav" class="ui-resizable side-nav-resizable"><!-- do not remove this div, it is closed by doxygen! -->
|
||||
<!--END FULL_SIDEBAR-->
|
||||
|
||||
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
|
||||
|
||||
<!--BEGIN TITLEAREA-->
|
||||
<div id="titlearea">
|
||||
<table cellspacing="0" cellpadding="0">
|
||||
<tbody>
|
||||
<tr id="projectrow">
|
||||
<!--BEGIN PROJECT_LOGO-->
|
||||
<td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"$logosize/></td>
|
||||
<!--END PROJECT_LOGO-->
|
||||
<!--BEGIN PROJECT_NAME-->
|
||||
<td id="projectalign">
|
||||
<div id="projectname">$projectname<!--BEGIN PROJECT_NUMBER--><span id="projectnumber"> $projectnumber</span><!--END PROJECT_NUMBER-->
|
||||
</div>
|
||||
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
|
||||
</td>
|
||||
<!--END PROJECT_NAME-->
|
||||
<!--BEGIN !PROJECT_NAME-->
|
||||
<!--BEGIN PROJECT_BRIEF-->
|
||||
<td>
|
||||
<div id="projectbrief">$projectbrief</div>
|
||||
</td>
|
||||
<!--END PROJECT_BRIEF-->
|
||||
<!--END !PROJECT_NAME-->
|
||||
<!--BEGIN DISABLE_INDEX-->
|
||||
<!--BEGIN SEARCHENGINE-->
|
||||
<!--BEGIN !FULL_SIDEBAR-->
|
||||
<td>$searchbox</td>
|
||||
<!--END !FULL_SIDEBAR-->
|
||||
<!--END SEARCHENGINE-->
|
||||
<!--END DISABLE_INDEX-->
|
||||
</tr>
|
||||
<!--BEGIN SEARCHENGINE-->
|
||||
<!--BEGIN FULL_SIDEBAR-->
|
||||
<tr><td colspan="2">$searchbox</td></tr>
|
||||
<!--END FULL_SIDEBAR-->
|
||||
<!--END SEARCHENGINE-->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--END TITLEAREA-->
|
||||
<!-- end header part -->
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* Mobile navigation toggle for Doxygen documentation
|
||||
*/
|
||||
|
||||
(function () {
|
||||
// Only run on mobile devices
|
||||
function isMobile() {
|
||||
return window.innerWidth <= 767;
|
||||
}
|
||||
|
||||
function initMobileNav() {
|
||||
if (!isMobile()) return;
|
||||
|
||||
const pageNav = document.getElementById("page-nav");
|
||||
if (!pageNav) return;
|
||||
|
||||
// Create toggle button
|
||||
const toggleBtn = document.createElement("button");
|
||||
toggleBtn.id = "page-nav-toggle";
|
||||
toggleBtn.setAttribute("aria-label", "Toggle page navigation");
|
||||
toggleBtn.innerHTML = "<span></span><span></span><span></span>";
|
||||
document.body.appendChild(toggleBtn);
|
||||
|
||||
// Create backdrop
|
||||
const backdrop = document.createElement("div");
|
||||
backdrop.id = "page-nav-backdrop";
|
||||
document.body.appendChild(backdrop);
|
||||
|
||||
// Toggle function
|
||||
function toggleNav() {
|
||||
const isOpen = pageNav.classList.toggle("mobile-open");
|
||||
backdrop.classList.toggle("active", isOpen);
|
||||
document.body.style.overflow = isOpen ? "hidden" : "";
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
toggleBtn.addEventListener("click", toggleNav);
|
||||
backdrop.addEventListener("click", toggleNav);
|
||||
|
||||
// Close on escape key
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape" && pageNav.classList.contains("mobile-open")) {
|
||||
toggleNav();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on load and resize
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", initMobileNav);
|
||||
} else {
|
||||
initMobileNav();
|
||||
}
|
||||
|
||||
window.addEventListener("resize", function () {
|
||||
const pageNav = document.getElementById("page-nav");
|
||||
const backdrop = document.getElementById("page-nav-backdrop");
|
||||
|
||||
if (!isMobile() && pageNav) {
|
||||
pageNav.classList.remove("mobile-open");
|
||||
if (backdrop) backdrop.classList.remove("active");
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,22 @@
|
|||
# Example: `ghostty-vt` C Key Encoding
|
||||
|
||||
This example demonstrates how to use the `ghostty-vt` C library to encode key
|
||||
events into terminal escape sequences.
|
||||
|
||||
This example specifically shows how to:
|
||||
|
||||
1. Create a key encoder with the C API
|
||||
2. Configure Kitty keyboard protocol flags (this example uses KKP)
|
||||
3. Create and configure a key event
|
||||
4. Encode the key event into a terminal escape sequence
|
||||
|
||||
The example encodes a Ctrl key release event with the Ctrl modifier set,
|
||||
producing the escape sequence `\x1b[57442;5:3u`.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the program:
|
||||
|
||||
```shell-session
|
||||
zig build run
|
||||
```
|
||||
|
|
@ -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_key_encode",
|
||||
.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);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.{
|
||||
.name = .c_vt,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x413a8529b1255f9a,
|
||||
.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",
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
int main() {
|
||||
GhosttyKeyEncoder encoder;
|
||||
GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
// Set kitty flags with all features enabled
|
||||
ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
|
||||
|
||||
// Create key event
|
||||
GhosttyKeyEvent event;
|
||||
result = ghostty_key_event_new(NULL, &event);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE);
|
||||
ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT);
|
||||
ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
|
||||
printf("Encoding event: left ctrl release with all Kitty flags enabled\n");
|
||||
|
||||
// Optionally, encode with null buffer to get required size. You can
|
||||
// skip this step and provide a sufficiently large buffer directly.
|
||||
// If there isn't enoug hspace, the function will return an out of memory
|
||||
// error.
|
||||
size_t required = 0;
|
||||
result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
|
||||
assert(result == GHOSTTY_OUT_OF_MEMORY);
|
||||
printf("Required buffer size: %zu bytes\n", required);
|
||||
|
||||
// Encode the key event. We don't use our required size above because
|
||||
// that was just an example; we know 128 bytes is enough.
|
||||
char buf[128];
|
||||
size_t written = 0;
|
||||
result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
printf("Encoded %zu bytes\n", written);
|
||||
|
||||
// Print the encoded sequence (hex and string)
|
||||
printf("Hex: ");
|
||||
for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]);
|
||||
printf("\n");
|
||||
|
||||
printf("String: ");
|
||||
for (size_t i = 0; i < written; i++) {
|
||||
if (buf[i] == 0x1b) {
|
||||
printf("\\x1b");
|
||||
} else {
|
||||
printf("%c", buf[i]);
|
||||
}
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
ghostty_key_event_free(event);
|
||||
ghostty_key_encoder_free(encoder);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Example: `ghostty-vt` Paste Safety Check
|
||||
|
||||
This contains a simple example of how to use the `ghostty-vt` paste
|
||||
utilities to check if paste data is safe.
|
||||
|
||||
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
|
||||
```
|
||||
|
|
@ -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_paste",
|
||||
.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);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.{
|
||||
.name = .c_vt_paste,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xa105002abbc8cf74,
|
||||
.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",
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
int main() {
|
||||
// Test safe paste data
|
||||
const char *safe_data = "hello world";
|
||||
if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
|
||||
printf("'%s' is safe to paste\n", safe_data);
|
||||
}
|
||||
|
||||
// Test unsafe paste data with newline
|
||||
const char *unsafe_newline = "rm -rf /\n";
|
||||
if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) {
|
||||
printf("'%s' is UNSAFE - contains newline\n", unsafe_newline);
|
||||
}
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
.name = .c_vt,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x413a8529b1255f9a,
|
||||
.minimum_zig_version = "0.14.1",
|
||||
.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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
int main() {
|
||||
|
|
@ -6,6 +8,29 @@ int main() {
|
|||
if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Setup change window title command to change the title to "hello"
|
||||
ghostty_osc_next(parser, '0');
|
||||
ghostty_osc_next(parser, ';');
|
||||
const char *title = "hello";
|
||||
for (size_t i = 0; i < strlen(title); i++) {
|
||||
ghostty_osc_next(parser, title[i]);
|
||||
}
|
||||
|
||||
// End parsing and get command
|
||||
GhosttyOscCommand command = ghostty_osc_end(parser, 0);
|
||||
|
||||
// Get and print command type
|
||||
GhosttyOscCommandType type = ghostty_osc_command_type(command);
|
||||
printf("Command type: %d\n", type);
|
||||
|
||||
// Extract and print the title
|
||||
if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) {
|
||||
printf("Extracted title: %s\n", title);
|
||||
} else {
|
||||
printf("Failed to extract title\n");
|
||||
}
|
||||
|
||||
ghostty_osc_free(parser);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
.name = .zig_vt,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x6045575a7a8387e6,
|
||||
.minimum_zig_version = "0.14.1",
|
||||
.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.
|
||||
|
|
|
|||
16
flake.lock
16
flake.lock
|
|
@ -36,15 +36,15 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1748189127,
|
||||
"narHash": "sha256-zRDR+EbbeObu4V2X5QCd2Bk5eltfDlCr5yvhBwUT6pY=",
|
||||
"rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334",
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=",
|
||||
"rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.802491.7c43f080a7f2/nixexprs.tar.xz"
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre877938.82c2e0d6dde5/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
|
|
@ -97,11 +97,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1748261582,
|
||||
"narHash": "sha256-3i0IL3s18hdDlbsf0/E+5kyPRkZwGPbSFngq5eToiAA=",
|
||||
"lastModified": 1760401936,
|
||||
"narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "aafb1b093fb838f7a02613b719e85ec912914221",
|
||||
"rev": "365085b6652259753b598d43b723858184980bbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
# We want to stay as up to date as possible but need to be careful that the
|
||||
# glibc versions used by our dependencies from Nix are compatible with the
|
||||
# system glibc that the user is building for.
|
||||
nixpkgs.url = "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz";
|
||||
#
|
||||
# We are currently on unstable to get Zig 0.15 for our package.nix
|
||||
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
# Used for shell.nix
|
||||
|
|
@ -47,7 +49,7 @@
|
|||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.14.1";
|
||||
zig = zig.packages.${system}."0.15.2";
|
||||
wraptest = pkgs.callPackage ./nix/wraptest.nix {};
|
||||
zon2nix = zon2nix;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ modules:
|
|||
- chmod a+x /app/zig/zig
|
||||
sources:
|
||||
- type: archive
|
||||
sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c
|
||||
url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz
|
||||
sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239
|
||||
url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz
|
||||
only-arches: [x86_64]
|
||||
- type: archive
|
||||
sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b
|
||||
url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz
|
||||
sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f
|
||||
url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz
|
||||
only-arches: [aarch64]
|
||||
|
||||
- name: bzip2-redirect
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/2025-09-20-20-1/ghostty-gobject-2025-09-20-20-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
||||
},
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3",
|
||||
"sha256": "24f63d339d1dfe7eab1b35add1a419214ec804c5abbb6200a9ef55bb5c7908cc"
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||
"dest": "vendor/p/N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq",
|
||||
"sha256": "bc59fa32247cb55906ca3495ee9cf89389acbe24828c61ca336494f38949a7f9"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -79,9 +79,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q",
|
||||
"sha256": "29aa3360a121853ffab089de7fbffc3bfeb42c304937ef1099d2ee358d469267"
|
||||
"url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
|
||||
"sha256": "6003ea6b96e4a518a128f932327d79a11bd30996b13b73baeb29916379487dd7"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -133,9 +133,21 @@
|
|||
},
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/rockorager/libvaxis",
|
||||
"commit": "1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
||||
"dest": "vendor/p/vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"
|
||||
"url": "https://github.com/jacobsandlund/uucode",
|
||||
"commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT",
|
||||
"sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
"sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -157,51 +169,39 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
|
||||
"sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d"
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
"sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
||||
"dest": "vendor/p/zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9",
|
||||
"sha256": "de7ba535077fe2b678a5a7972585f002588d37244db08397feadf3d4907c0bb2"
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/atman/zg",
|
||||
"commit": "4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
||||
"dest": "vendor/p/zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"
|
||||
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
"sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ",
|
||||
"sha256": "7f235e0956c2f5401a28963a261019953d00e3bf4cfc029830f2161196c3583d"
|
||||
"url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||
"dest": "vendor/p/zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
|
||||
"sha256": "4c2018e56015d39504b8090386ad9ce9393f38380085d9c32373bf7e56fc73a3"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
"sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
|
||||
"url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||
"sha256": "dd84af737625356fcd722cb30909f3b2e8d702667cf579714aa7eabc0ac08ecc"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
||||
"dest": "vendor/p/wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy",
|
||||
"sha256": "13bec6675e403d86db3b55b39ae262f1e1bdfe24056dcd82824341c6308b5219"
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/TUSF/zigimg",
|
||||
"commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d",
|
||||
"dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"
|
||||
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||
"dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||
"sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
"dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf",
|
||||
"sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9"
|
||||
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||
"dest": "vendor/p/zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms",
|
||||
"sha256": "2c1ed76ba2b35514544b0c27c9633ecba7c31be9080e37e7a010c93b5a1bc553"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ typedef struct {
|
|||
typedef struct {
|
||||
const char* ptr;
|
||||
uintptr_t len;
|
||||
bool sentinel;
|
||||
} ghostty_string_s;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -732,6 +733,21 @@ typedef struct {
|
|||
int8_t progress;
|
||||
} ghostty_action_progress_report_s;
|
||||
|
||||
// apprt.action.CommandFinished.C
|
||||
typedef struct {
|
||||
// -1 if no exit code was reported, otherwise 0-255
|
||||
int16_t exit_code;
|
||||
// number of nanoseconds that command was running for
|
||||
uint64_t duration;
|
||||
} ghostty_action_command_finished_s;
|
||||
|
||||
// terminal.Scrollbar
|
||||
typedef struct {
|
||||
uint64_t total;
|
||||
uint64_t offset;
|
||||
uint64_t len;
|
||||
} ghostty_action_scrollbar_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
|
|
@ -758,6 +774,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||
GHOSTTY_ACTION_CELL_SIZE,
|
||||
GHOSTTY_ACTION_SCROLLBAR,
|
||||
GHOSTTY_ACTION_RENDER,
|
||||
GHOSTTY_ACTION_INSPECTOR,
|
||||
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
||||
|
|
@ -787,6 +804,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
||||
GHOSTTY_ACTION_PROGRESS_REPORT,
|
||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||
GHOSTTY_ACTION_COMMAND_FINISHED,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
|
|
@ -799,6 +817,7 @@ typedef union {
|
|||
ghostty_action_size_limit_s size_limit;
|
||||
ghostty_action_initial_size_s initial_size;
|
||||
ghostty_action_cell_size_s cell_size;
|
||||
ghostty_action_scrollbar_s scrollbar;
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
|
|
@ -818,6 +837,7 @@ typedef union {
|
|||
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||
ghostty_surface_message_childexited_s child_exited;
|
||||
ghostty_action_progress_report_s progress_report;
|
||||
ghostty_action_command_finished_s command_finished;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @file vt.h
|
||||
*
|
||||
* libghostty-vt - Virtual terminal sequence parsing library
|
||||
* libghostty-vt - Virtual terminal emulator library
|
||||
*
|
||||
* This library provides functionality for parsing and handling terminal
|
||||
* escape sequences as well as maintaining terminal state such as styles,
|
||||
|
|
@ -11,6 +11,52 @@
|
|||
* stable and is definitely going to change.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @mainpage libghostty-vt - Virtual Terminal Emulator Library
|
||||
*
|
||||
* libghostty-vt is a C library which implements a modern terminal emulator,
|
||||
* extracted from the [Ghostty](https://ghostty.org) terminal emulator.
|
||||
*
|
||||
* libghostty-vt contains the logic for handling the core parts of a terminal
|
||||
* emulator: parsing terminal escape sequences, maintaining terminal state,
|
||||
* encoding input events, etc. It can handle scrollback, line wrapping,
|
||||
* reflow on resize, and more.
|
||||
*
|
||||
* @warning This library is currently in development and the API is not yet stable.
|
||||
* Breaking changes are expected in future versions. Use with caution in production code.
|
||||
*
|
||||
* @section groups_sec API Reference
|
||||
*
|
||||
* The API is organized into the following groups:
|
||||
* - @ref key "Key Encoding" - Encode key events into terminal sequences
|
||||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
|
||||
* - @ref paste "Paste Utilities" - Validate paste data safety
|
||||
* - @ref allocator "Memory Management" - Memory management and custom allocators
|
||||
*
|
||||
* @section examples_sec Examples
|
||||
*
|
||||
* Complete working examples:
|
||||
* - @ref c-vt/src/main.c - OSC parser example
|
||||
* - @ref c-vt-key-encode/src/main.c - Key encoding example
|
||||
* - @ref c-vt-paste/src/main.c - Paste safety check example
|
||||
*
|
||||
*/
|
||||
|
||||
/** @example c-vt/src/main.c
|
||||
* This example demonstrates how to use the OSC parser to parse an OSC sequence,
|
||||
* extract command information, and retrieve command-specific data like window titles.
|
||||
*/
|
||||
|
||||
/** @example c-vt-key-encode/src/main.c
|
||||
* This example demonstrates how to use the key encoder to convert key events
|
||||
* into terminal escape sequences using the Kitty keyboard protocol.
|
||||
*/
|
||||
|
||||
/** @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.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_H
|
||||
#define GHOSTTY_VT_H
|
||||
|
||||
|
|
@ -18,201 +64,11 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
/**
|
||||
* Opaque handle to an OSC parser instance.
|
||||
*
|
||||
* This handle represents an OSC (Operating System Command) parser that can
|
||||
* be used to parse the contents of OSC sequences. This isn't a full VT
|
||||
* parser; it is only the OSC parser component. This is useful if you have
|
||||
* a parser already and want to only extract and handle OSC sequences.
|
||||
*/
|
||||
typedef struct GhosttyOscParser *GhosttyOscParser;
|
||||
|
||||
/**
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
typedef enum {
|
||||
/** Operation completed successfully */
|
||||
GHOSTTY_SUCCESS = 0,
|
||||
/** Operation failed due to failed allocation */
|
||||
GHOSTTY_OUT_OF_MEMORY = -1,
|
||||
} GhosttyResult;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Allocator Interface
|
||||
|
||||
/**
|
||||
* Function table for custom memory allocator operations.
|
||||
*
|
||||
* This vtable defines the interface for a custom memory allocator. All
|
||||
* function pointers must be valid and non-NULL.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* overcomplicated". The reason for this complexity is well thought out by
|
||||
* the Zig folks, and it enables a diverse set of allocation strategies
|
||||
* as shown by the Zig ecosystem. As a consolation, please note that many
|
||||
* of the arguments are only needed for advanced use cases and can be
|
||||
* safely ignored in simple implementations. For example, if you look at
|
||||
* the Zig implementation of the libc allocator in `lib/std/heap.zig`
|
||||
* (search for CAllocator), you'll see it is very simple.
|
||||
*
|
||||
* We chose to align with the Zig allocator interface because:
|
||||
*
|
||||
* 1. It is a proven interface that serves a wide variety of use cases
|
||||
* in the real world via the Zig ecosystem. It's shown to work.
|
||||
*
|
||||
* 2. Our core implementation itself is Zig, and this lets us very
|
||||
* cheaply and easily convert between C and Zig allocators.
|
||||
*
|
||||
* NOTE(mitchellh): In the future, we can have default implementations of
|
||||
* resize/remap and allow those to be null.
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Return a pointer to `len` bytes with specified `alignment`, or return
|
||||
* `NULL` indicating the allocation failed.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param len Number of bytes to allocate
|
||||
* @param alignment Required alignment for the allocation. Guaranteed to
|
||||
* be a power of two between 1 and 16 inclusive.
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to allocated memory, or NULL if allocation failed
|
||||
*/
|
||||
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory in place.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to resize
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return true if resize was successful in-place, false if relocation would be required
|
||||
*/
|
||||
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory, allowing relocation.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* A non-`NULL` return value indicates the resize was successful. The
|
||||
* allocation may have same address, or may have been relocated. In either
|
||||
* case, the allocation now has size of `new_len`. A `NULL` return value
|
||||
* indicates that the resize would be equivalent to allocating new memory,
|
||||
* copying the bytes from the old memory, and then freeing the old memory.
|
||||
* In such case, it is more efficient for the caller to perform the copy.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to remap
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
|
||||
*/
|
||||
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Free and invalidate a region of memory.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to free
|
||||
* @param memory_len Size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
*/
|
||||
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
|
||||
} GhosttyAllocatorVtable;
|
||||
|
||||
/**
|
||||
* Custom memory allocator.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* GhosttyAllocator allocator = {
|
||||
* .vtable = &my_allocator_vtable,
|
||||
* .ctx = my_allocator_state
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Opaque context pointer passed to all vtable functions.
|
||||
* This allows the allocator implementation to maintain state
|
||||
* or reference external resources needed for memory management.
|
||||
*/
|
||||
void *ctx;
|
||||
|
||||
/**
|
||||
* Pointer to the allocator's vtable containing function pointers
|
||||
* for memory operations (alloc, resize, remap, free).
|
||||
*/
|
||||
const GhosttyAllocatorVtable *vtable;
|
||||
} GhosttyAllocator;
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Functions
|
||||
|
||||
/**
|
||||
* Create a new OSC parser instance.
|
||||
*
|
||||
* Creates a new OSC (Operating System Command) parser using the provided
|
||||
* allocator. The parser must be freed using ghostty_vt_osc_free() when
|
||||
* no longer needed.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
|
||||
* @param parser Pointer to store the created parser handle
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*/
|
||||
GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
|
||||
|
||||
/**
|
||||
* Free an OSC parser instance.
|
||||
*
|
||||
* Releases all resources associated with the OSC parser. After this call,
|
||||
* the parser handle becomes invalid and must not be used.
|
||||
*
|
||||
* @param parser The parser handle to free (may be NULL)
|
||||
*/
|
||||
void ghostty_osc_free(GhosttyOscParser parser);
|
||||
#include <ghostty/vt/result.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/osc.h>
|
||||
#include <ghostty/vt/key.h>
|
||||
#include <ghostty/vt/paste.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* @file allocator.h
|
||||
*
|
||||
* Memory management interface for libghostty-vt.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_ALLOCATOR_H
|
||||
#define GHOSTTY_VT_ALLOCATOR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** @defgroup allocator Memory Management
|
||||
*
|
||||
* libghostty-vt does require memory allocation for various operations,
|
||||
* but is resilient to allocation failures and will gracefully handle
|
||||
* out-of-memory situations by returning error codes.
|
||||
*
|
||||
* The exact memory management semantics are documented in the relevant
|
||||
* functions and data structures.
|
||||
*
|
||||
* libghostty-vt uses explicit memory allocation via an allocator
|
||||
* interface provided by GhosttyAllocator. The interface is based on the
|
||||
* [Zig](https://ziglang.org) allocator interface, since this has been
|
||||
* shown to be a flexible and powerful interface in practice and enables
|
||||
* a wide variety of allocation strategies.
|
||||
*
|
||||
* **For the common case, you can pass NULL as the allocator for any
|
||||
* 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.
|
||||
*
|
||||
* ## 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).
|
||||
*
|
||||
* To use a custom allocator:
|
||||
* 1. Implement the GhosttyAllocatorVtable function pointers
|
||||
* 2. Create a GhosttyAllocator struct with your vtable and context
|
||||
* 3. Pass the allocator to functions that accept one
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Function table for custom memory allocator operations.
|
||||
*
|
||||
* This vtable defines the interface for a custom memory allocator. All
|
||||
* function pointers must be valid and non-NULL.
|
||||
*
|
||||
* @ingroup allocator
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* overcomplicated". The reason for this complexity is well thought out by
|
||||
* the Zig folks, and it enables a diverse set of allocation strategies
|
||||
* as shown by the Zig ecosystem. As a consolation, please note that many
|
||||
* of the arguments are only needed for advanced use cases and can be
|
||||
* safely ignored in simple implementations. For example, if you look at
|
||||
* the Zig implementation of the libc allocator in `lib/std/heap.zig`
|
||||
* (search for CAllocator), you'll see it is very simple.
|
||||
*
|
||||
* We chose to align with the Zig allocator interface because:
|
||||
*
|
||||
* 1. It is a proven interface that serves a wide variety of use cases
|
||||
* in the real world via the Zig ecosystem. It's shown to work.
|
||||
*
|
||||
* 2. Our core implementation itself is Zig, and this lets us very
|
||||
* cheaply and easily convert between C and Zig allocators.
|
||||
*
|
||||
* NOTE(mitchellh): In the future, we can have default implementations of
|
||||
* resize/remap and allow those to be null.
|
||||
*/
|
||||
typedef struct {
|
||||
/**
|
||||
* Return a pointer to `len` bytes with specified `alignment`, or return
|
||||
* `NULL` indicating the allocation failed.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param len Number of bytes to allocate
|
||||
* @param alignment Required alignment for the allocation. Guaranteed to
|
||||
* be a power of two between 1 and 16 inclusive.
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to allocated memory, or NULL if allocation failed
|
||||
*/
|
||||
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory in place.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to resize
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return true if resize was successful in-place, false if relocation would be required
|
||||
*/
|
||||
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Attempt to expand or shrink memory, allowing relocation.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* A non-`NULL` return value indicates the resize was successful. The
|
||||
* allocation may have same address, or may have been relocated. In either
|
||||
* case, the allocation now has size of `new_len`. A `NULL` return value
|
||||
* indicates that the resize would be equivalent to allocating new memory,
|
||||
* copying the bytes from the old memory, and then freeing the old memory.
|
||||
* In such case, it is more efficient for the caller to perform the copy.
|
||||
*
|
||||
* `new_len` must be greater than zero.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to remap
|
||||
* @param memory_len Current size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param new_len New requested size
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
|
||||
*/
|
||||
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
|
||||
|
||||
/**
|
||||
* Free and invalidate a region of memory.
|
||||
*
|
||||
* `memory_len` must equal the length requested from the most recent
|
||||
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
|
||||
* equal the same value that was passed as the `alignment` parameter to
|
||||
* the original `alloc` call.
|
||||
*
|
||||
* @param ctx The allocator context
|
||||
* @param memory Pointer to the memory block to free
|
||||
* @param memory_len Size of the memory block
|
||||
* @param alignment Alignment (must match original allocation)
|
||||
* @param ret_addr First return address of the allocation call stack (0 if not provided)
|
||||
*/
|
||||
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
|
||||
} GhosttyAllocatorVtable;
|
||||
|
||||
/**
|
||||
* Custom memory allocator.
|
||||
*
|
||||
* 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).
|
||||
*
|
||||
* @ingroup allocator
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* GhosttyAllocator allocator = {
|
||||
* .vtable = &my_allocator_vtable,
|
||||
* .ctx = my_allocator_state
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
typedef struct GhosttyAllocator {
|
||||
/**
|
||||
* Opaque context pointer passed to all vtable functions.
|
||||
* This allows the allocator implementation to maintain state
|
||||
* or reference external resources needed for memory management.
|
||||
*/
|
||||
void *ctx;
|
||||
|
||||
/**
|
||||
* Pointer to the allocator's vtable containing function pointers
|
||||
* for memory operations (alloc, resize, remap, free).
|
||||
*/
|
||||
const GhosttyAllocatorVtable *vtable;
|
||||
} GhosttyAllocator;
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_ALLOCATOR_H */
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* @file key.h
|
||||
*
|
||||
* Key encoding module - encode key events into terminal escape sequences.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_KEY_H
|
||||
#define GHOSTTY_VT_KEY_H
|
||||
|
||||
/** @defgroup key Key Encoding
|
||||
*
|
||||
* Utilities for encoding key events into terminal escape sequences,
|
||||
* supporting both legacy encoding as well as Kitty Keyboard Protocol.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* 1. Create an encoder instance with ghostty_key_encoder_new()
|
||||
* 2. Configure encoder options with ghostty_key_encoder_setopt().
|
||||
* 3. For each key event:
|
||||
* - Create a key event with ghostty_key_event_new()
|
||||
* - Set event properties (action, key, modifiers, etc.)
|
||||
* - Encode with ghostty_key_encoder_encode()
|
||||
* - Free the event with ghostty_key_event_free()
|
||||
* - Note: You can also reuse the same key event multiple times by
|
||||
* changing its properties.
|
||||
* 4. Free the encoder with ghostty_key_encoder_free() when done
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @code{.c}
|
||||
* #include <assert.h>
|
||||
* #include <stdio.h>
|
||||
* #include <ghostty/vt.h>
|
||||
*
|
||||
* int main() {
|
||||
* // Create encoder
|
||||
* GhosttyKeyEncoder encoder;
|
||||
* GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
|
||||
* assert(result == GHOSTTY_SUCCESS);
|
||||
*
|
||||
* // Enable Kitty keyboard protocol with all features
|
||||
* ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS,
|
||||
* &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
|
||||
*
|
||||
* // Create and configure key event for Ctrl+C press
|
||||
* GhosttyKeyEvent event;
|
||||
* result = ghostty_key_event_new(NULL, &event);
|
||||
* assert(result == GHOSTTY_SUCCESS);
|
||||
* ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS);
|
||||
* ghostty_key_event_set_key(event, GHOSTTY_KEY_C);
|
||||
* ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
|
||||
*
|
||||
* // Encode the key event
|
||||
* char buf[128];
|
||||
* size_t written = 0;
|
||||
* result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
|
||||
* assert(result == GHOSTTY_SUCCESS);
|
||||
*
|
||||
* // Use the encoded sequence (e.g., write to terminal)
|
||||
* fwrite(buf, 1, written, stdout);
|
||||
*
|
||||
* // Cleanup
|
||||
* ghostty_key_event_free(event);
|
||||
* ghostty_key_encoder_free(encoder);
|
||||
* return 0;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* For a complete working example, see example/c-vt-key-encode in the
|
||||
* repository.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#include <ghostty/vt/key/event.h>
|
||||
#include <ghostty/vt/key/encoder.h>
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_KEY_H */
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* @file encoder.h
|
||||
*
|
||||
* Key event encoding to terminal escape sequences.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_KEY_ENCODER_H
|
||||
#define GHOSTTY_VT_KEY_ENCODER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/result.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/key/event.h>
|
||||
|
||||
/**
|
||||
* Opaque handle to a key encoder instance.
|
||||
*
|
||||
* This handle represents a key encoder that converts key events into terminal
|
||||
* escape sequences.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder;
|
||||
|
||||
/**
|
||||
* Kitty keyboard protocol flags.
|
||||
*
|
||||
* Bitflags representing the various modes of the Kitty keyboard protocol.
|
||||
* These can be combined using bitwise OR operations. Valid values all
|
||||
* start with `GHOSTTY_KITTY_KEY_`.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef uint8_t GhosttyKittyKeyFlags;
|
||||
|
||||
/** Kitty keyboard protocol disabled (all flags off) */
|
||||
#define GHOSTTY_KITTY_KEY_DISABLED 0
|
||||
|
||||
/** Disambiguate escape codes */
|
||||
#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0)
|
||||
|
||||
/** Report key press and release events */
|
||||
#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1)
|
||||
|
||||
/** Report alternate key codes */
|
||||
#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2)
|
||||
|
||||
/** Report all key events including those normally handled by the terminal */
|
||||
#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3)
|
||||
|
||||
/** Report associated text with key events */
|
||||
#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4)
|
||||
|
||||
/** All Kitty keyboard protocol flags enabled */
|
||||
#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED)
|
||||
|
||||
/**
|
||||
* macOS option key behavior.
|
||||
*
|
||||
* Determines whether the "option" key on macOS is treated as "alt" or not.
|
||||
* See the Ghostty `macos-option-as-alt` configuration option for more details.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef enum {
|
||||
/** Option key is not treated as alt */
|
||||
GHOSTTY_OPTION_AS_ALT_FALSE = 0,
|
||||
/** Option key is treated as alt */
|
||||
GHOSTTY_OPTION_AS_ALT_TRUE = 1,
|
||||
/** Only left option key is treated as alt */
|
||||
GHOSTTY_OPTION_AS_ALT_LEFT = 2,
|
||||
/** Only right option key is treated as alt */
|
||||
GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
|
||||
} GhosttyOptionAsAlt;
|
||||
|
||||
/**
|
||||
* Key encoder option identifiers.
|
||||
*
|
||||
* These values are used with ghostty_key_encoder_setopt() to configure
|
||||
* the behavior of the key encoder.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef enum {
|
||||
/** Terminal DEC mode 1: cursor key application mode (value: bool) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
|
||||
|
||||
/** Terminal DEC mode 66: keypad key application mode (value: bool) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1,
|
||||
|
||||
/** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2,
|
||||
|
||||
/** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3,
|
||||
|
||||
/** xterm modifyOtherKeys mode 2 (value: bool) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4,
|
||||
|
||||
/** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5,
|
||||
|
||||
/** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
|
||||
GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
|
||||
} GhosttyKeyEncoderOption;
|
||||
|
||||
/**
|
||||
* Create a new key encoder instance.
|
||||
*
|
||||
* Creates a new key encoder with default options. The encoder can be configured
|
||||
* using ghostty_key_encoder_setopt() and must be freed using
|
||||
* ghostty_key_encoder_free() when no longer needed.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
|
||||
* @param encoder Pointer to store the created encoder handle
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder);
|
||||
|
||||
/**
|
||||
* Free a key encoder instance.
|
||||
*
|
||||
* Releases all resources associated with the key encoder. After this call,
|
||||
* the encoder handle becomes invalid and must not be used.
|
||||
*
|
||||
* @param encoder The encoder handle to free (may be NULL)
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_encoder_free(GhosttyKeyEncoder encoder);
|
||||
|
||||
/**
|
||||
* Set an option on the key encoder.
|
||||
*
|
||||
* Configures the behavior of the key encoder. Options control various aspects
|
||||
* of encoding such as terminal modes (cursor key application mode, keypad mode),
|
||||
* protocol selection (Kitty keyboard protocol flags), and platform-specific
|
||||
* behaviors (macOS option-as-alt).
|
||||
*
|
||||
* A null pointer value does nothing. It does not reset the value to the
|
||||
* default. The setopt call will do nothing.
|
||||
*
|
||||
* @param encoder The encoder handle, must not be NULL
|
||||
* @param option The option to set
|
||||
* @param value Pointer to the value to set (type depends on the option)
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value);
|
||||
|
||||
/**
|
||||
* Encode a key event into a terminal escape sequence.
|
||||
*
|
||||
* Converts a key event into the appropriate terminal escape sequence based on
|
||||
* the encoder's current options. The sequence is written to the provided buffer.
|
||||
*
|
||||
* Not all key events produce output. For example, unmodified modifier keys
|
||||
* typically don't generate escape sequences. Check the out_len parameter to
|
||||
* determine if any data was written.
|
||||
*
|
||||
* If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY
|
||||
* and out_len will contain the required buffer size. The caller can then
|
||||
* allocate a larger buffer and call the function again.
|
||||
*
|
||||
* @param encoder The encoder handle, must not be NULL
|
||||
* @param event The key event to encode, must not be NULL
|
||||
* @param out_buf Buffer to write the encoded sequence to
|
||||
* @param out_buf_size Size of the output buffer in bytes
|
||||
* @param out_len Pointer to store the number of bytes written (may be NULL)
|
||||
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code
|
||||
*
|
||||
* ## Example: Calculate required buffer size
|
||||
*
|
||||
* @code{.c}
|
||||
* // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY)
|
||||
* size_t required = 0;
|
||||
* GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
|
||||
* assert(result == GHOSTTY_OUT_OF_MEMORY);
|
||||
*
|
||||
* // Allocate buffer of required size
|
||||
* char *buf = malloc(required);
|
||||
*
|
||||
* // Encode with properly sized buffer
|
||||
* size_t written = 0;
|
||||
* result = ghostty_key_encoder_encode(encoder, event, buf, required, &written);
|
||||
* assert(result == GHOSTTY_SUCCESS);
|
||||
*
|
||||
* // Use the encoded sequence...
|
||||
*
|
||||
* free(buf);
|
||||
* @endcode
|
||||
*
|
||||
* ## Example: Direct encoding with static buffer
|
||||
*
|
||||
* @code{.c}
|
||||
* // Most escape sequences are short, so a static buffer often suffices
|
||||
* char buf[128];
|
||||
* size_t written = 0;
|
||||
* GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
|
||||
*
|
||||
* if (result == GHOSTTY_SUCCESS) {
|
||||
* // Write the encoded sequence to the terminal
|
||||
* write(pty_fd, buf, written);
|
||||
* } else if (result == GHOSTTY_OUT_OF_MEMORY) {
|
||||
* // Buffer too small, written contains required size
|
||||
* char *dynamic_buf = malloc(written);
|
||||
* result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
|
||||
* assert(result == GHOSTTY_SUCCESS);
|
||||
* write(pty_fd, dynamic_buf, written);
|
||||
* free(dynamic_buf);
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len);
|
||||
|
||||
#endif /* GHOSTTY_VT_KEY_ENCODER_H */
|
||||
|
|
@ -0,0 +1,474 @@
|
|||
/**
|
||||
* @file event.h
|
||||
*
|
||||
* Key event representation and manipulation.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_KEY_EVENT_H
|
||||
#define GHOSTTY_VT_KEY_EVENT_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/result.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
|
||||
/**
|
||||
* Opaque handle to a key event.
|
||||
*
|
||||
* This handle represents a keyboard input event containing information about
|
||||
* the physical key pressed, modifiers, and generated text.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
|
||||
|
||||
/**
|
||||
* Keyboard input event types.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef enum {
|
||||
/** Key was released */
|
||||
GHOSTTY_KEY_ACTION_RELEASE = 0,
|
||||
/** Key was pressed */
|
||||
GHOSTTY_KEY_ACTION_PRESS = 1,
|
||||
/** Key is being repeated (held down) */
|
||||
GHOSTTY_KEY_ACTION_REPEAT = 2,
|
||||
} GhosttyKeyAction;
|
||||
|
||||
/**
|
||||
* Keyboard modifier keys bitmask.
|
||||
*
|
||||
* A bitmask representing all keyboard modifiers. This tracks which modifier keys
|
||||
* are pressed and, where supported by the platform, which side (left or right)
|
||||
* of each modifier is active.
|
||||
*
|
||||
* Use the GHOSTTY_MODS_* constants to test and set individual modifiers.
|
||||
*
|
||||
* Modifier side bits are only meaningful when the corresponding modifier bit is set.
|
||||
* Not all platforms support distinguishing between left and right modifier
|
||||
* keys and Ghostty is built to expect that some platforms may not provide this
|
||||
* information.
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef uint16_t GhosttyMods;
|
||||
|
||||
/** Shift key is pressed */
|
||||
#define GHOSTTY_MODS_SHIFT (1 << 0)
|
||||
/** Control key is pressed */
|
||||
#define GHOSTTY_MODS_CTRL (1 << 1)
|
||||
/** Alt/Option key is pressed */
|
||||
#define GHOSTTY_MODS_ALT (1 << 2)
|
||||
/** Super/Command/Windows key is pressed */
|
||||
#define GHOSTTY_MODS_SUPER (1 << 3)
|
||||
/** Caps Lock is active */
|
||||
#define GHOSTTY_MODS_CAPS_LOCK (1 << 4)
|
||||
/** Num Lock is active */
|
||||
#define GHOSTTY_MODS_NUM_LOCK (1 << 5)
|
||||
|
||||
/**
|
||||
* Right shift is pressed (0 = left, 1 = right).
|
||||
* Only meaningful when GHOSTTY_MODS_SHIFT is set.
|
||||
*/
|
||||
#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6)
|
||||
/**
|
||||
* Right ctrl is pressed (0 = left, 1 = right).
|
||||
* Only meaningful when GHOSTTY_MODS_CTRL is set.
|
||||
*/
|
||||
#define GHOSTTY_MODS_CTRL_SIDE (1 << 7)
|
||||
/**
|
||||
* Right alt is pressed (0 = left, 1 = right).
|
||||
* Only meaningful when GHOSTTY_MODS_ALT is set.
|
||||
*/
|
||||
#define GHOSTTY_MODS_ALT_SIDE (1 << 8)
|
||||
/**
|
||||
* Right super is pressed (0 = left, 1 = right).
|
||||
* Only meaningful when GHOSTTY_MODS_SUPER is set.
|
||||
*/
|
||||
#define GHOSTTY_MODS_SUPER_SIDE (1 << 9)
|
||||
|
||||
/**
|
||||
* Physical key codes.
|
||||
*
|
||||
* The set of key codes that Ghostty is aware of. These represent physical keys
|
||||
* on the keyboard and are layout-independent. For example, the "a" key on a US
|
||||
* keyboard is the same as the "ф" key on a Russian keyboard, but both will
|
||||
* report the same key_a value.
|
||||
*
|
||||
* Layout-dependent strings are provided separately as UTF-8 text and are produced
|
||||
* by the platform. These values are based on the W3C UI Events KeyboardEvent code
|
||||
* standard. See: https://www.w3.org/TR/uievents-code
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
typedef enum {
|
||||
GHOSTTY_KEY_UNIDENTIFIED = 0,
|
||||
|
||||
// Writing System Keys (W3C § 3.1.1)
|
||||
GHOSTTY_KEY_BACKQUOTE,
|
||||
GHOSTTY_KEY_BACKSLASH,
|
||||
GHOSTTY_KEY_BRACKET_LEFT,
|
||||
GHOSTTY_KEY_BRACKET_RIGHT,
|
||||
GHOSTTY_KEY_COMMA,
|
||||
GHOSTTY_KEY_DIGIT_0,
|
||||
GHOSTTY_KEY_DIGIT_1,
|
||||
GHOSTTY_KEY_DIGIT_2,
|
||||
GHOSTTY_KEY_DIGIT_3,
|
||||
GHOSTTY_KEY_DIGIT_4,
|
||||
GHOSTTY_KEY_DIGIT_5,
|
||||
GHOSTTY_KEY_DIGIT_6,
|
||||
GHOSTTY_KEY_DIGIT_7,
|
||||
GHOSTTY_KEY_DIGIT_8,
|
||||
GHOSTTY_KEY_DIGIT_9,
|
||||
GHOSTTY_KEY_EQUAL,
|
||||
GHOSTTY_KEY_INTL_BACKSLASH,
|
||||
GHOSTTY_KEY_INTL_RO,
|
||||
GHOSTTY_KEY_INTL_YEN,
|
||||
GHOSTTY_KEY_A,
|
||||
GHOSTTY_KEY_B,
|
||||
GHOSTTY_KEY_C,
|
||||
GHOSTTY_KEY_D,
|
||||
GHOSTTY_KEY_E,
|
||||
GHOSTTY_KEY_F,
|
||||
GHOSTTY_KEY_G,
|
||||
GHOSTTY_KEY_H,
|
||||
GHOSTTY_KEY_I,
|
||||
GHOSTTY_KEY_J,
|
||||
GHOSTTY_KEY_K,
|
||||
GHOSTTY_KEY_L,
|
||||
GHOSTTY_KEY_M,
|
||||
GHOSTTY_KEY_N,
|
||||
GHOSTTY_KEY_O,
|
||||
GHOSTTY_KEY_P,
|
||||
GHOSTTY_KEY_Q,
|
||||
GHOSTTY_KEY_R,
|
||||
GHOSTTY_KEY_S,
|
||||
GHOSTTY_KEY_T,
|
||||
GHOSTTY_KEY_U,
|
||||
GHOSTTY_KEY_V,
|
||||
GHOSTTY_KEY_W,
|
||||
GHOSTTY_KEY_X,
|
||||
GHOSTTY_KEY_Y,
|
||||
GHOSTTY_KEY_Z,
|
||||
GHOSTTY_KEY_MINUS,
|
||||
GHOSTTY_KEY_PERIOD,
|
||||
GHOSTTY_KEY_QUOTE,
|
||||
GHOSTTY_KEY_SEMICOLON,
|
||||
GHOSTTY_KEY_SLASH,
|
||||
|
||||
// Functional Keys (W3C § 3.1.2)
|
||||
GHOSTTY_KEY_ALT_LEFT,
|
||||
GHOSTTY_KEY_ALT_RIGHT,
|
||||
GHOSTTY_KEY_BACKSPACE,
|
||||
GHOSTTY_KEY_CAPS_LOCK,
|
||||
GHOSTTY_KEY_CONTEXT_MENU,
|
||||
GHOSTTY_KEY_CONTROL_LEFT,
|
||||
GHOSTTY_KEY_CONTROL_RIGHT,
|
||||
GHOSTTY_KEY_ENTER,
|
||||
GHOSTTY_KEY_META_LEFT,
|
||||
GHOSTTY_KEY_META_RIGHT,
|
||||
GHOSTTY_KEY_SHIFT_LEFT,
|
||||
GHOSTTY_KEY_SHIFT_RIGHT,
|
||||
GHOSTTY_KEY_SPACE,
|
||||
GHOSTTY_KEY_TAB,
|
||||
GHOSTTY_KEY_CONVERT,
|
||||
GHOSTTY_KEY_KANA_MODE,
|
||||
GHOSTTY_KEY_NON_CONVERT,
|
||||
|
||||
// Control Pad Section (W3C § 3.2)
|
||||
GHOSTTY_KEY_DELETE,
|
||||
GHOSTTY_KEY_END,
|
||||
GHOSTTY_KEY_HELP,
|
||||
GHOSTTY_KEY_HOME,
|
||||
GHOSTTY_KEY_INSERT,
|
||||
GHOSTTY_KEY_PAGE_DOWN,
|
||||
GHOSTTY_KEY_PAGE_UP,
|
||||
|
||||
// Arrow Pad Section (W3C § 3.3)
|
||||
GHOSTTY_KEY_ARROW_DOWN,
|
||||
GHOSTTY_KEY_ARROW_LEFT,
|
||||
GHOSTTY_KEY_ARROW_RIGHT,
|
||||
GHOSTTY_KEY_ARROW_UP,
|
||||
|
||||
// Numpad Section (W3C § 3.4)
|
||||
GHOSTTY_KEY_NUM_LOCK,
|
||||
GHOSTTY_KEY_NUMPAD_0,
|
||||
GHOSTTY_KEY_NUMPAD_1,
|
||||
GHOSTTY_KEY_NUMPAD_2,
|
||||
GHOSTTY_KEY_NUMPAD_3,
|
||||
GHOSTTY_KEY_NUMPAD_4,
|
||||
GHOSTTY_KEY_NUMPAD_5,
|
||||
GHOSTTY_KEY_NUMPAD_6,
|
||||
GHOSTTY_KEY_NUMPAD_7,
|
||||
GHOSTTY_KEY_NUMPAD_8,
|
||||
GHOSTTY_KEY_NUMPAD_9,
|
||||
GHOSTTY_KEY_NUMPAD_ADD,
|
||||
GHOSTTY_KEY_NUMPAD_BACKSPACE,
|
||||
GHOSTTY_KEY_NUMPAD_CLEAR,
|
||||
GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
|
||||
GHOSTTY_KEY_NUMPAD_COMMA,
|
||||
GHOSTTY_KEY_NUMPAD_DECIMAL,
|
||||
GHOSTTY_KEY_NUMPAD_DIVIDE,
|
||||
GHOSTTY_KEY_NUMPAD_ENTER,
|
||||
GHOSTTY_KEY_NUMPAD_EQUAL,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
|
||||
GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
|
||||
GHOSTTY_KEY_NUMPAD_MULTIPLY,
|
||||
GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
|
||||
GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
|
||||
GHOSTTY_KEY_NUMPAD_SUBTRACT,
|
||||
GHOSTTY_KEY_NUMPAD_SEPARATOR,
|
||||
GHOSTTY_KEY_NUMPAD_UP,
|
||||
GHOSTTY_KEY_NUMPAD_DOWN,
|
||||
GHOSTTY_KEY_NUMPAD_RIGHT,
|
||||
GHOSTTY_KEY_NUMPAD_LEFT,
|
||||
GHOSTTY_KEY_NUMPAD_BEGIN,
|
||||
GHOSTTY_KEY_NUMPAD_HOME,
|
||||
GHOSTTY_KEY_NUMPAD_END,
|
||||
GHOSTTY_KEY_NUMPAD_INSERT,
|
||||
GHOSTTY_KEY_NUMPAD_DELETE,
|
||||
GHOSTTY_KEY_NUMPAD_PAGE_UP,
|
||||
GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
|
||||
|
||||
// Function Section (W3C § 3.5)
|
||||
GHOSTTY_KEY_ESCAPE,
|
||||
GHOSTTY_KEY_F1,
|
||||
GHOSTTY_KEY_F2,
|
||||
GHOSTTY_KEY_F3,
|
||||
GHOSTTY_KEY_F4,
|
||||
GHOSTTY_KEY_F5,
|
||||
GHOSTTY_KEY_F6,
|
||||
GHOSTTY_KEY_F7,
|
||||
GHOSTTY_KEY_F8,
|
||||
GHOSTTY_KEY_F9,
|
||||
GHOSTTY_KEY_F10,
|
||||
GHOSTTY_KEY_F11,
|
||||
GHOSTTY_KEY_F12,
|
||||
GHOSTTY_KEY_F13,
|
||||
GHOSTTY_KEY_F14,
|
||||
GHOSTTY_KEY_F15,
|
||||
GHOSTTY_KEY_F16,
|
||||
GHOSTTY_KEY_F17,
|
||||
GHOSTTY_KEY_F18,
|
||||
GHOSTTY_KEY_F19,
|
||||
GHOSTTY_KEY_F20,
|
||||
GHOSTTY_KEY_F21,
|
||||
GHOSTTY_KEY_F22,
|
||||
GHOSTTY_KEY_F23,
|
||||
GHOSTTY_KEY_F24,
|
||||
GHOSTTY_KEY_F25,
|
||||
GHOSTTY_KEY_FN,
|
||||
GHOSTTY_KEY_FN_LOCK,
|
||||
GHOSTTY_KEY_PRINT_SCREEN,
|
||||
GHOSTTY_KEY_SCROLL_LOCK,
|
||||
GHOSTTY_KEY_PAUSE,
|
||||
|
||||
// Media Keys (W3C § 3.6)
|
||||
GHOSTTY_KEY_BROWSER_BACK,
|
||||
GHOSTTY_KEY_BROWSER_FAVORITES,
|
||||
GHOSTTY_KEY_BROWSER_FORWARD,
|
||||
GHOSTTY_KEY_BROWSER_HOME,
|
||||
GHOSTTY_KEY_BROWSER_REFRESH,
|
||||
GHOSTTY_KEY_BROWSER_SEARCH,
|
||||
GHOSTTY_KEY_BROWSER_STOP,
|
||||
GHOSTTY_KEY_EJECT,
|
||||
GHOSTTY_KEY_LAUNCH_APP_1,
|
||||
GHOSTTY_KEY_LAUNCH_APP_2,
|
||||
GHOSTTY_KEY_LAUNCH_MAIL,
|
||||
GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
|
||||
GHOSTTY_KEY_MEDIA_SELECT,
|
||||
GHOSTTY_KEY_MEDIA_STOP,
|
||||
GHOSTTY_KEY_MEDIA_TRACK_NEXT,
|
||||
GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
|
||||
GHOSTTY_KEY_POWER,
|
||||
GHOSTTY_KEY_SLEEP,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
|
||||
GHOSTTY_KEY_AUDIO_VOLUME_UP,
|
||||
GHOSTTY_KEY_WAKE_UP,
|
||||
|
||||
// Legacy, Non-standard, and Special Keys (W3C § 3.7)
|
||||
GHOSTTY_KEY_COPY,
|
||||
GHOSTTY_KEY_CUT,
|
||||
GHOSTTY_KEY_PASTE,
|
||||
} GhosttyKey;
|
||||
|
||||
/**
|
||||
* Create a new key event instance.
|
||||
*
|
||||
* Creates a new key event with default values. The event must be freed using
|
||||
* ghostty_key_event_free() when no longer needed.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
|
||||
* @param event Pointer to store the created key event handle
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event);
|
||||
|
||||
/**
|
||||
* Free a key event instance.
|
||||
*
|
||||
* Releases all resources associated with the key event. After this call,
|
||||
* the event handle becomes invalid and must not be used.
|
||||
*
|
||||
* @param event The key event handle to free (may be NULL)
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_free(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set the key action (press, release, repeat).
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param action The action to set
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action);
|
||||
|
||||
/**
|
||||
* Get the key action (press, release, repeat).
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return The key action
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set the physical key code.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param key The physical key code to set
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key);
|
||||
|
||||
/**
|
||||
* Get the physical key code.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return The physical key code
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set the modifier keys bitmask.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param mods The modifier keys bitmask to set
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods);
|
||||
|
||||
/**
|
||||
* Get the modifier keys bitmask.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return The modifier keys bitmask
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set the consumed modifiers bitmask.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param consumed_mods The consumed modifiers bitmask to set
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods);
|
||||
|
||||
/**
|
||||
* Get the consumed modifiers bitmask.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return The consumed modifiers bitmask
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set whether the key event is part of a composition sequence.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param composing Whether the key event is part of a composition sequence
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing);
|
||||
|
||||
/**
|
||||
* Get whether the key event is part of a composition sequence.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return Whether the key event is part of a composition sequence
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
bool ghostty_key_event_get_composing(GhosttyKeyEvent event);
|
||||
|
||||
/**
|
||||
* Set the UTF-8 text generated by the key event.
|
||||
*
|
||||
* The key event does NOT take ownership of the text pointer. The caller
|
||||
* must ensure the string remains valid for the lifetime needed by the event.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param utf8 The UTF-8 text to set (or NULL for empty)
|
||||
* @param len Length of the UTF-8 text in bytes
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len);
|
||||
|
||||
/**
|
||||
* Get the UTF-8 text generated by the key event.
|
||||
*
|
||||
* The returned pointer is valid until the event is freed or the UTF-8 text is modified.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL)
|
||||
* @return The UTF-8 text (or NULL for empty)
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len);
|
||||
|
||||
/**
|
||||
* Set the unshifted Unicode codepoint.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @param codepoint The unshifted Unicode codepoint to set
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint);
|
||||
|
||||
/**
|
||||
* Get the unshifted Unicode codepoint.
|
||||
*
|
||||
* @param event The key event handle, must not be NULL
|
||||
* @return The unshifted Unicode codepoint
|
||||
*
|
||||
* @ingroup key
|
||||
*/
|
||||
uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event);
|
||||
|
||||
#endif /* GHOSTTY_VT_KEY_EVENT_H */
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
/**
|
||||
* @file osc.h
|
||||
*
|
||||
* OSC (Operating System Command) sequence parser and command handling.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_OSC_H
|
||||
#define GHOSTTY_VT_OSC_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <ghostty/vt/result.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
|
||||
/**
|
||||
* Opaque handle to an OSC parser instance.
|
||||
*
|
||||
* This handle represents an OSC (Operating System Command) parser that can
|
||||
* be used to parse the contents of OSC sequences.
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
typedef struct GhosttyOscParser *GhosttyOscParser;
|
||||
|
||||
/**
|
||||
* Opaque handle to a single OSC command.
|
||||
*
|
||||
* This handle represents a parsed OSC (Operating System Command) command.
|
||||
* The command can be queried for its type and associated data.
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
typedef struct GhosttyOscCommand *GhosttyOscCommand;
|
||||
|
||||
/** @defgroup osc OSC Parser
|
||||
*
|
||||
* OSC (Operating System Command) sequence parser and command handling.
|
||||
*
|
||||
* The parser operates in a streaming fashion, processing input byte-by-byte
|
||||
* to handle OSC sequences that may arrive in fragments across multiple reads.
|
||||
* This interface makes it easy to integrate into most environments and avoids
|
||||
* over-allocating buffers.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* 1. Create a parser instance with ghostty_osc_new()
|
||||
* 2. Feed bytes to the parser using ghostty_osc_next()
|
||||
* 3. Finalize parsing with ghostty_osc_end() to get the command
|
||||
* 4. Query command type and extract data using ghostty_osc_command_type()
|
||||
* and ghostty_osc_command_data()
|
||||
* 5. Free the parser with ghostty_osc_free() when done
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* OSC command types.
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
typedef enum {
|
||||
GHOSTTY_OSC_COMMAND_INVALID = 0,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
|
||||
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_START = 3,
|
||||
GHOSTTY_OSC_COMMAND_PROMPT_END = 4,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5,
|
||||
GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6,
|
||||
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7,
|
||||
GHOSTTY_OSC_COMMAND_REPORT_PWD = 8,
|
||||
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9,
|
||||
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11,
|
||||
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13,
|
||||
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20,
|
||||
} GhosttyOscCommandType;
|
||||
|
||||
/**
|
||||
* OSC command data types.
|
||||
*
|
||||
* These values specify what type of data to extract from an OSC command
|
||||
* using `ghostty_osc_command_data`.
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
typedef enum {
|
||||
/** Invalid data type. Never results in any data extraction. */
|
||||
GHOSTTY_OSC_DATA_INVALID = 0,
|
||||
|
||||
/**
|
||||
* Window title string data.
|
||||
*
|
||||
* Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
|
||||
*
|
||||
* Output type: const char ** (pointer to null-terminated string)
|
||||
*
|
||||
* Lifetime: Valid until the next call to any ghostty_osc_* function with
|
||||
* the same parser instance. Memory is owned by the parser.
|
||||
*/
|
||||
GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
|
||||
} GhosttyOscCommandData;
|
||||
|
||||
/**
|
||||
* Create a new OSC parser instance.
|
||||
*
|
||||
* Creates a new OSC (Operating System Command) parser using the provided
|
||||
* allocator. The parser must be freed using ghostty_vt_osc_free() when
|
||||
* no longer needed.
|
||||
*
|
||||
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
|
||||
* @param parser Pointer to store the created parser handle
|
||||
* @return GHOSTTY_SUCCESS on success, or an error code on failure
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
|
||||
|
||||
/**
|
||||
* Free an OSC parser instance.
|
||||
*
|
||||
* Releases all resources associated with the OSC parser. After this call,
|
||||
* the parser handle becomes invalid and must not be used.
|
||||
*
|
||||
* @param parser The parser handle to free (may be NULL)
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
void ghostty_osc_free(GhosttyOscParser parser);
|
||||
|
||||
/**
|
||||
* Reset an OSC parser instance to its initial state.
|
||||
*
|
||||
* Resets the parser state, clearing any partially parsed OSC sequences
|
||||
* and returning the parser to its initial state. This is useful for
|
||||
* reusing a parser instance or recovering from parse errors.
|
||||
*
|
||||
* @param parser The parser handle to reset, must not be null.
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
void ghostty_osc_reset(GhosttyOscParser parser);
|
||||
|
||||
/**
|
||||
* Parse the next byte in an OSC sequence.
|
||||
*
|
||||
* Processes a single byte as part of an OSC sequence. The parser maintains
|
||||
* internal state to track the progress through the sequence. Call this
|
||||
* function for each byte in the sequence data.
|
||||
*
|
||||
* When finished pumping the parser with bytes, call ghostty_osc_end
|
||||
* to get the final result.
|
||||
*
|
||||
* @param parser The parser handle, must not be null.
|
||||
* @param byte The next byte to parse
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
|
||||
|
||||
/**
|
||||
* Finalize OSC parsing and retrieve the parsed command.
|
||||
*
|
||||
* Call this function after feeding all bytes of an OSC sequence to the parser
|
||||
* using ghostty_osc_next() with the exception of the terminating character
|
||||
* (ESC or ST). This function finalizes the parsing process and returns the
|
||||
* parsed OSC command.
|
||||
*
|
||||
* The return value is never NULL. Invalid commands will return a command
|
||||
* with type GHOSTTY_OSC_COMMAND_INVALID.
|
||||
*
|
||||
* The terminator parameter specifies the byte that terminated the OSC sequence
|
||||
* (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
|
||||
* preserved in the parsed command so that responses can use the same terminator
|
||||
* format for better compatibility with the calling program. For commands that
|
||||
* do not require a response, this parameter is ignored and the resulting
|
||||
* command will not retain the terminator information.
|
||||
*
|
||||
* The returned command handle is valid until the next call to any
|
||||
* `ghostty_osc_*` function with the same parser instance with the exception
|
||||
* of command introspection functions such as `ghostty_osc_command_type`.
|
||||
*
|
||||
* @param parser The parser handle, must not be null.
|
||||
* @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
|
||||
* @return Handle to the parsed OSC command
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
|
||||
|
||||
/**
|
||||
* Get the type of an OSC command.
|
||||
*
|
||||
* Returns the type identifier for the given OSC command. This can be used
|
||||
* to determine what kind of command was parsed and what data might be
|
||||
* available from it.
|
||||
*
|
||||
* @param command The OSC command handle to query (may be NULL)
|
||||
* @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
|
||||
|
||||
/**
|
||||
* Extract data from an OSC command.
|
||||
*
|
||||
* Extracts typed data from the given OSC command based on the specified
|
||||
* data type. The output pointer must be of the appropriate type for the
|
||||
* requested data kind. Valid command types, output types, and memory
|
||||
* safety information are documented in the `GhosttyOscCommandData` enum.
|
||||
*
|
||||
* @param command The OSC command handle to query (may be NULL)
|
||||
* @param data The type of data to extract
|
||||
* @param out Pointer to store the extracted data (type depends on data parameter)
|
||||
* @return true if data extraction was successful, false otherwise
|
||||
*
|
||||
* @ingroup osc
|
||||
*/
|
||||
bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_OSC_H */
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @file paste.h
|
||||
*
|
||||
* Paste utilities - validate and encode paste data for terminal input.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_PASTE_H
|
||||
#define GHOSTTY_VT_PASTE_H
|
||||
|
||||
/** @defgroup paste Paste Utilities
|
||||
*
|
||||
* Utilities for validating paste data safety.
|
||||
*
|
||||
* ## Basic Usage
|
||||
*
|
||||
* Use ghostty_paste_is_safe() to check if paste data contains potentially
|
||||
* dangerous sequences before sending it to the terminal.
|
||||
*
|
||||
* ## Example
|
||||
*
|
||||
* @code{.c}
|
||||
* #include <stdio.h>
|
||||
* #include <string.h>
|
||||
* #include <ghostty/vt.h>
|
||||
*
|
||||
* int main() {
|
||||
* const char* safe_data = "hello world";
|
||||
* const char* unsafe_data = "rm -rf /\n";
|
||||
*
|
||||
* if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
|
||||
* printf("Safe to paste\n");
|
||||
* }
|
||||
*
|
||||
* if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) {
|
||||
* printf("Unsafe! Contains newline\n");
|
||||
* }
|
||||
*
|
||||
* return 0;
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Check if paste data is safe to paste into the terminal.
|
||||
*
|
||||
* Data is considered unsafe if it contains:
|
||||
* - Newlines (`\n`) which can inject commands
|
||||
* - The bracketed paste end sequence (`\x1b[201~`) which can be used
|
||||
* to exit bracketed paste mode and inject commands
|
||||
*
|
||||
* This check is conservative and considers data unsafe regardless of
|
||||
* current terminal state.
|
||||
*
|
||||
* @param data The paste data to check (must not be NULL)
|
||||
* @param len The length of the data in bytes
|
||||
* @return true if the data is safe to paste, false otherwise
|
||||
*/
|
||||
bool ghostty_paste_is_safe(const char* data, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
/** @} */
|
||||
|
||||
#endif /* GHOSTTY_VT_PASTE_H */
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @file result.h
|
||||
*
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
|
||||
#ifndef GHOSTTY_VT_RESULT_H
|
||||
#define GHOSTTY_VT_RESULT_H
|
||||
|
||||
/**
|
||||
* Result codes for libghostty-vt operations.
|
||||
*/
|
||||
typedef enum {
|
||||
/** Operation completed successfully */
|
||||
GHOSTTY_SUCCESS = 0,
|
||||
/** Operation failed due to failed allocation */
|
||||
GHOSTTY_OUT_OF_MEMORY = -1,
|
||||
} GhosttyResult;
|
||||
|
||||
#endif /* GHOSTTY_VT_RESULT_H */
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
<key>NSMenuItem</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>New Ghostty Tab Here</string>
|
||||
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here</string>
|
||||
</dict>
|
||||
<key>NSMessage</key>
|
||||
<string>openTab</string>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<key>NSMenuItem</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>New Ghostty Window Here</string>
|
||||
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here</string>
|
||||
</dict>
|
||||
<key>NSMessage</key>
|
||||
<string>openWindow</string>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@
|
|||
"Features/App Intents/CommandPaletteIntent.swift",
|
||||
"Features/App Intents/Entities/CommandEntity.swift",
|
||||
"Features/App Intents/Entities/TerminalEntity.swift",
|
||||
"Features/App Intents/FocusTerminalIntent.swift",
|
||||
"Features/App Intents/GetTerminalDetailsIntent.swift",
|
||||
"Features/App Intents/GhosttyIntentError.swift",
|
||||
"Features/App Intents/InputIntent.swift",
|
||||
|
|
@ -95,6 +96,7 @@
|
|||
Features/QuickTerminal/QuickTerminalController.swift,
|
||||
Features/QuickTerminal/QuickTerminalPosition.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreen.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreenStateCache.swift,
|
||||
Features/QuickTerminal/QuickTerminalSize.swift,
|
||||
Features/QuickTerminal/QuickTerminalSpaceBehavior.swift,
|
||||
Features/QuickTerminal/QuickTerminalWindow.swift,
|
||||
|
|
@ -124,7 +126,14 @@
|
|||
"Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift",
|
||||
"Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift",
|
||||
"Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift",
|
||||
Features/Update/UpdateBadge.swift,
|
||||
Features/Update/UpdateController.swift,
|
||||
Features/Update/UpdateDelegate.swift,
|
||||
Features/Update/UpdateDriver.swift,
|
||||
Features/Update/UpdatePill.swift,
|
||||
Features/Update/UpdatePopoverView.swift,
|
||||
Features/Update/UpdateSimulator.swift,
|
||||
Features/Update/UpdateViewModel.swift,
|
||||
"Ghostty/FullscreenMode+Extension.swift",
|
||||
Ghostty/Ghostty.Command.swift,
|
||||
Ghostty/Ghostty.Error.swift,
|
||||
|
|
@ -133,6 +142,7 @@
|
|||
Ghostty/Ghostty.Surface.swift,
|
||||
Ghostty/InspectorView.swift,
|
||||
"Ghostty/NSEvent+Extension.swift",
|
||||
Ghostty/SurfaceScrollView.swift,
|
||||
Ghostty/SurfaceView_AppKit.swift,
|
||||
Helpers/AppInfo.swift,
|
||||
Helpers/CodableBridge.swift,
|
||||
|
|
@ -159,6 +169,7 @@
|
|||
Helpers/KeyboardLayout.swift,
|
||||
Helpers/LastWindowPosition.swift,
|
||||
Helpers/MetalView.swift,
|
||||
Helpers/NonDraggableHostingView.swift,
|
||||
Helpers/PermissionRequest.swift,
|
||||
Helpers/Private/CGS.swift,
|
||||
Helpers/Private/Dock.swift,
|
||||
|
|
@ -544,6 +555,7 @@
|
|||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
@ -766,7 +778,7 @@
|
|||
EXECUTABLE_NAME = ghostty;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
|
|
@ -784,6 +796,7 @@
|
|||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
@ -838,6 +851,7 @@
|
|||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d",
|
||||
"version" : "2.7.3"
|
||||
"revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import Sparkle
|
||||
|
|
@ -98,8 +99,10 @@ class AppDelegate: NSObject,
|
|||
)
|
||||
|
||||
/// Manages updates
|
||||
let updaterController: SPUStandardUpdaterController
|
||||
let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
|
||||
let updateController = UpdateController()
|
||||
var updateViewModel: UpdateViewModel {
|
||||
updateController.viewModel
|
||||
}
|
||||
|
||||
/// The elapsed time since the process was started
|
||||
var timeSinceLaunch: TimeInterval {
|
||||
|
|
@ -118,7 +121,12 @@ class AppDelegate: NSObject,
|
|||
/// The custom app icon image that is currently in use.
|
||||
@Published private(set) var appIcon: NSImage? = nil {
|
||||
didSet {
|
||||
#if DEBUG
|
||||
// if no custom icon specified, we use blueprint to distinguish from release app
|
||||
NSApplication.shared.applicationIconImage = appIcon ?? NSImage(named: "BlueprintImage")
|
||||
#else
|
||||
NSApplication.shared.applicationIconImage = appIcon
|
||||
#endif
|
||||
let appPath = Bundle.main.bundlePath
|
||||
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
|
||||
NSWorkspace.shared.noteFileSystemChanged(appPath)
|
||||
|
|
@ -126,15 +134,6 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
override init() {
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
// Important: we must not start the updater here because we need to read our configuration
|
||||
// first to determine whether we're automatically checking, downloading, etc. The updater
|
||||
// is started later in applicationDidFinishLaunching
|
||||
startingUpdater: false,
|
||||
updaterDelegate: updaterDelegate,
|
||||
userDriverDelegate: nil
|
||||
)
|
||||
|
||||
super.init()
|
||||
|
||||
ghostty.delegate = self
|
||||
|
|
@ -179,7 +178,7 @@ class AppDelegate: NSObject,
|
|||
ghosttyConfigDidChange(config: ghostty.config)
|
||||
|
||||
// Start our update checker.
|
||||
updaterController.startUpdater()
|
||||
updateController.startUpdater()
|
||||
|
||||
// Register our service provider. This must happen after everything is initialized.
|
||||
NSApp.servicesProvider = ServiceProvider()
|
||||
|
|
@ -323,6 +322,12 @@ class AppDelegate: NSObject,
|
|||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
let windows = NSApplication.shared.windows
|
||||
if (windows.isEmpty) { return .terminateNow }
|
||||
|
||||
// If we've already accepted to install an update, then we don't need to
|
||||
// confirm quit. The user is already expecting the update to happen.
|
||||
if updateController.isInstalling {
|
||||
return .terminateNow
|
||||
}
|
||||
|
||||
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
|
||||
// quite work with SwiftUI because windows are retained on close. So instead we check
|
||||
|
|
@ -471,7 +476,12 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
switch ghostty.config.macosDockDropBehavior {
|
||||
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
|
||||
case .new_tab:
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: TerminalController.preferredParent?.window,
|
||||
withBaseConfig: config
|
||||
)
|
||||
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||
}
|
||||
|
||||
|
|
@ -806,12 +816,12 @@ class AppDelegate: NSObject,
|
|||
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
|
||||
// user-based defaults.
|
||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
||||
updaterController.updater.automaticallyChecksForUpdates = false
|
||||
updaterController.updater.automaticallyDownloadsUpdates = false
|
||||
updateController.updater.automaticallyChecksForUpdates = false
|
||||
updateController.updater.automaticallyDownloadsUpdates = false
|
||||
} else if let autoUpdate = config.autoUpdate {
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
updateController.updater.automaticallyChecksForUpdates =
|
||||
autoUpdate == .check || autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
updateController.updater.automaticallyDownloadsUpdates =
|
||||
autoUpdate == .download
|
||||
}
|
||||
|
||||
|
|
@ -1004,7 +1014,8 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
@IBAction func checkForUpdates(_ sender: Any?) {
|
||||
updaterController.checkForUpdates(sender)
|
||||
updateController.checkForUpdates()
|
||||
//UpdateSimulator.happyPath.simulate(with: updateViewModel)
|
||||
}
|
||||
|
||||
@IBAction func newWindow(_ sender: Any?) {
|
||||
|
|
@ -1012,7 +1023,10 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
@IBAction func newTab(_ sender: Any?) {
|
||||
_ = TerminalController.newTab(ghostty)
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: TerminalController.preferredParent?.window
|
||||
)
|
||||
}
|
||||
|
||||
@IBAction func closeAllWindows(_ sender: Any?) {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent {
|
|||
)
|
||||
var command: CommandEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import AppKit
|
||||
import AppIntents
|
||||
import GhosttyKit
|
||||
|
||||
struct FocusTerminalIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Focus Terminal"
|
||||
static var description = IntentDescription("Move focus to an existing terminal.")
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to focus.",
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surfaceView = terminal.surfaceView else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
|
||||
return .result()
|
||||
}
|
||||
|
||||
controller.focusSurface(surfaceView)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,10 @@ struct GetTerminalDetailsIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Get \(\.$detail) from \(\.$terminal)")
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent {
|
|||
)
|
||||
var action: String
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
|
|
|
|||
|
|
@ -43,11 +43,15 @@ struct NewTerminalIntent: AppIntent {
|
|||
)
|
||||
var parent: TerminalEntity?
|
||||
|
||||
// Performing in the background can avoid opening multiple windows at the same time
|
||||
// using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .foreground(.immediate)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
|
||||
static var openAppWhenRun = true
|
||||
static var openAppWhenRun = false
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<TerminalEntity?> {
|
||||
|
|
@ -96,6 +100,11 @@ struct NewTerminalIntent: AppIntent {
|
|||
parent = nil
|
||||
}
|
||||
|
||||
defer {
|
||||
if !NSApp.isActive {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
switch location {
|
||||
case .window:
|
||||
let newController = TerminalController.newWindow(
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent {
|
|||
static var title: LocalizedStringResource = "Open the Quick Terminal"
|
||||
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,28 @@ struct CommandOption: Identifiable, Hashable {
|
|||
let title: String
|
||||
let description: String?
|
||||
let symbols: [String]?
|
||||
let leadingIcon: String?
|
||||
let badge: String?
|
||||
let emphasis: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
description: String? = nil,
|
||||
symbols: [String]? = nil,
|
||||
leadingIcon: String? = nil,
|
||||
badge: String? = nil,
|
||||
emphasis: Bool = false,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.symbols = symbols
|
||||
self.leadingIcon = leadingIcon
|
||||
self.badge = badge
|
||||
self.emphasis = emphasis
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func == (lhs: CommandOption, rhs: CommandOption) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
|
|
@ -198,7 +219,7 @@ fileprivate struct CommandTable: View {
|
|||
} else {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
|
||||
CommandRow(
|
||||
option: option,
|
||||
|
|
@ -240,15 +261,36 @@ fileprivate struct CommandRow: View {
|
|||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
HStack(spacing: 8) {
|
||||
if let icon = option.leadingIcon {
|
||||
Image(systemName: icon)
|
||||
.foregroundStyle(option.emphasis ? Color.accentColor : .secondary)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
}
|
||||
|
||||
Text(option.title)
|
||||
.fontWeight(option.emphasis ? .medium : .regular)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let badge = option.badge, !badge.isEmpty {
|
||||
Text(badge)
|
||||
.font(.caption2.weight(.medium))
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
Capsule().fill(Color.accentColor.opacity(0.15))
|
||||
)
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
|
||||
if let symbols = option.symbols {
|
||||
ShortcutSymbolsView(symbols: symbols)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
isSelected
|
||||
? Color.accentColor.opacity(0.2)
|
||||
|
|
@ -256,6 +298,10 @@ fileprivate struct CommandRow: View {
|
|||
? Color.secondary.opacity(0.2)
|
||||
: Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5)
|
||||
)
|
||||
.cornerRadius(5)
|
||||
}
|
||||
.help(option.description ?? "")
|
||||
|
|
|
|||
|
|
@ -11,15 +11,54 @@ struct TerminalCommandPaletteView: View {
|
|||
|
||||
/// The configuration so we can lookup keyboard shortcuts.
|
||||
@ObservedObject var ghosttyConfig: Ghostty.Config
|
||||
|
||||
/// The update view model for showing update commands.
|
||||
var updateViewModel: UpdateViewModel?
|
||||
|
||||
/// The callback when an action is submitted.
|
||||
var onAction: ((String) -> Void)
|
||||
|
||||
// The commands available to the command palette.
|
||||
private var commandOptions: [CommandOption] {
|
||||
guard let surface = surfaceView.surfaceModel else { return [] }
|
||||
var options: [CommandOption] = []
|
||||
|
||||
// Add update command if an update is installable. This must always be the first so
|
||||
// it is at the top.
|
||||
if let updateViewModel, updateViewModel.state.isInstallable {
|
||||
// We override the update available one only because we want to properly
|
||||
// convey it'll go all the way through.
|
||||
let title: String
|
||||
if case .updateAvailable = updateViewModel.state {
|
||||
title = "Update Ghostty and Restart"
|
||||
} else {
|
||||
title = updateViewModel.text
|
||||
}
|
||||
|
||||
options.append(CommandOption(
|
||||
title: title,
|
||||
description: updateViewModel.description,
|
||||
leadingIcon: updateViewModel.iconName ?? "shippingbox.fill",
|
||||
badge: updateViewModel.badge,
|
||||
emphasis: true
|
||||
) {
|
||||
(NSApp.delegate as? AppDelegate)?.updateController.installUpdate()
|
||||
})
|
||||
}
|
||||
|
||||
// Add cancel/skip update command if the update is installable
|
||||
if let updateViewModel, updateViewModel.state.isInstallable {
|
||||
options.append(CommandOption(
|
||||
title: "Cancel or Skip Update",
|
||||
description: "Dismiss the current update process"
|
||||
) {
|
||||
updateViewModel.state.cancel()
|
||||
})
|
||||
}
|
||||
|
||||
// Add terminal commands
|
||||
guard let surface = surfaceView.surfaceModel else { return options }
|
||||
do {
|
||||
return try surface.commands().map { c in
|
||||
let terminalCommands = try surface.commands().map { c in
|
||||
return CommandOption(
|
||||
title: c.title,
|
||||
description: c.description,
|
||||
|
|
@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View {
|
|||
onAction(c.action)
|
||||
}
|
||||
}
|
||||
options.append(contentsOf: terminalCommands)
|
||||
} catch {
|
||||
return []
|
||||
return options
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// The active space when the quick terminal was last shown.
|
||||
private var previousActiveSpace: CGSSpace? = nil
|
||||
|
||||
/// The saved state when the quick terminal's surface tree becomes empty.
|
||||
///
|
||||
/// This preserves the user's window size and position when all terminal surfaces
|
||||
/// are closed (e.g., via the `exit` command). When a new surface is created,
|
||||
/// the window will be restored to this frame, preventing SwiftUI from resetting
|
||||
/// the window to its default minimum size.
|
||||
private var lastClosedFrames: NSMapTable<NSScreen, LastClosedState>
|
||||
/// Cache for per-screen window state.
|
||||
private let screenStateCache = QuickTerminalScreenStateCache()
|
||||
|
||||
/// Non-nil if we have hidden dock state.
|
||||
private var hiddenDock: HiddenDock? = nil
|
||||
|
|
@ -37,7 +32,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
|
||||
/// Tracks if we're currently handling a manual resize to prevent recursion
|
||||
private var isHandlingResize: Bool = false
|
||||
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
|
|
@ -45,10 +40,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
) {
|
||||
self.position = position
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
// This is a weak to strong mapping, so that our keys being NSScreens
|
||||
// can remove themselves when they disappear.
|
||||
self.lastClosedFrames = .weakToStrongObjects()
|
||||
|
||||
// Important detail here: we initialize with an empty surface tree so
|
||||
// that we don't start a terminal process. This gets started when the
|
||||
|
|
@ -247,6 +238,22 @@ class QuickTerminalController: BaseTerminalController {
|
|||
|
||||
// MARK: Base Controller Overrides
|
||||
|
||||
override func focusSurface(_ view: Ghostty.SurfaceView) {
|
||||
if visible {
|
||||
// If we're visible, we just focus the surface as normal.
|
||||
super.focusSurface(view)
|
||||
return
|
||||
}
|
||||
// Check if target surface belongs to this quick terminal
|
||||
guard surfaceTree.contains(view) else { return }
|
||||
// Set the target surface as focused
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: view)
|
||||
}
|
||||
// Animation completion handler will handle window/app activation
|
||||
animateIn()
|
||||
}
|
||||
|
||||
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||
super.surfaceTreeDidChange(from: from, to: to)
|
||||
|
||||
|
|
@ -363,17 +370,15 @@ class QuickTerminalController: BaseTerminalController {
|
|||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Grab our last closed frame to use, and clear our state since we're animating in.
|
||||
// We only use the last closed frame if we're opening on the same screen.
|
||||
let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame
|
||||
lastClosedFrames.removeObject(forKey: screen)
|
||||
// Grab our last closed frame to use from the cache.
|
||||
let closedFrame = screenStateCache.frame(for: screen)
|
||||
|
||||
// Move our window off screen to the initial animation position.
|
||||
position.setInitial(
|
||||
in: window,
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
closedFrame: closedFrame)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
|
|
@ -408,7 +413,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
in: window.animator(),
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
closedFrame: closedFrame)
|
||||
}, completionHandler: {
|
||||
// There is a very minor delay here so waiting at least an event loop tick
|
||||
// keeps us safe from the view not being on the window.
|
||||
|
|
@ -497,7 +502,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
// would reset the window to its minimum content size.
|
||||
if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen {
|
||||
lastClosedFrames.setObject(.init(frame: window.frame), forKey: screen)
|
||||
screenStateCache.save(frame: window.frame, for: screen)
|
||||
}
|
||||
|
||||
// If we hid the dock then we unhide it.
|
||||
|
|
@ -582,7 +587,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
|
||||
// MARK: First Responder
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
|
|
@ -720,14 +724,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
hidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private class LastClosedState {
|
||||
let frame: NSRect
|
||||
|
||||
init(frame: NSRect) {
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
/// Manages cached window state per screen for the quick terminal.
|
||||
///
|
||||
/// This cache tracks the last closed window frame for each screen, allowing the quick terminal
|
||||
/// to restore to its previous size and position when reopened. It uses stable display UUIDs
|
||||
/// to survive NSScreen garbage collection and automatically prunes stale entries.
|
||||
class QuickTerminalScreenStateCache {
|
||||
/// The maximum number of saved screen states we retain. This is to avoid some kind of
|
||||
/// pathological memory growth in case we get our screen state serializing wrong. I don't
|
||||
/// know anyone with more than 10 screens, so let's just arbitrarily go with that.
|
||||
private static let maxSavedScreens = 10
|
||||
|
||||
/// Time-to-live for screen entries that are no longer present (14 days).
|
||||
private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60
|
||||
|
||||
/// Keyed by display UUID to survive NSScreen garbage collection.
|
||||
private var stateByDisplay: [UUID: DisplayEntry] = [:]
|
||||
|
||||
init() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(onScreensChanged(_:)),
|
||||
name: NSApplication.didChangeScreenParametersNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
/// Save the window frame for a screen.
|
||||
func save(frame: NSRect, for screen: NSScreen) {
|
||||
guard let key = screen.displayUUID else { return }
|
||||
let entry = DisplayEntry(
|
||||
frame: frame,
|
||||
screenSize: screen.frame.size,
|
||||
scale: screen.backingScaleFactor,
|
||||
lastSeen: Date()
|
||||
)
|
||||
stateByDisplay[key] = entry
|
||||
pruneCapacity()
|
||||
}
|
||||
|
||||
/// Retrieve the last closed frame for a screen, if valid.
|
||||
func frame(for screen: NSScreen) -> NSRect? {
|
||||
guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil }
|
||||
|
||||
// Drop on dimension/scale change that makes the entry invalid
|
||||
if !entry.isValid(for: screen) {
|
||||
stateByDisplay.removeValue(forKey: key)
|
||||
return nil
|
||||
}
|
||||
|
||||
entry.lastSeen = Date()
|
||||
stateByDisplay[key] = entry
|
||||
return entry.frame
|
||||
}
|
||||
|
||||
@objc private func onScreensChanged(_ note: Notification) {
|
||||
let screens = NSScreen.screens
|
||||
let now = Date()
|
||||
let currentIDs = Set(screens.compactMap { $0.displayUUID })
|
||||
|
||||
for screen in screens {
|
||||
guard let key = screen.displayUUID else { continue }
|
||||
if var entry = stateByDisplay[key] {
|
||||
// Drop on dimension/scale change that makes the entry invalid
|
||||
if !entry.isValid(for: screen) {
|
||||
stateByDisplay.removeValue(forKey: key)
|
||||
} else {
|
||||
// Update the screen size if it grew (keep entry valid for larger screens)
|
||||
entry.screenSize = screen.frame.size
|
||||
entry.lastSeen = now
|
||||
stateByDisplay[key] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TTL prune for non-present screens
|
||||
stateByDisplay = stateByDisplay.filter { key, entry in
|
||||
currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL
|
||||
}
|
||||
|
||||
pruneCapacity()
|
||||
}
|
||||
|
||||
private func pruneCapacity() {
|
||||
guard stateByDisplay.count > Self.maxSavedScreens else { return }
|
||||
let toRemove = stateByDisplay
|
||||
.sorted { $0.value.lastSeen < $1.value.lastSeen }
|
||||
.prefix(stateByDisplay.count - Self.maxSavedScreens)
|
||||
for (key, _) in toRemove {
|
||||
stateByDisplay.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DisplayEntry {
|
||||
var frame: NSRect
|
||||
var screenSize: CGSize
|
||||
var scale: CGFloat
|
||||
var lastSeen: Date
|
||||
|
||||
/// Returns true if this entry is still valid for the given screen.
|
||||
/// Valid if the scale matches and the cached size is not larger than the current screen size.
|
||||
/// This allows entries to persist when screens grow, but invalidates them when screens shrink.
|
||||
func isValid(for screen: NSScreen) -> Bool {
|
||||
guard scale == screen.backingScaleFactor else { return false }
|
||||
return screenSize.width <= screen.frame.size.width && screenSize.height <= screen.frame.size.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ struct SettingsView: View {
|
|||
VStack(alignment: .leading) {
|
||||
Text("Coming Soon. 🚧").font(.title)
|
||||
Text("You can't configure settings in the GUI yet. To modify settings, " +
|
||||
"edit the file at $HOME/.config/ghostty/config and restart Ghostty.")
|
||||
"edit the file at $HOME/.config/ghostty/config.ghostty and restart Ghostty.")
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct TerminalSplitTreeView: View {
|
|||
onResize: onResize)
|
||||
// This is necessary because we can't rely on SwiftUI's implicit
|
||||
// structural identity to detect changes to this view. Due to
|
||||
// the tree structure of splits it could result in bad beaviors.
|
||||
// the tree structure of splits it could result in bad behaviors.
|
||||
// See: https://github.com/ghostty-org/ghostty/issues/7546
|
||||
.id(node.structuralIdentity)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
/// This can be set to show/hide the command palette.
|
||||
@Published var commandPaletteIsShowing: Bool = false
|
||||
|
||||
/// Set if the terminal view should show the update overlay.
|
||||
@Published var updateOverlayIsVisible: Bool = false
|
||||
|
||||
/// Whether the terminal surface should focus when the mouse is over it.
|
||||
var focusFollowsMouse: Bool {
|
||||
|
|
@ -233,6 +236,21 @@ class BaseTerminalController: NSWindowController,
|
|||
return newView
|
||||
}
|
||||
|
||||
/// Move focus to a surface view.
|
||||
func focusSurface(_ view: Ghostty.SurfaceView) {
|
||||
// Check if target surface is in our tree
|
||||
guard surfaceTree.contains(view) else { return }
|
||||
|
||||
// Move focus to the target surface and activate the window/app
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: view)
|
||||
view.window?.makeKeyAndOrderFront(nil)
|
||||
if !NSApp.isActive {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called when the surfaceTree variable changed.
|
||||
///
|
||||
/// Subclasses should call super first.
|
||||
|
|
@ -551,23 +569,12 @@ class BaseTerminalController: NSWindowController,
|
|||
// Get the direction from the notification
|
||||
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
|
||||
guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return }
|
||||
|
||||
// Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection
|
||||
let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection
|
||||
switch direction {
|
||||
case .previous: focusDirection = .previous
|
||||
case .next: focusDirection = .next
|
||||
case .up: focusDirection = .spatial(.up)
|
||||
case .down: focusDirection = .spatial(.down)
|
||||
case .left: focusDirection = .spatial(.left)
|
||||
case .right: focusDirection = .spatial(.right)
|
||||
}
|
||||
|
||||
// Find the node for the target surface
|
||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
||||
|
||||
// Find the next surface to focus
|
||||
guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else {
|
||||
guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -728,6 +735,10 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
func cellSizeDidChange(to: NSSize) {
|
||||
guard derivedConfig.windowStepResize else { return }
|
||||
// Stage manager can sometimes present windows in such a way that the
|
||||
// cell size is temporarily zero due to the window being tiny. We can't
|
||||
// set content resize increments to this value, so avoid an assertion failure.
|
||||
guard to.width > 0 && to.height > 0 else { return }
|
||||
self.window?.contentResizeIncrements = to
|
||||
}
|
||||
|
||||
|
|
@ -799,7 +810,18 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
}
|
||||
|
||||
func fullscreenDidChange() {}
|
||||
func fullscreenDidChange() {
|
||||
guard let fullscreenStyle else { return }
|
||||
|
||||
// When we enter fullscreen, we want to show the update overlay so that it
|
||||
// is easily visible. For native fullscreen this is visible by showing the
|
||||
// menubar but we don't want to rely on that.
|
||||
if fullscreenStyle.isFullscreen {
|
||||
updateOverlayIsVisible = true
|
||||
} else {
|
||||
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Clipboard Confirmation
|
||||
|
||||
|
|
@ -881,6 +903,28 @@ class BaseTerminalController: NSWindowController,
|
|||
fullscreenStyle = NativeFullscreen(window)
|
||||
fullscreenStyle?.delegate = self
|
||||
}
|
||||
|
||||
// Set our update overlay state
|
||||
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
|
||||
}
|
||||
|
||||
func defaultUpdateOverlayVisibility() -> Bool {
|
||||
guard let window else { return true }
|
||||
|
||||
// No titlebar we always show the update overlay because it can't support
|
||||
// updates in the titlebar
|
||||
guard window.styleMask.contains(.titled) else {
|
||||
return true
|
||||
}
|
||||
|
||||
// If it's a non terminal window we can't trust it has an update accessory,
|
||||
// so we always want to show the overlay.
|
||||
guard let window = window as? TerminalWindow else {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show the overlay if the window isn't.
|
||||
return !window.supportsUpdateAccessory
|
||||
}
|
||||
|
||||
// MARK: NSWindowDelegate
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
case "hidden": "TerminalHiddenTitlebar"
|
||||
case "transparent": "TerminalTransparentTitlebar"
|
||||
case "tabs":
|
||||
#if compiler(>=6.2)
|
||||
if #available(macOS 26.0, *) {
|
||||
"TerminalTabsTitlebarTahoe"
|
||||
} else {
|
||||
"TerminalTabsTitlebarVentura"
|
||||
}
|
||||
#else
|
||||
"TerminalTabsTitlebarVentura"
|
||||
#endif
|
||||
default: defaultValue
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject {
|
|||
|
||||
/// The command palette state.
|
||||
var commandPaletteIsShowing: Bool { get set }
|
||||
|
||||
/// The update overlay should be visible.
|
||||
var updateOverlayIsVisible: Bool { get }
|
||||
}
|
||||
|
||||
/// The main terminal view. This terminal view supports splits.
|
||||
|
|
@ -105,10 +108,33 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||
TerminalCommandPaletteView(
|
||||
surfaceView: surfaceView,
|
||||
isPresented: $viewModel.commandPaletteIsShowing,
|
||||
ghosttyConfig: ghostty.config) { action in
|
||||
ghosttyConfig: ghostty.config,
|
||||
updateViewModel: (NSApp.delegate as? AppDelegate)?.updateViewModel) { action in
|
||||
self.delegate?.performAction(action, on: surfaceView)
|
||||
}
|
||||
}
|
||||
|
||||
// Show update information above all else.
|
||||
if viewModel.updateOverlayIsVisible {
|
||||
UpdateOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct UpdateOverlay: View {
|
||||
var body: some View {
|
||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
UpdatePill(model: appDelegate.updateViewModel)
|
||||
.padding(.bottom, 9)
|
||||
.padding(.trailing, 9)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import AppKit
|
||||
|
||||
class HiddenTitlebarTerminalWindow: TerminalWindow {
|
||||
// No titlebar, we don't support accessories.
|
||||
override var supportsUpdateAccessory: Bool { false }
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import GhosttyKit
|
|||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||
/// style and configuration of the window based on the app configuration.
|
||||
class TerminalWindow: NSWindow {
|
||||
/// Posted when a terminal window awakes from nib.
|
||||
static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake")
|
||||
|
||||
/// Posted when a terminal window will close
|
||||
static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose")
|
||||
|
||||
/// This is the key in UserDefaults to use for the default `level` value. This is
|
||||
/// used by the manual float on top menu item feature.
|
||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||
|
|
@ -14,15 +20,25 @@ class TerminalWindow: NSWindow {
|
|||
|
||||
/// Reset split zoom button in titlebar
|
||||
private let resetZoomAccessory = NSTitlebarAccessoryViewController()
|
||||
|
||||
/// Update notification UI in titlebar
|
||||
private let updateAccessory = NSTitlebarAccessoryViewController()
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private(set) var derivedConfig: DerivedConfig = .init()
|
||||
|
||||
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||
/// window should determine how to show update notifications.
|
||||
var supportsUpdateAccessory: Bool {
|
||||
// Native window supports it.
|
||||
true
|
||||
}
|
||||
|
||||
/// Gets the terminal controller from the window controller.
|
||||
var terminalController: TerminalController? {
|
||||
windowController as? TerminalController
|
||||
}
|
||||
|
||||
|
||||
// MARK: NSWindow Overrides
|
||||
|
||||
override var toolbar: NSToolbar? {
|
||||
|
|
@ -35,6 +51,9 @@ class TerminalWindow: NSWindow {
|
|||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
// Notify that this terminal window has loaded
|
||||
NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
|
||||
|
||||
// This is required so that window restoration properly creates our tabs
|
||||
// again. I'm not sure why this is required. If you don't do this, then
|
||||
// tabs restore as separate windows.
|
||||
|
|
@ -85,6 +104,17 @@ class TerminalWindow: NSWindow {
|
|||
}))
|
||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
// Create update notification accessory
|
||||
if supportsUpdateAccessory {
|
||||
updateAccessory.layoutAttribute = .right
|
||||
updateAccessory.view = NonDraggableHostingView(rootView: UpdateAccessoryView(
|
||||
viewModel: viewModel,
|
||||
model: appDelegate.updateViewModel
|
||||
))
|
||||
addTitlebarAccessoryViewController(updateAccessory)
|
||||
updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||
|
|
@ -103,6 +133,11 @@ class TerminalWindow: NSWindow {
|
|||
// still become key/main and receive events.
|
||||
override var canBecomeKey: Bool { return true }
|
||||
override var canBecomeMain: Bool { return true }
|
||||
|
||||
override func close() {
|
||||
NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
|
||||
super.close()
|
||||
}
|
||||
|
||||
override func becomeKey() {
|
||||
super.becomeKey()
|
||||
|
|
@ -124,6 +159,12 @@ class TerminalWindow: NSWindow {
|
|||
} else {
|
||||
tabBarDidDisappear()
|
||||
}
|
||||
viewModel.isMainWindow = true
|
||||
}
|
||||
|
||||
override func resignMain() {
|
||||
super.resignMain()
|
||||
viewModel.isMainWindow = false
|
||||
}
|
||||
|
||||
override func mergeAllWindows(_ sender: Any?) {
|
||||
|
|
@ -164,9 +205,16 @@ class TerminalWindow: NSWindow {
|
|||
|
||||
/// Returns true if there is a tab bar visible on this window.
|
||||
var hasTabBar: Bool {
|
||||
// TODO: use titlebarView to find it instead
|
||||
contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil
|
||||
}
|
||||
|
||||
var hasMoreThanOneTabs: Bool {
|
||||
/// accessing ``tabGroup?.windows`` here
|
||||
/// will cause other edge cases, be careful
|
||||
(tabbedWindows?.count ?? 0) > 1
|
||||
}
|
||||
|
||||
func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool {
|
||||
if childViewController.identifier == nil {
|
||||
// The good case
|
||||
|
|
@ -198,6 +246,9 @@ class TerminalWindow: NSWindow {
|
|||
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
||||
removeTitlebarAccessoryViewController(at: idx)
|
||||
}
|
||||
|
||||
// We don't need to do this with the update accessory. I don't know why but
|
||||
// everything works fine.
|
||||
}
|
||||
|
||||
private func tabBarDidDisappear() {
|
||||
|
|
@ -260,7 +311,7 @@ class TerminalWindow: NSWindow {
|
|||
button.isBordered = false
|
||||
button.allowsExpansionToolTips = true
|
||||
button.toolTip = "Reset Zoom"
|
||||
button.contentTintColor = .controlAccentColor
|
||||
button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor
|
||||
button.state = .on
|
||||
button.image = NSImage(named:"ResetZoom")
|
||||
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
|
||||
|
|
@ -277,6 +328,12 @@ class TerminalWindow: NSWindow {
|
|||
// Whenever we change the window title we must also update our
|
||||
// tab title if we're using custom fonts.
|
||||
tab.attributedTitle = attributedTitle
|
||||
/// We also needs to update this here, just in case
|
||||
/// the value is not what we want
|
||||
///
|
||||
/// Check ``titlebarFont`` down below
|
||||
/// to see why we need to check `hasMoreThanOneTabs` here
|
||||
titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,6 +343,12 @@ class TerminalWindow: NSWindow {
|
|||
let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize)
|
||||
|
||||
titlebarTextField?.font = font
|
||||
/// We check `hasMoreThanOneTabs` here because the system
|
||||
/// may copy this setting to the tab’s text field at some point(e.g. entering/exiting fullscreen),
|
||||
/// which can cause the title to be vertically misaligned (shifted downward).
|
||||
///
|
||||
/// This behaviour is the opposite of what happens in the title bar’s text field, which is quite odd...
|
||||
titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs
|
||||
tab.attributedTitle = attributedTitle
|
||||
}
|
||||
}
|
||||
|
|
@ -436,7 +499,7 @@ class TerminalWindow: NSWindow {
|
|||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
// MARK: Config
|
||||
|
||||
struct DerivedConfig {
|
||||
|
|
@ -467,28 +530,28 @@ extension TerminalWindow {
|
|||
class ViewModel: ObservableObject {
|
||||
@Published var isSurfaceZoomed: Bool = false
|
||||
@Published var hasToolbar: Bool = false
|
||||
@Published var isMainWindow: Bool = true
|
||||
|
||||
/// Calculates the top padding based on toolbar visibility and macOS version
|
||||
fileprivate var accessoryTopPadding: CGFloat {
|
||||
if #available(macOS 26.0, *) {
|
||||
return hasToolbar ? 10 : 5
|
||||
} else {
|
||||
return hasToolbar ? 9 : 4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResetZoomAccessoryView: View {
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
let action: () -> Void
|
||||
|
||||
// The padding from the top that the view appears. This was all just manually
|
||||
// measured based on the OS.
|
||||
var topPadding: CGFloat {
|
||||
if #available(macOS 26.0, *) {
|
||||
return viewModel.hasToolbar ? 10 : 5
|
||||
} else {
|
||||
return viewModel.hasToolbar ? 9 : 4
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if viewModel.isSurfaceZoomed {
|
||||
VStack {
|
||||
Button(action: action) {
|
||||
Image("ResetZoom")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Reset Split Zoom")
|
||||
|
|
@ -497,10 +560,24 @@ extension TerminalWindow {
|
|||
}
|
||||
// With a toolbar, the window title is taller, so we need more padding
|
||||
// to properly align.
|
||||
.padding(.top, topPadding)
|
||||
.padding(.top, viewModel.accessoryTopPadding)
|
||||
// We always need space at the end of the titlebar
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pill-shaped button that displays update status and provides access to update actions.
|
||||
struct UpdateAccessoryView: View {
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
@ObservedObject var model: UpdateViewModel
|
||||
|
||||
var body: some View {
|
||||
// We use the same top/trailing padding so that it hugs the same.
|
||||
UpdatePill(model: model)
|
||||
.padding(.top, viewModel.accessoryTopPadding)
|
||||
.padding(.trailing, viewModel.accessoryTopPadding)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ import SwiftUI
|
|||
class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate {
|
||||
/// The view model for SwiftUI views
|
||||
private var viewModel = ViewModel()
|
||||
|
||||
/// Titlebar tabs can't support the update accessory because of the way we layout
|
||||
/// the native tabs back into the menu bar.
|
||||
override var supportsUpdateAccessory: Bool { false }
|
||||
|
||||
deinit {
|
||||
tabBarObserver = nil
|
||||
|
|
@ -15,9 +19,21 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
|
||||
// MARK: NSWindow
|
||||
|
||||
override var titlebarFont: NSFont? {
|
||||
didSet {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.viewModel.titleFont = self.titlebarFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var title: String {
|
||||
didSet {
|
||||
viewModel.title = title
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.viewModel.title = self.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,17 +58,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
// Check if we have a tab bar and set it up if we have to. See the comment
|
||||
// on this function to learn why we need to check this here.
|
||||
setupTabBar()
|
||||
|
||||
viewModel.isMainWindow = true
|
||||
}
|
||||
|
||||
override func resignMain() {
|
||||
super.resignMain()
|
||||
|
||||
viewModel.isMainWindow = false
|
||||
}
|
||||
// This is called by macOS for native tabbing in order to add the tab bar. We hook into
|
||||
// this, detect the tab bar being added, and override its behavior.
|
||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||
// If this is the tab bar then we need to set it up for the titlebar
|
||||
guard isTabBar(childViewController) else {
|
||||
// After dragging a tab into a new window, `hasTabBar` needs to be
|
||||
// updated to properly review window title
|
||||
viewModel.hasTabBar = false
|
||||
|
||||
super.addTitlebarAccessoryViewController(childViewController)
|
||||
return
|
||||
}
|
||||
|
||||
// When an existing tab is being dragged in to another tab group,
|
||||
// system will also try to add tab bar to this window, so we want to reset observer,
|
||||
// to put tab bar where we want again
|
||||
tabBarObserver = nil
|
||||
|
||||
// Some setup needs to happen BEFORE it is added, such as layout. If
|
||||
// we don't do this before the call below, we'll trigger an AppKit
|
||||
// assertion.
|
||||
|
|
@ -112,18 +144,33 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
guard tabBarObserver == nil else { return }
|
||||
|
||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
||||
guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return }
|
||||
//
|
||||
// In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`.
|
||||
// In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes;
|
||||
// in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`.
|
||||
// ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205
|
||||
guard let themeFrameView = contentView?.rootView else { return }
|
||||
let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) {
|
||||
themeFrameView.value(forKey: "titlebarView") as? NSView
|
||||
} else {
|
||||
NSView?.none
|
||||
}
|
||||
guard let tabBar = titlebarView?.firstDescendant(withClassName: "NSTabBar") else { return }
|
||||
|
||||
// View model updates must happen on their own ticks.
|
||||
DispatchQueue.main.async {
|
||||
self.viewModel.hasTabBar = true
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.viewModel.hasTabBar = true
|
||||
}
|
||||
|
||||
// Find our clip view
|
||||
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
|
||||
guard let accessoryView = clipView.subviews[safe: 0] else { return }
|
||||
guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return }
|
||||
guard let titlebarView else { return }
|
||||
guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return }
|
||||
|
||||
// Make sure tabBar's height won't be stretched
|
||||
guard let newTabButton = titlebarView.firstDescendant(withClassName: "NSTabBarNewTabButton") else { return }
|
||||
tabBar.frame.size.height = newTabButton.frame.width
|
||||
|
||||
// The container is the view that we'll constrain our tab bar within.
|
||||
let container = toolbarView
|
||||
|
|
@ -205,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
case .title:
|
||||
let item = NSToolbarItem(itemIdentifier: .title)
|
||||
item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel))
|
||||
// Fix: https://github.com/ghostty-org/ghostty/discussions/9027
|
||||
item.view?.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
item.visibilityPriority = .user
|
||||
item.isEnabled = true
|
||||
|
||||
|
|
@ -221,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
|||
// MARK: SwiftUI
|
||||
|
||||
class ViewModel: ObservableObject {
|
||||
@Published var titleFont: NSFont?
|
||||
@Published var title: String = "👻 Ghostty"
|
||||
@Published var hasTabBar: Bool = false
|
||||
@Published var isMainWindow: Bool = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -245,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow {
|
|||
|
||||
var body: some View {
|
||||
if !viewModel.hasTabBar {
|
||||
Text(title)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
titleText
|
||||
} else {
|
||||
// 1x1.gif strikes again! For real: if we render a zero-sized
|
||||
// view here then the toolbar just disappears our view. I don't
|
||||
// know.
|
||||
// know. This appears fixed in 26.1 Beta but keep it safe for 26.0.
|
||||
Color.clear.frame(width: 1, height: 1)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var titleText: some View {
|
||||
Text(title)
|
||||
.font(viewModel.titleFont.flatMap(Font.init(_:)))
|
||||
.foregroundStyle(viewModel.isMainWindow ? .primary : .secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .greatestFiniteMagnitude, alignment: .center)
|
||||
.opacity(viewModel.hasTabBar ? 0 : 1) // hide when in fullscreen mode, where title bar will appear in the leading area under window buttons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import Cocoa
|
|||
|
||||
/// Titlebar tabs for macOS 13 to 15.
|
||||
class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||
/// Titlebar tabs can't support the update accessory because of the way we layout
|
||||
/// the native tabs back into the menu bar.
|
||||
override var supportsUpdateAccessory: Bool { false }
|
||||
|
||||
/// This is used to determine if certain elements should be drawn light or dark and should
|
||||
/// be updated whenever the window background color or surrounding elements changes.
|
||||
fileprivate var isLightTheme: Bool = false
|
||||
|
|
@ -141,6 +145,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
|||
super.syncAppearance(surfaceConfig)
|
||||
|
||||
// Update our window light/darkness based on our updated background color
|
||||
let themeChanged = isLightTheme != OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
|
||||
// Update our titlebar color
|
||||
|
|
@ -150,7 +155,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
|||
titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity)
|
||||
}
|
||||
|
||||
if (isOpaque) {
|
||||
if (isOpaque || themeChanged) {
|
||||
// If there is transparency, calling this will make the titlebar opaque
|
||||
// so we only call this if we are opaque.
|
||||
updateTabBar()
|
||||
|
|
@ -183,41 +188,33 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
|||
// so we need to do it manually.
|
||||
private func updateNewTabButtonOpacity() {
|
||||
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
||||
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
||||
$0 as? NSImageView != nil
|
||||
}) as? NSImageView else { return }
|
||||
guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return }
|
||||
|
||||
newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5
|
||||
}
|
||||
|
||||
// Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,
|
||||
// just as it does in the stock tab bar.
|
||||
/// Update: This method only add a vibrant overlay now,
|
||||
/// since the image itself supports light/dark tint,
|
||||
/// and system could restore it any time,
|
||||
/// altering it will only cause maintenance burden for us.
|
||||
///
|
||||
/// And if we hide original image,
|
||||
/// ``updateNewTabButtonOpacity`` will not work
|
||||
///
|
||||
/// ~~Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,~~
|
||||
/// ~~just as it does in the stock tab bar.~~
|
||||
private func updateNewTabButtonImage() {
|
||||
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
||||
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
||||
$0 as? NSImageView != nil
|
||||
}) as? NSImageView else { return }
|
||||
guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return }
|
||||
guard let newTabButtonImage = newTabButtonImageView.image else { return }
|
||||
|
||||
let imageLayer = newTabButtonImageLayer ?? VibrantLayer(forAppearance: isLightTheme ? .light : .dark)!
|
||||
imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size)
|
||||
imageLayer.contentsGravity = .resizeAspect
|
||||
imageLayer.opacity = 0.5
|
||||
|
||||
if newTabButtonImageLayer == nil {
|
||||
let fillColor: NSColor = isLightTheme ? .black.withAlphaComponent(0.85) : .white.withAlphaComponent(0.85)
|
||||
let newImage = NSImage(size: newTabButtonImage.size, flipped: false) { rect in
|
||||
newTabButtonImage.draw(in: rect)
|
||||
fillColor.setFill()
|
||||
rect.fill(using: .sourceAtop)
|
||||
return true
|
||||
}
|
||||
let imageLayer = VibrantLayer(forAppearance: isLightTheme ? .light : .dark)!
|
||||
imageLayer.frame = NSRect(origin: NSPoint(x: newTabButton.bounds.midX - newTabButtonImage.size.width/2, y: newTabButton.bounds.midY - newTabButtonImage.size.height/2), size: newTabButtonImage.size)
|
||||
imageLayer.contentsGravity = .resizeAspect
|
||||
imageLayer.contents = newImage
|
||||
imageLayer.opacity = 0.5
|
||||
newTabButtonImageLayer = imageLayer
|
||||
|
||||
newTabButtonImageLayer = imageLayer
|
||||
}
|
||||
|
||||
newTabButtonImageView.isHidden = true
|
||||
newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer()
|
||||
newTabButton.layer?.addSublayer(newTabButtonImageLayer!)
|
||||
}
|
||||
|
|
@ -448,6 +445,13 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
|||
}
|
||||
|
||||
private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) {
|
||||
guard windowButtonsBackdrop?.superview != titlebarView else {
|
||||
/// replacing existing backdrop aggressively
|
||||
/// may cause incorrect hierarchy
|
||||
///
|
||||
/// because multiple windows are adding this around the 'same time'
|
||||
return
|
||||
}
|
||||
windowButtonsBackdrop?.removeFromSuperview()
|
||||
windowButtonsBackdrop = nil
|
||||
|
||||
|
|
@ -466,16 +470,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
|||
|
||||
private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) {
|
||||
// If we already made the view, just make sure it's unhidden and correctly placed as a subview.
|
||||
if let view = windowDragHandle {
|
||||
view.removeFromSuperview()
|
||||
view.isHidden = false
|
||||
titlebarView.superview?.addSubview(view)
|
||||
view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true
|
||||
view.rightAnchor.constraint(equalTo: toolbarView.rightAnchor).isActive = true
|
||||
view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true
|
||||
view.bottomAnchor.constraint(equalTo: toolbarView.topAnchor, constant: 12).isActive = true
|
||||
guard windowDragHandle?.superview != titlebarView.superview else {
|
||||
// similar to `addWindowButtonsBackdrop`
|
||||
return
|
||||
}
|
||||
windowDragHandle?.removeFromSuperview()
|
||||
|
||||
let view = WindowDragView()
|
||||
view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle")
|
||||
|
|
@ -536,7 +535,10 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
|||
// This must be weak because the window has this view. Otherwise
|
||||
// a retain cycle occurs.
|
||||
private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow?
|
||||
private let isLightTheme: Bool
|
||||
private var isLightTheme: Bool {
|
||||
// using up-to-date value from hosting window directly
|
||||
terminalWindow?.isLightTheme ?? false
|
||||
}
|
||||
private let overlayLayer = VibrantLayer()
|
||||
|
||||
var isHighlighted: Bool = true {
|
||||
|
|
@ -565,7 +567,6 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
|||
|
||||
init(window: TitlebarTabsVenturaTerminalWindow) {
|
||||
self.terminalWindow = window
|
||||
self.isLightTheme = window.isLightTheme
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import SwiftUI
|
||||
|
||||
/// A badge view that displays the current state of an update operation.
|
||||
///
|
||||
/// Shows different visual indicators based on the update state:
|
||||
/// - Progress ring for downloading/extracting with progress
|
||||
/// - Animated rotating icon for checking/installing
|
||||
/// - Static icon for other states
|
||||
struct UpdateBadge: View {
|
||||
/// The update view model that provides the current state and progress
|
||||
@ObservedObject var model: UpdateViewModel
|
||||
|
||||
/// Current rotation angle for animated icon states
|
||||
@State private var rotationAngle: Double = 0
|
||||
|
||||
var body: some View {
|
||||
badgeContent
|
||||
.accessibilityLabel(model.text)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var badgeContent: some View {
|
||||
switch model.state {
|
||||
case .downloading(let download):
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
let progress = min(1, max(0, Double(download.progress) / Double(expectedLength)))
|
||||
ProgressRingView(progress: progress)
|
||||
} else {
|
||||
Image(systemName: "arrow.down.circle")
|
||||
}
|
||||
|
||||
case .extracting(let extracting):
|
||||
ProgressRingView(progress: min(1, max(0, extracting.progress)))
|
||||
|
||||
case .checking:
|
||||
if let iconName = model.iconName {
|
||||
Image(systemName: iconName)
|
||||
.rotationEffect(.degrees(rotationAngle))
|
||||
.onAppear {
|
||||
withAnimation(.linear(duration: 2.5).repeatForever(autoreverses: false)) {
|
||||
rotationAngle = 360
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
rotationAngle = 0
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
default:
|
||||
if let iconName = model.iconName {
|
||||
Image(systemName: iconName)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A circular progress indicator with a stroke-based ring design.
|
||||
///
|
||||
/// Displays a partially filled circle that represents progress from 0.0 to 1.0.
|
||||
fileprivate struct ProgressRingView: View {
|
||||
/// The current progress value, ranging from 0.0 (empty) to 1.0 (complete)
|
||||
let progress: Double
|
||||
|
||||
/// The width of the progress ring stroke
|
||||
let lineWidth: CGFloat = 2
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.primary.opacity(0.2), lineWidth: lineWidth)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: progress)
|
||||
.stroke(Color.primary, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import Sparkle
|
||||
import Cocoa
|
||||
import Combine
|
||||
|
||||
/// Standard controller for managing Sparkle updates in Ghostty.
|
||||
///
|
||||
/// This controller wraps SPUStandardUpdaterController to provide a simpler interface
|
||||
/// for managing updates with Ghostty's custom driver and delegate. It handles
|
||||
/// initialization, starting the updater, and provides the check for updates action.
|
||||
class UpdateController {
|
||||
private(set) var updater: SPUUpdater
|
||||
private let userDriver: UpdateDriver
|
||||
private let updaterDelegate = UpdaterDelegate()
|
||||
private var installCancellable: AnyCancellable?
|
||||
|
||||
var viewModel: UpdateViewModel {
|
||||
userDriver.viewModel
|
||||
}
|
||||
|
||||
/// True if we're installing an update.
|
||||
var isInstalling: Bool {
|
||||
installCancellable != nil
|
||||
}
|
||||
|
||||
/// Initialize a new update controller.
|
||||
init() {
|
||||
let hostBundle = Bundle.main
|
||||
self.userDriver = UpdateDriver(
|
||||
viewModel: .init(),
|
||||
hostBundle: hostBundle)
|
||||
self.updater = SPUUpdater(
|
||||
hostBundle: hostBundle,
|
||||
applicationBundle: hostBundle,
|
||||
userDriver: userDriver,
|
||||
delegate: updaterDelegate
|
||||
)
|
||||
}
|
||||
|
||||
deinit {
|
||||
installCancellable?.cancel()
|
||||
}
|
||||
|
||||
/// Start the updater.
|
||||
///
|
||||
/// This must be called before the updater can check for updates. If starting fails,
|
||||
/// the error will be shown to the user.
|
||||
func startUpdater() {
|
||||
do {
|
||||
try updater.start()
|
||||
} catch {
|
||||
userDriver.viewModel.state = .error(.init(
|
||||
error: error,
|
||||
retry: { [weak self] in
|
||||
self?.userDriver.viewModel.state = .idle
|
||||
self?.startUpdater()
|
||||
},
|
||||
dismiss: { [weak self] in
|
||||
self?.userDriver.viewModel.state = .idle
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Force install the current update. As long as we're in some "update available" state this will
|
||||
/// trigger all the steps necessary to complete the update.
|
||||
func installUpdate() {
|
||||
// Must be in an installable state
|
||||
guard viewModel.state.isInstallable else { return }
|
||||
|
||||
// If we're already force installing then do nothing.
|
||||
guard installCancellable == nil else { return }
|
||||
|
||||
// Setup a combine listener to listen for state changes and to always
|
||||
// confirm them. If we go to a non-installable state, cancel the listener.
|
||||
// The sink runs immediately with the current state, so we don't need to
|
||||
// manually confirm the first state.
|
||||
installCancellable = viewModel.$state.sink { [weak self] state in
|
||||
guard let self else { return }
|
||||
|
||||
// If we move to a non-installable state (error, idle, etc.) then we
|
||||
// stop force installing.
|
||||
guard state.isInstallable else {
|
||||
self.installCancellable = nil
|
||||
return
|
||||
}
|
||||
|
||||
// Continue the `yes` chain!
|
||||
state.confirm()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for updates.
|
||||
///
|
||||
/// This is typically connected to a menu item action.
|
||||
@objc func checkForUpdates() {
|
||||
// If we're already idle, then just check for updates immediately.
|
||||
if viewModel.state == .idle {
|
||||
updater.checkForUpdates()
|
||||
return
|
||||
}
|
||||
|
||||
// If we're not idle then we need to cancel any prior state.
|
||||
installCancellable?.cancel()
|
||||
viewModel.state.cancel()
|
||||
|
||||
// The above will take time to settle, so we delay the check for some time.
|
||||
// The 100ms is arbitrary and I'd rather not, but we have to wait more than
|
||||
// one loop tick it seems.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { [weak self] in
|
||||
self?.updater.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the check for updates menu item.
|
||||
///
|
||||
/// - Parameter item: The menu item to validate
|
||||
/// - Returns: Whether the menu item should be enabled
|
||||
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
if item.action == #selector(checkForUpdates) {
|
||||
return updater.canCheckForUpdates
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
|
|||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Sparkle supports a native concept of "channels" but it requires that
|
||||
// you share a single appcast file. We don't want to do that so we
|
||||
// do this instead.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,207 @@
|
|||
import Cocoa
|
||||
import Sparkle
|
||||
|
||||
/// Implement the SPUUserDriver to modify our UpdateViewModel for custom presentation.
|
||||
class UpdateDriver: NSObject, SPUUserDriver {
|
||||
let viewModel: UpdateViewModel
|
||||
let standard: SPUStandardUserDriver
|
||||
|
||||
init(viewModel: UpdateViewModel, hostBundle: Bundle) {
|
||||
self.viewModel = viewModel
|
||||
self.standard = SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil)
|
||||
super.init()
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleTerminalWindowWillClose),
|
||||
name: TerminalWindow.terminalWillCloseNotification,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@objc private func handleTerminalWindowWillClose() {
|
||||
// If we lost the ability to show unobtrusive states, cancel whatever
|
||||
// update state we're in. This will allow the manual `check for updates`
|
||||
// call to initialize the standard driver.
|
||||
//
|
||||
// We have to do this after a short delay so that the window can fully
|
||||
// close.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in
|
||||
guard let self else { return }
|
||||
guard !hasUnobtrusiveTarget else { return }
|
||||
viewModel.state.cancel()
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
func show(_ request: SPUUpdatePermissionRequest,
|
||||
reply: @escaping @Sendable (SUUpdatePermissionResponse) -> Void) {
|
||||
viewModel.state = .permissionRequest(.init(request: request, reply: { [weak viewModel] response in
|
||||
viewModel?.state = .idle
|
||||
reply(response)
|
||||
}))
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.show(request, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {
|
||||
viewModel.state = .checking(.init(cancel: cancellation))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUserInitiatedUpdateCheck(cancellation: cancellation)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateFound(with appcastItem: SUAppcastItem,
|
||||
state: SPUUserUpdateState,
|
||||
reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||
viewModel.state = .updateAvailable(.init(appcastItem: appcastItem, reply: reply))
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateFound(with: appcastItem, state: state, reply: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {
|
||||
// We don't do anything with the release notes here because Ghostty
|
||||
// doesn't use the release notes feature of Sparkle currently.
|
||||
}
|
||||
|
||||
func showUpdateReleaseNotesFailedToDownloadWithError(_ error: any Error) {
|
||||
// We don't do anything with release notes. See `showUpdateReleaseNotes`
|
||||
}
|
||||
|
||||
func showUpdateNotFoundWithError(_ error: any Error,
|
||||
acknowledgement: @escaping () -> Void) {
|
||||
viewModel.state = .notFound(.init(acknowledgement: acknowledgement))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateNotFoundWithError(error, acknowledgement: acknowledgement)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdaterError(_ error: any Error,
|
||||
acknowledgement: @escaping () -> Void) {
|
||||
viewModel.state = .error(.init(
|
||||
error: error,
|
||||
retry: { [weak self, weak viewModel] in
|
||||
viewModel?.state = .idle
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
guard let delegate = NSApp.delegate as? AppDelegate else { return }
|
||||
delegate.checkForUpdates(self)
|
||||
}
|
||||
},
|
||||
dismiss: { [weak viewModel] in
|
||||
viewModel?.state = .idle
|
||||
}))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdaterError(error, acknowledgement: acknowledgement)
|
||||
} else {
|
||||
acknowledgement()
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadInitiated(cancellation: @escaping () -> Void) {
|
||||
viewModel.state = .downloading(.init(
|
||||
cancel: cancellation,
|
||||
expectedLength: nil,
|
||||
progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadInitiated(cancellation: cancellation)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {
|
||||
guard case let .downloading(downloading) = viewModel.state else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.state = .downloading(.init(
|
||||
cancel: downloading.cancel,
|
||||
expectedLength: expectedContentLength,
|
||||
progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidReceiveExpectedContentLength(expectedContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidReceiveData(ofLength length: UInt64) {
|
||||
guard case let .downloading(downloading) = viewModel.state else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.state = .downloading(.init(
|
||||
cancel: downloading.cancel,
|
||||
expectedLength: downloading.expectedLength,
|
||||
progress: downloading.progress + length))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidReceiveData(ofLength: length)
|
||||
}
|
||||
}
|
||||
|
||||
func showDownloadDidStartExtractingUpdate() {
|
||||
viewModel.state = .extracting(.init(progress: 0))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showDownloadDidStartExtractingUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func showExtractionReceivedProgress(_ progress: Double) {
|
||||
viewModel.state = .extracting(.init(progress: progress))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showExtractionReceivedProgress(progress)
|
||||
}
|
||||
}
|
||||
|
||||
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||
viewModel.state = .readyToInstall(.init(reply: reply))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showReady(toInstallAndRelaunch: reply)
|
||||
}
|
||||
}
|
||||
|
||||
func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {
|
||||
viewModel.state = .installing(.init(retryTerminatingApplication: retryTerminatingApplication))
|
||||
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showInstallingUpdate(withApplicationTerminated: applicationTerminated, retryTerminatingApplication: retryTerminatingApplication)
|
||||
}
|
||||
}
|
||||
|
||||
func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) {
|
||||
standard.showUpdateInstalledAndRelaunched(relaunched, acknowledgement: acknowledgement)
|
||||
viewModel.state = .idle
|
||||
}
|
||||
|
||||
func showUpdateInFocus() {
|
||||
if !hasUnobtrusiveTarget {
|
||||
standard.showUpdateInFocus()
|
||||
}
|
||||
}
|
||||
|
||||
func dismissUpdateInstallation() {
|
||||
viewModel.state = .idle
|
||||
standard.dismissUpdateInstallation()
|
||||
}
|
||||
|
||||
// MARK: No-Window Fallback
|
||||
|
||||
/// True if there is a target that can render our unobtrusive update checker.
|
||||
var hasUnobtrusiveTarget: Bool {
|
||||
NSApp.windows.contains { window in
|
||||
window is TerminalWindow &&
|
||||
window.isVisible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import SwiftUI
|
||||
|
||||
/// A pill-shaped button that displays update status and provides access to update actions.
|
||||
struct UpdatePill: View {
|
||||
/// The update view model that provides the current state and information
|
||||
@ObservedObject var model: UpdateViewModel
|
||||
|
||||
/// Whether the update popover is currently visible
|
||||
@State private var showPopover = false
|
||||
|
||||
/// Task for auto-dismissing the "No Updates" state
|
||||
@State private var resetTask: Task<Void, Never>?
|
||||
|
||||
/// The font used for the pill text
|
||||
private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium)
|
||||
|
||||
var body: some View {
|
||||
if !model.state.isIdle {
|
||||
pillButton
|
||||
.popover(isPresented: $showPopover, arrowEdge: .bottom) {
|
||||
UpdatePopoverView(model: model)
|
||||
}
|
||||
.transition(.opacity.combined(with: .scale(scale: 0.95)))
|
||||
.onChange(of: model.state) { newState in
|
||||
resetTask?.cancel()
|
||||
if case .notFound(let notFound) = newState {
|
||||
resetTask = Task { [weak model] in
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
guard !Task.isCancelled, case .notFound? = model?.state else { return }
|
||||
model?.state = .idle
|
||||
notFound.acknowledgement()
|
||||
}
|
||||
} else {
|
||||
resetTask = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The pill-shaped button view that displays the update badge and text
|
||||
@ViewBuilder
|
||||
private var pillButton: some View {
|
||||
Button(action: {
|
||||
if case .notFound(let notFound) = model.state {
|
||||
model.state = .idle
|
||||
notFound.acknowledgement()
|
||||
} else {
|
||||
showPopover.toggle()
|
||||
}
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
UpdateBadge(model: model)
|
||||
.frame(width: 14, height: 14)
|
||||
|
||||
Text(model.text)
|
||||
.font(Font(textFont))
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(width: textWidth)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(model.backgroundColor)
|
||||
)
|
||||
.foregroundColor(model.foregroundColor)
|
||||
.contentShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help(model.text)
|
||||
.accessibilityLabel(model.text)
|
||||
}
|
||||
|
||||
/// Calculated width for the text to prevent resizing during progress updates
|
||||
private var textWidth: CGFloat? {
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: textFont]
|
||||
let size = (model.maxWidthText as NSString).size(withAttributes: attributes)
|
||||
return size.width
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
import SwiftUI
|
||||
import Sparkle
|
||||
|
||||
/// A popover view that displays detailed update information and action buttons.
|
||||
///
|
||||
/// The view adapts its content based on the current update state, showing appropriate
|
||||
/// UI for checking, downloading, installing, or handling errors.
|
||||
struct UpdatePopoverView: View {
|
||||
/// The update view model that provides the current state and information
|
||||
@ObservedObject var model: UpdateViewModel
|
||||
|
||||
/// Environment value for dismissing the popover
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
switch model.state {
|
||||
case .idle:
|
||||
// Shouldn't happen in a well-formed view stack. Higher levels
|
||||
// should not call the popover for idles.
|
||||
EmptyView()
|
||||
|
||||
case .permissionRequest(let request):
|
||||
PermissionRequestView(request: request, dismiss: dismiss)
|
||||
|
||||
case .checking(let checking):
|
||||
CheckingView(checking: checking, dismiss: dismiss)
|
||||
|
||||
case .updateAvailable(let update):
|
||||
UpdateAvailableView(update: update, dismiss: dismiss)
|
||||
|
||||
case .downloading(let download):
|
||||
DownloadingView(download: download, dismiss: dismiss)
|
||||
|
||||
case .extracting(let extracting):
|
||||
ExtractingView(extracting: extracting)
|
||||
|
||||
case .readyToInstall(let ready):
|
||||
ReadyToInstallView(ready: ready, dismiss: dismiss)
|
||||
|
||||
case .installing(let installing):
|
||||
InstallingView(installing: installing, dismiss: dismiss)
|
||||
|
||||
case .notFound(let notFound):
|
||||
NotFoundView(notFound: notFound, dismiss: dismiss)
|
||||
|
||||
case .error(let error):
|
||||
UpdateErrorView(error: error, dismiss: dismiss)
|
||||
}
|
||||
}
|
||||
.frame(width: 300)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct PermissionRequestView: View {
|
||||
let request: UpdateState.PermissionRequest
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Enable automatic updates?")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("Ghostty can automatically check for updates in the background.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Not Now") {
|
||||
request.reply(SUUpdatePermissionResponse(
|
||||
automaticUpdateChecks: false,
|
||||
sendSystemProfile: false))
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Allow") {
|
||||
request.reply(SUUpdatePermissionResponse(
|
||||
automaticUpdateChecks: true,
|
||||
sendSystemProfile: false))
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct CheckingView: View {
|
||||
let checking: UpdateState.Checking
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack(spacing: 10) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Checking for updates…")
|
||||
.font(.system(size: 13))
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") {
|
||||
checking.cancel()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct UpdateAvailableView: View {
|
||||
let update: UpdateState.UpdateAvailable
|
||||
let dismiss: DismissAction
|
||||
|
||||
private let labelWidth: CGFloat = 60
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Update Available")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Version:")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(update.appcastItem.displayVersionString)
|
||||
}
|
||||
.font(.system(size: 11))
|
||||
|
||||
if update.appcastItem.contentLength > 0 {
|
||||
HStack(spacing: 6) {
|
||||
Text("Size:")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(ByteCountFormatter.string(fromByteCount: Int64(update.appcastItem.contentLength), countStyle: .file))
|
||||
}
|
||||
.font(.system(size: 11))
|
||||
}
|
||||
|
||||
if let date = update.appcastItem.date {
|
||||
HStack(spacing: 6) {
|
||||
Text("Released:")
|
||||
.foregroundColor(.secondary)
|
||||
.frame(width: labelWidth, alignment: .trailing)
|
||||
Text(date.formatted(date: .abbreviated, time: .omitted))
|
||||
}
|
||||
.font(.system(size: 11))
|
||||
}
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Skip") {
|
||||
update.reply(.skip)
|
||||
dismiss()
|
||||
}
|
||||
.controlSize(.small)
|
||||
|
||||
Button("Later") {
|
||||
update.reply(.dismiss)
|
||||
dismiss()
|
||||
}
|
||||
.controlSize(.small)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Install") {
|
||||
update.reply(.install)
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
|
||||
if let notes = update.releaseNotes {
|
||||
Divider()
|
||||
|
||||
Link(destination: notes.url) {
|
||||
HStack {
|
||||
Image(systemName: "doc.text")
|
||||
.font(.system(size: 11))
|
||||
Text(notes.label)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
Spacer()
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.system(size: 10))
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.padding(12)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(nsColor: .controlBackgroundColor))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct DownloadingView: View {
|
||||
let download: UpdateState.Downloading
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Downloading Update")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
let progress = min(1, max(0, Double(download.progress) / Double(expectedLength)))
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ProgressView(value: progress)
|
||||
Text(String(format: "%.0f%%", progress * 100))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") {
|
||||
download.cancel()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ExtractingView: View {
|
||||
let extracting: UpdateState.Extracting
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Preparing Update")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0)
|
||||
Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct ReadyToInstallView: View {
|
||||
let ready: UpdateState.ReadyToInstall
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Ready to Install")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("The update is ready to install.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("Later") {
|
||||
ready.reply(.dismiss)
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.controlSize(.small)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Install and Relaunch") {
|
||||
ready.reply(.install)
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct InstallingView: View {
|
||||
let installing: UpdateState.Installing
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Restart Required")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("The update is ready. Please restart the application to complete the installation.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Restart Now") {
|
||||
installing.retryTerminatingApplication()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct NotFoundView: View {
|
||||
let notFound: UpdateState.NotFound
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("No Updates Found")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
|
||||
Text("You're already running the latest version.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("OK") {
|
||||
notFound.acknowledgement()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct UpdateErrorView: View {
|
||||
let error: UpdateState.Error
|
||||
let dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 13))
|
||||
Text("Update Failed")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
}
|
||||
|
||||
Text(error.error.localizedDescription)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button("OK") {
|
||||
error.dismiss()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.cancelAction)
|
||||
.controlSize(.small)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Retry") {
|
||||
error.retry()
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
import Foundation
|
||||
import Sparkle
|
||||
|
||||
/// Simulates various update scenarios for testing the update UI.
|
||||
///
|
||||
/// The expected usage is by overriding the `checkForUpdates` function in AppDelegate and
|
||||
/// calling one of these instead. This will allow us to test the update flows without having to use
|
||||
/// real updates.
|
||||
enum UpdateSimulator {
|
||||
/// Complete successful update flow: checking → available → download → extract → ready → install → idle
|
||||
case happyPath
|
||||
|
||||
/// No updates available: checking (2s) → "No Updates Available" (3s) → idle
|
||||
case notFound
|
||||
|
||||
/// Error during check: checking (2s) → error with retry callback
|
||||
case error
|
||||
|
||||
/// Slower download for testing progress UI: checking → available → download (20 steps, ~10s) → extract → install
|
||||
case slowDownload
|
||||
|
||||
/// Initial permission request flow: shows permission dialog → proceeds with happy path if accepted
|
||||
case permissionRequest
|
||||
|
||||
/// User cancels during download: checking → available → download (5 steps) → cancels → idle
|
||||
case cancelDuringDownload
|
||||
|
||||
/// User cancels while checking: checking (1s) → cancels → idle
|
||||
case cancelDuringChecking
|
||||
|
||||
/// Shows the installing state with restart button: installing (stays until dismissed)
|
||||
case installing
|
||||
|
||||
func simulate(with viewModel: UpdateViewModel) {
|
||||
switch self {
|
||||
case .happyPath:
|
||||
simulateHappyPath(viewModel)
|
||||
case .notFound:
|
||||
simulateNotFound(viewModel)
|
||||
case .error:
|
||||
simulateError(viewModel)
|
||||
case .slowDownload:
|
||||
simulateSlowDownload(viewModel)
|
||||
case .permissionRequest:
|
||||
simulatePermissionRequest(viewModel)
|
||||
case .cancelDuringDownload:
|
||||
simulateCancelDuringDownload(viewModel)
|
||||
case .cancelDuringChecking:
|
||||
simulateCancelDuringChecking(viewModel)
|
||||
case .installing:
|
||||
simulateInstalling(viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateHappyPath(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
viewModel.state = .updateAvailable(.init(
|
||||
appcastItem: SUAppcastItem.empty(),
|
||||
reply: { choice in
|
||||
if choice == .install {
|
||||
simulateDownload(viewModel)
|
||||
} else {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateNotFound(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
viewModel.state = .notFound(.init(acknowledgement: {
|
||||
// Acknowledgement called when dismissed
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateError(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
viewModel.state = .error(.init(
|
||||
error: NSError(domain: "UpdateError", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to check for updates"
|
||||
]),
|
||||
retry: {
|
||||
simulateHappyPath(viewModel)
|
||||
},
|
||||
dismiss: {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateSlowDownload(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
viewModel.state = .updateAvailable(.init(
|
||||
appcastItem: SUAppcastItem.empty(),
|
||||
reply: { choice in
|
||||
if choice == .install {
|
||||
simulateSlowDownloadProgress(viewModel)
|
||||
} else {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateSlowDownloadProgress(_ viewModel: UpdateViewModel) {
|
||||
let download = UpdateState.Downloading(
|
||||
cancel: {
|
||||
viewModel.state = .idle
|
||||
},
|
||||
expectedLength: nil,
|
||||
progress: 0
|
||||
)
|
||||
viewModel.state = .downloading(download)
|
||||
|
||||
for i in 1...20 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.5) {
|
||||
let updatedDownload = UpdateState.Downloading(
|
||||
cancel: download.cancel,
|
||||
expectedLength: 2000,
|
||||
progress: UInt64(i * 100)
|
||||
)
|
||||
viewModel.state = .downloading(updatedDownload)
|
||||
|
||||
if i == 20 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
simulateExtract(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simulatePermissionRequest(_ viewModel: UpdateViewModel) {
|
||||
let request = SPUUpdatePermissionRequest(systemProfile: [])
|
||||
viewModel.state = .permissionRequest(.init(
|
||||
request: request,
|
||||
reply: { response in
|
||||
if response.automaticUpdateChecks {
|
||||
simulateHappyPath(viewModel)
|
||||
} else {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
private func simulateCancelDuringDownload(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
viewModel.state = .updateAvailable(.init(
|
||||
appcastItem: SUAppcastItem.empty(),
|
||||
reply: { choice in
|
||||
if choice == .install {
|
||||
simulateDownloadThenCancel(viewModel)
|
||||
} else {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateDownloadThenCancel(_ viewModel: UpdateViewModel) {
|
||||
let download = UpdateState.Downloading(
|
||||
cancel: {
|
||||
viewModel.state = .idle
|
||||
},
|
||||
expectedLength: nil,
|
||||
progress: 0
|
||||
)
|
||||
viewModel.state = .downloading(download)
|
||||
|
||||
for i in 1...5 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) {
|
||||
let updatedDownload = UpdateState.Downloading(
|
||||
cancel: download.cancel,
|
||||
expectedLength: 1000,
|
||||
progress: UInt64(i * 100)
|
||||
)
|
||||
viewModel.state = .downloading(updatedDownload)
|
||||
|
||||
if i == 5 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateCancelDuringChecking(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .checking(.init(cancel: {
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateDownload(_ viewModel: UpdateViewModel) {
|
||||
let download = UpdateState.Downloading(
|
||||
cancel: {
|
||||
viewModel.state = .idle
|
||||
},
|
||||
expectedLength: nil,
|
||||
progress: 0
|
||||
)
|
||||
viewModel.state = .downloading(download)
|
||||
|
||||
for i in 1...10 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.3) {
|
||||
let updatedDownload = UpdateState.Downloading(
|
||||
cancel: download.cancel,
|
||||
expectedLength: 1000,
|
||||
progress: UInt64(i * 100)
|
||||
)
|
||||
viewModel.state = .downloading(updatedDownload)
|
||||
|
||||
if i == 10 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
simulateExtract(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateExtract(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .extracting(.init(progress: 0.0))
|
||||
|
||||
for j in 1...5 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(j) * 0.3) {
|
||||
viewModel.state = .extracting(.init(progress: Double(j) / 5.0))
|
||||
|
||||
if j == 5 {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
viewModel.state = .readyToInstall(.init(
|
||||
reply: { choice in
|
||||
if choice == .install {
|
||||
viewModel.state = .installing(.init(retryTerminatingApplication: {
|
||||
print("Restart button clicked in simulator - resetting to idle")
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
} else {
|
||||
viewModel.state = .idle
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func simulateInstalling(_ viewModel: UpdateViewModel) {
|
||||
viewModel.state = .installing(.init(retryTerminatingApplication: {
|
||||
print("Restart button clicked in simulator - resetting to idle")
|
||||
viewModel.state = .idle
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
import Sparkle
|
||||
|
||||
class UpdateViewModel: ObservableObject {
|
||||
@Published var state: UpdateState = .idle
|
||||
|
||||
/// The text to display for the current update state.
|
||||
/// Returns an empty string for idle state, progress percentages for downloading/extracting,
|
||||
/// or descriptive text for other states.
|
||||
var text: String {
|
||||
switch state {
|
||||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
return "Enable Automatic Updates?"
|
||||
case .checking:
|
||||
return "Checking for Updates…"
|
||||
case .updateAvailable(let update):
|
||||
let version = update.appcastItem.displayVersionString
|
||||
if !version.isEmpty {
|
||||
return "Update Available: \(version)"
|
||||
}
|
||||
return "Update Available"
|
||||
case .downloading(let download):
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
let progress = Double(download.progress) / Double(expectedLength)
|
||||
return String(format: "Downloading: %.0f%%", progress * 100)
|
||||
}
|
||||
return "Downloading…"
|
||||
case .extracting(let extracting):
|
||||
return String(format: "Preparing: %.0f%%", extracting.progress * 100)
|
||||
case .readyToInstall:
|
||||
return "Ready to Install Update"
|
||||
case .installing:
|
||||
return "Restart to Complete Update"
|
||||
case .notFound:
|
||||
return "No Updates Available"
|
||||
case .error(let err):
|
||||
return err.error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
/// The maximum width text for states that show progress.
|
||||
/// Used to prevent the pill from resizing as percentages change.
|
||||
var maxWidthText: String {
|
||||
switch state {
|
||||
case .downloading:
|
||||
return "Downloading: 100%"
|
||||
case .extracting:
|
||||
return "Preparing: 100%"
|
||||
default:
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
/// The SF Symbol icon name for the current update state.
|
||||
var iconName: String? {
|
||||
switch state {
|
||||
case .idle:
|
||||
return nil
|
||||
case .permissionRequest:
|
||||
return "questionmark.circle"
|
||||
case .checking:
|
||||
return "arrow.triangle.2.circlepath"
|
||||
case .updateAvailable:
|
||||
return "shippingbox.fill"
|
||||
case .downloading:
|
||||
return "arrow.down.circle"
|
||||
case .extracting:
|
||||
return "shippingbox"
|
||||
case .readyToInstall:
|
||||
return "restart.circle.fill"
|
||||
case .installing:
|
||||
return "power.circle"
|
||||
case .notFound:
|
||||
return "info.circle"
|
||||
case .error:
|
||||
return "exclamationmark.triangle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
/// A longer description for the current update state.
|
||||
/// Used in contexts like the command palette where more detail is helpful.
|
||||
var description: String {
|
||||
switch state {
|
||||
case .idle:
|
||||
return ""
|
||||
case .permissionRequest:
|
||||
return "Configure automatic update preferences"
|
||||
case .checking:
|
||||
return "Please wait while we check for available updates"
|
||||
case .updateAvailable(let update):
|
||||
return update.releaseNotes?.label ?? "Download and install the latest version"
|
||||
case .downloading:
|
||||
return "Downloading the update package"
|
||||
case .extracting:
|
||||
return "Extracting and preparing the update"
|
||||
case .readyToInstall:
|
||||
return "Update is ready to install"
|
||||
case .installing:
|
||||
return "Installing update and preparing to restart"
|
||||
case .notFound:
|
||||
return "You are running the latest version"
|
||||
case .error:
|
||||
return "An error occurred during the update process"
|
||||
}
|
||||
}
|
||||
|
||||
/// A badge to display for the current update state.
|
||||
/// Returns version numbers, progress percentages, or nil.
|
||||
var badge: String? {
|
||||
switch state {
|
||||
case .updateAvailable(let update):
|
||||
let version = update.appcastItem.displayVersionString
|
||||
return version.isEmpty ? nil : version
|
||||
case .downloading(let download):
|
||||
if let expectedLength = download.expectedLength, expectedLength > 0 {
|
||||
let percentage = Double(download.progress) / Double(expectedLength) * 100
|
||||
return String(format: "%.0f%%", percentage)
|
||||
}
|
||||
return nil
|
||||
case .extracting(let extracting):
|
||||
return String(format: "%.0f%%", extracting.progress * 100)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// The color to apply to the icon for the current update state.
|
||||
var iconColor: Color {
|
||||
switch state {
|
||||
case .idle:
|
||||
return .secondary
|
||||
case .permissionRequest:
|
||||
return .white
|
||||
case .checking:
|
||||
return .secondary
|
||||
case .updateAvailable, .readyToInstall:
|
||||
return .accentColor
|
||||
case .downloading, .extracting, .installing:
|
||||
return .secondary
|
||||
case .notFound:
|
||||
return .secondary
|
||||
case .error:
|
||||
return .orange
|
||||
}
|
||||
}
|
||||
|
||||
/// The background color for the update pill.
|
||||
var backgroundColor: Color {
|
||||
switch state {
|
||||
case .permissionRequest:
|
||||
return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.3, of: .black) ?? .systemBlue)
|
||||
case .updateAvailable:
|
||||
return .accentColor
|
||||
case .readyToInstall:
|
||||
return Color(nsColor: NSColor.systemGreen.blended(withFraction: 0.3, of: .black) ?? .systemGreen)
|
||||
case .notFound:
|
||||
return Color(nsColor: NSColor.systemBlue.blended(withFraction: 0.5, of: .black) ?? .systemBlue)
|
||||
case .error:
|
||||
return .orange.opacity(0.2)
|
||||
default:
|
||||
return Color(nsColor: .controlBackgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
/// The foreground (text) color for the update pill.
|
||||
var foregroundColor: Color {
|
||||
switch state {
|
||||
case .permissionRequest:
|
||||
return .white
|
||||
case .updateAvailable, .readyToInstall:
|
||||
return .white
|
||||
case .notFound:
|
||||
return .white
|
||||
case .error:
|
||||
return .orange
|
||||
default:
|
||||
return .primary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum UpdateState: Equatable {
|
||||
case idle
|
||||
case permissionRequest(PermissionRequest)
|
||||
case checking(Checking)
|
||||
case updateAvailable(UpdateAvailable)
|
||||
case notFound(NotFound)
|
||||
case error(Error)
|
||||
case downloading(Downloading)
|
||||
case extracting(Extracting)
|
||||
case readyToInstall(ReadyToInstall)
|
||||
case installing(Installing)
|
||||
|
||||
var isIdle: Bool {
|
||||
if case .idle = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// This is true if we're in a state that can be force installed.
|
||||
var isInstallable: Bool {
|
||||
switch (self) {
|
||||
case .checking,
|
||||
.updateAvailable,
|
||||
.downloading,
|
||||
.extracting,
|
||||
.readyToInstall,
|
||||
.installing:
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
switch self {
|
||||
case .checking(let checking):
|
||||
checking.cancel()
|
||||
case .updateAvailable(let available):
|
||||
available.reply(.dismiss)
|
||||
case .downloading(let downloading):
|
||||
downloading.cancel()
|
||||
case .readyToInstall(let ready):
|
||||
ready.reply(.dismiss)
|
||||
case .notFound(let notFound):
|
||||
notFound.acknowledgement()
|
||||
case .error(let err):
|
||||
err.dismiss()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/// Confirms or accepts the current update state.
|
||||
/// - For available updates: begins installation
|
||||
/// - For ready-to-install: proceeds with installation
|
||||
func confirm() {
|
||||
switch self {
|
||||
case .updateAvailable(let available):
|
||||
available.reply(.install)
|
||||
case .readyToInstall(let ready):
|
||||
ready.reply(.install)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: UpdateState, rhs: UpdateState) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.idle, .idle):
|
||||
return true
|
||||
case (.permissionRequest, .permissionRequest):
|
||||
return true
|
||||
case (.checking, .checking):
|
||||
return true
|
||||
case (.updateAvailable(let lUpdate), .updateAvailable(let rUpdate)):
|
||||
return lUpdate.appcastItem.displayVersionString == rUpdate.appcastItem.displayVersionString
|
||||
case (.notFound, .notFound):
|
||||
return true
|
||||
case (.error(let lErr), .error(let rErr)):
|
||||
return lErr.error.localizedDescription == rErr.error.localizedDescription
|
||||
case (.downloading(let lDown), .downloading(let rDown)):
|
||||
return lDown.progress == rDown.progress && lDown.expectedLength == rDown.expectedLength
|
||||
case (.extracting(let lExt), .extracting(let rExt)):
|
||||
return lExt.progress == rExt.progress
|
||||
case (.readyToInstall, .readyToInstall):
|
||||
return true
|
||||
case (.installing, .installing):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
struct NotFound {
|
||||
let acknowledgement: () -> Void
|
||||
}
|
||||
|
||||
struct PermissionRequest {
|
||||
let request: SPUUpdatePermissionRequest
|
||||
let reply: @Sendable (SUUpdatePermissionResponse) -> Void
|
||||
}
|
||||
|
||||
struct Checking {
|
||||
let cancel: () -> Void
|
||||
}
|
||||
|
||||
struct UpdateAvailable {
|
||||
let appcastItem: SUAppcastItem
|
||||
let reply: @Sendable (SPUUserUpdateChoice) -> Void
|
||||
|
||||
var releaseNotes: ReleaseNotes? {
|
||||
let currentCommit = Bundle.main.infoDictionary?["GhosttyCommit"] as? String
|
||||
return ReleaseNotes(displayVersionString: appcastItem.displayVersionString, currentCommit: currentCommit)
|
||||
}
|
||||
}
|
||||
|
||||
enum ReleaseNotes {
|
||||
case commit(URL)
|
||||
case compareTip(URL)
|
||||
case tagged(URL)
|
||||
|
||||
init?(displayVersionString: String, currentCommit: String?) {
|
||||
let version = displayVersionString
|
||||
|
||||
// Check for semantic version (x.y.z)
|
||||
if let semver = Self.extractSemanticVersion(from: version) {
|
||||
let slug = semver.replacingOccurrences(of: ".", with: "-")
|
||||
if let url = URL(string: "https://ghostty.org/docs/install/release-notes/\(slug)") {
|
||||
self = .tagged(url)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to git hash detection
|
||||
guard let newHash = Self.extractGitHash(from: version) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let currentHash = currentCommit, !currentHash.isEmpty,
|
||||
let url = URL(string: "https://github.com/ghostty-org/ghostty/compare/\(currentHash)...\(newHash)") {
|
||||
self = .compareTip(url)
|
||||
} else if let url = URL(string: "https://github.com/ghostty-org/ghostty/commit/\(newHash)") {
|
||||
self = .commit(url)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static func extractSemanticVersion(from version: String) -> String? {
|
||||
let pattern = #"^\d+\.\d+\.\d+$"#
|
||||
if version.range(of: pattern, options: .regularExpression) != nil {
|
||||
return version
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func extractGitHash(from version: String) -> String? {
|
||||
let pattern = #"[0-9a-f]{7,40}"#
|
||||
if let range = version.range(of: pattern, options: .regularExpression) {
|
||||
return String(version[range])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var url: URL {
|
||||
switch self {
|
||||
case .commit(let url): return url
|
||||
case .compareTip(let url): return url
|
||||
case .tagged(let url): return url
|
||||
}
|
||||
}
|
||||
|
||||
var label: String {
|
||||
switch (self) {
|
||||
case .commit: return "View GitHub Commit"
|
||||
case .compareTip: return "Changes Since This Tip Release"
|
||||
case .tagged: return "View Release Notes"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Error {
|
||||
let error: any Swift.Error
|
||||
let retry: () -> Void
|
||||
let dismiss: () -> Void
|
||||
}
|
||||
|
||||
struct Downloading {
|
||||
let cancel: () -> Void
|
||||
let expectedLength: UInt64?
|
||||
let progress: UInt64
|
||||
}
|
||||
|
||||
struct Extracting {
|
||||
let progress: Double
|
||||
}
|
||||
|
||||
struct ReadyToInstall {
|
||||
let reply: @Sendable (SPUUserUpdateChoice) -> Void
|
||||
}
|
||||
|
||||
struct Installing {
|
||||
let retryTerminatingApplication: () -> Void
|
||||
}
|
||||
}
|
||||
|
|
@ -100,6 +100,18 @@ extension Ghostty.Action {
|
|||
let state: State
|
||||
let progress: UInt8?
|
||||
}
|
||||
|
||||
struct Scrollbar {
|
||||
let total: UInt64
|
||||
let offset: UInt64
|
||||
let len: UInt64
|
||||
|
||||
init(c: ghostty_action_scrollbar_s) {
|
||||
total = c.total
|
||||
offset = c.offset
|
||||
len = c.len
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Putting the initializer in an extension preserves the automatic one.
|
||||
|
|
|
|||
|
|
@ -571,6 +571,9 @@ extension Ghostty {
|
|||
case GHOSTTY_ACTION_REDO:
|
||||
return redo(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_SCROLLBAR:
|
||||
scrollbar(app, target: target, v: action.action.scrollbar)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||
fallthrough
|
||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||
|
|
@ -637,8 +640,9 @@ extension Ghostty {
|
|||
|
||||
switch action.kind {
|
||||
case .text:
|
||||
// Open with the default text editor
|
||||
if let textEditor = NSWorkspace.shared.defaultTextEditor {
|
||||
// Open with the default editor for `*.ghostty` file or just system text editor
|
||||
let editor = NSWorkspace.shared.defaultApplicationURL(forExtension: url.pathExtension) ?? NSWorkspace.shared.defaultTextEditor
|
||||
if let textEditor = editor {
|
||||
NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
|
||||
return true
|
||||
}
|
||||
|
|
@ -1025,26 +1029,38 @@ extension Ghostty {
|
|||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }
|
||||
|
||||
// For now, we return false if the window has no splits and we return
|
||||
// true if the window has ANY splits. This isn't strictly correct because
|
||||
// we should only be returning true if we actually performed the action,
|
||||
// but this handles the most common case of caring about goto_split performability
|
||||
// which is the no-split case.
|
||||
// If the window has no splits, the action is not performable
|
||||
guard controller.surfaceTree.isSplit else { return false }
|
||||
|
||||
// Convert the C API direction to our Swift type
|
||||
guard let splitDirection = SplitFocusDirection.from(direction: direction) else { return false }
|
||||
|
||||
// Find the current node in the tree
|
||||
guard let targetNode = controller.surfaceTree.root?.node(view: surfaceView) else { return false }
|
||||
|
||||
// Check if a split actually exists in the target direction before
|
||||
// returning true. This ensures performable keybinds only consume
|
||||
// the key event when we actually perform navigation.
|
||||
let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection = splitDirection.toSplitTreeFocusDirection()
|
||||
guard controller.surfaceTree.focusTarget(for: focusDirection, from: targetNode) != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
// We have a valid target, post the notification to perform the navigation
|
||||
NotificationCenter.default.post(
|
||||
name: Notification.ghosttyFocusSplit,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any,
|
||||
Notification.SplitDirectionKey: splitDirection as Any,
|
||||
]
|
||||
)
|
||||
|
||||
return true
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private static func resizeSplit(
|
||||
|
|
@ -1559,6 +1575,33 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
private static func scrollbar(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_scrollbar_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("scrollbar does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
let scrollbar = Ghostty.Action.Scrollbar(c: v)
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyDidUpdateScrollbar,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.ScrollbarKey: scrollbar
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func configReload(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
|
|
|||
|
|
@ -314,17 +314,14 @@ extension Ghostty {
|
|||
|
||||
var macosCustomIcon: String {
|
||||
#if os(macOS)
|
||||
let homeDirURL = FileManager.default.homeDirectoryForCurrentUser
|
||||
let ghosttyConfigIconPath = homeDirURL.appendingPathComponent(
|
||||
".config/ghostty/Ghostty.icns",
|
||||
conformingTo: .fileURL).path()
|
||||
let defaultValue = ghosttyConfigIconPath
|
||||
let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "macos-custom-icon"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
return String(cString: ptr)
|
||||
guard let path = NSString(utf8String: ptr) else { return defaultValue }
|
||||
return path.expandingTildeInPath
|
||||
#else
|
||||
return ""
|
||||
#endif
|
||||
|
|
@ -606,6 +603,17 @@ extension Ghostty {
|
|||
let str = String(cString: ptr)
|
||||
return MacShortcuts(rawValue: str) ?? defaultValue
|
||||
}
|
||||
|
||||
var scrollbar: Scrollbar {
|
||||
let defaultValue = Scrollbar.system
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "scrollbar"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return Scrollbar(rawValue: str) ?? defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -644,6 +652,11 @@ extension Ghostty.Config {
|
|||
case ask
|
||||
}
|
||||
|
||||
enum Scrollbar: String {
|
||||
case system
|
||||
case never
|
||||
}
|
||||
|
||||
enum ResizeOverlay : String {
|
||||
case always
|
||||
case never
|
||||
|
|
|
|||
|
|
@ -223,7 +223,38 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// MARK: SplitFocusDirection Extensions
|
||||
|
||||
extension Ghostty.SplitFocusDirection {
|
||||
/// Convert to a SplitTree.FocusDirection for the given ViewType.
|
||||
func toSplitTreeFocusDirection<ViewType>() -> SplitTree<ViewType>.FocusDirection {
|
||||
switch self {
|
||||
case .previous:
|
||||
return .previous
|
||||
|
||||
case .next:
|
||||
return .next
|
||||
|
||||
case .up:
|
||||
return .spatial(.up)
|
||||
|
||||
case .down:
|
||||
return .spatial(.down)
|
||||
|
||||
case .left:
|
||||
return .spatial(.left)
|
||||
|
||||
case .right:
|
||||
return .spatial(.right)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
extension Ghostty {
|
||||
/// The type of a clipboard request
|
||||
enum ClipboardRequest {
|
||||
/// A direct paste of clipboard contents
|
||||
|
|
@ -344,6 +375,10 @@ extension Notification.Name {
|
|||
|
||||
/// Toggle maximize of current window
|
||||
static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle")
|
||||
|
||||
/// Notification sent when scrollbar updates
|
||||
static let ghosttyDidUpdateScrollbar = Notification.Name("com.mitchellh.ghostty.didUpdateScrollbar")
|
||||
static let ScrollbarKey = ghosttyDidUpdateScrollbar.rawValue + ".scrollbar"
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
|
|
|
|||
|
|
@ -0,0 +1,273 @@
|
|||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// Wraps a Ghostty surface view in an NSScrollView to provide native macOS scrollbar support.
|
||||
///
|
||||
/// ## Coordinate System
|
||||
/// AppKit uses a +Y-up coordinate system (origin at bottom-left), while terminals conceptually
|
||||
/// use +Y-down (row 0 at top). This class handles the inversion when converting between row
|
||||
/// offsets and pixel positions.
|
||||
///
|
||||
/// ## Architecture
|
||||
/// - `scrollView`: The outermost NSScrollView that manages scrollbar rendering and behavior
|
||||
/// - `documentView`: A blank NSView whose height represents total scrollback (in pixels)
|
||||
/// - `surfaceView`: The actual Ghostty renderer, positioned to fill the visible rect
|
||||
class SurfaceScrollView: NSView {
|
||||
private let scrollView: NSScrollView
|
||||
private let documentView: NSView
|
||||
private let surfaceView: Ghostty.SurfaceView
|
||||
private var observers: [NSObjectProtocol] = []
|
||||
private var cancellables: Set<AnyCancellable> = []
|
||||
private var isLiveScrolling = false
|
||||
|
||||
/// The last row position sent via scroll_to_row action. Used to avoid
|
||||
/// sending redundant actions when the user drags the scrollbar but stays
|
||||
/// on the same row.
|
||||
private var lastSentRow: Int?
|
||||
|
||||
init(contentSize: CGSize, surfaceView: Ghostty.SurfaceView) {
|
||||
self.surfaceView = surfaceView
|
||||
// The scroll view is our outermost view that controls all our scrollbar
|
||||
// rendering and behavior.
|
||||
scrollView = NSScrollView()
|
||||
scrollView.hasVerticalScroller = false
|
||||
scrollView.hasHorizontalScroller = false
|
||||
scrollView.autohidesScrollers = true
|
||||
scrollView.usesPredominantAxisScrolling = true
|
||||
// hide default background to show blur effect properly
|
||||
scrollView.drawsBackground = false
|
||||
|
||||
// The document view is what the scrollview is actually going
|
||||
// to be directly scrolling. We set it up to a "blank" NSView
|
||||
// with the desired content size.
|
||||
documentView = NSView(frame: NSRect(origin: .zero, size: contentSize))
|
||||
scrollView.documentView = documentView
|
||||
|
||||
// The document view contains our actual surface as a child.
|
||||
// We synchronize the scrolling of the document with this surface
|
||||
// so that our primary Ghostty renderer only needs to render the viewport.
|
||||
documentView.addSubview(surfaceView)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
// Our scroll view is our only view
|
||||
addSubview(scrollView)
|
||||
|
||||
// Apply initial scrollbar settings
|
||||
synchronizeAppearance()
|
||||
|
||||
// We listen for scroll events through bounds notifications on our NSClipView.
|
||||
// This is based on: https://christiantietze.de/posts/2018/07/synchronize-nsscrollview/
|
||||
scrollView.contentView.postsBoundsChangedNotifications = true
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSView.boundsDidChangeNotification,
|
||||
object: scrollView.contentView,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
self?.handleScrollChange(notification)
|
||||
})
|
||||
|
||||
// Listen for scrollbar updates from Ghostty
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: .ghosttyDidUpdateScrollbar,
|
||||
object: surfaceView,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
self?.handleScrollbarUpdate(notification)
|
||||
})
|
||||
|
||||
// Listen for live scroll events
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSScrollView.willStartLiveScrollNotification,
|
||||
object: scrollView,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.isLiveScrolling = true
|
||||
})
|
||||
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSScrollView.didEndLiveScrollNotification,
|
||||
object: scrollView,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.isLiveScrolling = false
|
||||
})
|
||||
|
||||
observers.append(NotificationCenter.default.addObserver(
|
||||
forName: NSScrollView.didLiveScrollNotification,
|
||||
object: scrollView,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
self?.handleLiveScroll()
|
||||
})
|
||||
|
||||
// Listen for derived config changes to update scrollbar settings live
|
||||
surfaceView.$derivedConfig
|
||||
.sink { [weak self] _ in
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.synchronizeAppearance()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) not implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
observers.forEach { NotificationCenter.default.removeObserver($0) }
|
||||
}
|
||||
|
||||
// The entire bounds is a safe area, so we override any default
|
||||
// insets. This is necessary for the content view to match the
|
||||
// surface view if we have the "hidden" titlebar style.
|
||||
override var safeAreaInsets: NSEdgeInsets { return NSEdgeInsetsZero }
|
||||
|
||||
override func setFrameSize(_ newSize: NSSize) {
|
||||
super.setFrameSize(newSize)
|
||||
|
||||
// Force layout to be called to fix up our various subviews.
|
||||
needsLayout = true
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
|
||||
// Fill entire bounds with scroll view
|
||||
scrollView.frame = bounds
|
||||
|
||||
// Use contentSize to account for visible scrollers
|
||||
//
|
||||
// Only update sizes if we have a valid (non-zero) content size. The content size
|
||||
// can be zero when this is added early to a view, or to an invisible hierarchy.
|
||||
// Practically, this happened in the quick terminal.
|
||||
var contentSize = scrollView.contentSize
|
||||
guard contentSize.width > 0 && contentSize.height > 0 else {
|
||||
synchronizeSurfaceView()
|
||||
return
|
||||
}
|
||||
|
||||
// If we have a legacy scrollbar and its not visible, then we account for that
|
||||
// in advance, because legacy scrollbars change our contentSize and force reflow
|
||||
// of our terminal which is not desirable.
|
||||
// See: https://github.com/ghostty-org/ghostty/discussions/9254
|
||||
let style = scrollView.verticalScroller?.scrollerStyle ?? NSScroller.preferredScrollerStyle
|
||||
if style == .legacy {
|
||||
if (scrollView.verticalScroller?.isHidden ?? true) {
|
||||
let scrollerWidth = NSScroller.scrollerWidth(for: .regular, scrollerStyle: .legacy)
|
||||
contentSize.width -= scrollerWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Keep document width synchronized with content width
|
||||
documentView.setFrameSize(CGSize(
|
||||
width: contentSize.width,
|
||||
height: documentView.frame.height
|
||||
))
|
||||
|
||||
// Inform the actual pty of our size change. This doesn't change the actual view
|
||||
// frame because we do want to render the whole thing, but it will prevent our
|
||||
// rows/cols from going into the non-content area.
|
||||
surfaceView.sizeDidChange(contentSize)
|
||||
|
||||
// When our scrollview changes make sure our surface view is synchronized
|
||||
synchronizeSurfaceView()
|
||||
}
|
||||
|
||||
// MARK: Scrolling
|
||||
|
||||
private func synchronizeAppearance() {
|
||||
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
|
||||
scrollView.hasVerticalScroller = scrollbarConfig != .never
|
||||
}
|
||||
|
||||
/// Positions the surface view to fill the currently visible rectangle.
|
||||
///
|
||||
/// This is called whenever the scroll position changes. The surface view (which does the
|
||||
/// actual terminal rendering) always fills exactly the visible portion of the document view,
|
||||
/// so the renderer only needs to render what's currently on screen.
|
||||
private func synchronizeSurfaceView() {
|
||||
let visibleRect = scrollView.contentView.documentVisibleRect
|
||||
surfaceView.frame = visibleRect
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
/// Handles bounds changes in the scroll view's clip view, keeping the surface view synchronized.
|
||||
private func handleScrollChange(_ notification: Notification) {
|
||||
synchronizeSurfaceView()
|
||||
}
|
||||
|
||||
/// Handles live scroll events (user actively dragging the scrollbar).
|
||||
///
|
||||
/// Converts the current scroll position to a row number and sends a `scroll_to_row` action
|
||||
/// to the terminal core. Only sends actions when the row changes to avoid IPC spam.
|
||||
private func handleLiveScroll() {
|
||||
// If our cell height is currently zero then we avoid a div by zero below
|
||||
// and just don't scroll (there's no where to scroll anyways). This can
|
||||
// happen with a tiny terminal.
|
||||
let cellHeight = surfaceView.cellSize.height
|
||||
guard cellHeight > 0 else { return }
|
||||
|
||||
// AppKit views are +Y going up, so we calculate from the bottom
|
||||
let visibleRect = scrollView.contentView.documentVisibleRect
|
||||
let documentHeight = documentView.frame.height
|
||||
let scrollOffset = documentHeight - visibleRect.origin.y - visibleRect.height
|
||||
let row = Int(scrollOffset / cellHeight)
|
||||
|
||||
// Only send action if the row changed to avoid action spam
|
||||
guard row != lastSentRow else { return }
|
||||
lastSentRow = row
|
||||
|
||||
// Use the keybinding action to scroll.
|
||||
_ = surfaceView.surfaceModel?.perform(action: "scroll_to_row:\(row)")
|
||||
}
|
||||
|
||||
/// Handles scrollbar state updates from the terminal core.
|
||||
///
|
||||
/// Updates the document view size to reflect total scrollback and adjusts scroll position
|
||||
/// to match the terminal's viewport. During live scrolling, updates document size but skips
|
||||
/// programmatic position changes to avoid fighting the user's drag.
|
||||
///
|
||||
/// ## Scrollbar State
|
||||
/// The scrollbar struct contains:
|
||||
/// - `total`: Total rows in scrollback + active area
|
||||
/// - `offset`: First visible row (0 = top of history)
|
||||
/// - `len`: Number of visible rows (viewport height)
|
||||
private func handleScrollbarUpdate(_ notification: Notification) {
|
||||
guard let scrollbar = notification.userInfo?[SwiftUI.Notification.Name.ScrollbarKey] as? Ghostty.Action.Scrollbar else {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert row units to pixels using cell height, ignore zero height.
|
||||
let cellHeight = surfaceView.cellSize.height
|
||||
guard cellHeight > 0 else { return }
|
||||
|
||||
// The full document height must include the vertical padding around the cell
|
||||
// grid, otherwise the content view ends up misaligned with the surface.
|
||||
let documentGridHeight = CGFloat(scrollbar.total) * cellHeight
|
||||
let gridHeight = CGFloat(scrollbar.len) * cellHeight
|
||||
let padding = scrollView.contentSize.height - gridHeight
|
||||
let documentHeight = documentGridHeight + padding
|
||||
|
||||
// Our width should be the content width to account for visible scrollers.
|
||||
// We don't do horizontal scrolling in terminals.
|
||||
let newSize = CGSize(width: scrollView.contentSize.width, height: documentHeight)
|
||||
documentView.setFrameSize(newSize)
|
||||
|
||||
// Only update our actual scroll position if we're not actively scrolling.
|
||||
if !isLiveScrolling {
|
||||
// Invert coordinate system: terminal offset is from top, AppKit position from bottom
|
||||
let offsetY = CGFloat(scrollbar.total - scrollbar.offset - scrollbar.len) * cellHeight
|
||||
scrollView.contentView.scroll(to: CGPoint(x: 0, y: offsetY))
|
||||
|
||||
// Track the current row position to avoid redundant movements when we
|
||||
// move the scrollbar.
|
||||
lastSentRow = Int(scrollbar.offset)
|
||||
}
|
||||
|
||||
// Always update our scrolled view with the latest dimensions
|
||||
scrollView.reflectScrolledClipView(scrollView.contentView)
|
||||
}
|
||||
}
|
||||
|
|
@ -386,10 +386,6 @@ extension Ghostty {
|
|||
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
|
||||
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
|
||||
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
|
||||
///
|
||||
/// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible
|
||||
/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to
|
||||
/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
|
||||
struct SurfaceRepresentable: OSViewRepresentable {
|
||||
/// The view to render for the terminal surface.
|
||||
let view: SurfaceView
|
||||
|
|
@ -404,16 +400,26 @@ extension Ghostty {
|
|||
/// The best approach is to wrap this view in a GeometryReader and pass in the geo.size.
|
||||
let size: CGSize
|
||||
|
||||
#if canImport(AppKit)
|
||||
func makeOSView(context: Context) -> SurfaceScrollView {
|
||||
// On macOS, wrap the surface view in a scroll view
|
||||
return SurfaceScrollView(contentSize: size, surfaceView: view)
|
||||
}
|
||||
|
||||
func updateOSView(_ scrollView: SurfaceScrollView, context: Context) {
|
||||
// Our scrollview always takes up the full size.
|
||||
scrollView.frame.size = size
|
||||
}
|
||||
#else
|
||||
func makeOSView(context: Context) -> SurfaceView {
|
||||
// We need the view as part of the state to be created previously because
|
||||
// the view is sent to the Ghostty API so that it can manipulate it
|
||||
// directly since we draw on a render thread.
|
||||
return view;
|
||||
// On iOS, return the surface view directly
|
||||
return view
|
||||
}
|
||||
|
||||
func updateOSView(_ view: SurfaceView, context: Context) {
|
||||
view.sizeDidChange(size)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// The configuration for a surface. For any configuration not set, defaults will be chosen from
|
||||
|
|
|
|||
|
|
@ -1027,7 +1027,7 @@ extension Ghostty {
|
|||
|
||||
// If we are in a keyDown then we don't need to redispatch a command-modded
|
||||
// key event (see docs for this field) so reset this to nil because
|
||||
// `interpretKeyEvents` may dispach it.
|
||||
// `interpretKeyEvents` may dispatch it.
|
||||
self.lastPerformKeyEvent = nil
|
||||
|
||||
self.interpretKeyEvents([translationEvent])
|
||||
|
|
@ -1532,6 +1532,7 @@ extension Ghostty {
|
|||
let macosWindowShadow: Bool
|
||||
let windowTitleFontFamily: String?
|
||||
let windowAppearance: NSAppearance?
|
||||
let scrollbar: Ghostty.Config.Scrollbar
|
||||
|
||||
init() {
|
||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||
|
|
@ -1539,6 +1540,7 @@ extension Ghostty {
|
|||
self.macosWindowShadow = true
|
||||
self.windowTitleFontFamily = nil
|
||||
self.windowAppearance = nil
|
||||
self.scrollbar = .system
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
|
|
@ -1547,6 +1549,7 @@ extension Ghostty {
|
|||
self.macosWindowShadow = config.macosWindowShadow
|
||||
self.windowTitleFontFamily = config.windowTitleFontFamily
|
||||
self.windowAppearance = .init(ghosttyConfig: config)
|
||||
self.scrollbar = config.scrollbar
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ extension NSScreen {
|
|||
var displayID: UInt32? {
|
||||
deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? UInt32
|
||||
}
|
||||
|
||||
/// The stable UUID for this display, suitable for tracking across reconnects and NSScreen garbage collection.
|
||||
var displayUUID: UUID? {
|
||||
guard let displayID = displayID else { return nil }
|
||||
guard let cfuuid = CGDisplayCreateUUIDFromDisplayID(displayID)?.takeRetainedValue() else { return nil }
|
||||
return UUID(cfuuid)
|
||||
}
|
||||
|
||||
// Returns true if the given screen has a visible dock. This isn't
|
||||
// point-in-time visible, this is true if the dock is always visible
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import Foundation
|
||||
|
||||
extension UUID {
|
||||
/// Initialize a UUID from a CFUUID.
|
||||
init?(_ cfuuid: CFUUID) {
|
||||
guard let uuidString = CFUUIDCreateString(nil, cfuuid) as String? else { return nil }
|
||||
self.init(uuidString: uuidString)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import SwiftUI
|
||||
|
||||
/// An NSHostingView subclass that prevents window dragging when clicking on the view.
|
||||
///
|
||||
/// By default, NSHostingViews in the titlebar allow the window to be dragged when
|
||||
/// clicked. This subclass overrides `mouseDownCanMoveWindow` to return false,
|
||||
/// preventing the window from being dragged when the user clicks on this view.
|
||||
///
|
||||
/// This is useful for titlebar accessories that contain interactive elements
|
||||
/// (buttons, links, etc.) where you don't want accidental window dragging.
|
||||
class NonDraggableHostingView<Content: View>: NSHostingView<Content> {
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import Testing
|
||||
import Foundation
|
||||
@testable import Ghostty
|
||||
|
||||
struct ReleaseNotesTests {
|
||||
/// Test tagged release (semantic version)
|
||||
@Test func testTaggedRelease() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "1.2.3",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .tagged(let url) = notes {
|
||||
#expect(url.absoluteString == "https://ghostty.org/docs/install/release-notes/1-2-3")
|
||||
#expect(notes?.label == "View Release Notes")
|
||||
} else {
|
||||
Issue.record("Expected tagged case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Test tip release comparison with current commit
|
||||
@Test func testTipReleaseComparison() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "tip-abc1234",
|
||||
currentCommit: "def5678"
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .compareTip(let url) = notes {
|
||||
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234")
|
||||
#expect(notes?.label == "Changes Since This Tip Release")
|
||||
} else {
|
||||
Issue.record("Expected compareTip case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Test tip release without current commit
|
||||
@Test func testTipReleaseWithoutCurrentCommit() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "tip-abc1234",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .commit(let url) = notes {
|
||||
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234")
|
||||
#expect(notes?.label == "View GitHub Commit")
|
||||
} else {
|
||||
Issue.record("Expected commit case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Test tip release with empty current commit
|
||||
@Test func testTipReleaseWithEmptyCurrentCommit() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "tip-abc1234",
|
||||
currentCommit: ""
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .commit(let url) = notes {
|
||||
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/abc1234")
|
||||
} else {
|
||||
Issue.record("Expected commit case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Test version with full 40-character hash
|
||||
@Test func testFullGitHash() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "tip-1234567890abcdef1234567890abcdef12345678",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .commit(let url) = notes {
|
||||
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/commit/1234567890abcdef1234567890abcdef12345678")
|
||||
} else {
|
||||
Issue.record("Expected commit case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Test version with no recognizable pattern
|
||||
@Test func testInvalidVersion() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "unknown-version",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
#expect(notes == nil)
|
||||
}
|
||||
|
||||
/// Test semantic version with prerelease suffix should not match
|
||||
@Test func testSemanticVersionWithSuffix() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "1.2.3-beta",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
// Should not match semantic version pattern, falls back to hash detection
|
||||
#expect(notes == nil)
|
||||
}
|
||||
|
||||
/// Test semantic version with 4 components should not match
|
||||
@Test func testSemanticVersionFourComponents() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "1.2.3.4",
|
||||
currentCommit: nil
|
||||
)
|
||||
|
||||
// Should not match pattern
|
||||
#expect(notes == nil)
|
||||
}
|
||||
|
||||
/// Test version string with git hash embedded
|
||||
@Test func testVersionWithEmbeddedHash() async throws {
|
||||
let notes = UpdateState.ReleaseNotes(
|
||||
displayVersionString: "v2024.01.15-abc1234",
|
||||
currentCommit: "def5678"
|
||||
)
|
||||
|
||||
#expect(notes != nil)
|
||||
if case .compareTip(let url) = notes {
|
||||
#expect(url.absoluteString == "https://github.com/ghostty-org/ghostty/compare/def5678...abc1234")
|
||||
} else {
|
||||
Issue.record("Expected compareTip case")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import Testing
|
||||
import Foundation
|
||||
import Sparkle
|
||||
@testable import Ghostty
|
||||
|
||||
struct UpdateStateTests {
|
||||
// MARK: - Equatable Tests
|
||||
|
||||
@Test func testIdleEquality() {
|
||||
let state1: UpdateState = .idle
|
||||
let state2: UpdateState = .idle
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testCheckingEquality() {
|
||||
let state1: UpdateState = .checking(.init(cancel: {}))
|
||||
let state2: UpdateState = .checking(.init(cancel: {}))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testNotFoundEquality() {
|
||||
let state1: UpdateState = .notFound(.init(acknowledgement: {}))
|
||||
let state2: UpdateState = .notFound(.init(acknowledgement: {}))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testInstallingEquality() {
|
||||
let state1: UpdateState = .installing(.init(retryTerminatingApplication: {}))
|
||||
let state2: UpdateState = .installing(.init(retryTerminatingApplication: {}))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testPermissionRequestEquality() {
|
||||
let request1 = SPUUpdatePermissionRequest(systemProfile: [])
|
||||
let request2 = SPUUpdatePermissionRequest(systemProfile: [])
|
||||
let state1: UpdateState = .permissionRequest(.init(request: request1, reply: { _ in }))
|
||||
let state2: UpdateState = .permissionRequest(.init(request: request2, reply: { _ in }))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testReadyToInstallEquality() {
|
||||
let state1: UpdateState = .readyToInstall(.init(reply: { _ in }))
|
||||
let state2: UpdateState = .readyToInstall(.init(reply: { _ in }))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testDownloadingEqualityWithSameProgress() {
|
||||
let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500))
|
||||
let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testDownloadingInequalityWithDifferentProgress() {
|
||||
let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500))
|
||||
let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 600))
|
||||
#expect(state1 != state2)
|
||||
}
|
||||
|
||||
@Test func testDownloadingInequalityWithDifferentExpectedLength() {
|
||||
let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500))
|
||||
let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: 2000, progress: 500))
|
||||
#expect(state1 != state2)
|
||||
}
|
||||
|
||||
@Test func testDownloadingEqualityWithNilExpectedLength() {
|
||||
let state1: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500))
|
||||
let state2: UpdateState = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testExtractingEqualityWithSameProgress() {
|
||||
let state1: UpdateState = .extracting(.init(progress: 0.5))
|
||||
let state2: UpdateState = .extracting(.init(progress: 0.5))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testExtractingInequalityWithDifferentProgress() {
|
||||
let state1: UpdateState = .extracting(.init(progress: 0.5))
|
||||
let state2: UpdateState = .extracting(.init(progress: 0.6))
|
||||
#expect(state1 != state2)
|
||||
}
|
||||
|
||||
@Test func testErrorEqualityWithSameDescription() {
|
||||
let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error message"])
|
||||
let error2 = NSError(domain: "Test", code: 2, userInfo: [NSLocalizedDescriptionKey: "Error message"])
|
||||
let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {}))
|
||||
let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {}))
|
||||
#expect(state1 == state2)
|
||||
}
|
||||
|
||||
@Test func testErrorInequalityWithDifferentDescription() {
|
||||
let error1 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 1"])
|
||||
let error2 = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Error 2"])
|
||||
let state1: UpdateState = .error(.init(error: error1, retry: {}, dismiss: {}))
|
||||
let state2: UpdateState = .error(.init(error: error2, retry: {}, dismiss: {}))
|
||||
#expect(state1 != state2)
|
||||
}
|
||||
|
||||
@Test func testDifferentStatesAreNotEqual() {
|
||||
let state1: UpdateState = .idle
|
||||
let state2: UpdateState = .checking(.init(cancel: {}))
|
||||
#expect(state1 != state2)
|
||||
}
|
||||
|
||||
// MARK: - isIdle Tests
|
||||
|
||||
@Test func testIsIdleTrue() {
|
||||
let state: UpdateState = .idle
|
||||
#expect(state.isIdle == true)
|
||||
}
|
||||
|
||||
@Test func testIsIdleFalse() {
|
||||
let state: UpdateState = .checking(.init(cancel: {}))
|
||||
#expect(state.isIdle == false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import Testing
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Sparkle
|
||||
@testable import Ghostty
|
||||
|
||||
struct UpdateViewModelTests {
|
||||
// MARK: - Text Formatting Tests
|
||||
|
||||
@Test func testIdleText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .idle
|
||||
#expect(viewModel.text == "")
|
||||
}
|
||||
|
||||
@Test func testPermissionRequestText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
let request = SPUUpdatePermissionRequest(systemProfile: [])
|
||||
viewModel.state = .permissionRequest(.init(request: request, reply: { _ in }))
|
||||
#expect(viewModel.text == "Enable Automatic Updates?")
|
||||
}
|
||||
|
||||
@Test func testCheckingText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .checking(.init(cancel: {}))
|
||||
#expect(viewModel.text == "Checking for Updates…")
|
||||
}
|
||||
|
||||
@Test func testDownloadingTextWithKnownLength() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 500))
|
||||
#expect(viewModel.text == "Downloading: 50%")
|
||||
}
|
||||
|
||||
@Test func testDownloadingTextWithUnknownLength() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .downloading(.init(cancel: {}, expectedLength: nil, progress: 500))
|
||||
#expect(viewModel.text == "Downloading…")
|
||||
}
|
||||
|
||||
@Test func testDownloadingTextWithZeroExpectedLength() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .downloading(.init(cancel: {}, expectedLength: 0, progress: 500))
|
||||
#expect(viewModel.text == "Downloading…")
|
||||
}
|
||||
|
||||
@Test func testExtractingText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .extracting(.init(progress: 0.75))
|
||||
#expect(viewModel.text == "Preparing: 75%")
|
||||
}
|
||||
|
||||
@Test func testReadyToInstallText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .readyToInstall(.init(reply: { _ in }))
|
||||
#expect(viewModel.text == "Ready to Install Update")
|
||||
}
|
||||
|
||||
@Test func testInstallingText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .installing(.init(retryTerminatingApplication: {}))
|
||||
#expect(viewModel.text == "Restart to Complete Update")
|
||||
}
|
||||
|
||||
@Test func testNotFoundText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .notFound(.init(acknowledgement: {}))
|
||||
#expect(viewModel.text == "No Updates Available")
|
||||
}
|
||||
|
||||
@Test func testErrorText() {
|
||||
let viewModel = UpdateViewModel()
|
||||
let error = NSError(domain: "Test", code: 1, userInfo: [NSLocalizedDescriptionKey: "Network error"])
|
||||
viewModel.state = .error(.init(error: error, retry: {}, dismiss: {}))
|
||||
#expect(viewModel.text == "Network error")
|
||||
}
|
||||
|
||||
// MARK: - Max Width Text Tests
|
||||
|
||||
@Test func testMaxWidthTextForDownloading() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .downloading(.init(cancel: {}, expectedLength: 1000, progress: 50))
|
||||
#expect(viewModel.maxWidthText == "Downloading: 100%")
|
||||
}
|
||||
|
||||
@Test func testMaxWidthTextForExtracting() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .extracting(.init(progress: 0.5))
|
||||
#expect(viewModel.maxWidthText == "Preparing: 100%")
|
||||
}
|
||||
|
||||
@Test func testMaxWidthTextForNonProgressState() {
|
||||
let viewModel = UpdateViewModel()
|
||||
viewModel.state = .checking(.init(cancel: {}))
|
||||
#expect(viewModel.maxWidthText == viewModel.text)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ elif [ "$1" != "--update" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
zon2nix "$BUILD_ZIG_ZON" --14 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json"
|
||||
zon2nix "$BUILD_ZIG_ZON" --15 --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json"
|
||||
alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
|
||||
prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json"
|
||||
prettier --log-level warn --write "$WORK_DIR/zig-packages.json"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# This script downloads external dependencies from build.zig.zon.json that
|
||||
# are not already mirrored at deps.files.ghostty.org, saves them to a local
|
||||
# directory, and updates build.zig.zon to point to the new mirror URLs.
|
||||
#
|
||||
# The downloaded files are unmodified so their checksums and content hashes
|
||||
# will match the originals.
|
||||
#
|
||||
# After running this script, the files in the output directory can be uploaded
|
||||
# to blob storage, and build.zig.zon will already be updated with the new URLs.
|
||||
def main [
|
||||
--output: string = "tmp-mirror", # Output directory for the mirrored files
|
||||
--prefix: string = "https://deps.files.ghostty.org/", # Final URL prefix to ignore
|
||||
--dry-run, # Print what would be downloaded without downloading
|
||||
] {
|
||||
let script_dir = ($env.CURRENT_FILE | path dirname)
|
||||
let input_file = ($script_dir | path join ".." ".." "build.zig.zon.json")
|
||||
let zon_file = ($script_dir | path join ".." ".." "build.zig.zon")
|
||||
let output_dir = $output
|
||||
|
||||
# Ensure the output directory exists
|
||||
mkdir $output_dir
|
||||
|
||||
# Read and parse the JSON file
|
||||
let deps = open $input_file
|
||||
|
||||
# Track URL replacements for build.zig.zon
|
||||
mut url_replacements = []
|
||||
|
||||
# Process each dependency
|
||||
for entry in ($deps | transpose key value) {
|
||||
let key = $entry.key
|
||||
let name = $entry.value.name
|
||||
let url = $entry.value.url
|
||||
|
||||
# Skip URLs that don't start with http(s)
|
||||
if not ($url | str starts-with "http") {
|
||||
continue
|
||||
}
|
||||
|
||||
# Skip URLs already hosted at the prefix
|
||||
if ($url | str starts-with $prefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
# Extract the file extension from the URL
|
||||
let extension = ($url | parse -r '(\.[a-z0-9]+(?:\.[a-z0-9]+)?)$' | get -o capture0.0 | default "")
|
||||
|
||||
# Try to extract commit hash (40 hex chars) from URL
|
||||
let commit_hash = ($url | parse -r '([a-f0-9]{40})' | get -o capture0.0 | default "")
|
||||
|
||||
# Try to extract date pattern (YYYY-MM-DD or YYYYMMDD with optional suffixes)
|
||||
let date_pattern = ($url | parse -r '((?:release-)?20\d{2}(?:-?\d{2}){2}(?:[-]\d+)*(?:[-][a-z0-9]+)?)' | get -o capture0.0 | default "")
|
||||
|
||||
# Build filename based on what we found
|
||||
let filename = if (not ($commit_hash | is-empty)) {
|
||||
$"($name)-($commit_hash)($extension)"
|
||||
} else if (not ($date_pattern | is-empty)) {
|
||||
$"($name)-($date_pattern)($extension)"
|
||||
} else {
|
||||
$"($key)($extension)"
|
||||
}
|
||||
let new_url = $"($prefix)($filename)"
|
||||
print $"($url) -> ($filename)"
|
||||
|
||||
# Track the replacement
|
||||
$url_replacements = ($url_replacements | append {old: $url, new: $new_url})
|
||||
|
||||
# Download the file
|
||||
if not $dry_run {
|
||||
http get $url | save -f ($output_dir | path join $filename)
|
||||
}
|
||||
}
|
||||
|
||||
if $dry_run {
|
||||
print "Dry run complete - no files were downloaded\n"
|
||||
print $"Would update ($url_replacements | length) URLs in build.zig.zon"
|
||||
} else {
|
||||
print "All dependencies downloaded successfully\n"
|
||||
print $"Updating ($zon_file)..."
|
||||
|
||||
# Backup the old file
|
||||
let backup_file = $"($zon_file).bak"
|
||||
cp $zon_file $backup_file
|
||||
print $"Backed up to ($backup_file)"
|
||||
|
||||
mut zon_content = (open $zon_file)
|
||||
for replacement in $url_replacements {
|
||||
$zon_content = ($zon_content | str replace $replacement.old $replacement.new)
|
||||
}
|
||||
$zon_content | save -f $zon_file
|
||||
|
||||
print $"Updated ($url_replacements | length) URLs in build.zig.zon"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# This script generates a directory that can be uploaded to blob
|
||||
# storage to mirror our dependencies. The dependencies are unmodified
|
||||
# so their checksum and content hashes will match.
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
SCRIPT_PATH="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
|
||||
INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock"
|
||||
OUTPUT_DIR="blob"
|
||||
|
||||
# Ensure the output directory exists
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Use jq to iterate over the JSON and download files
|
||||
jq -r 'to_entries[] | "\(.key) \(.value.name) \(.value.url)"' "$INPUT_FILE" | while read -r key name url; do
|
||||
# Skip URLs that don't start with http(s). They aren't necessary for
|
||||
# our mirror.
|
||||
if ! echo "$url" | grep -Eq "^https?://"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract the file extension from the URL
|
||||
extension=$(echo "$url" | grep -oE '\.[a-z0-9]+(\.[a-z0-9]+)?$')
|
||||
|
||||
filename="${name}-${key}${extension}"
|
||||
echo "$url -> $filename"
|
||||
curl -L -o "$OUTPUT_DIR/$filename" "$url"
|
||||
done
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue