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/config.h linguist-generated=true
|
||||||
pkg/libintl/libintl.h linguist-generated=true
|
pkg/libintl/libintl.h linguist-generated=true
|
||||||
pkg/simdutf/vendor/** linguist-vendored
|
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
|
src/terminal/res/** linguist-vendored
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
name: Milestone Update
|
name: Milestone Update
|
||||||
steps:
|
steps:
|
||||||
- name: Set Milestone for PR
|
- 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
|
if: github.event.pull_request.merged == true
|
||||||
with:
|
with:
|
||||||
action: bind-pr # `bind-pr` is the default action
|
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
|
# Bind milestone to closed issue that has a merged PR fix
|
||||||
- name: Set Milestone for Issue
|
- 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'
|
if: github.event.issue.state == 'closed'
|
||||||
with:
|
with:
|
||||||
action: bind-issue
|
action: bind-issue
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,13 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
|
|
||||||
echo "Version is valid: ${{ github.event.inputs.version }}"
|
echo "Version is valid: ${{ github.event.inputs.version }}"
|
||||||
|
|
||||||
- name: Exract the Version
|
- name: Extract the Version
|
||||||
id: extract_version
|
id: extract_version
|
||||||
run: |
|
run: |
|
||||||
VERSION=${{ github.event.inputs.version }}
|
VERSION=${{ github.event.inputs.version }}
|
||||||
|
|
|
||||||
|
|
@ -83,13 +83,13 @@ jobs:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ jobs:
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
(
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
)
|
)
|
||||||
|
|
@ -34,7 +33,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Important so that build number generation works
|
# Important so that build number generation works
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -151,7 +150,6 @@ jobs:
|
||||||
(
|
(
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
(
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
)
|
)
|
||||||
|
|
@ -163,12 +161,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- 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
|
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
|
||||||
|
|
||||||
- name: Update Release
|
- name: Update Release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
name: 'Ghostty Tip ("Nightly")'
|
name: 'Ghostty Tip ("Nightly")'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
@ -206,7 +204,6 @@ jobs:
|
||||||
(
|
(
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
(
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
)
|
)
|
||||||
|
|
@ -359,7 +356,7 @@ jobs:
|
||||||
|
|
||||||
# Update Release
|
# Update Release
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
name: 'Ghostty Tip ("Nightly")'
|
name: 'Ghostty Tip ("Nightly")'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
@ -373,7 +370,6 @@ jobs:
|
||||||
# Create our appcast for Sparkle
|
# Create our appcast for Sparkle
|
||||||
- name: Generate Appcast
|
- name: Generate Appcast
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
env:
|
env:
|
||||||
|
|
@ -408,7 +404,6 @@ jobs:
|
||||||
# gets out of sync with the binaries.
|
# gets out of sync with the binaries.
|
||||||
- name: Prep R2 Storage for Appcast
|
- name: Prep R2 Storage for Appcast
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -418,7 +413,6 @@ jobs:
|
||||||
|
|
||||||
- name: Upload Appcast to R2
|
- name: Upload Appcast to R2
|
||||||
if: |
|
if: |
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||||
|
|
@ -444,7 +438,6 @@ jobs:
|
||||||
(
|
(
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
(
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
)
|
)
|
||||||
|
|
@ -590,7 +583,7 @@ jobs:
|
||||||
|
|
||||||
# Update Release
|
# Update Release
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
name: 'Ghostty Tip ("Nightly")'
|
name: 'Ghostty Tip ("Nightly")'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
@ -629,7 +622,6 @@ jobs:
|
||||||
(
|
(
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
(
|
||||||
github.event.workflow_run.conclusion == 'success' &&
|
|
||||||
github.repository_owner == 'ghostty-org' &&
|
github.repository_owner == 'ghostty-org' &&
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
)
|
)
|
||||||
|
|
@ -775,7 +767,7 @@ jobs:
|
||||||
|
|
||||||
# Update Release
|
# Update Release
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
|
||||||
with:
|
with:
|
||||||
name: 'Ghostty Tip ("Nightly")'
|
name: 'Ghostty Tip ("Nightly")'
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ jobs:
|
||||||
- build-dist
|
- build-dist
|
||||||
- build-examples
|
- build-examples
|
||||||
- build-flatpak
|
- build-flatpak
|
||||||
- build-freebsd
|
|
||||||
- build-libghostty-vt
|
- build-libghostty-vt
|
||||||
- build-linux
|
- build-linux
|
||||||
- build-linux-libghostty
|
- build-linux-libghostty
|
||||||
|
|
@ -73,14 +72,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -95,7 +94,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
dir: [c-vt, zig-vt]
|
dir: [c-vt, c-vt-key-encode, c-vt-paste, zig-vt]
|
||||||
name: Example ${{ matrix.dir }}
|
name: Example ${{ matrix.dir }}
|
||||||
runs-on: namespace-profile-ghostty-sm
|
runs-on: namespace-profile-ghostty-sm
|
||||||
needs: test
|
needs: test
|
||||||
|
|
@ -107,14 +106,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -140,14 +139,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -174,14 +173,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -205,6 +204,7 @@ jobs:
|
||||||
aarch64-linux,
|
aarch64-linux,
|
||||||
x86_64-linux,
|
x86_64-linux,
|
||||||
x86_64-windows,
|
x86_64-windows,
|
||||||
|
wasm32-freestanding,
|
||||||
]
|
]
|
||||||
runs-on: namespace-profile-ghostty-sm
|
runs-on: namespace-profile-ghostty-sm
|
||||||
needs: test
|
needs: test
|
||||||
|
|
@ -216,14 +216,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -252,14 +252,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -281,14 +281,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -314,14 +314,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -333,7 +333,7 @@ jobs:
|
||||||
run: nix build .#ghostty-releasefast
|
run: nix build .#ghostty-releasefast
|
||||||
|
|
||||||
- name: Check version
|
- 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
|
- name: Check to see if the binary has been stripped
|
||||||
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols'
|
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols'
|
||||||
|
|
@ -342,7 +342,7 @@ jobs:
|
||||||
run: nix build .#ghostty-debug
|
run: nix build .#ghostty-debug
|
||||||
|
|
||||||
- name: Check version
|
- 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
|
- name: Check to see if the binary has not been stripped
|
||||||
run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main'
|
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
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -509,11 +509,11 @@ jobs:
|
||||||
- name: Install zig
|
- name: Install zig
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
# Get the zig version from build.zig so that it only needs to be updated
|
# Get the zig version from build.zig.zon so that it only needs to be updated
|
||||||
$fileContent = Get-Content -Path "build.zig" -Raw
|
$fileContent = Get-Content -Path "build.zig.zon" -Raw
|
||||||
$pattern = 'buildpkg\.requireZig\("(.*?)"\);'
|
$pattern = 'minimum_zig_version\s*=\s*"([^"]+)"'
|
||||||
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
||||||
$version = "zig-windows-x86_64-$zigVersion"
|
$version = "zig-x86_64-windows-$zigVersion"
|
||||||
Write-Output $version
|
Write-Output $version
|
||||||
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
|
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
|
||||||
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
|
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
|
||||||
|
|
@ -564,6 +564,8 @@ jobs:
|
||||||
test:
|
test:
|
||||||
if: github.repository == 'ghostty-org/ghostty'
|
if: github.repository == 'ghostty-org/ghostty'
|
||||||
runs-on: namespace-profile-ghostty-md
|
runs-on: namespace-profile-ghostty-md
|
||||||
|
outputs:
|
||||||
|
zig_version: ${{ steps.zig.outputs.version }}
|
||||||
env:
|
env:
|
||||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||||
|
|
@ -571,15 +573,20 @@ jobs:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
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
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -614,14 +621,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -662,14 +669,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -697,14 +704,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -761,14 +768,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -790,12 +797,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -818,12 +825,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -845,12 +852,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -872,12 +879,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -899,12 +906,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -926,12 +933,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -960,12 +967,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -987,12 +994,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
- uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1022,14 +1029,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1051,7 +1058,7 @@ jobs:
|
||||||
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
||||||
|
|
||||||
- name: Configure Namespace powered Buildx
|
- 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
|
- name: Download Source Tarball Artifacts
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
|
|
@ -1089,7 +1096,7 @@ jobs:
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- 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:
|
with:
|
||||||
bundle: com.mitchellh.ghostty
|
bundle: com.mitchellh.ghostty
|
||||||
manifest-path: flatpak/com.mitchellh.ghostty.yml
|
manifest-path: flatpak/com.mitchellh.ghostty.yml
|
||||||
|
|
@ -1110,14 +1117,14 @@ jobs:
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# 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:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1142,7 +1149,8 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
release:
|
release:
|
||||||
- "14.3"
|
- "14.3"
|
||||||
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
|
- "15.0"
|
||||||
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Ghostty
|
- name: Checkout Ghostty
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
@ -1163,14 +1171,19 @@ jobs:
|
||||||
devel/gettext \
|
devel/gettext \
|
||||||
devel/git \
|
devel/git \
|
||||||
devel/pkgconf \
|
devel/pkgconf \
|
||||||
|
ftp/curl \
|
||||||
graphics/wayland \
|
graphics/wayland \
|
||||||
lang/zig \
|
|
||||||
security/ca_root_nss \
|
security/ca_root_nss \
|
||||||
textproc/hs-pandoc \
|
textproc/hs-pandoc \
|
||||||
x11-fonts/jetbrains-mono \
|
x11-fonts/jetbrains-mono \
|
||||||
x11-toolkits/libadwaita \
|
x11-toolkits/libadwaita \
|
||||||
x11-toolkits/gtk40 \
|
x11-toolkits/gtk40 \
|
||||||
x11-toolkits/gtk4-layer-shell
|
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: |
|
run: |
|
||||||
zig env
|
zig env
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,14 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Cache
|
- name: Setup Cache
|
||||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
uses: namespacelabs/nscloud-cache-action@caff5c9dc51d8126e7d16141fb015c478374256b # v1.2.19
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
uses: cachix/install-nix-action@fd24c48048070c1be9acd18c9d369a83f0fe94d7 # v31.8.1
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- 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
|
## Directory Structure
|
||||||
|
|
||||||
- Shared Zig core: `src/`
|
- Shared Zig core: `src/`
|
||||||
- C API: `include/ghostty.h`
|
- C API: `include`
|
||||||
- macOS app: `macos/`
|
- macOS app: `macos/`
|
||||||
- GTK (Linux and FreeBSD) app: `src/apprt/gtk`
|
- 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
|
## macOS App
|
||||||
|
|
||||||
- Do not use `xcodebuild`
|
- Do not use `xcodebuild`
|
||||||
- Use `zig build` to build the macOS app and any shared Zig code
|
- 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/ko_KR.UTF-8.po @ghostty-org/ko_KR
|
||||||
/po/he_IL.UTF-8.po @ghostty-org/he_IL
|
/po/he_IL.UTF-8.po @ghostty-org/he_IL
|
||||||
/po/it_IT.UTF-8.po @ghostty-org/it_IT
|
/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/zh_TW.UTF-8.po @ghostty-org/zh_TW
|
||||||
/po/hr_HR.UTF-8.po @ghostty-org/hr_HR
|
/po/hr_HR.UTF-8.po @ghostty-org/hr_HR
|
||||||
/po/vi_VN.UTF-8.po @ghostty-org/vi_VN
|
/po/vi_VN.UTF-8.po @ghostty-org/vi_VN
|
||||||
|
|
|
||||||
58
Doxyfile
58
Doxyfile
|
|
@ -2,9 +2,42 @@
|
||||||
|
|
||||||
DOXYFILE_ENCODING = UTF-8
|
DOXYFILE_ENCODING = UTF-8
|
||||||
PROJECT_NAME = "libghostty"
|
PROJECT_NAME = "libghostty"
|
||||||
INPUT = include/ghostty/vt.h
|
PROJECT_LOGO = images/gnome/64.png
|
||||||
|
INPUT = include/ghostty
|
||||||
INPUT_ENCODING = UTF-8
|
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
|
# HTML Output
|
||||||
|
|
@ -12,6 +45,26 @@ RECURSIVE = NO
|
||||||
|
|
||||||
GENERATE_HTML = YES
|
GENERATE_HTML = YES
|
||||||
HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
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
|
# Man Output
|
||||||
|
|
@ -20,6 +73,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
||||||
GENERATE_MAN = YES
|
GENERATE_MAN = YES
|
||||||
MAN_OUTPUT = zig-out/share/man
|
MAN_OUTPUT = zig-out/share/man
|
||||||
MAN_EXTENSION = .3
|
MAN_EXTENSION = .3
|
||||||
|
MAN_LINKS = YES
|
||||||
|
|
||||||
#---------------------------------------------------------------------------
|
#---------------------------------------------------------------------------
|
||||||
# Other Output
|
# 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
|
## Xcode Version and SDKs
|
||||||
|
|
||||||
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
|
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
|
A common issue is that the incorrect version of Xcode is either
|
||||||
installed or selected. Use the `xcode-select` command to
|
installed or selected. Use the `xcode-select` command to
|
||||||
ensure that the correct version of Xcode is selected:
|
ensure that the correct version of Xcode is selected:
|
||||||
|
|
||||||
```shell-session
|
```shell-session
|
||||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
sudo xcode-select --switch /Applications/Xcode.app
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> Main branch development of Ghostty is preparing for the next major
|
> Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**.
|
||||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
|
||||||
> **Xcode 26 and the macOS 26 SDK**.
|
|
||||||
>
|
>
|
||||||
> You do not need to be running on macOS 26 to build Ghostty, you can
|
> 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
|
## AI and Agents
|
||||||
|
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf \
|
rm -rf \
|
||||||
zig-out zig-cache \
|
zig-out .zig-cache \
|
||||||
macos/build \
|
macos/build \
|
||||||
macos/GhosttyKit.xcframework
|
macos/GhosttyKit.xcframework
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
|
|
|
||||||
|
|
@ -193,4 +193,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
|
||||||
> purposely contain sensitive information, but it does contain the full
|
> purposely contain sensitive information, but it does contain the full
|
||||||
> stack memory of each thread at the time of the crash. This information
|
> 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
|
> is used to rebuild the stack trace but can also contain sensitive data
|
||||||
> depending when the crash occurred.
|
> depending on when the crash occurred.
|
||||||
|
|
|
||||||
31
build.zig
31
build.zig
|
|
@ -2,16 +2,19 @@ const std = @import("std");
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const buildpkg = @import("src/build/main.zig");
|
const buildpkg = @import("src/build/main.zig");
|
||||||
|
const appVersion = @import("build.zig.zon").version;
|
||||||
|
const minimumZigVersion = @import("build.zig.zon").minimum_zig_version;
|
||||||
|
|
||||||
comptime {
|
comptime {
|
||||||
buildpkg.requireZig("0.14.0");
|
buildpkg.requireZig(minimumZigVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(b: *std.Build) !void {
|
pub fn build(b: *std.Build) !void {
|
||||||
// This defines all the available build options (e.g. `-D`). If you
|
// This defines all the available build options (e.g. `-D`). If you
|
||||||
// want to know what options are available, you can run `--help` or
|
// want to know what options are available, you can run `--help` or
|
||||||
// you can read `src/build/Config.zig`.
|
// 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 test_filters = b.option(
|
||||||
[][]const u8,
|
[][]const u8,
|
||||||
"test-filter",
|
"test-filter",
|
||||||
|
|
@ -98,10 +101,19 @@ pub fn build(b: *std.Build) !void {
|
||||||
);
|
);
|
||||||
|
|
||||||
// libghostty-vt
|
// libghostty-vt
|
||||||
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
|
const libghostty_vt_shared = shared: {
|
||||||
|
if (config.target.result.cpu.arch.isWasm()) {
|
||||||
|
break :shared try buildpkg.GhosttyLibVt.initWasm(
|
||||||
b,
|
b,
|
||||||
&mod,
|
&mod,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break :shared try buildpkg.GhosttyLibVt.initShared(
|
||||||
|
b,
|
||||||
|
&mod,
|
||||||
|
);
|
||||||
|
};
|
||||||
libghostty_vt_shared.install(libvt_step);
|
libghostty_vt_shared.install(libvt_step);
|
||||||
libghostty_vt_shared.install(b.getInstallStep());
|
libghostty_vt_shared.install(b.getInstallStep());
|
||||||
|
|
||||||
|
|
@ -245,12 +257,17 @@ pub fn build(b: *std.Build) !void {
|
||||||
{
|
{
|
||||||
const mod_vt_test = b.addTest(.{
|
const mod_vt_test = b.addTest(.{
|
||||||
.root_module = mod.vt,
|
.root_module = mod.vt,
|
||||||
.target = config.target,
|
|
||||||
.optimize = config.optimize,
|
|
||||||
.filters = test_filters,
|
.filters = test_filters,
|
||||||
});
|
});
|
||||||
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
|
const mod_vt_test_run = b.addRunArtifact(mod_vt_test);
|
||||||
test_lib_vt_step.dependOn(&mod_vt_test_run.step);
|
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
|
// Tests
|
||||||
|
|
@ -267,6 +284,8 @@ pub fn build(b: *std.Build) !void {
|
||||||
.omit_frame_pointer = false,
|
.omit_frame_pointer = false,
|
||||||
.unwind_tables = .sync,
|
.unwind_tables = .sync,
|
||||||
}),
|
}),
|
||||||
|
// Crash on x86_64 without this
|
||||||
|
.use_llvm = true,
|
||||||
});
|
});
|
||||||
if (config.emit_test_exe) b.installArtifact(test_exe);
|
if (config.emit_test_exe) b.installArtifact(test_exe);
|
||||||
_ = try deps.add(test_exe);
|
_ = try deps.add(test_exe);
|
||||||
|
|
@ -276,7 +295,7 @@ pub fn build(b: *std.Build) !void {
|
||||||
test_step.dependOn(&test_run.step);
|
test_step.dependOn(&test_run.step);
|
||||||
|
|
||||||
// Normal tests always test our libghostty modules
|
// Normal tests always test our libghostty modules
|
||||||
test_step.dependOn(test_lib_vt_step);
|
//test_step.dependOn(test_lib_vt_step);
|
||||||
|
|
||||||
// Valgrind test running
|
// Valgrind test running
|
||||||
const valgrind_run = b.addSystemCommand(&.{
|
const valgrind_run = b.addSystemCommand(&.{
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,63 @@
|
||||||
.{
|
.{
|
||||||
.name = .ghostty,
|
.name = .ghostty,
|
||||||
.version = "1.2.1",
|
.version = "1.3.0-dev",
|
||||||
.paths = .{""},
|
.paths = .{""},
|
||||||
.fingerprint = 0x64407a2a0b4147e5,
|
.fingerprint = 0x64407a2a0b4147e5,
|
||||||
|
.minimum_zig_version = "0.15.2",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
// Zig libs
|
// Zig libs
|
||||||
|
|
||||||
.libxev = .{
|
.libxev = .{
|
||||||
// mitchellh/libxev
|
// mitchellh/libxev
|
||||||
.url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||||
.hash = "libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q",
|
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.vaxis = .{
|
.vaxis = .{
|
||||||
// rockorager/libvaxis
|
// rockorager/libvaxis
|
||||||
.url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||||
.hash = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn",
|
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.z2d = .{
|
.z2d = .{
|
||||||
// vancluever/z2d
|
// vancluever/z2d
|
||||||
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||||
.hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
|
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.zig_objc = .{
|
.zig_objc = .{
|
||||||
// mitchellh/zig-objc
|
// mitchellh/zig-objc
|
||||||
.url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
.url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||||
.hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
.hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.zig_js = .{
|
.zig_js = .{
|
||||||
// mitchellh/zig-js
|
// mitchellh/zig-js
|
||||||
.url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
.url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||||
.hash = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ",
|
.hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.ziglyph = .{
|
.uucode = .{
|
||||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
// TODO: currently the use-llvm branch because its broken on self-hosted
|
||||||
.hash = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf",
|
.url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||||
.lazy = true,
|
.hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT",
|
||||||
},
|
},
|
||||||
.zig_wayland = .{
|
.zig_wayland = .{
|
||||||
// codeberg ifreund/zig-wayland
|
// codeberg ifreund/zig-wayland
|
||||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
.url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||||
.hash = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy",
|
.hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.zf = .{
|
.zf = .{
|
||||||
// natecraddock/zf
|
// natecraddock/zf
|
||||||
.url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||||
.hash = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9",
|
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
.gobject = .{
|
.gobject = .{
|
||||||
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
|
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
|
||||||
// Temporary until we generate them at build time automatically.
|
// 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",
|
.hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
|
|
@ -115,8 +116,8 @@
|
||||||
// Other
|
// Other
|
||||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||||
.iterm2_themes = .{
|
.iterm2_themes = .{
|
||||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||||
.hash = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3",
|
.hash = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq",
|
||||||
.lazy = true,
|
.lazy = true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
},
|
},
|
||||||
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
||||||
"name": "gobject",
|
"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="
|
"hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="
|
||||||
},
|
},
|
||||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||||
|
|
@ -49,10 +49,10 @@
|
||||||
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
||||||
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
||||||
},
|
},
|
||||||
"N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3": {
|
"N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq": {
|
||||||
"name": "iterm2_themes",
|
"name": "iterm2_themes",
|
||||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||||
"hash": "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw="
|
"hash": "sha256-vFn6MiR8tVkGyjSV7pz4k4msviSCjGHKM2SU84lJp/k="
|
||||||
},
|
},
|
||||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||||
"name": "jetbrains_mono",
|
"name": "jetbrains_mono",
|
||||||
|
|
@ -64,10 +64,10 @@
|
||||||
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
|
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
|
||||||
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
|
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
|
||||||
},
|
},
|
||||||
"libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q": {
|
"libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs": {
|
||||||
"name": "libxev",
|
"name": "libxev",
|
||||||
"url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
"url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||||
"hash": "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc="
|
"hash": "sha256-YAPqa5bkpRihKPkyMn15oRvTCZaxO3O66ymRY3lIfdc="
|
||||||
},
|
},
|
||||||
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
|
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
|
||||||
"name": "libxml2",
|
"name": "libxml2",
|
||||||
|
|
@ -109,10 +109,20 @@
|
||||||
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
|
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
|
||||||
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
|
"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",
|
"name": "vaxis",
|
||||||
"url": "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
"url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||||
"hash": "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="
|
"hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="
|
||||||
},
|
},
|
||||||
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
|
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
|
||||||
"name": "wayland",
|
"name": "wayland",
|
||||||
|
|
@ -129,45 +139,35 @@
|
||||||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||||
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
||||||
},
|
},
|
||||||
"z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": {
|
"z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": {
|
||||||
"name": "z2d",
|
"name": "z2d",
|
||||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||||
"hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="
|
"hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="
|
||||||
},
|
},
|
||||||
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
|
"zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {
|
||||||
"name": "zf",
|
"name": "zf",
|
||||||
"url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||||
"hash": "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="
|
"hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="
|
||||||
},
|
},
|
||||||
"zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM": {
|
"zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": {
|
||||||
"name": "zg",
|
|
||||||
"url": "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
|
||||||
"hash": "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="
|
|
||||||
},
|
|
||||||
"N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ": {
|
|
||||||
"name": "zig_js",
|
"name": "zig_js",
|
||||||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||||
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
|
"hash": "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M="
|
||||||
},
|
},
|
||||||
"zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
|
"zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK": {
|
||||||
"name": "zig_objc",
|
"name": "zig_objc",
|
||||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||||
"hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
|
"hash": "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw="
|
||||||
},
|
},
|
||||||
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
|
"wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe": {
|
||||||
"name": "zig_wayland",
|
"name": "zig_wayland",
|
||||||
"url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||||
"hash": "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="
|
"hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="
|
||||||
},
|
},
|
||||||
"zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj": {
|
"zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": {
|
||||||
"name": "zigimg",
|
"name": "zigimg",
|
||||||
"url": "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d",
|
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||||
"hash": "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="
|
"hash": "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM="
|
||||||
},
|
|
||||||
"ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf": {
|
|
||||||
"name": "ziglyph",
|
|
||||||
"url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
|
||||||
"hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="
|
|
||||||
},
|
},
|
||||||
"N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": {
|
"N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o": {
|
||||||
"name": "zlib",
|
"name": "zlib",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
fetchurl,
|
fetchurl,
|
||||||
fetchgit,
|
fetchgit,
|
||||||
runCommandLocal,
|
runCommandLocal,
|
||||||
zig_0_14,
|
zig_0_15,
|
||||||
name ? "zig-packages",
|
name ? "zig-packages",
|
||||||
}: let
|
}: let
|
||||||
unpackZigArtifact = {
|
unpackZigArtifact = {
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
}:
|
}:
|
||||||
runCommandLocal name
|
runCommandLocal name
|
||||||
{
|
{
|
||||||
nativeBuildInputs = [zig_0_14];
|
nativeBuildInputs = [zig_0_15];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
|
hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
|
||||||
|
|
@ -126,7 +126,7 @@ in
|
||||||
name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV";
|
name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "gobject";
|
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=";
|
hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -163,11 +163,11 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3";
|
name = "N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "iterm2_themes";
|
name = "iterm2_themes";
|
||||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz";
|
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz";
|
||||||
hash = "sha256-JPY9M50d/n6rGzWt0aQZIU7IBMWru2IAqe9Vu1x5CMw=";
|
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 {
|
path = fetchZigArtifact {
|
||||||
name = "libxev";
|
name = "libxev";
|
||||||
url = "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz";
|
url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz";
|
||||||
hash = "sha256-KaozYKEhhT/6sInef7/8O/60LDBJN+8QmdLuNY1Gkmc=";
|
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 {
|
path = fetchZigArtifact {
|
||||||
name = "vaxis";
|
name = "vaxis";
|
||||||
url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23";
|
url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
|
||||||
hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY=";
|
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 {
|
path = fetchZigArtifact {
|
||||||
name = "z2d";
|
name = "z2d";
|
||||||
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz";
|
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz";
|
||||||
hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=";
|
hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9";
|
name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "zf";
|
name = "zf";
|
||||||
url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz";
|
url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
|
||||||
hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I=";
|
hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM";
|
name = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi";
|
||||||
path = fetchZigArtifact {
|
|
||||||
name = "zg";
|
|
||||||
url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc";
|
|
||||||
hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA=";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ";
|
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "zig_js";
|
name = "zig_js";
|
||||||
url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz";
|
url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz";
|
||||||
hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=";
|
hash = "sha256-TCAY5WAV05UEuAkDhq2c6Tk/ODgAhdnDI3O/flb8c6M=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk";
|
name = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "zig_objc";
|
name = "zig_objc";
|
||||||
url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
|
url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz";
|
||||||
hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
|
hash = "sha256-3YSvc3YlNW/NciyzCQnzsujXAmZ89XlxSqfqvArAjsw=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy";
|
name = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "zig_wayland";
|
name = "zig_wayland";
|
||||||
url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz";
|
url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz";
|
||||||
hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk=";
|
hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=";
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj";
|
name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms";
|
||||||
path = fetchZigArtifact {
|
path = fetchZigArtifact {
|
||||||
name = "zigimg";
|
name = "zigimg";
|
||||||
url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d";
|
url = "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz";
|
||||||
hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0=";
|
hash = "sha256-LB7Xa6KzVRRUSwwnyWM+y6fDG+kIDjfnoBDJO1obxVM=";
|
||||||
};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
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=";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc
|
git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732
|
||||||
git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d
|
|
||||||
git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23
|
|
||||||
https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz
|
|
||||||
https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz
|
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/NerdFontsSymbolsOnly-3.4.0.tar.gz
|
||||||
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.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/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
|
||||||
https://deps.files.ghostty.org/gettext-0.24.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/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/gtk4-layer-shell-1.1.0.tar.gz
|
||||||
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
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/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
|
||||||
https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.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/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/libxml2-2.11.5.tar.gz
|
||||||
https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
|
https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
|
||||||
https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.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/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
|
||||||
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.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/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-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
|
||||||
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
|
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
|
||||||
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
|
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
|
||||||
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
|
https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz
|
||||||
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.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://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/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz
|
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz
|
||||||
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
|
https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
|
||||||
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
|
https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
|
||||||
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
|
https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz
|
||||||
https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.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,
|
.name = .c_vt,
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0x413a8529b1255f9a,
|
.fingerprint = 0x413a8529b1255f9a,
|
||||||
.minimum_zig_version = "0.14.1",
|
.minimum_zig_version = "0.15.1",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
// Ghostty dependency. In reality, you'd probably use a URL-based
|
// Ghostty dependency. In reality, you'd probably use a URL-based
|
||||||
// dependency like the one showed (and commented out) below this one.
|
// dependency like the one showed (and commented out) below this one.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
#include <ghostty/vt.h>
|
#include <ghostty/vt.h>
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
|
|
@ -6,6 +8,29 @@ int main() {
|
||||||
if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) {
|
if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) {
|
||||||
return 1;
|
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);
|
ghostty_osc_free(parser);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
.name = .zig_vt,
|
.name = .zig_vt,
|
||||||
.version = "0.0.0",
|
.version = "0.0.0",
|
||||||
.fingerprint = 0x6045575a7a8387e6,
|
.fingerprint = 0x6045575a7a8387e6,
|
||||||
.minimum_zig_version = "0.14.1",
|
.minimum_zig_version = "0.15.1",
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
// Ghostty dependency. In reality, you'd probably use a URL-based
|
// Ghostty dependency. In reality, you'd probably use a URL-based
|
||||||
// dependency like the one showed (and commented out) below this one.
|
// dependency like the one showed (and commented out) below this one.
|
||||||
|
|
|
||||||
16
flake.lock
16
flake.lock
|
|
@ -36,15 +36,15 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748189127,
|
"lastModified": 315532800,
|
||||||
"narHash": "sha256-zRDR+EbbeObu4V2X5QCd2Bk5eltfDlCr5yvhBwUT6pY=",
|
"narHash": "sha256-sV6pJNzFkiPc6j9Bi9JuHBnWdVhtKB/mHgVmMPvDFlk=",
|
||||||
"rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334",
|
"rev": "82c2e0d6dde50b17ae366d2aa36f224dc19af469",
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"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": {
|
"nixpkgs_2": {
|
||||||
|
|
@ -97,11 +97,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1748261582,
|
"lastModified": 1760401936,
|
||||||
"narHash": "sha256-3i0IL3s18hdDlbsf0/E+5kyPRkZwGPbSFngq5eToiAA=",
|
"narHash": "sha256-/zj5GYO5PKhBWGzbHbqT+ehY8EghuABdQ2WGfCwZpCQ=",
|
||||||
"owner": "mitchellh",
|
"owner": "mitchellh",
|
||||||
"repo": "zig-overlay",
|
"repo": "zig-overlay",
|
||||||
"rev": "aafb1b093fb838f7a02613b719e85ec912914221",
|
"rev": "365085b6652259753b598d43b723858184980bbe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@
|
||||||
# We want to stay as up to date as possible but need to be careful that the
|
# 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
|
# glibc versions used by our dependencies from Nix are compatible with the
|
||||||
# system glibc that the user is building for.
|
# 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";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
# Used for shell.nix
|
# Used for shell.nix
|
||||||
|
|
@ -47,7 +49,7 @@
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
in {
|
||||||
devShell.${system} = pkgs.callPackage ./nix/devShell.nix {
|
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 {};
|
wraptest = pkgs.callPackage ./nix/wraptest.nix {};
|
||||||
zon2nix = zon2nix;
|
zon2nix = zon2nix;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ modules:
|
||||||
- chmod a+x /app/zig/zig
|
- chmod a+x /app/zig/zig
|
||||||
sources:
|
sources:
|
||||||
- type: archive
|
- type: archive
|
||||||
sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c
|
sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239
|
||||||
url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz
|
url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz
|
||||||
only-arches: [x86_64]
|
only-arches: [x86_64]
|
||||||
- type: archive
|
- type: archive
|
||||||
sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b
|
sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f
|
||||||
url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz
|
url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz
|
||||||
only-arches: [aarch64]
|
only-arches: [aarch64]
|
||||||
|
|
||||||
- name: bzip2-redirect
|
- name: bzip2-redirect
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"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",
|
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||||
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
||||||
},
|
},
|
||||||
|
|
@ -61,9 +61,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250916-134637-76894f0/ghostty-themes.tgz",
|
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251013-150525-147b9d3/ghostty-themes.tgz",
|
||||||
"dest": "vendor/p/N-V-__8AAGsjAwAxRB3Y9Akv_HeLfvJA-tIqW6ACnBhWosM3",
|
"dest": "vendor/p/N-V-__8AAPk1AwDvSIEm8ftVzPLJr-Uuzz65Ss2R4ljTXjSq",
|
||||||
"sha256": "24f63d339d1dfe7eab1b35add1a419214ec804c5abbb6200a9ef55bb5c7908cc"
|
"sha256": "bc59fa32247cb55906ca3495ee9cf89389acbe24828c61ca336494f38949a7f9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
|
|
@ -79,9 +79,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz",
|
"url": "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||||
"dest": "vendor/p/libxev-0.0.0-86vtc2UaEwDfiTKX3iBI-s_hdzfzWQUarT3MUrmUQl-Q",
|
"dest": "vendor/p/libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
|
||||||
"sha256": "29aa3360a121853ffab089de7fbffc3bfeb42c304937ef1099d2ee358d469267"
|
"sha256": "6003ea6b96e4a518a128f932327d79a11bd30996b13b73baeb29916379487dd7"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
|
|
@ -133,9 +133,21 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/rockorager/libvaxis",
|
"url": "https://github.com/jacobsandlund/uucode",
|
||||||
"commit": "1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23",
|
"commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
|
||||||
"dest": "vendor/p/vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"
|
"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",
|
"type": "archive",
|
||||||
|
|
@ -157,51 +169,39 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz",
|
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||||
"dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP",
|
"dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||||
"sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d"
|
"sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz",
|
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||||
"dest": "vendor/p/zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9",
|
"dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||||
"sha256": "de7ba535077fe2b678a5a7972585f002588d37244db08397feadf3d4907c0bb2"
|
"sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://codeberg.org/atman/zg",
|
|
||||||
"commit": "4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
|
||||||
"dest": "vendor/p/zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||||
"dest": "vendor/p/N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ",
|
"dest": "vendor/p/zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
|
||||||
"sha256": "7f235e0956c2f5401a28963a261019953d00e3bf4cfc029830f2161196c3583d"
|
"sha256": "4c2018e56015d39504b8090386ad9ce9393f38380085d9c32373bf7e56fc73a3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||||
"sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
|
"sha256": "dd84af737625356fcd722cb30909f3b2e8d702667cf579714aa7eabc0ac08ecc"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz",
|
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||||
"dest": "vendor/p/wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy",
|
"dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||||
"sha256": "13bec6675e403d86db3b55b39ae262f1e1bdfe24056dcd82824341c6308b5219"
|
"sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/TUSF/zigimg",
|
|
||||||
"commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d",
|
|
||||||
"dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||||
"dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf",
|
"dest": "vendor/p/zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms",
|
||||||
"sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9"
|
"sha256": "2c1ed76ba2b35514544b0c27c9633ecba7c31be9080e37e7a010c93b5a1bc553"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,7 @@ typedef struct {
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char* ptr;
|
const char* ptr;
|
||||||
uintptr_t len;
|
uintptr_t len;
|
||||||
|
bool sentinel;
|
||||||
} ghostty_string_s;
|
} ghostty_string_s;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -732,6 +733,21 @@ typedef struct {
|
||||||
int8_t progress;
|
int8_t progress;
|
||||||
} ghostty_action_progress_report_s;
|
} 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
|
// apprt.Action.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_QUIT,
|
GHOSTTY_ACTION_QUIT,
|
||||||
|
|
@ -758,6 +774,7 @@ typedef enum {
|
||||||
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
||||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||||
GHOSTTY_ACTION_CELL_SIZE,
|
GHOSTTY_ACTION_CELL_SIZE,
|
||||||
|
GHOSTTY_ACTION_SCROLLBAR,
|
||||||
GHOSTTY_ACTION_RENDER,
|
GHOSTTY_ACTION_RENDER,
|
||||||
GHOSTTY_ACTION_INSPECTOR,
|
GHOSTTY_ACTION_INSPECTOR,
|
||||||
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
||||||
|
|
@ -787,6 +804,7 @@ typedef enum {
|
||||||
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
||||||
GHOSTTY_ACTION_PROGRESS_REPORT,
|
GHOSTTY_ACTION_PROGRESS_REPORT,
|
||||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||||
|
GHOSTTY_ACTION_COMMAND_FINISHED,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
|
|
@ -799,6 +817,7 @@ typedef union {
|
||||||
ghostty_action_size_limit_s size_limit;
|
ghostty_action_size_limit_s size_limit;
|
||||||
ghostty_action_initial_size_s initial_size;
|
ghostty_action_initial_size_s initial_size;
|
||||||
ghostty_action_cell_size_s cell_size;
|
ghostty_action_cell_size_s cell_size;
|
||||||
|
ghostty_action_scrollbar_s scrollbar;
|
||||||
ghostty_action_inspector_e inspector;
|
ghostty_action_inspector_e inspector;
|
||||||
ghostty_action_desktop_notification_s desktop_notification;
|
ghostty_action_desktop_notification_s desktop_notification;
|
||||||
ghostty_action_set_title_s set_title;
|
ghostty_action_set_title_s set_title;
|
||||||
|
|
@ -818,6 +837,7 @@ typedef union {
|
||||||
ghostty_action_close_tab_mode_e close_tab_mode;
|
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||||
ghostty_surface_message_childexited_s child_exited;
|
ghostty_surface_message_childexited_s child_exited;
|
||||||
ghostty_action_progress_report_s progress_report;
|
ghostty_action_progress_report_s progress_report;
|
||||||
|
ghostty_action_command_finished_s command_finished;
|
||||||
} ghostty_action_u;
|
} ghostty_action_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @file vt.h
|
* @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
|
* This library provides functionality for parsing and handling terminal
|
||||||
* escape sequences as well as maintaining terminal state such as styles,
|
* escape sequences as well as maintaining terminal state such as styles,
|
||||||
|
|
@ -11,6 +11,52 @@
|
||||||
* stable and is definitely going to change.
|
* 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
|
#ifndef GHOSTTY_VT_H
|
||||||
#define GHOSTTY_VT_H
|
#define GHOSTTY_VT_H
|
||||||
|
|
||||||
|
|
@ -18,201 +64,11 @@
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <ghostty/vt/result.h>
|
||||||
#include <stddef.h>
|
#include <ghostty/vt/allocator.h>
|
||||||
#include <stdint.h>
|
#include <ghostty/vt/osc.h>
|
||||||
|
#include <ghostty/vt/key.h>
|
||||||
//-------------------------------------------------------------------
|
#include <ghostty/vt/paste.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);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#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>
|
<key>NSMenuItem</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>default</key>
|
<key>default</key>
|
||||||
<string>New Ghostty Tab Here</string>
|
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSMessage</key>
|
<key>NSMessage</key>
|
||||||
<string>openTab</string>
|
<string>openTab</string>
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
<key>NSMenuItem</key>
|
<key>NSMenuItem</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>default</key>
|
<key>default</key>
|
||||||
<string>New Ghostty Window Here</string>
|
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here</string>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSMessage</key>
|
<key>NSMessage</key>
|
||||||
<string>openWindow</string>
|
<string>openWindow</string>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@
|
||||||
"Features/App Intents/CommandPaletteIntent.swift",
|
"Features/App Intents/CommandPaletteIntent.swift",
|
||||||
"Features/App Intents/Entities/CommandEntity.swift",
|
"Features/App Intents/Entities/CommandEntity.swift",
|
||||||
"Features/App Intents/Entities/TerminalEntity.swift",
|
"Features/App Intents/Entities/TerminalEntity.swift",
|
||||||
|
"Features/App Intents/FocusTerminalIntent.swift",
|
||||||
"Features/App Intents/GetTerminalDetailsIntent.swift",
|
"Features/App Intents/GetTerminalDetailsIntent.swift",
|
||||||
"Features/App Intents/GhosttyIntentError.swift",
|
"Features/App Intents/GhosttyIntentError.swift",
|
||||||
"Features/App Intents/InputIntent.swift",
|
"Features/App Intents/InputIntent.swift",
|
||||||
|
|
@ -95,6 +96,7 @@
|
||||||
Features/QuickTerminal/QuickTerminalController.swift,
|
Features/QuickTerminal/QuickTerminalController.swift,
|
||||||
Features/QuickTerminal/QuickTerminalPosition.swift,
|
Features/QuickTerminal/QuickTerminalPosition.swift,
|
||||||
Features/QuickTerminal/QuickTerminalScreen.swift,
|
Features/QuickTerminal/QuickTerminalScreen.swift,
|
||||||
|
Features/QuickTerminal/QuickTerminalScreenStateCache.swift,
|
||||||
Features/QuickTerminal/QuickTerminalSize.swift,
|
Features/QuickTerminal/QuickTerminalSize.swift,
|
||||||
Features/QuickTerminal/QuickTerminalSpaceBehavior.swift,
|
Features/QuickTerminal/QuickTerminalSpaceBehavior.swift,
|
||||||
Features/QuickTerminal/QuickTerminalWindow.swift,
|
Features/QuickTerminal/QuickTerminalWindow.swift,
|
||||||
|
|
@ -124,7 +126,14 @@
|
||||||
"Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift",
|
"Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift",
|
||||||
"Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift",
|
"Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift",
|
||||||
"Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift",
|
"Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift",
|
||||||
|
Features/Update/UpdateBadge.swift,
|
||||||
|
Features/Update/UpdateController.swift,
|
||||||
Features/Update/UpdateDelegate.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/FullscreenMode+Extension.swift",
|
||||||
Ghostty/Ghostty.Command.swift,
|
Ghostty/Ghostty.Command.swift,
|
||||||
Ghostty/Ghostty.Error.swift,
|
Ghostty/Ghostty.Error.swift,
|
||||||
|
|
@ -133,6 +142,7 @@
|
||||||
Ghostty/Ghostty.Surface.swift,
|
Ghostty/Ghostty.Surface.swift,
|
||||||
Ghostty/InspectorView.swift,
|
Ghostty/InspectorView.swift,
|
||||||
"Ghostty/NSEvent+Extension.swift",
|
"Ghostty/NSEvent+Extension.swift",
|
||||||
|
Ghostty/SurfaceScrollView.swift,
|
||||||
Ghostty/SurfaceView_AppKit.swift,
|
Ghostty/SurfaceView_AppKit.swift,
|
||||||
Helpers/AppInfo.swift,
|
Helpers/AppInfo.swift,
|
||||||
Helpers/CodableBridge.swift,
|
Helpers/CodableBridge.swift,
|
||||||
|
|
@ -159,6 +169,7 @@
|
||||||
Helpers/KeyboardLayout.swift,
|
Helpers/KeyboardLayout.swift,
|
||||||
Helpers/LastWindowPosition.swift,
|
Helpers/LastWindowPosition.swift,
|
||||||
Helpers/MetalView.swift,
|
Helpers/MetalView.swift,
|
||||||
|
Helpers/NonDraggableHostingView.swift,
|
||||||
Helpers/PermissionRequest.swift,
|
Helpers/PermissionRequest.swift,
|
||||||
Helpers/Private/CGS.swift,
|
Helpers/Private/CGS.swift,
|
||||||
Helpers/Private/Dock.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_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_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||||
|
INFOPLIST_PREPROCESS = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
|
|
@ -766,7 +778,7 @@
|
||||||
EXECUTABLE_NAME = ghostty;
|
EXECUTABLE_NAME = ghostty;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
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.";
|
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_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_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||||
|
INFOPLIST_PREPROCESS = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
|
|
@ -838,6 +851,7 @@
|
||||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
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_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||||
|
INFOPLIST_PREPROCESS = YES;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d",
|
"revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb",
|
||||||
"version" : "2.7.3"
|
"version" : "2.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
import OSLog
|
import OSLog
|
||||||
import Sparkle
|
import Sparkle
|
||||||
|
|
@ -98,8 +99,10 @@ class AppDelegate: NSObject,
|
||||||
)
|
)
|
||||||
|
|
||||||
/// Manages updates
|
/// Manages updates
|
||||||
let updaterController: SPUStandardUpdaterController
|
let updateController = UpdateController()
|
||||||
let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
|
var updateViewModel: UpdateViewModel {
|
||||||
|
updateController.viewModel
|
||||||
|
}
|
||||||
|
|
||||||
/// The elapsed time since the process was started
|
/// The elapsed time since the process was started
|
||||||
var timeSinceLaunch: TimeInterval {
|
var timeSinceLaunch: TimeInterval {
|
||||||
|
|
@ -118,7 +121,12 @@ class AppDelegate: NSObject,
|
||||||
/// The custom app icon image that is currently in use.
|
/// The custom app icon image that is currently in use.
|
||||||
@Published private(set) var appIcon: NSImage? = nil {
|
@Published private(set) var appIcon: NSImage? = nil {
|
||||||
didSet {
|
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
|
NSApplication.shared.applicationIconImage = appIcon
|
||||||
|
#endif
|
||||||
let appPath = Bundle.main.bundlePath
|
let appPath = Bundle.main.bundlePath
|
||||||
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
|
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
|
||||||
NSWorkspace.shared.noteFileSystemChanged(appPath)
|
NSWorkspace.shared.noteFileSystemChanged(appPath)
|
||||||
|
|
@ -126,15 +134,6 @@ class AppDelegate: NSObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
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()
|
super.init()
|
||||||
|
|
||||||
ghostty.delegate = self
|
ghostty.delegate = self
|
||||||
|
|
@ -179,7 +178,7 @@ class AppDelegate: NSObject,
|
||||||
ghosttyConfigDidChange(config: ghostty.config)
|
ghosttyConfigDidChange(config: ghostty.config)
|
||||||
|
|
||||||
// Start our update checker.
|
// Start our update checker.
|
||||||
updaterController.startUpdater()
|
updateController.startUpdater()
|
||||||
|
|
||||||
// Register our service provider. This must happen after everything is initialized.
|
// Register our service provider. This must happen after everything is initialized.
|
||||||
NSApp.servicesProvider = ServiceProvider()
|
NSApp.servicesProvider = ServiceProvider()
|
||||||
|
|
@ -324,6 +323,12 @@ class AppDelegate: NSObject,
|
||||||
let windows = NSApplication.shared.windows
|
let windows = NSApplication.shared.windows
|
||||||
if (windows.isEmpty) { return .terminateNow }
|
if (windows.isEmpty) { return .terminateNow }
|
||||||
|
|
||||||
|
// If we've already accepted to install an update, then we don't need to
|
||||||
|
// confirm quit. The user is already expecting the update to happen.
|
||||||
|
if updateController.isInstalling {
|
||||||
|
return .terminateNow
|
||||||
|
}
|
||||||
|
|
||||||
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
|
// 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
|
// quite work with SwiftUI because windows are retained on close. So instead we check
|
||||||
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
|
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
|
||||||
|
|
@ -471,7 +476,12 @@ class AppDelegate: NSObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ghostty.config.macosDockDropBehavior {
|
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)
|
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
|
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
|
||||||
// user-based defaults.
|
// user-based defaults.
|
||||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
||||||
updaterController.updater.automaticallyChecksForUpdates = false
|
updateController.updater.automaticallyChecksForUpdates = false
|
||||||
updaterController.updater.automaticallyDownloadsUpdates = false
|
updateController.updater.automaticallyDownloadsUpdates = false
|
||||||
} else if let autoUpdate = config.autoUpdate {
|
} else if let autoUpdate = config.autoUpdate {
|
||||||
updaterController.updater.automaticallyChecksForUpdates =
|
updateController.updater.automaticallyChecksForUpdates =
|
||||||
autoUpdate == .check || autoUpdate == .download
|
autoUpdate == .check || autoUpdate == .download
|
||||||
updaterController.updater.automaticallyDownloadsUpdates =
|
updateController.updater.automaticallyDownloadsUpdates =
|
||||||
autoUpdate == .download
|
autoUpdate == .download
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1004,7 +1014,8 @@ class AppDelegate: NSObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func checkForUpdates(_ sender: Any?) {
|
@IBAction func checkForUpdates(_ sender: Any?) {
|
||||||
updaterController.checkForUpdates(sender)
|
updateController.checkForUpdates()
|
||||||
|
//UpdateSimulator.happyPath.simulate(with: updateViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func newWindow(_ sender: Any?) {
|
@IBAction func newWindow(_ sender: Any?) {
|
||||||
|
|
@ -1012,7 +1023,10 @@ class AppDelegate: NSObject,
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func newTab(_ sender: Any?) {
|
@IBAction func newTab(_ sender: Any?) {
|
||||||
_ = TerminalController.newTab(ghostty)
|
_ = TerminalController.newTab(
|
||||||
|
ghostty,
|
||||||
|
from: TerminalController.preferredParent?.window
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func closeAllWindows(_ sender: Any?) {
|
@IBAction func closeAllWindows(_ sender: Any?) {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = .background
|
static var supportedModes: IntentModes = .background
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var command: CommandEntity
|
var command: CommandEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = .background
|
static var supportedModes: IntentModes = .background
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
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
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = .background
|
static var supportedModes: IntentModes = .background
|
||||||
|
#endif
|
||||||
|
|
||||||
static var parameterSummary: some ParameterSummary {
|
static var parameterSummary: some ParameterSummary {
|
||||||
Summary("Get \(\.$detail) from \(\.$terminal)")
|
Summary("Get \(\.$detail) from \(\.$terminal)")
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var terminal: TerminalEntity
|
var terminal: TerminalEntity
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult {
|
func perform() async throws -> some IntentResult {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var action: String
|
var action: String
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = [.background, .foreground]
|
static var supportedModes: IntentModes = [.background, .foreground]
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||||
|
|
|
||||||
|
|
@ -43,11 +43,15 @@ struct NewTerminalIntent: AppIntent {
|
||||||
)
|
)
|
||||||
var parent: TerminalEntity?
|
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, *)
|
@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")
|
@available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
|
||||||
static var openAppWhenRun = true
|
static var openAppWhenRun = false
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult & ReturnsValue<TerminalEntity?> {
|
func perform() async throws -> some IntentResult & ReturnsValue<TerminalEntity?> {
|
||||||
|
|
@ -96,6 +100,11 @@ struct NewTerminalIntent: AppIntent {
|
||||||
parent = nil
|
parent = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if !NSApp.isActive {
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
switch location {
|
switch location {
|
||||||
case .window:
|
case .window:
|
||||||
let newController = TerminalController.newWindow(
|
let newController = TerminalController.newWindow(
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent {
|
||||||
static var title: LocalizedStringResource = "Open the Quick Terminal"
|
static var title: LocalizedStringResource = "Open the Quick Terminal"
|
||||||
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
|
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
|
||||||
|
|
||||||
|
#if compiler(>=6.2)
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
static var supportedModes: IntentModes = .background
|
static var supportedModes: IntentModes = .background
|
||||||
|
#endif
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
|
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,29 @@ struct CommandOption: Identifiable, Hashable {
|
||||||
let title: String
|
let title: String
|
||||||
let description: String?
|
let description: String?
|
||||||
let symbols: [String]?
|
let symbols: [String]?
|
||||||
|
let leadingIcon: String?
|
||||||
|
let badge: String?
|
||||||
|
let emphasis: Bool
|
||||||
let action: () -> Void
|
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 {
|
static func == (lhs: CommandOption, rhs: CommandOption) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +219,7 @@ fileprivate struct CommandTable: View {
|
||||||
} else {
|
} else {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
|
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
|
||||||
CommandRow(
|
CommandRow(
|
||||||
option: option,
|
option: option,
|
||||||
|
|
@ -240,15 +261,36 @@ fileprivate struct CommandRow: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
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)
|
Text(option.title)
|
||||||
|
.fontWeight(option.emphasis ? .medium : .regular)
|
||||||
|
|
||||||
Spacer()
|
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 {
|
if let symbols = option.symbols {
|
||||||
ShortcutSymbolsView(symbols: symbols)
|
ShortcutSymbolsView(symbols: symbols)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(8)
|
.padding(8)
|
||||||
|
.contentShape(Rectangle())
|
||||||
.background(
|
.background(
|
||||||
isSelected
|
isSelected
|
||||||
? Color.accentColor.opacity(0.2)
|
? Color.accentColor.opacity(0.2)
|
||||||
|
|
@ -256,6 +298,10 @@ fileprivate struct CommandRow: View {
|
||||||
? Color.secondary.opacity(0.2)
|
? Color.secondary.opacity(0.2)
|
||||||
: Color.clear)
|
: Color.clear)
|
||||||
)
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 5)
|
||||||
|
.strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5)
|
||||||
|
)
|
||||||
.cornerRadius(5)
|
.cornerRadius(5)
|
||||||
}
|
}
|
||||||
.help(option.description ?? "")
|
.help(option.description ?? "")
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,53 @@ struct TerminalCommandPaletteView: View {
|
||||||
/// The configuration so we can lookup keyboard shortcuts.
|
/// The configuration so we can lookup keyboard shortcuts.
|
||||||
@ObservedObject var ghosttyConfig: Ghostty.Config
|
@ObservedObject var ghosttyConfig: Ghostty.Config
|
||||||
|
|
||||||
|
/// The update view model for showing update commands.
|
||||||
|
var updateViewModel: UpdateViewModel?
|
||||||
|
|
||||||
/// The callback when an action is submitted.
|
/// The callback when an action is submitted.
|
||||||
var onAction: ((String) -> Void)
|
var onAction: ((String) -> Void)
|
||||||
|
|
||||||
// The commands available to the command palette.
|
// The commands available to the command palette.
|
||||||
private var commandOptions: [CommandOption] {
|
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 {
|
do {
|
||||||
return try surface.commands().map { c in
|
let terminalCommands = try surface.commands().map { c in
|
||||||
return CommandOption(
|
return CommandOption(
|
||||||
title: c.title,
|
title: c.title,
|
||||||
description: c.description,
|
description: c.description,
|
||||||
|
|
@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View {
|
||||||
onAction(c.action)
|
onAction(c.action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
options.append(contentsOf: terminalCommands)
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|
|
||||||
|
|
@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
// The active space when the quick terminal was last shown.
|
// The active space when the quick terminal was last shown.
|
||||||
private var previousActiveSpace: CGSSpace? = nil
|
private var previousActiveSpace: CGSSpace? = nil
|
||||||
|
|
||||||
/// The saved state when the quick terminal's surface tree becomes empty.
|
/// Cache for per-screen window state.
|
||||||
///
|
private let screenStateCache = QuickTerminalScreenStateCache()
|
||||||
/// 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>
|
|
||||||
|
|
||||||
/// Non-nil if we have hidden dock state.
|
/// Non-nil if we have hidden dock state.
|
||||||
private var hiddenDock: HiddenDock? = nil
|
private var hiddenDock: HiddenDock? = nil
|
||||||
|
|
@ -46,10 +41,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
self.position = position
|
self.position = position
|
||||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
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
|
// Important detail here: we initialize with an empty surface tree so
|
||||||
// that we don't start a terminal process. This gets started when the
|
// that we don't start a terminal process. This gets started when the
|
||||||
// first terminal is shown in `animateIn`.
|
// first terminal is shown in `animateIn`.
|
||||||
|
|
@ -247,6 +238,22 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
|
|
||||||
// MARK: Base Controller Overrides
|
// 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>) {
|
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||||
super.surfaceTreeDidChange(from: from, to: to)
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
|
|
||||||
|
|
@ -363,17 +370,15 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||||
|
|
||||||
// Grab our last closed frame to use, and clear our state since we're animating in.
|
// Grab our last closed frame to use from the cache.
|
||||||
// We only use the last closed frame if we're opening on the same screen.
|
let closedFrame = screenStateCache.frame(for: screen)
|
||||||
let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame
|
|
||||||
lastClosedFrames.removeObject(forKey: screen)
|
|
||||||
|
|
||||||
// Move our window off screen to the initial animation position.
|
// Move our window off screen to the initial animation position.
|
||||||
position.setInitial(
|
position.setInitial(
|
||||||
in: window,
|
in: window,
|
||||||
on: screen,
|
on: screen,
|
||||||
terminalSize: derivedConfig.quickTerminalSize,
|
terminalSize: derivedConfig.quickTerminalSize,
|
||||||
closedFrame: lastClosedFrame)
|
closedFrame: closedFrame)
|
||||||
|
|
||||||
// We need to set our window level to a high value. In testing, only
|
// 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
|
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||||
|
|
@ -408,7 +413,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
in: window.animator(),
|
in: window.animator(),
|
||||||
on: screen,
|
on: screen,
|
||||||
terminalSize: derivedConfig.quickTerminalSize,
|
terminalSize: derivedConfig.quickTerminalSize,
|
||||||
closedFrame: lastClosedFrame)
|
closedFrame: closedFrame)
|
||||||
}, completionHandler: {
|
}, completionHandler: {
|
||||||
// There is a very minor delay here so waiting at least an event loop tick
|
// 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.
|
// 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
|
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||||
// would reset the window to its minimum content size.
|
// would reset the window to its minimum content size.
|
||||||
if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen {
|
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.
|
// If we hid the dock then we unhide it.
|
||||||
|
|
@ -582,7 +587,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
alert.alertStyle = .warning
|
alert.alertStyle = .warning
|
||||||
alert.beginSheetModal(for: window)
|
alert.beginSheetModal(for: window)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: First Responder
|
// MARK: First Responder
|
||||||
|
|
||||||
@IBAction override func closeWindow(_ sender: Any) {
|
@IBAction override func closeWindow(_ sender: Any) {
|
||||||
|
|
@ -720,14 +724,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||||
hidden = false
|
hidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LastClosedState {
|
|
||||||
let frame: NSRect
|
|
||||||
|
|
||||||
init(frame: NSRect) {
|
|
||||||
self.frame = frame
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Notification.Name {
|
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) {
|
VStack(alignment: .leading) {
|
||||||
Text("Coming Soon. 🚧").font(.title)
|
Text("Coming Soon. 🚧").font(.title)
|
||||||
Text("You can't configure settings in the GUI yet. To modify settings, " +
|
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)
|
.multilineTextAlignment(.leading)
|
||||||
.lineLimit(nil)
|
.lineLimit(nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ struct TerminalSplitTreeView: View {
|
||||||
onResize: onResize)
|
onResize: onResize)
|
||||||
// This is necessary because we can't rely on SwiftUI's implicit
|
// This is necessary because we can't rely on SwiftUI's implicit
|
||||||
// structural identity to detect changes to this view. Due to
|
// 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
|
// See: https://github.com/ghostty-org/ghostty/issues/7546
|
||||||
.id(node.structuralIdentity)
|
.id(node.structuralIdentity)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,9 @@ class BaseTerminalController: NSWindowController,
|
||||||
/// This can be set to show/hide the command palette.
|
/// This can be set to show/hide the command palette.
|
||||||
@Published var commandPaletteIsShowing: Bool = false
|
@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.
|
/// Whether the terminal surface should focus when the mouse is over it.
|
||||||
var focusFollowsMouse: Bool {
|
var focusFollowsMouse: Bool {
|
||||||
self.derivedConfig.focusFollowsMouse
|
self.derivedConfig.focusFollowsMouse
|
||||||
|
|
@ -233,6 +236,21 @@ class BaseTerminalController: NSWindowController,
|
||||||
return newView
|
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.
|
/// Called when the surfaceTree variable changed.
|
||||||
///
|
///
|
||||||
/// Subclasses should call super first.
|
/// Subclasses should call super first.
|
||||||
|
|
@ -552,22 +570,11 @@ class BaseTerminalController: NSWindowController,
|
||||||
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
|
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
|
||||||
guard let direction = directionAny as? Ghostty.SplitFocusDirection 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
|
// Find the node for the target surface
|
||||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
||||||
|
|
||||||
// Find the next surface to focus
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -728,6 +735,10 @@ class BaseTerminalController: NSWindowController,
|
||||||
|
|
||||||
func cellSizeDidChange(to: NSSize) {
|
func cellSizeDidChange(to: NSSize) {
|
||||||
guard derivedConfig.windowStepResize else { return }
|
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
|
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
|
// MARK: Clipboard Confirmation
|
||||||
|
|
||||||
|
|
@ -881,6 +903,28 @@ class BaseTerminalController: NSWindowController,
|
||||||
fullscreenStyle = NativeFullscreen(window)
|
fullscreenStyle = NativeFullscreen(window)
|
||||||
fullscreenStyle?.delegate = self
|
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
|
// MARK: NSWindowDelegate
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
case "hidden": "TerminalHiddenTitlebar"
|
case "hidden": "TerminalHiddenTitlebar"
|
||||||
case "transparent": "TerminalTransparentTitlebar"
|
case "transparent": "TerminalTransparentTitlebar"
|
||||||
case "tabs":
|
case "tabs":
|
||||||
|
#if compiler(>=6.2)
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
"TerminalTabsTitlebarTahoe"
|
"TerminalTabsTitlebarTahoe"
|
||||||
} else {
|
} else {
|
||||||
"TerminalTabsTitlebarVentura"
|
"TerminalTabsTitlebarVentura"
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
"TerminalTabsTitlebarVentura"
|
||||||
|
#endif
|
||||||
default: defaultValue
|
default: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject {
|
||||||
|
|
||||||
/// The command palette state.
|
/// The command palette state.
|
||||||
var commandPaletteIsShowing: Bool { get set }
|
var commandPaletteIsShowing: Bool { get set }
|
||||||
|
|
||||||
|
/// The update overlay should be visible.
|
||||||
|
var updateOverlayIsVisible: Bool { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main terminal view. This terminal view supports splits.
|
/// The main terminal view. This terminal view supports splits.
|
||||||
|
|
@ -105,10 +108,33 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||||
TerminalCommandPaletteView(
|
TerminalCommandPaletteView(
|
||||||
surfaceView: surfaceView,
|
surfaceView: surfaceView,
|
||||||
isPresented: $viewModel.commandPaletteIsShowing,
|
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)
|
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
|
import AppKit
|
||||||
|
|
||||||
class HiddenTitlebarTerminalWindow: TerminalWindow {
|
class HiddenTitlebarTerminalWindow: TerminalWindow {
|
||||||
|
// No titlebar, we don't support accessories.
|
||||||
|
override var supportsUpdateAccessory: Bool { false }
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
super.awakeFromNib()
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ import GhosttyKit
|
||||||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||||
/// style and configuration of the window based on the app configuration.
|
/// style and configuration of the window based on the app configuration.
|
||||||
class TerminalWindow: NSWindow {
|
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
|
/// 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.
|
/// used by the manual float on top menu item feature.
|
||||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||||
|
|
@ -15,9 +21,19 @@ class TerminalWindow: NSWindow {
|
||||||
/// Reset split zoom button in titlebar
|
/// Reset split zoom button in titlebar
|
||||||
private let resetZoomAccessory = NSTitlebarAccessoryViewController()
|
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.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private(set) var derivedConfig: DerivedConfig = .init()
|
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.
|
/// Gets the terminal controller from the window controller.
|
||||||
var terminalController: TerminalController? {
|
var terminalController: TerminalController? {
|
||||||
windowController as? TerminalController
|
windowController as? TerminalController
|
||||||
|
|
@ -35,6 +51,9 @@ class TerminalWindow: NSWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func awakeFromNib() {
|
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
|
// 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
|
// again. I'm not sure why this is required. If you don't do this, then
|
||||||
// tabs restore as separate windows.
|
// tabs restore as separate windows.
|
||||||
|
|
@ -85,6 +104,17 @@ class TerminalWindow: NSWindow {
|
||||||
}))
|
}))
|
||||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||||
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
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,
|
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||||
|
|
@ -104,6 +134,11 @@ class TerminalWindow: NSWindow {
|
||||||
override var canBecomeKey: Bool { return true }
|
override var canBecomeKey: Bool { return true }
|
||||||
override var canBecomeMain: 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() {
|
override func becomeKey() {
|
||||||
super.becomeKey()
|
super.becomeKey()
|
||||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
resetZoomTabButton.contentTintColor = .controlAccentColor
|
||||||
|
|
@ -124,6 +159,12 @@ class TerminalWindow: NSWindow {
|
||||||
} else {
|
} else {
|
||||||
tabBarDidDisappear()
|
tabBarDidDisappear()
|
||||||
}
|
}
|
||||||
|
viewModel.isMainWindow = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func resignMain() {
|
||||||
|
super.resignMain()
|
||||||
|
viewModel.isMainWindow = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mergeAllWindows(_ sender: Any?) {
|
override func mergeAllWindows(_ sender: Any?) {
|
||||||
|
|
@ -164,9 +205,16 @@ class TerminalWindow: NSWindow {
|
||||||
|
|
||||||
/// Returns true if there is a tab bar visible on this window.
|
/// Returns true if there is a tab bar visible on this window.
|
||||||
var hasTabBar: Bool {
|
var hasTabBar: Bool {
|
||||||
|
// TODO: use titlebarView to find it instead
|
||||||
contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil
|
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 {
|
func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool {
|
||||||
if childViewController.identifier == nil {
|
if childViewController.identifier == nil {
|
||||||
// The good case
|
// The good case
|
||||||
|
|
@ -198,6 +246,9 @@ class TerminalWindow: NSWindow {
|
||||||
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
||||||
removeTitlebarAccessoryViewController(at: idx)
|
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() {
|
private func tabBarDidDisappear() {
|
||||||
|
|
@ -260,7 +311,7 @@ class TerminalWindow: NSWindow {
|
||||||
button.isBordered = false
|
button.isBordered = false
|
||||||
button.allowsExpansionToolTips = true
|
button.allowsExpansionToolTips = true
|
||||||
button.toolTip = "Reset Zoom"
|
button.toolTip = "Reset Zoom"
|
||||||
button.contentTintColor = .controlAccentColor
|
button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor
|
||||||
button.state = .on
|
button.state = .on
|
||||||
button.image = NSImage(named:"ResetZoom")
|
button.image = NSImage(named:"ResetZoom")
|
||||||
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
|
button.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
|
// Whenever we change the window title we must also update our
|
||||||
// tab title if we're using custom fonts.
|
// tab title if we're using custom fonts.
|
||||||
tab.attributedTitle = attributedTitle
|
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)
|
let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize)
|
||||||
|
|
||||||
titlebarTextField?.font = font
|
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
|
tab.attributedTitle = attributedTitle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -467,28 +530,28 @@ extension TerminalWindow {
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
@Published var isSurfaceZoomed: Bool = false
|
@Published var isSurfaceZoomed: Bool = false
|
||||||
@Published var hasToolbar: 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 {
|
struct ResetZoomAccessoryView: View {
|
||||||
@ObservedObject var viewModel: ViewModel
|
@ObservedObject var viewModel: ViewModel
|
||||||
let action: () -> Void
|
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 {
|
var body: some View {
|
||||||
if viewModel.isSurfaceZoomed {
|
if viewModel.isSurfaceZoomed {
|
||||||
VStack {
|
VStack {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
Image("ResetZoom")
|
Image("ResetZoom")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.help("Reset Split Zoom")
|
.help("Reset Split Zoom")
|
||||||
|
|
@ -497,10 +560,24 @@ extension TerminalWindow {
|
||||||
}
|
}
|
||||||
// With a toolbar, the window title is taller, so we need more padding
|
// With a toolbar, the window title is taller, so we need more padding
|
||||||
// to properly align.
|
// to properly align.
|
||||||
.padding(.top, topPadding)
|
.padding(.top, viewModel.accessoryTopPadding)
|
||||||
// We always need space at the end of the titlebar
|
// We always need space at the end of the titlebar
|
||||||
.padding(.trailing, 10)
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,31 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
/// The view model for SwiftUI views
|
/// The view model for SwiftUI views
|
||||||
private var viewModel = ViewModel()
|
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 {
|
deinit {
|
||||||
tabBarObserver = nil
|
tabBarObserver = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSWindow
|
// 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 {
|
override var title: String {
|
||||||
didSet {
|
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
|
// 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.
|
// on this function to learn why we need to check this here.
|
||||||
setupTabBar()
|
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 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.
|
// this, detect the tab bar being added, and override its behavior.
|
||||||
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) {
|
||||||
// If this is the tab bar then we need to set it up for the titlebar
|
// If this is the tab bar then we need to set it up for the titlebar
|
||||||
guard isTabBar(childViewController) else {
|
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)
|
super.addTitlebarAccessoryViewController(childViewController)
|
||||||
return
|
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
|
// 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
|
// we don't do this before the call below, we'll trigger an AppKit
|
||||||
// assertion.
|
// assertion.
|
||||||
|
|
@ -112,19 +144,34 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
guard tabBarObserver == nil else { return }
|
guard tabBarObserver == nil else { return }
|
||||||
|
|
||||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
// 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.
|
// View model updates must happen on their own ticks.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [weak self] in
|
||||||
self.viewModel.hasTabBar = true
|
self?.viewModel.hasTabBar = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find our clip view
|
// Find our clip view
|
||||||
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
|
guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return }
|
||||||
guard let accessoryView = clipView.subviews[safe: 0] 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 }
|
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.
|
// The container is the view that we'll constrain our tab bar within.
|
||||||
let container = toolbarView
|
let container = toolbarView
|
||||||
|
|
||||||
|
|
@ -205,6 +252,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
case .title:
|
case .title:
|
||||||
let item = NSToolbarItem(itemIdentifier: .title)
|
let item = NSToolbarItem(itemIdentifier: .title)
|
||||||
item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel))
|
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.visibilityPriority = .user
|
||||||
item.isEnabled = true
|
item.isEnabled = true
|
||||||
|
|
||||||
|
|
@ -221,8 +270,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool
|
||||||
// MARK: SwiftUI
|
// MARK: SwiftUI
|
||||||
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
|
@Published var titleFont: NSFont?
|
||||||
@Published var title: String = "👻 Ghostty"
|
@Published var title: String = "👻 Ghostty"
|
||||||
@Published var hasTabBar: Bool = false
|
@Published var hasTabBar: Bool = false
|
||||||
|
@Published var isMainWindow: Bool = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,15 +296,24 @@ extension TitlebarTabsTahoeTerminalWindow {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if !viewModel.hasTabBar {
|
if !viewModel.hasTabBar {
|
||||||
Text(title)
|
titleText
|
||||||
.lineLimit(1)
|
|
||||||
.truncationMode(.tail)
|
|
||||||
} else {
|
} else {
|
||||||
// 1x1.gif strikes again! For real: if we render a zero-sized
|
// 1x1.gif strikes again! For real: if we render a zero-sized
|
||||||
// view here then the toolbar just disappears our view. I don't
|
// 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)
|
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.
|
/// Titlebar tabs for macOS 13 to 15.
|
||||||
class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
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
|
/// 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.
|
/// be updated whenever the window background color or surrounding elements changes.
|
||||||
fileprivate var isLightTheme: Bool = false
|
fileprivate var isLightTheme: Bool = false
|
||||||
|
|
@ -141,6 +145,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||||
super.syncAppearance(surfaceConfig)
|
super.syncAppearance(surfaceConfig)
|
||||||
|
|
||||||
// Update our window light/darkness based on our updated background color
|
// Update our window light/darkness based on our updated background color
|
||||||
|
let themeChanged = isLightTheme != OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||||
isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||||
|
|
||||||
// Update our titlebar color
|
// Update our titlebar color
|
||||||
|
|
@ -150,7 +155,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||||
titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity)
|
titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOpaque) {
|
if (isOpaque || themeChanged) {
|
||||||
// If there is transparency, calling this will make the titlebar opaque
|
// If there is transparency, calling this will make the titlebar opaque
|
||||||
// so we only call this if we are opaque.
|
// so we only call this if we are opaque.
|
||||||
updateTabBar()
|
updateTabBar()
|
||||||
|
|
@ -183,41 +188,33 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||||
// so we need to do it manually.
|
// so we need to do it manually.
|
||||||
private func updateNewTabButtonOpacity() {
|
private func updateNewTabButtonOpacity() {
|
||||||
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
||||||
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return }
|
||||||
$0 as? NSImageView != nil
|
|
||||||
}) as? NSImageView else { return }
|
|
||||||
|
|
||||||
newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5
|
newTabButtonImageView.alphaValue = isKeyWindow ? 1 : 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color the new tab button's image to match the color of the tab title/keyboard shortcut labels,
|
/// Update: This method only add a vibrant overlay now,
|
||||||
// just as it does in the stock tab bar.
|
/// 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() {
|
private func updateNewTabButtonImage() {
|
||||||
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
guard let newTabButton: NSButton = titlebarContainer?.firstDescendant(withClassName: "NSTabBarNewTabButton") as? NSButton else { return }
|
||||||
guard let newTabButtonImageView: NSImageView = newTabButton.subviews.first(where: {
|
guard let newTabButtonImageView = newTabButton.firstDescendant(withClassName: "NSImageView") as? NSImageView else { return }
|
||||||
$0 as? NSImageView != nil
|
|
||||||
}) as? NSImageView else { return }
|
|
||||||
guard let newTabButtonImage = newTabButtonImageView.image else { return }
|
guard let newTabButtonImage = newTabButtonImageView.image else { return }
|
||||||
|
|
||||||
|
let imageLayer = newTabButtonImageLayer ?? VibrantLayer(forAppearance: isLightTheme ? .light : .dark)!
|
||||||
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.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.contentsGravity = .resizeAspect
|
||||||
imageLayer.contents = newImage
|
|
||||||
imageLayer.opacity = 0.5
|
imageLayer.opacity = 0.5
|
||||||
|
|
||||||
newTabButtonImageLayer = imageLayer
|
newTabButtonImageLayer = imageLayer
|
||||||
}
|
|
||||||
|
|
||||||
newTabButtonImageView.isHidden = true
|
|
||||||
newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer()
|
newTabButton.layer?.sublayers?.first(where: { $0.className == "VibrantLayer" })?.removeFromSuperlayer()
|
||||||
newTabButton.layer?.addSublayer(newTabButtonImageLayer!)
|
newTabButton.layer?.addSublayer(newTabButtonImageLayer!)
|
||||||
}
|
}
|
||||||
|
|
@ -448,6 +445,13 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addWindowButtonsBackdrop(titlebarView: NSView, toolbarView: NSView) {
|
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?.removeFromSuperview()
|
||||||
windowButtonsBackdrop = nil
|
windowButtonsBackdrop = nil
|
||||||
|
|
||||||
|
|
@ -466,16 +470,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow {
|
||||||
|
|
||||||
private func addWindowDragHandle(titlebarView: NSView, toolbarView: NSView) {
|
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 we already made the view, just make sure it's unhidden and correctly placed as a subview.
|
||||||
if let view = windowDragHandle {
|
guard windowDragHandle?.superview != titlebarView.superview else {
|
||||||
view.removeFromSuperview()
|
// similar to `addWindowButtonsBackdrop`
|
||||||
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
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
windowDragHandle?.removeFromSuperview()
|
||||||
|
|
||||||
let view = WindowDragView()
|
let view = WindowDragView()
|
||||||
view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle")
|
view.identifier = NSUserInterfaceItemIdentifier("_windowDragHandle")
|
||||||
|
|
@ -536,7 +535,10 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
||||||
// This must be weak because the window has this view. Otherwise
|
// This must be weak because the window has this view. Otherwise
|
||||||
// a retain cycle occurs.
|
// a retain cycle occurs.
|
||||||
private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow?
|
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()
|
private let overlayLayer = VibrantLayer()
|
||||||
|
|
||||||
var isHighlighted: Bool = true {
|
var isHighlighted: Bool = true {
|
||||||
|
|
@ -565,7 +567,6 @@ fileprivate class WindowButtonsBackdropView: NSView {
|
||||||
|
|
||||||
init(window: TitlebarTabsVenturaTerminalWindow) {
|
init(window: TitlebarTabsVenturaTerminalWindow) {
|
||||||
self.terminalWindow = window
|
self.terminalWindow = window
|
||||||
self.isLightTheme = window.isLightTheme
|
|
||||||
|
|
||||||
super.init(frame: .zero)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 state: State
|
||||||
let progress: UInt8?
|
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.
|
// Putting the initializer in an extension preserves the automatic one.
|
||||||
|
|
|
||||||
|
|
@ -571,6 +571,9 @@ extension Ghostty {
|
||||||
case GHOSTTY_ACTION_REDO:
|
case GHOSTTY_ACTION_REDO:
|
||||||
return redo(app, target: target)
|
return redo(app, target: target)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_SCROLLBAR:
|
||||||
|
scrollbar(app, target: target, v: action.action.scrollbar)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||||
|
|
@ -637,8 +640,9 @@ extension Ghostty {
|
||||||
|
|
||||||
switch action.kind {
|
switch action.kind {
|
||||||
case .text:
|
case .text:
|
||||||
// Open with the default text editor
|
// Open with the default editor for `*.ghostty` file or just system text editor
|
||||||
if let textEditor = NSWorkspace.shared.defaultTextEditor {
|
let editor = NSWorkspace.shared.defaultApplicationURL(forExtension: url.pathExtension) ?? NSWorkspace.shared.defaultTextEditor
|
||||||
|
if let textEditor = editor {
|
||||||
NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
|
NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -1025,26 +1029,38 @@ extension Ghostty {
|
||||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController 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
|
// If the window has no splits, the action is not performable
|
||||||
// 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.
|
|
||||||
guard controller.surfaceTree.isSplit else { return false }
|
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(
|
NotificationCenter.default.post(
|
||||||
name: Notification.ghosttyFocusSplit,
|
name: Notification.ghosttyFocusSplit,
|
||||||
object: surfaceView,
|
object: surfaceView,
|
||||||
userInfo: [
|
userInfo: [
|
||||||
Notification.SplitDirectionKey: SplitFocusDirection.from(direction: direction) as Any,
|
Notification.SplitDirectionKey: splitDirection as Any,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func resizeSplit(
|
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(
|
private static func configReload(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
target: ghostty_target_s,
|
target: ghostty_target_s,
|
||||||
|
|
|
||||||
|
|
@ -314,17 +314,14 @@ extension Ghostty {
|
||||||
|
|
||||||
var macosCustomIcon: String {
|
var macosCustomIcon: String {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
let homeDirURL = FileManager.default.homeDirectoryForCurrentUser
|
let defaultValue = NSString("~/.config/ghostty/Ghostty.icns").expandingTildeInPath
|
||||||
let ghosttyConfigIconPath = homeDirURL.appendingPathComponent(
|
|
||||||
".config/ghostty/Ghostty.icns",
|
|
||||||
conformingTo: .fileURL).path()
|
|
||||||
let defaultValue = ghosttyConfigIconPath
|
|
||||||
guard let config = self.config else { return defaultValue }
|
guard let config = self.config else { return defaultValue }
|
||||||
var v: UnsafePointer<Int8>? = nil
|
var v: UnsafePointer<Int8>? = nil
|
||||||
let key = "macos-custom-icon"
|
let key = "macos-custom-icon"
|
||||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||||
guard let ptr = v 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
|
#else
|
||||||
return ""
|
return ""
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -606,6 +603,17 @@ extension Ghostty {
|
||||||
let str = String(cString: ptr)
|
let str = String(cString: ptr)
|
||||||
return MacShortcuts(rawValue: str) ?? defaultValue
|
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
|
case ask
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Scrollbar: String {
|
||||||
|
case system
|
||||||
|
case never
|
||||||
|
}
|
||||||
|
|
||||||
enum ResizeOverlay : String {
|
enum ResizeOverlay : String {
|
||||||
case always
|
case always
|
||||||
case never
|
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
|
/// The type of a clipboard request
|
||||||
enum ClipboardRequest {
|
enum ClipboardRequest {
|
||||||
/// A direct paste of clipboard contents
|
/// A direct paste of clipboard contents
|
||||||
|
|
@ -344,6 +375,10 @@ extension Notification.Name {
|
||||||
|
|
||||||
/// Toggle maximize of current window
|
/// Toggle maximize of current window
|
||||||
static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle")
|
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
|
// 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
|
/// 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,
|
/// 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.
|
/// 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 {
|
struct SurfaceRepresentable: OSViewRepresentable {
|
||||||
/// The view to render for the terminal surface.
|
/// The view to render for the terminal surface.
|
||||||
let view: SurfaceView
|
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.
|
/// The best approach is to wrap this view in a GeometryReader and pass in the geo.size.
|
||||||
let size: CGSize
|
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 {
|
func makeOSView(context: Context) -> SurfaceView {
|
||||||
// We need the view as part of the state to be created previously because
|
// On iOS, return the surface view directly
|
||||||
// the view is sent to the Ghostty API so that it can manipulate it
|
return view
|
||||||
// directly since we draw on a render thread.
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOSView(_ view: SurfaceView, context: Context) {
|
func updateOSView(_ view: SurfaceView, context: Context) {
|
||||||
view.sizeDidChange(size)
|
view.sizeDidChange(size)
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The configuration for a surface. For any configuration not set, defaults will be chosen from
|
/// 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
|
// 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
|
// 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.lastPerformKeyEvent = nil
|
||||||
|
|
||||||
self.interpretKeyEvents([translationEvent])
|
self.interpretKeyEvents([translationEvent])
|
||||||
|
|
@ -1532,6 +1532,7 @@ extension Ghostty {
|
||||||
let macosWindowShadow: Bool
|
let macosWindowShadow: Bool
|
||||||
let windowTitleFontFamily: String?
|
let windowTitleFontFamily: String?
|
||||||
let windowAppearance: NSAppearance?
|
let windowAppearance: NSAppearance?
|
||||||
|
let scrollbar: Ghostty.Config.Scrollbar
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||||
|
|
@ -1539,6 +1540,7 @@ extension Ghostty {
|
||||||
self.macosWindowShadow = true
|
self.macosWindowShadow = true
|
||||||
self.windowTitleFontFamily = nil
|
self.windowTitleFontFamily = nil
|
||||||
self.windowAppearance = nil
|
self.windowAppearance = nil
|
||||||
|
self.scrollbar = .system
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ config: Ghostty.Config) {
|
init(_ config: Ghostty.Config) {
|
||||||
|
|
@ -1547,6 +1549,7 @@ extension Ghostty {
|
||||||
self.macosWindowShadow = config.macosWindowShadow
|
self.macosWindowShadow = config.macosWindowShadow
|
||||||
self.windowTitleFontFamily = config.windowTitleFontFamily
|
self.windowTitleFontFamily = config.windowTitleFontFamily
|
||||||
self.windowAppearance = .init(ghosttyConfig: config)
|
self.windowAppearance = .init(ghosttyConfig: config)
|
||||||
|
self.scrollbar = config.scrollbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,13 @@ extension NSScreen {
|
||||||
deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? 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
|
// 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
|
// point-in-time visible, this is true if the dock is always visible
|
||||||
// AND present on this screen.
|
// AND present on this screen.
|
||||||
|
|
|
||||||
|
|
@ -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
|
exit 1
|
||||||
fi
|
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"
|
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/build.zig.zon.json"
|
||||||
prettier --log-level warn --write "$WORK_DIR/zig-packages.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
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
git,
|
git,
|
||||||
ncurses,
|
ncurses,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
zig_0_14,
|
zig_0_15,
|
||||||
pandoc,
|
pandoc,
|
||||||
revision ? "dirty",
|
revision ? "dirty",
|
||||||
optimize ? "Debug",
|
optimize ? "Debug",
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
# https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is
|
# https://github.com/ziglang/zig/issues/14281#issuecomment-1624220653 is
|
||||||
# ultimately acted on and has made its way to a nixpkgs implementation, this
|
# ultimately acted on and has made its way to a nixpkgs implementation, this
|
||||||
# can probably be removed in favor of that.
|
# can probably be removed in favor of that.
|
||||||
zig_hook = zig_0_14.hook.overrideAttrs {
|
zig_hook = zig_0_15.hook.overrideAttrs {
|
||||||
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
|
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
|
||||||
};
|
};
|
||||||
gi_typelib_path = import ./build-support/gi-typelib-path.nix {
|
gi_typelib_path = import ./build-support/gi-typelib-path.nix {
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
in
|
in
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
pname = "ghostty";
|
pname = "ghostty";
|
||||||
version = "1.2.1";
|
version = "1.3.0-dev";
|
||||||
|
|
||||||
# We limit source like this to try and reduce the amount of rebuilds as possible
|
# We limit source like this to try and reduce the amount of rebuilds as possible
|
||||||
# thus we only provide the source that is needed for the build
|
# thus we only provide the source that is needed for the build
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue