diff --git a/.github/pinact.yml b/.github/pinact.yml
new file mode 100644
index 000000000..3a29d18ab
--- /dev/null
+++ b/.github/pinact.yml
@@ -0,0 +1,4 @@
+version: 3
+ignore_actions:
+ - name: "DeterminateSystems/nix-installer-action"
+ ref: "main"
diff --git a/.github/workflows/clean-artifacts.yml b/.github/workflows/clean-artifacts.yml
index 5337c264a..69cb74ae5 100644
--- a/.github/workflows/clean-artifacts.yml
+++ b/.github/workflows/clean-artifacts.yml
@@ -10,7 +10,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Remove old artifacts
- uses: c-hive/gha-remove-artifacts@v1
+ uses: c-hive/gha-remove-artifacts@44fc7acaf1b3d0987da0e8d4707a989d80e9554b # v1.4.0
with:
age: "1 week"
skip-tags: true
diff --git a/.github/workflows/milestone.yml b/.github/workflows/milestone.yml
index 9ac5536ff..bc5c3d76c 100644
--- a/.github/workflows/milestone.yml
+++ b/.github/workflows/milestone.yml
@@ -15,7 +15,7 @@ jobs:
name: Milestone Update
steps:
- name: Set Milestone for PR
- uses: hustcer/milestone-action@v2
+ uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8
if: github.event.pull_request.merged == true
with:
action: bind-pr # `bind-pr` is the default action
@@ -24,7 +24,7 @@ jobs:
# Bind milestone to closed issue that has a merged PR fix
- name: Set Milestone for Issue
- uses: hustcer/milestone-action@v2
+ uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8
if: github.event.issue.state == 'closed'
with:
action: bind-issue
diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml
index a905531c2..bf8fd7208 100644
--- a/.github/workflows/nix.yml
+++ b/.github/workflows/nix.yml
@@ -34,18 +34,18 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- name: Setup Nix
- uses: cachix/install-nix-action@v31
+ uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml
index 458982140..710d04647 100644
--- a/.github/workflows/publish-tag.yml
+++ b/.github/workflows/publish-tag.yml
@@ -64,7 +64,7 @@ jobs:
mkdir blob
mv appcast.xml blob/appcast.xml
- name: Upload Appcast to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1
with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 37d5ba79b..cf96ffb21 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli
run: |
@@ -29,7 +29,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli
run: |
@@ -51,16 +51,16 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -189,7 +189,7 @@ jobs:
cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}
@@ -203,16 +203,16 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -341,7 +341,7 @@ jobs:
cp ghostty-macos-universal-debug.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug.zip
cp ghostty-macos-universal-debug-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-dsym.zip
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}
diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml
index 33cf9f3a8..98ecf2fa3 100644
--- a/.github/workflows/release-tag.yml
+++ b/.github/workflows/release-tag.yml
@@ -56,7 +56,7 @@ jobs:
fi
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
@@ -80,20 +80,20 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -111,7 +111,7 @@ jobs:
nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: source-tarball
path: |-
@@ -128,12 +128,12 @@ jobs:
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -260,7 +260,7 @@ jobs:
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: macos
path: |-
@@ -277,7 +277,7 @@ jobs:
curl -sL https://sentry.io/get-cli/ | bash
- name: Download macOS Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: macos
@@ -297,10 +297,10 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download macOS Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: macos
@@ -331,7 +331,7 @@ jobs:
mv appcast_new.xml appcast.xml
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sparkle
path: |-
@@ -348,17 +348,17 @@ jobs:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Download macOS Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: macos
- name: Download Sparkle Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: sparkle
- name: Download Source Tarball Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: source-tarball
@@ -378,7 +378,7 @@ jobs:
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml
index 4d009ab7b..b0916e657 100644
--- a/.github/workflows/release-tip.yml
+++ b/.github/workflows/release-tip.yml
@@ -19,7 +19,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tip Tag
run: |
git config user.name "github-actions[bot]"
@@ -31,7 +31,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-slow]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli
run: |
@@ -52,7 +52,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-fast]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli
run: |
@@ -73,7 +73,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli
run: |
@@ -105,17 +105,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -132,7 +132,7 @@ jobs:
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
- name: Update Release
- uses: softprops/action-gh-release@v2.3.2
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -158,16 +158,16 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -299,7 +299,7 @@ jobs:
# Update Release
- name: Release
- uses: softprops/action-gh-release@v2.3.2
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -331,7 +331,7 @@ jobs:
cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@@ -349,7 +349,7 @@ jobs:
cp appcast_new.xml blob/appcast.xml
- name: Upload Appcast to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@@ -373,16 +373,16 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -507,7 +507,7 @@ jobs:
# Update Release
- name: Release
- uses: softprops/action-gh-release@v2.3.2
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -524,7 +524,7 @@ jobs:
cp ghostty-macos-universal-debug-slow.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow.zip
cp ghostty-macos-universal-debug-slow-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@@ -548,16 +548,16 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -682,7 +682,7 @@ jobs:
# Update Release
- name: Release
- uses: softprops/action-gh-release@v2.3.2
+ uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -699,7 +699,7 @@ jobs:
cp ghostty-macos-universal-debug-fast.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast.zip
cp ghostty-macos-universal-debug-fast-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip
- name: Upload to R2
- uses: ryand56/r2-upload-action@latest
+ uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b34327f7d..8af7140c6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -27,6 +27,7 @@ jobs:
- test-gtk
- test-sentry-linux
- test-macos
+ - pinact
- prettier
- alejandra
- typos
@@ -64,20 +65,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -95,20 +96,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -131,20 +132,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -160,20 +161,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -193,20 +194,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -237,20 +238,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -262,7 +263,7 @@ jobs:
cp zig-out/dist/*.tar.gz ghostty-source.tar.gz
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: source-tarball
path: |-
@@ -273,13 +274,13 @@ jobs:
needs: test
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -313,13 +314,13 @@ jobs:
needs: test
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
with:
determinate: true
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -353,13 +354,13 @@ jobs:
needs: test
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -400,7 +401,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Download Source Tarball Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: source-tarball
- name: Extract tarball
@@ -408,7 +409,7 @@ jobs:
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
@@ -420,7 +421,7 @@ jobs:
_LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device"
sudo mkdir -p /var/lib/snapd/cgroup
echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}"
- - uses: snapcore/action-build@v1
+ - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
with:
path: dist
@@ -431,7 +432,7 @@ jobs:
needs: test
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# This could be from a script if we wanted to but inlining here for now
# in one place.
@@ -500,20 +501,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -542,20 +543,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -581,20 +582,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -608,13 +609,13 @@ jobs:
needs: test
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -637,17 +638,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -656,6 +657,34 @@ jobs:
- name: zig fmt
run: nix develop -c zig fmt --check .
+ pinact:
+ name: "GitHub Actions Pins"
+ if: github.repository == 'ghostty-org/ghostty'
+ runs-on: namespace-profile-ghostty-xsm
+ timeout-minutes: 60
+ env:
+ ZIG_LOCAL_CACHE_DIR: /zig/local-cache
+ ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - name: Setup Cache
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
+ with:
+ path: |
+ /nix
+ /zig
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
+ with:
+ nix_path: nixpkgs=channel:nixos-unstable
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
+ with:
+ name: ghostty
+ authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
+ skipPush: true
+ useDaemon: false # sometimes fails on short jobs
+ - name: pinact check
+ run: nix develop -c pinact run --check
+
prettier:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-xsm
@@ -664,17 +693,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -691,17 +720,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -718,17 +747,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -745,17 +774,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -772,17 +801,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- - uses: actions/checkout@v4 # Check out repo so we can lint it
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -806,20 +835,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- - uses: cachix/install-nix-action@v31
+ - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -834,13 +863,13 @@ jobs:
needs: [test, build-dist]
steps:
- name: Install and configure Namespace CLI
- uses: namespacelabs/nscloud-setup@v0
+ uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
- name: Configure Namespace powered Buildx
- uses: namespacelabs/nscloud-setup-buildx-action@v0
+ uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18
- name: Download Source Tarball Artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: source-tarball
@@ -850,7 +879,7 @@ jobs:
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Build and push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: dist
file: dist/src/build/docker/debian/Dockerfile
@@ -865,18 +894,18 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- name: Setup Nix
- uses: cachix/install-nix-action@v31
+ uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -901,8 +930,8 @@ jobs:
runs-on: ${{ matrix.variant.runner }}
needs: [flatpak-check-zig-cache, test]
steps:
- - uses: actions/checkout@v4
- - uses: flatpak/flatpak-github-actions/flatpak-builder@v6
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
with:
bundle: com.mitchellh.ghostty
manifest-path: flatpak/com.mitchellh.ghostty.yml
diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml
index 2533285e6..b9ded559e 100644
--- a/.github/workflows/update-colorschemes.yml
+++ b/.github/workflows/update-colorschemes.yml
@@ -17,22 +17,22 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Setup Cache
- uses: namespacelabs/nscloud-cache-action@v1.2.8
+ uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with:
path: |
/nix
/zig
- name: Setup Nix
- uses: cachix/install-nix-action@v31
+ uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with:
nix_path: nixpkgs=channel:nixos-unstable
- - uses: cachix/cachix-action@v16
+ - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@@ -60,7 +60,7 @@ jobs:
run: nix build .#ghostty
- name: Create pull request
- uses: peter-evans/create-pull-request@v7
+ uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
title: Update iTerm2 colorschemes
base: main
diff --git a/CODEOWNERS b/CODEOWNERS
index 56768d5ae..3bb6a4123 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -155,6 +155,7 @@
/src/input/KeyEncoder.zig @ghostty-org/terminal
/src/terminal/ @ghostty-org/terminal
/src/terminfo/ @ghostty-org/terminal
+/src/termio/ @ghostty-org/terminal
/src/unicode/ @ghostty-org/terminal
/src/Surface.zig @ghostty-org/terminal
/src/surface_mouse.zig @ghostty-org/terminal
@@ -180,6 +181,7 @@
/po/zh_CN.UTF-8.po @ghostty-org/zh_CN
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR
+/po/he_IL.UTF-8.po @ghostty-org/he_IL
# Packaging - Snap
/snap/ @ghostty-org/snap
diff --git a/dist/linux/app.desktop.in b/dist/linux/app.desktop.in
index c39164158..32ba00cfd 100644
--- a/dist/linux/app.desktop.in
+++ b/dist/linux/app.desktop.in
@@ -19,6 +19,7 @@ X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command
DBusActivatable=true
+X-KDE-Shortcuts=Ctrl+Alt+T
[Desktop Action new-window]
Name=New Window
diff --git a/include/ghostty.h b/include/ghostty.h
index 181f7b7f8..312e6595a 100644
--- a/include/ghostty.h
+++ b/include/ghostty.h
@@ -350,6 +350,11 @@ typedef struct {
const char* message;
} ghostty_diagnostic_s;
+typedef struct {
+ const char* ptr;
+ uintptr_t len;
+} ghostty_string_s;
+
typedef struct {
double tl_px_x;
double tl_px_y;
@@ -662,6 +667,19 @@ typedef struct {
bool soft;
} ghostty_action_reload_config_s;
+// apprt.action.OpenUrlKind
+typedef enum {
+ GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
+ GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
+} ghostty_action_open_url_kind_e;
+
+// apprt.action.OpenUrl.C
+typedef struct {
+ ghostty_action_open_url_kind_e kind;
+ const char* url;
+ uintptr_t len;
+} ghostty_action_open_url_s;
+
// apprt.Action.Key
typedef enum {
GHOSTTY_ACTION_QUIT,
@@ -711,7 +729,8 @@ typedef enum {
GHOSTTY_ACTION_RING_BELL,
GHOSTTY_ACTION_UNDO,
GHOSTTY_ACTION_REDO,
- GHOSTTY_ACTION_CHECK_FOR_UPDATES
+ GHOSTTY_ACTION_CHECK_FOR_UPDATES,
+ GHOSTTY_ACTION_OPEN_URL,
} ghostty_action_tag_e;
typedef union {
@@ -739,6 +758,7 @@ typedef union {
ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
+ ghostty_action_open_url_s open_url;
} ghostty_action_u;
typedef struct {
@@ -778,10 +798,11 @@ typedef struct {
//-------------------------------------------------------------------
// Published API
-int ghostty_init(void);
-void ghostty_cli_main(uintptr_t, char**);
+int ghostty_init(uintptr_t, char**);
+void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*);
+void ghostty_string_free(ghostty_string_s);
ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t);
@@ -796,7 +817,7 @@ ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
uintptr_t);
uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t);
-void ghostty_config_open();
+ghostty_string_s ghostty_config_open_path(void);
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t);
diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist
index dcce61373..ff391c0f8 100644
--- a/macos/Ghostty-Info.plist
+++ b/macos/Ghostty-Info.plist
@@ -48,8 +48,8 @@
LSEnvironment
- GHOSTTY_MAC_APP
- 1
+ GHOSTTY_MAC_LAUNCH_SOURCE
+ app
MDItemKeywords
Terminal
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index cf806c7bd..f6eedd864 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -13,6 +13,8 @@
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
+ A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; };
+ A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */; };
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
@@ -158,6 +160,8 @@
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; };
+ A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = ""; };
+ A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = ""; };
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; };
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; };
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; };
@@ -516,6 +520,7 @@
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
+ A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
A51194122E05D003007258CC /* Optional+Extension.swift */,
@@ -528,6 +533,7 @@
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */,
+ A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A58636722DF4813000E04A10 /* UndoManager+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
@@ -799,6 +805,7 @@
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */,
+ A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
@@ -815,6 +822,7 @@
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
+ A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */,
diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift
index efc09ede9..38500b7d3 100644
--- a/macos/Sources/App/macOS/AppDelegate.swift
+++ b/macos/Sources/App/macOS/AppDelegate.swift
@@ -256,9 +256,8 @@ class AppDelegate: NSObject,
// Setup signal handlers
setupSignals()
- // This is a hack used by our build scripts, specifically `zig build run`,
- // to force our app to the foreground.
- if ProcessInfo.processInfo.environment["GHOSTTY_MAC_ACTIVATE"] == "1" {
+ // If we launched via zig run then we need to force foreground.
+ if Ghostty.launchSource == .zig_run {
// This never gets called until we click the dock icon. This forces it
// activate immediately.
applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification))
@@ -933,7 +932,7 @@ class AppDelegate: NSObject,
//MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) {
- ghostty.openConfig()
+ Ghostty.App.openConfig()
}
@IBAction func reloadConfig(_ sender: Any?) {
diff --git a/macos/Sources/App/macOS/main.swift b/macos/Sources/App/macOS/main.swift
index 990ef8ef1..ad32f4e70 100644
--- a/macos/Sources/App/macOS/main.swift
+++ b/macos/Sources/App/macOS/main.swift
@@ -2,13 +2,32 @@ import AppKit
import Cocoa
import GhosttyKit
-// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect
-// whether we launch from the app or not. A user can fake this if
-// they want but they're doing so at their own detriment...
-let process = ProcessInfo.processInfo
-if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") {
- ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv)
- exit(1)
+// Initialize Ghostty global state. We do this once right away because the
+// CLI APIs require it and it lets us ensure it is done immediately for the
+// rest of the app.
+if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS {
+ Ghostty.logger.critical("ghostty_init failed")
+
+ // We also write to stderr if this is executed from the CLI or zig run
+ switch Ghostty.launchSource {
+ case .cli, .zig_run:
+ let stderrHandle = FileHandle.standardError
+ stderrHandle.write(
+ "Ghostty failed to initialize! If you're executing Ghostty from the command line\n" +
+ "then this is usually because an invalid action or multiple actions were specified.\n" +
+ "Actions start with the `+` character.\n\n" +
+ "View all available actions by running `ghostty +help`.\n")
+ exit(1)
+
+ case .app:
+ // For the app we exit immediately. We should handle this case more
+ // gracefully in the future.
+ exit(1)
+ }
}
+// This will run the CLI action and exit if one was specified. A CLI
+// action is a command starting with a `+`, such as `ghostty +boo`.
+ghostty_cli_try_action();
+
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift
index dfdb0bff5..a6559600d 100644
--- a/macos/Sources/Ghostty/Ghostty.Action.swift
+++ b/macos/Sources/Ghostty/Ghostty.Action.swift
@@ -40,4 +40,34 @@ extension Ghostty.Action {
self.amount = c.amount
}
}
+
+ struct OpenURL {
+ enum Kind {
+ case unknown
+ case text
+
+ init(_ c: ghostty_action_open_url_kind_e) {
+ switch c {
+ case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT:
+ self = .text
+ default:
+ self = .unknown
+ }
+ }
+ }
+
+ let kind: Kind
+ let url: String
+
+ init(c: ghostty_action_open_url_s) {
+ self.kind = Kind(c.kind)
+
+ if let urlCString = c.url {
+ let data = Data(bytes: urlCString, count: Int(c.len))
+ self.url = String(data: data, encoding: .utf8) ?? ""
+ } else {
+ self.url = ""
+ }
+ }
+ }
}
diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift
index ba0b95212..0fdea1760 100644
--- a/macos/Sources/Ghostty/Ghostty.App.swift
+++ b/macos/Sources/Ghostty/Ghostty.App.swift
@@ -45,12 +45,6 @@ extension Ghostty {
}
init() {
- // Initialize ghostty global state. This happens once per process.
- if ghostty_init() != GHOSTTY_SUCCESS {
- logger.critical("ghostty_init failed, weird things may happen")
- readiness = .error
- }
-
// Initialize the global configuration.
self.config = Config()
if self.config.config == nil {
@@ -120,9 +114,21 @@ extension Ghostty {
ghostty_app_tick(app)
}
- func openConfig() {
- guard let app = self.app else { return }
- ghostty_app_open_config(app)
+ static func openConfig() {
+ let str = Ghostty.AllocatedString(ghostty_config_open_path()).string
+ guard !str.isEmpty else { return }
+ #if os(macOS)
+ let fileURL = URL(fileURLWithPath: str).absoluteString
+ var action = ghostty_action_open_url_s()
+ action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT
+ fileURL.withCString { cStr in
+ action.url = cStr
+ action.len = UInt(fileURL.count)
+ _ = openURL(action)
+ }
+ #else
+ fatalError("Unsupported platform for opening config file")
+ #endif
}
/// Reload the configuration.
@@ -494,7 +500,7 @@ extension Ghostty {
pwdChanged(app, target: target, v: action.action.pwd)
case GHOSTTY_ACTION_OPEN_CONFIG:
- ghostty_config_open()
+ openConfig()
case GHOSTTY_ACTION_FLOAT_WINDOW:
toggleFloatWindow(app, target: target, mode: action.action.float_window)
@@ -552,6 +558,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app)
+
+ case GHOSTTY_ACTION_OPEN_URL:
+ return openURL(action.action.open_url)
case GHOSTTY_ACTION_UNDO:
return undo(app, target: target)
@@ -604,6 +613,34 @@ extension Ghostty {
appDelegate.checkForUpdates(nil)
}
}
+
+ private static func openURL(
+ _ v: ghostty_action_open_url_s
+ ) -> Bool {
+ let action = Ghostty.Action.OpenURL(c: v)
+
+ // Convert the URL string to a URL object
+ guard let url = URL(string: action.url) else {
+ Ghostty.logger.warning("invalid URL for open URL action: \(action.url)")
+ return false
+ }
+
+ switch action.kind {
+ case .text:
+ // Open with the default text editor
+ if let textEditor = NSWorkspace.shared.defaultTextEditor {
+ NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
+ return true
+ }
+
+ case .unknown:
+ break
+ }
+
+ // Open with the default application for the URL
+ NSWorkspace.shared.open(url)
+ return true
+ }
private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
let undoManager: UndoManager?
diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift
index 125a09825..9b05934df 100644
--- a/macos/Sources/Ghostty/Package.swift
+++ b/macos/Sources/Ghostty/Package.swift
@@ -48,8 +48,51 @@ extension Ghostty {
}
}
+// MARK: General Helpers
+
+extension Ghostty {
+ enum LaunchSource: String {
+ case cli
+ case app
+ case zig_run
+ }
+
+ /// Returns the mechanism that launched the app. This is based on an env var so
+ /// its up to the env var being set in the correct circumstance.
+ static var launchSource: LaunchSource {
+ guard let envValue = ProcessInfo.processInfo.environment["GHOSTTY_MAC_LAUNCH_SOURCE"] else {
+ // We default to the CLI because the app bundle always sets the
+ // source. If its unset we assume we're in a CLI environment.
+ return .cli
+ }
+
+ // If the env var is set but its unknown then we default back to the app.
+ return LaunchSource(rawValue: envValue) ?? .app
+ }
+}
+
// MARK: Swift Types for C Types
+extension Ghostty {
+ class AllocatedString {
+ private let cString: ghostty_string_s
+
+ init(_ c: ghostty_string_s) {
+ self.cString = c
+ }
+
+ var string: String {
+ guard let ptr = cString.ptr else { return "" }
+ let data = Data(bytes: ptr, count: Int(cString.len))
+ return String(data: data, encoding: .utf8) ?? ""
+ }
+
+ deinit {
+ ghostty_string_free(cString)
+ }
+ }
+}
+
extension Ghostty {
enum SetFloatWIndow {
case on
diff --git a/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift
new file mode 100644
index 000000000..b6df4a60f
--- /dev/null
+++ b/macos/Sources/Helpers/Extensions/FileHandle+Extension.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+extension FileHandle: @retroactive TextOutputStream {
+ /// Write a string to a filehandle.
+ public func write(_ string: String) {
+ let data = Data(string.utf8)
+ self.write(data)
+ }
+}
diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift
new file mode 100644
index 000000000..bc2d028b5
--- /dev/null
+++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift
@@ -0,0 +1,29 @@
+import AppKit
+import UniformTypeIdentifiers
+
+extension NSWorkspace {
+ /// Returns the URL of the default text editor application.
+ /// - Returns: The URL of the default text editor, or nil if no default text editor is found.
+ var defaultTextEditor: URL? {
+ defaultApplicationURL(forContentType: UTType.plainText.identifier)
+ }
+
+ /// Returns the URL of the default application for opening files with the specified content type.
+ /// - Parameter contentType: The content type identifier (UTI) to find the default application for.
+ /// - Returns: The URL of the default application, or nil if no default application is found.
+ func defaultApplicationURL(forContentType contentType: String) -> URL? {
+ return LSCopyDefaultApplicationURLForContentType(
+ contentType as CFString,
+ .all,
+ nil
+ )?.takeRetainedValue() as? URL
+ }
+
+ /// Returns the URL of the default application for opening files with the specified file extension.
+ /// - Parameter ext: The file extension to find the default application for.
+ /// - Returns: The URL of the default application, or nil if no default application is found.
+ func defaultApplicationURL(forExtension ext: String) -> URL? {
+ guard let uti = UTType(filenameExtension: ext) else { return nil}
+ return defaultApplicationURL(forContentType: uti.identifier)
+ }
+}
diff --git a/nix/devShell.nix b/nix/devShell.nix
index f4ea62235..8a8ab441f 100644
--- a/nix/devShell.nix
+++ b/nix/devShell.nix
@@ -58,6 +58,7 @@
jq,
minisign,
pandoc,
+ pinact,
hyperfine,
typos,
uv,
@@ -98,6 +99,7 @@ in
# Linting
nodePackages.prettier
alejandra
+ pinact
typos
# Testing
diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig
index 2c8e05eff..03e794855 100644
--- a/pkg/opengl/Texture.zig
+++ b/pkg/opengl/Texture.zig
@@ -92,6 +92,30 @@ pub const Format = enum(c_uint) {
_,
};
+/// Minification filter for textures.
+pub const MinFilter = enum(c_int) {
+ nearest = c.GL_NEAREST,
+ linear = c.GL_LINEAR,
+ nearest_mipmap_nearest = c.GL_NEAREST_MIPMAP_NEAREST,
+ linear_mipmap_nearest = c.GL_LINEAR_MIPMAP_NEAREST,
+ nearest_mipmap_linear = c.GL_NEAREST_MIPMAP_LINEAR,
+ linear_mipmap_linear = c.GL_LINEAR_MIPMAP_LINEAR,
+};
+
+/// Magnification filter for textures.
+pub const MagFilter = enum(c_int) {
+ nearest = c.GL_NEAREST,
+ linear = c.GL_LINEAR,
+};
+
+/// Texture coordinate wrapping mode.
+pub const Wrap = enum(c_int) {
+ clamp_to_edge = c.GL_CLAMP_TO_EDGE,
+ clamp_to_border = c.GL_CLAMP_TO_BORDER,
+ mirrored_repeat = c.GL_MIRRORED_REPEAT,
+ repeat = c.GL_REPEAT,
+};
+
/// Data type for texture images.
pub const DataType = enum(c_uint) {
UnsignedByte = c.GL_UNSIGNED_BYTE,
diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po
new file mode 100644
index 000000000..636bf46e3
--- /dev/null
+++ b/po/he_IL.UTF-8.po
@@ -0,0 +1,298 @@
+# Hebrew translations for com.mitchellh.ghostty.
+# Copyright (C) 2025 Mitchell Hashimoto
+# This file is distributed under the same license as the com.mitchellh.ghostty package.
+# Sl (Shahaf Levi), Sl's Repository Ltd , 2025.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: com.mitchellh.ghostty\n"
+"Report-Msgid-Bugs-To: m@mitchellh.com\n"
+"POT-Creation-Date: 2025-06-28 17:01+0200\n"
+"PO-Revision-Date: 2025-03-13 00:00+0000\n"
+"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n"
+"Language-Team: Hebrew \n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
+msgid "Change Terminal Title"
+msgstr "שינוי כותרת המסוף"
+
+#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
+msgid "Leave blank to restore the default title."
+msgstr "השאר/י ריק כדי לשחזר את כותרת ברירת המחדל."
+
+#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
+#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
+#: src/apprt/gtk/CloseDialog.zig:44
+msgid "Cancel"
+msgstr "ביטול"
+
+#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
+msgid "OK"
+msgstr "אישור"
+
+#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
+#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
+msgid "Configuration Errors"
+msgstr "שגיאות בהגדרות"
+
+#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
+#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
+msgid ""
+"One or more configuration errors were found. Please review the errors below, "
+"and either reload your configuration or ignore these errors."
+msgstr "נמצאו אחת או יותר שגיאות בהגדרות. אנא בדוק/י את השגיאות המופיעות מטה ולאחר מכן טען/י את ההגדרות מחדש או התעלם/י מהשגיאות."
+
+#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
+#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
+msgid "Ignore"
+msgstr "התעלמות"
+
+#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
+#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
+msgid "Reload Configuration"
+msgstr "טעינה מחדש של ההגדרות"
+
+#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
+msgid "Split Up"
+msgstr "פיצול למעלה"
+
+#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
+msgid "Split Down"
+msgstr "פיצול למטה"
+
+#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
+msgid "Split Left"
+msgstr "פיצול שמאלה"
+
+#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
+msgid "Split Right"
+msgstr "פיצול ימינה"
+
+#: src/apprt/gtk/ui/1.5/command-palette.blp:16
+msgid "Execute a command…"
+msgstr "הרץ/י פקודה…"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
+msgid "Copy"
+msgstr "העתקה"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
+#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
+msgid "Paste"
+msgstr "הדבקה"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
+msgid "Clear"
+msgstr "ניקוי"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
+msgid "Reset"
+msgstr "איפוס"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
+msgid "Split"
+msgstr "פיצול"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
+msgid "Change Title…"
+msgstr "שינוי כותרת…"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
+msgid "Tab"
+msgstr "כרטיסייה"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
+#: src/apprt/gtk/Window.zig:263
+msgid "New Tab"
+msgstr "כרטיסייה חדשה"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
+msgid "Close Tab"
+msgstr "סגור/י כרטיסייה"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
+msgid "Window"
+msgstr "חלון"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
+msgid "New Window"
+msgstr "חלון חדש"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
+msgid "Close Window"
+msgstr "סגור/י חלון"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
+msgid "Config"
+msgstr "הגדרות"
+
+#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
+msgid "Open Configuration"
+msgstr "פתיחת ההגדרות"
+
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
+msgid "Command Palette"
+msgstr "לוח פקודות"
+
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
+msgid "Terminal Inspector"
+msgstr "בודק המסוף"
+
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
+#: src/apprt/gtk/Window.zig:1036
+msgid "About Ghostty"
+msgstr "אודות Ghostty"
+
+#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
+msgid "Quit"
+msgstr "יציאה"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
+msgid "Authorize Clipboard Access"
+msgstr "אשר/י גישה ללוח ההעתקה"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
+msgid ""
+"An application is attempting to read from the clipboard. The current "
+"clipboard contents are shown below."
+msgstr "יש אפליקציה שמנסה לקרוא מלוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה."
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
+msgid "Deny"
+msgstr "דחייה"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
+msgid "Allow"
+msgstr "אישור"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
+msgid "Remember choice for this split"
+msgstr "זכור/י את הבחירה עבור פיצול זה"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
+msgid "Reload configuration to show this prompt again"
+msgstr "טען/י את ההגדרות מחדש כדי להציג את הבקשה הזו שוב"
+
+#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
+#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
+msgid ""
+"An application is attempting to write to the clipboard. The current "
+"clipboard contents are shown below."
+msgstr "יש אפליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה."
+
+#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
+msgid "Warning: Potentially Unsafe Paste"
+msgstr "אזהרה: ההדבקה עלולה להיות מסוכנת"
+
+#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
+msgid ""
+"Pasting this text into the terminal may be dangerous as it looks like some "
+"commands may be executed."
+msgstr "הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של פקודות מסוימות."
+
+#: src/apprt/gtk/Window.zig:216
+msgid "Main Menu"
+msgstr "תפריט ראשי"
+
+#: src/apprt/gtk/Window.zig:238
+msgid "View Open Tabs"
+msgstr "הצג/י כרטיסיות פתוחות"
+
+#: src/apprt/gtk/Window.zig:264
+msgid "New Split"
+msgstr "פיצול חדש"
+
+#: src/apprt/gtk/Window.zig:327
+msgid ""
+"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
+msgstr "⚠️ את/ה מריץ/ה גרסת ניפוי שגיאות של Ghostty! הביצועים יהיו ירודים."
+
+#: src/apprt/gtk/Window.zig:773
+msgid "Reloaded the configuration"
+msgstr "ההגדרות הוטענו מחדש"
+
+#: src/apprt/gtk/Window.zig:1017
+msgid "Ghostty Developers"
+msgstr "המפתחים של Ghostty"
+
+#: src/apprt/gtk/inspector.zig:144
+msgid "Ghostty: Terminal Inspector"
+msgstr "Ghostty: בודק המסוף"
+
+#: src/apprt/gtk/CloseDialog.zig:47
+msgid "Close"
+msgstr "סגירה"
+
+#: src/apprt/gtk/CloseDialog.zig:87
+msgid "Quit Ghostty?"
+msgstr "לצאת מGhostty?"
+
+#: src/apprt/gtk/CloseDialog.zig:88
+msgid "Close Window?"
+msgstr "לסגור את החלון?"
+
+#: src/apprt/gtk/CloseDialog.zig:89
+msgid "Close Tab?"
+msgstr "לסגור את הכרטיסייה?"
+
+#: src/apprt/gtk/CloseDialog.zig:90
+msgid "Close Split?"
+msgstr "לסגור את הפיצול?"
+
+#: src/apprt/gtk/CloseDialog.zig:96
+msgid "All terminal sessions will be terminated."
+msgstr "כל הפעלות המסוף יסתיימו."
+
+#: src/apprt/gtk/CloseDialog.zig:97
+msgid "All terminal sessions in this window will be terminated."
+msgstr "כל הפעלות המסוף בחלון זה יסתיימו."
+
+#: src/apprt/gtk/CloseDialog.zig:98
+msgid "All terminal sessions in this tab will be terminated."
+msgstr "כל הפעלות המסוף בכרטיסייה זו יסתיימו."
+
+#: src/apprt/gtk/CloseDialog.zig:99
+msgid "The currently running process in this split will be terminated."
+msgstr "התהליך שרץ כרגע בפיצול זה יסתיים."
+
+#: src/apprt/gtk/Surface.zig:1257
+msgid "Copied to clipboard"
+msgstr "הועתק ללוח ההעתקה"
diff --git a/src/Surface.zig b/src/Surface.zig
index 6d0f1584b..a4a8d46df 100644
--- a/src/Surface.zig
+++ b/src/Surface.zig
@@ -270,6 +270,7 @@ const DerivedConfig = struct {
title: ?[:0]const u8,
title_report: bool,
links: []Link,
+ link_previews: configpkg.LinkPreviews,
const Link = struct {
regex: oni.Regex,
@@ -336,6 +337,7 @@ const DerivedConfig = struct {
.title = config.title,
.title_report = config.@"title-report",
.links = links,
+ .link_previews = config.@"link-previews",
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
@@ -1242,7 +1244,7 @@ fn mouseRefreshLinks(
// Get our link at the current position. This returns null if there
// isn't a link OR if we shouldn't be showing links for some reason
// (see further comments for cases).
- const link_: ?apprt.action.MouseOverLink = link: {
+ const link_: ?apprt.action.MouseOverLink, const preview: bool = link: {
// If we clicked and our mouse moved cells then we never
// highlight links until the mouse is unclicked. This follows
// standard macOS and Linux behavior where a click and drag cancels
@@ -1257,18 +1259,21 @@ fn mouseRefreshLinks(
if (!click_pt.coord().eql(pos_vp)) {
log.debug("mouse moved while left click held, ignoring link hover", .{});
- break :link null;
+ break :link .{ null, false };
}
}
- const link = (try self.linkAtPos(pos)) orelse break :link null;
+ const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false };
switch (link[0]) {
.open => {
const str = try self.io.terminal.screen.selectionString(alloc, .{
.sel = link[1],
.trim = false,
});
- break :link .{ .url = str };
+ break :link .{
+ .{ .url = str },
+ self.config.link_previews == .true,
+ };
},
._open_osc8 => {
@@ -1276,9 +1281,14 @@ fn mouseRefreshLinks(
const pin = link[1].start();
const uri = self.osc8URI(pin) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{});
- break :link null;
+ break :link .{ null, false };
+ };
+ break :link .{
+ .{
+ .url = uri,
+ },
+ self.config.link_previews != .false,
};
- break :link .{ .url = uri };
},
}
};
@@ -1294,11 +1304,15 @@ fn mouseRefreshLinks(
.mouse_shape,
.pointer,
);
- _ = try self.rt_app.performAction(
- .{ .surface = self },
- .mouse_over_link,
- link,
- );
+
+ if (preview) {
+ _ = try self.rt_app.performAction(
+ .{ .surface = self },
+ .mouse_over_link,
+ link,
+ );
+ }
+
try self.queueRender();
return;
}
@@ -3710,7 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false,
});
defer self.alloc.free(str);
- try internal_os.open(self.alloc, .unknown, str);
+ try self.openUrl(.{ .kind = .unknown, .url = str });
},
._open_osc8 => {
@@ -3718,13 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
- try internal_os.open(self.alloc, .unknown, uri);
+ try self.openUrl(.{ .kind = .unknown, .url = uri });
},
}
return true;
}
+fn openUrl(
+ self: *Surface,
+ action: apprt.action.OpenUrl,
+) !void {
+ // If the apprt handles it then we're done.
+ if (try self.rt_app.performAction(
+ .{ .surface = self },
+ .open_url,
+ action,
+ )) return;
+
+ // apprt didn't handle it, fallback to our simple cross-platform
+ // URL opener. We log a warning because we want well-behaved
+ // apprts to handle this themselves.
+ log.warn("apprt did not handle open URL action, falling back to default opener", .{});
+ try internal_os.open(
+ self.alloc,
+ action.kind,
+ action.url,
+ );
+}
+
/// Return the URI for an OSC8 hyperlink at the given position or null
/// if there is no hyperlink.
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
@@ -4443,6 +4479,18 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return false;
},
+ .copy_title_to_clipboard => {
+ const title = self.rt_surface.getTitle() orelse return false;
+ if (title.len == 0) return false;
+
+ self.rt_surface.setClipboardString(title, .standard, false) catch |err| {
+ log.err("error copying title to clipboard err={}", .{err});
+ return true;
+ };
+
+ return true;
+ },
+
.paste_from_clipboard => try self.startClipboardRequest(
.standard,
.{ .paste = {} },
@@ -4484,6 +4532,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size);
},
+ .set_font_size => |points| {
+ log.debug("set font size={d}", .{points});
+
+ var size = self.font_size;
+ size.points = std.math.clamp(points, 1.0, 255.0);
+ try self.setFontSize(size);
+ },
+
.prompt_surface_title => return try self.rt_app.performAction(
.{ .surface = self },
.prompt_title,
@@ -4923,7 +4979,7 @@ fn writeScreenFile(
defer self.alloc.free(pathZ);
try self.rt_surface.setClipboardString(pathZ, .standard, false);
},
- .open => try internal_os.open(self.alloc, .text, path),
+ .open => try self.openUrl(.{ .kind = .text, .url = path }),
.paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc,
path,
diff --git a/src/apprt/action.zig b/src/apprt/action.zig
index b4c5164c2..1c3c7c72c 100644
--- a/src/apprt/action.zig
+++ b/src/apprt/action.zig
@@ -267,6 +267,11 @@ pub const Action = union(Key) {
check_for_updates,
+ /// Open a URL using the native OS mechanisms. On macOS this might be `open`
+ /// or on Linux this might be `xdg-open`. The exact mechanism is up to the
+ /// apprt.
+ open_url: OpenUrl,
+
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@@ -317,6 +322,7 @@ pub const Action = union(Key) {
undo,
redo,
check_for_updates,
+ open_url,
};
/// Sync with: ghostty_action_u
@@ -357,7 +363,11 @@ pub const Action = union(Key) {
// For ABI compatibility, we expect that this is our union size.
// At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it.
- assert(@sizeOf(CValue) == 16);
+ assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
+ 4 => 16,
+ 8 => 24,
+ else => unreachable,
+ });
}
/// Returns the value type for the given key.
@@ -614,3 +624,44 @@ pub const ConfigChange = struct {
};
}
};
+
+/// Open a URL
+pub const OpenUrl = struct {
+ /// The type of data that the URL refers to.
+ kind: Kind,
+
+ /// The URL.
+ url: []const u8,
+
+ /// The type of the data at the URL to open. This is used as a hint to
+ /// potentially open the URL in a different way.
+ ///
+ /// Sync with: ghostty_action_open_url_kind_e
+ pub const Kind = enum(c_int) {
+ /// The type is unknown. This is the default and apprts should
+ /// open the URL in the most generic way possible. For example,
+ /// on macOS this would be the equivalent of `open` or on Linux
+ /// this would be `xdg-open`.
+ unknown,
+
+ /// The URL is known to be a text file. In this case, the apprt
+ /// should try to open the URL in a text editor or viewer or
+ /// some equivalent, if possible.
+ text,
+ };
+
+ // Sync with: ghostty_action_open_url_s
+ pub const C = extern struct {
+ kind: Kind,
+ url: [*]const u8,
+ len: usize,
+ };
+
+ pub fn cval(self: OpenUrl) C {
+ return .{
+ .kind = self.kind,
+ .url = self.url.ptr,
+ .len = self.url.len,
+ };
+ }
+};
diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig
index 0121494b7..30a2d9ff6 100644
--- a/src/apprt/embedded.zig
+++ b/src/apprt/embedded.zig
@@ -884,7 +884,7 @@ pub const Surface = struct {
}
// Remove this so that running `ghostty` within Ghostty works.
- env.remove("GHOSTTY_MAC_APP");
+ env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to
// remove the LANGUAGE env var so that we don't inherit
diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig
index c61254fbd..907f3a36d 100644
--- a/src/apprt/gtk/App.zig
+++ b/src/apprt/gtk/App.zig
@@ -496,7 +496,7 @@ pub fn performAction(
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
.goto_split => return self.gotoSplit(target, value),
- .open_config => try configpkg.edit.open(self.core_app.alloc),
+ .open_config => return self.openConfig(),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value),
@@ -519,6 +519,7 @@ pub fn performAction(
.secure_input => self.setSecureInput(target, value),
.ring_bell => try self.ringBell(target),
.toggle_command_palette => try self.toggleCommandPalette(target),
+ .open_url => self.openUrl(value),
// Unimplemented
.close_all_windows,
@@ -1757,3 +1758,34 @@ fn initActions(self: *App) void {
action_map.addAction(action.as(gio.Action));
}
}
+
+fn openConfig(self: *App) !bool {
+ // Get the config file path
+ const alloc = self.core_app.alloc;
+ const path = configpkg.edit.openPath(alloc) catch |err| {
+ log.warn("error getting config file path: {}", .{err});
+ return false;
+ };
+ defer alloc.free(path);
+
+ // Open it using openURL. "path" isn't actually a URL but
+ // at the time of writing that works just fine for GTK.
+ self.openUrl(.{ .kind = .text, .url = path });
+ return true;
+}
+
+fn openUrl(
+ app: *App,
+ value: apprt.action.OpenUrl,
+) void {
+ // TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
+
+ // Fallback to the minimal cross-platform way of opening a URL.
+ // This is always a safe fallback and enables for example Windows
+ // to open URLs (GTK on Windows via WSL is a thing).
+ internal_os.open(
+ app.core_app.alloc,
+ value.kind,
+ value.url,
+ ) catch |err| log.warn("unable to open url: {}", .{err});
+}
diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig
index 555edb1e4..e6b502c80 100644
--- a/src/apprt/gtk/Window.zig
+++ b/src/apprt/gtk/Window.zig
@@ -214,6 +214,7 @@ pub fn init(self: *Window, app: *App) !void {
{
const btn = gtk.MenuButton.new();
btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu"));
+ btn.as(gtk.Widget).setCanFocus(0);
btn.setIconName("open-menu-symbolic");
btn.setPopover(self.titlebar_menu.asWidget());
_ = gobject.Object.signals.notify.connect(
@@ -253,6 +254,7 @@ pub fn init(self: *Window, app: *App) !void {
},
};
+ btn.setCanFocus(0);
btn.setFocusOnClick(0);
self.headerbar.packEnd(btn);
}
diff --git a/src/build/GhosttyXcodebuild.zig b/src/build/GhosttyXcodebuild.zig
index 9b472eda8..7fa2d2f95 100644
--- a/src/build/GhosttyXcodebuild.zig
+++ b/src/build/GhosttyXcodebuild.zig
@@ -88,6 +88,19 @@ pub fn init(
// Our step to open the resulting Ghostty app.
const open = open: {
+ const disable_save_state = RunStep.create(b, "disable save state");
+ disable_save_state.has_side_effects = true;
+ disable_save_state.addArgs(&.{
+ "/usr/libexec/PlistBuddy",
+ "-c",
+ // We'll have to change this to `Set` if we ever put this
+ // into our Info.plist.
+ "Add :NSQuitAlwaysKeepsWindows bool false",
+ b.fmt("{s}/Contents/Info.plist", .{app_path}),
+ });
+ disable_save_state.expectExitCode(0);
+ disable_save_state.step.dependOn(&build.step);
+
const open = RunStep.create(b, "run Ghostty app");
open.has_side_effects = true;
open.cwd = b.path("");
@@ -98,22 +111,17 @@ pub fn init(
// Open depends on the app
open.step.dependOn(&build.step);
+ open.step.dependOn(&disable_save_state.step);
// This overrides our default behavior and forces logs to show
// up on stderr (in addition to the centralized macOS log).
open.setEnvironmentVariable("GHOSTTY_LOG", "1");
- // This is hack so that we can activate the app and bring it to
- // the front forcibly even though we're executing directly
- // via the binary and not launch services.
- open.setEnvironmentVariable("GHOSTTY_MAC_ACTIVATE", "1");
+ // Configure how we're launching
+ open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
if (b.args) |args| {
open.addArgs(args);
- } else {
- // This tricks the app into thinking it's running from the
- // app bundle so we don't execute our CLI mode.
- open.setEnvironmentVariable("GHOSTTY_MAC_APP", "1");
}
break :open open;
diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig
index 0aab5ecf8..b6e9900e2 100644
--- a/src/build/SharedDeps.zig
+++ b/src/build/SharedDeps.zig
@@ -760,6 +760,9 @@ pub fn gtkDistResources(
});
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
generate_c.addFileArg(gresource_xml);
+ for (gresource.dependencies) |file| {
+ generate_c.addFileInput(b.path(file));
+ }
const generate_h = b.addSystemCommand(&.{
"glib-compile-resources",
@@ -770,6 +773,9 @@ pub fn gtkDistResources(
});
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
generate_h.addFileArg(gresource_xml);
+ for (gresource.dependencies) |file| {
+ generate_h.addFileInput(b.path(file));
+ }
return .{
.resources_c = .{
diff --git a/src/cli/version.zig b/src/cli/version.zig
index a27d1050d..22608fa88 100644
--- a/src/cli/version.zig
+++ b/src/cli/version.zig
@@ -15,8 +15,6 @@ pub const Options = struct {};
/// The `version` command is used to display information about Ghostty. Recognized as
/// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 {
- _ = alloc;
-
const stdout = std.io.getStdOut().writer();
const tty = std.io.getStdOut().isTty();
@@ -34,32 +32,37 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)});
try stdout.print("Build Config\n", .{});
- try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string});
- try stdout.print(" - build mode : {}\n", .{builtin.mode});
- try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime});
- try stdout.print(" - font engine: {}\n", .{build_config.font_backend});
- try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
- try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
+ try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string});
+ try stdout.print(" - build mode : {}\n", .{builtin.mode});
+ try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime});
+ try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
+ try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
+ try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
if (comptime build_config.app_runtime == .gtk) {
- try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())});
- try stdout.print(" - GTK version:\n", .{});
- try stdout.print(" build : {}\n", .{gtk_version.comptime_version});
- try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()});
- try stdout.print(" - libadwaita : enabled\n", .{});
- try stdout.print(" build : {}\n", .{adw_version.comptime_version});
- try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()});
+ if (comptime builtin.os.tag == .linux) {
+ const kernel_info = internal_os.getKernelInfo(alloc);
+ defer if (kernel_info) |k| alloc.free(k);
+ try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"});
+ }
+ try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())});
+ try stdout.print(" - GTK version :\n", .{});
+ try stdout.print(" build : {}\n", .{gtk_version.comptime_version});
+ try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()});
+ try stdout.print(" - libadwaita : enabled\n", .{});
+ try stdout.print(" build : {}\n", .{adw_version.comptime_version});
+ try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()});
if (comptime build_options.x11) {
- try stdout.print(" - libX11 : enabled\n", .{});
+ try stdout.print(" - libX11 : enabled\n", .{});
} else {
- try stdout.print(" - libX11 : disabled\n", .{});
+ try stdout.print(" - libX11 : disabled\n", .{});
}
// We say `libwayland` since it is possible to build Ghostty without
// Wayland integration but with Wayland-enabled GTK
if (comptime build_options.wayland) {
- try stdout.print(" - libwayland : enabled\n", .{});
+ try stdout.print(" - libwayland : enabled\n", .{});
} else {
- try stdout.print(" - libwayland : disabled\n", .{});
+ try stdout.print(" - libwayland : disabled\n", .{});
}
}
return 0;
diff --git a/src/config.zig b/src/config.zig
index ac38eb89c..efc9fd973 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -14,6 +14,7 @@ pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry;
// Field types
+pub const BoldColor = Config.BoldColor;
pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
@@ -37,6 +38,7 @@ pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit;
+pub const LinkPreviews = Config.LinkPreviews;
// Alternate APIs
pub const CAPI = @import("config/CAPI.zig");
diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig
index 0b7108a59..bdc59797a 100644
--- a/src/config/CAPI.zig
+++ b/src/config/CAPI.zig
@@ -1,7 +1,9 @@
const std = @import("std");
+const assert = std.debug.assert;
const cli = @import("../cli.zig");
const inputpkg = @import("../input.zig");
-const global = &@import("../global.zig").state;
+const state = &@import("../global.zig").state;
+const c = @import("../main_c.zig");
const Config = @import("Config.zig");
const c_get = @import("c_get.zig");
@@ -12,14 +14,14 @@ const log = std.log.scoped(.config);
/// Create a new configuration filled with the initial default values.
export fn ghostty_config_new() ?*Config {
- const result = global.alloc.create(Config) catch |err| {
+ const result = state.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
- result.* = Config.default(global.alloc) catch |err| {
+ result.* = Config.default(state.alloc) catch |err| {
log.err("error creating config err={}", .{err});
- global.alloc.destroy(result);
+ state.alloc.destroy(result);
return null;
};
@@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config {
export fn ghostty_config_free(ptr: ?*Config) void {
if (ptr) |v| {
v.deinit();
- global.alloc.destroy(v);
+ state.alloc.destroy(v);
}
}
/// Deep clone the configuration.
export fn ghostty_config_clone(self: *Config) ?*Config {
- const result = global.alloc.create(Config) catch |err| {
+ const result = state.alloc.create(Config) catch |err| {
log.err("error allocating config err={}", .{err});
return null;
};
- result.* = self.clone(global.alloc) catch |err| {
+ result.* = self.clone(state.alloc) catch |err| {
log.err("error cloning config err={}", .{err});
- global.alloc.destroy(result);
+ state.alloc.destroy(result);
return null;
};
@@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config {
/// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void {
- self.loadCliArgs(global.alloc) catch |err| {
+ self.loadCliArgs(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -60,7 +62,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void {
/// is usually done first. The default file locations are locations
/// such as the home directory.
export fn ghostty_config_load_default_files(self: *Config) void {
- self.loadDefaultFiles(global.alloc) catch |err| {
+ self.loadDefaultFiles(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -69,7 +71,7 @@ export fn ghostty_config_load_default_files(self: *Config) void {
/// file locations in the previously loaded configuration. This will
/// recursively continue to load up to a built-in limit.
export fn ghostty_config_load_recursive_files(self: *Config) void {
- self.loadRecursiveFiles(global.alloc) catch |err| {
+ self.loadRecursiveFiles(state.alloc) catch |err| {
log.err("error loading config err={}", .{err});
};
}
@@ -122,10 +124,13 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic {
return .{ .message = message.ptr };
}
-export fn ghostty_config_open() void {
- edit.open(global.alloc) catch |err| {
+export fn ghostty_config_open_path() c.String {
+ const path = edit.openPath(state.alloc) catch |err| {
log.err("error opening config in editor err={}", .{err});
+ return .empty;
};
+
+ return .fromSlice(path);
}
/// Sync with ghostty_diagnostic_s
diff --git a/src/config/Config.zig b/src/config/Config.zig
index 1690c49a6..70f7b2ae8 100644
--- a/src/config/Config.zig
+++ b/src/config/Config.zig
@@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
// this behavior. This applies to selection too.
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
+
+ // Ghostty 1.2 merged `bold-is-bright` into the new `bold-color`
+ // by setting the value to "bright".
+ .{ "bold-is-bright", compatBoldIsBright },
});
/// The font families to use.
@@ -435,7 +439,7 @@ pub const compatibility = std.StaticStringMap(
/// * `hinting` - Enable or disable hinting. Enabled by default.
///
/// * `force-autohint` - Always use the freetype auto-hinter instead of
-/// the font's native hinter. Enabled by default.
+/// the font's native hinter. Disabled by default.
///
/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering.
/// This will disable anti-aliasing, and probably not look very good unless
@@ -1046,6 +1050,14 @@ link: RepeatableLink = .{},
/// `link`). If you want to customize URL matching, use `link` and disable this.
@"link-url": bool = true,
+/// Show link previews for a matched URL.
+///
+/// When true, link previews are shown for all matched URLs. When false, link
+/// previews are never shown. When set to "osc8", link previews are only shown
+/// for hyperlinks created with the OSC 8 sequence (in this case, the link text
+/// can differ from the link destination).
+@"link-previews": LinkPreviews = .true,
+
/// Whether to start the window in a maximized state. This setting applies
/// to new windows and does not apply to tabs, splits, etc. However, this setting
/// will apply to all new windows, not just the first one.
@@ -2815,8 +2827,24 @@ else
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true,
-/// If `true`, the bold text will use the bright color palette.
-@"bold-is-bright": bool = false,
+/// Modifies the color used for bold text in the terminal.
+///
+/// This can be set to a specific color, using the same format as
+/// `background` or `foreground` (e.g. `#RRGGBB` but other formats
+/// are also supported; see the aforementioned documentation). If a
+/// specific color is set, this color will always be used for all
+/// bold text regardless of the terminal's color scheme.
+///
+/// This can also be set to `bright`, which uses the bright color palette
+/// for bold text. For example, if the text is red, then the bold will
+/// use the bright red color. The terminal palette is set with `palette`
+/// but can also be overridden by the terminal application itself using
+/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous
+/// configuration `bold-is-bright` is deprecated and replaced by this
+/// usage).
+///
+/// Available since Ghostty 1.2.0.
+@"bold-color": ?BoldColor = null,
/// This will be used to set the `TERM` environment variable.
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
@@ -3921,6 +3949,23 @@ fn compatSelectionInvertFgBg(
return true;
}
+fn compatBoldIsBright(
+ self: *Config,
+ alloc: Allocator,
+ key: []const u8,
+ value_: ?[]const u8,
+) bool {
+ _ = alloc;
+ assert(std.mem.eql(u8, key, "bold-is-bright"));
+
+ const set = cli.args.parseBool(value_ orelse "t") catch return false;
+ if (set) {
+ self.@"bold-color" = .bright;
+ }
+
+ return true;
+}
+
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`
@@ -4345,6 +4390,12 @@ pub const WindowSubtitle = enum {
@"working-directory",
};
+pub const LinkPreviews = enum {
+ false,
+ true,
+ osc8,
+};
+
/// Color represents a color using RGB.
///
/// This is a packed struct so that the C API to read color values just
@@ -4542,6 +4593,58 @@ pub const TerminalColor = union(enum) {
}
};
+/// Represents color values that can be used for bold. See `bold-color`.
+pub const BoldColor = union(enum) {
+ color: Color,
+ bright,
+
+ pub fn parseCLI(input_: ?[]const u8) !BoldColor {
+ const input = input_ orelse return error.ValueRequired;
+ if (std.mem.eql(u8, input, "bright")) return .bright;
+ return .{ .color = try Color.parseCLI(input) };
+ }
+
+ /// Used by Formatter
+ pub fn formatEntry(self: BoldColor, formatter: anytype) !void {
+ switch (self) {
+ .color => try self.color.formatEntry(formatter),
+ .bright => try formatter.formatEntry(
+ [:0]const u8,
+ @tagName(self),
+ ),
+ }
+ }
+
+ test "parseCLI" {
+ const testing = std.testing;
+
+ try testing.expectEqual(
+ BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } },
+ try BoldColor.parseCLI("#4e2a84"),
+ );
+ try testing.expectEqual(
+ BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } },
+ try BoldColor.parseCLI("black"),
+ );
+ try testing.expectEqual(
+ BoldColor.bright,
+ try BoldColor.parseCLI("bright"),
+ );
+
+ try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a"));
+ }
+
+ test "formatConfig" {
+ const testing = std.testing;
+ var buf = std.ArrayList(u8).init(testing.allocator);
+ defer buf.deinit();
+
+ var sc: BoldColor = .bright;
+ try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
+ try testing.expectEqualSlices(u8, "a = bright\n", buf.items);
+ }
+};
+
pub const ColorList = struct {
const Self = @This();
@@ -6552,8 +6655,9 @@ pub const RepeatableCommand = struct {
try list.parseCLI(alloc, "title:Foo,action:ignore");
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
+ try list.parseCLI(alloc, "title:Baz,description:Raspberry Pie,action:set_font_size:3.14");
- try testing.expectEqual(@as(usize, 3), list.value.items.len);
+ try testing.expectEqual(@as(usize, 4), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title);
@@ -6570,6 +6674,13 @@ pub const RepeatableCommand = struct {
try testing.expectEqualStrings("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description);
+ try testing.expectEqual(
+ inputpkg.Binding.Action{ .set_font_size = 3.14 },
+ list.value.items[3].action,
+ );
+ try testing.expectEqualStrings("Baz", list.value.items[3].title);
+ try testing.expectEqualStrings("Raspberry Pie", list.value.items[3].description);
+
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len);
}
@@ -7105,7 +7216,7 @@ pub const FreetypeLoadFlags = packed struct {
// for Freetype itself. Ghostty hasn't made any opinionated changes
// to these defaults.
hinting: bool = true,
- @"force-autohint": bool = true,
+ @"force-autohint": bool = false,
monochrome: bool = false,
autohint: bool = true,
};
@@ -8235,3 +8346,23 @@ test "compatibility: removed selection-invert-fg-bg" {
);
}
}
+
+test "compatibility: removed bold-is-bright" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ {
+ var cfg = try Config.default(alloc);
+ defer cfg.deinit();
+ var it: TestIterator = .{ .data = &.{
+ "--bold-is-bright",
+ } };
+ try cfg.loadIter(alloc, &it);
+ try cfg.finalize();
+
+ try testing.expectEqual(
+ BoldColor.bright,
+ cfg.@"bold-color",
+ );
+ }
+}
diff --git a/src/config/edit.zig b/src/config/edit.zig
index ae4394942..38dc98169 100644
--- a/src/config/edit.zig
+++ b/src/config/edit.zig
@@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig");
-/// Open the configuration in the OS default editor according to the default
-/// paths the main config file could be in.
+/// The path to the configuration that should be opened for editing.
///
-/// On Linux, this will open the file at the XDG config path. This is the
+/// On Linux, this will use the file at the XDG config path. This is the
/// only valid path for Linux so we don't need to check for other paths.
///
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
-/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
+/// prioritizes AppSupport over XDG, we will use AppSupport if it exists,
/// followed by XDG if it exists, and finally AppSupport if neither exist.
/// For the existence check, we also prefer non-empty files over empty
/// files.
-pub fn open(alloc_gpa: Allocator) !void {
+///
+/// The returned value is allocated using the provided allocator.
+pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
// Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
@@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void {
}
};
- try internal_os.open(alloc_gpa, .text, config_path);
+ return try alloc_gpa.dupeZ(u8, config_path);
}
/// Returns the config path to use for open for the current OS.
diff --git a/src/font/Collection.zig b/src/font/Collection.zig
index 8533331bc..1d85d8a28 100644
--- a/src/font/Collection.zig
+++ b/src/font/Collection.zig
@@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
if (self.load_options) |*v| v.deinit(alloc);
}
-pub const AddError = Allocator.Error || error{
- CollectionFull,
- DeferredLoadingUnavailable,
-};
+pub const AddError =
+ Allocator.Error ||
+ AdjustSizeError ||
+ error{
+ CollectionFull,
+ DeferredLoadingUnavailable,
+ SetSizeFailed,
+ };
/// Add a face to the collection for the given style. This face will be added
/// next in priority if others exist already, i.e. it'll be the _last_ to be
@@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{
/// If no error is encountered then the collection takes ownership of the face,
/// in which case face will be deallocated when the collection is deallocated.
///
-/// If a loaded face is added to the collection, it should be the same
-/// size as all the other faces in the collection. This function will not
-/// verify or modify the size until the size of the entire collection is
-/// changed.
+/// If a loaded face is added to the collection, its size will be changed to
+/// match the size specified in load_options, adjusted for harmonization with
+/// the primary face.
pub fn add(
self: *Collection,
alloc: Allocator,
@@ -103,9 +106,107 @@ pub fn add(
return error.DeferredLoadingUnavailable;
try list.append(alloc, face);
+
+ var owned: *Entry = list.at(idx);
+
+ // If the face is already loaded, apply font size adjustment
+ // now, otherwise we'll apply it whenever we do load it.
+ if (owned.getLoaded()) |loaded| {
+ if (try self.adjustedSize(loaded)) |opts| {
+ loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed;
+ }
+ }
+
return .{ .style = style, .idx = @intCast(idx) };
}
+pub const AdjustSizeError = font.Face.GetMetricsError;
+
+// Calculate a size for the provided face that will match it with the primary
+// font, metrically, to improve consistency with fallback fonts. Right now we
+// match the font based on the ex height, or the ideograph width if the font
+// has ideographs in it.
+//
+// This returns null if load options is null or if self.load_options is null.
+//
+// This is very much like the `font-size-adjust` CSS property in how it works.
+// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
+//
+// TODO: In the future, provide config options that allow the user to select
+// which metric should be matched for fallback fonts, instead of hard
+// coding it as ex height.
+pub fn adjustedSize(
+ self: *Collection,
+ face: *Face,
+) AdjustSizeError!?LoadOptions {
+ const load_options = self.load_options orelse return null;
+
+ // We silently do nothing if we can't get the primary
+ // face, because this might be the primary face itself.
+ const primary_face = self.getFace(.{ .idx = 0 }) catch return null;
+
+ // We do nothing if the primary face and this face are the same.
+ if (@intFromPtr(primary_face) == @intFromPtr(face)) return null;
+
+ const primary_metrics = try primary_face.getMetrics();
+ const face_metrics = try face.getMetrics();
+
+ // We use the ex height to match our font sizes, so that the height of
+ // lower-case letters matches between all fonts in the fallback chain.
+ //
+ // We estimate ex height as 0.75 * cap height if it's not specifically
+ // provided, and we estimate cap height as 0.75 * ascent in the same case.
+ //
+ // If the fallback font has an ic_width we prefer that, for normalization
+ // of CJK font sizes when mixed with latin fonts.
+ //
+ // We estimate the ic_width as twice the cell width if it isn't provided.
+ var primary_cap = primary_metrics.cap_height orelse 0.0;
+ if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75;
+
+ var primary_ex = primary_metrics.ex_height orelse 0.0;
+ if (primary_ex <= 0) primary_ex = primary_cap * 0.75;
+
+ var primary_ic = primary_metrics.ic_width orelse 0.0;
+ if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2;
+
+ var face_cap = face_metrics.cap_height orelse 0.0;
+ if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75;
+
+ var face_ex = face_metrics.ex_height orelse 0.0;
+ if (face_ex <= 0) face_ex = face_cap * 0.75;
+
+ var face_ic = face_metrics.ic_width orelse 0.0;
+ if (face_ic <= 0) face_ic = face_metrics.cell_width * 2;
+
+ // If the line height of the scaled font would be larger than
+ // the line height of the primary font, we don't want that, so
+ // we take the minimum between matching the ic/ex and the line
+ // height.
+ //
+ // NOTE: We actually allow the line height to be up to 1.2
+ // times the primary line height because empirically
+ // this is usually fine and is better for CJK.
+ //
+ // TODO: We should probably provide a config option that lets
+ // the user pick what metric to use for size adjustment.
+ const scale = @min(
+ 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(),
+ if (face_metrics.ic_width != null)
+ primary_ic / face_ic
+ else
+ primary_ex / face_ex,
+ );
+
+ // Make a copy of our load options, set the size to the size of
+ // the provided face, and then multiply that by our scaling factor.
+ var opts = load_options;
+ opts.size = face.size;
+ opts.size.points *= @as(f32, @floatCast(scale));
+
+ return opts;
+}
+
/// Return the Face represented by a given Index. The returned pointer
/// is only valid as long as this collection is not modified.
///
@@ -129,21 +230,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
break :item item;
};
- return try self.getFaceFromEntry(item);
+ const face = try self.getFaceFromEntry(
+ item,
+ // We only want to adjust the size if this isn't the primary face.
+ index.style != .regular or index.idx > 0,
+ );
+
+ return face;
}
/// Get the face from an entry.
///
/// This entry must not be an alias.
-fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face {
+fn getFaceFromEntry(
+ self: *Collection,
+ entry: *Entry,
+ /// Whether to adjust the font size to match the primary face after loading.
+ adjust: bool,
+) !*Face {
assert(entry.* != .alias);
return switch (entry.*) {
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse
return error.DeferredLoadingUnavailable;
- const face = try d.load(opts.library, opts.faceOptions());
+ var face = try d.load(opts.library, opts.faceOptions());
d.deinit();
+
+ // If we need to adjust the size, do so.
+ if (adjust) if (try self.adjustedSize(&face)) |new_opts| {
+ try face.setSize(new_opts.faceOptions());
+ };
+
entry.* = switch (tag) {
.deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face },
@@ -247,7 +365,7 @@ pub fn completeStyles(
while (it.next()) |entry| {
// Load our face. If we fail to load it, we just skip it and
// continue on to try the next one.
- const face = self.getFaceFromEntry(entry) catch |err| {
+ const face = self.getFaceFromEntry(entry, false) catch |err| {
log.warn("error loading regular entry={d} err={}", .{
it.index - 1,
err,
@@ -371,7 +489,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to bold it.
- const regular = try self.getFaceFromEntry(entry);
+ const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticBold(opts.faceOptions());
var buf: [256]u8 = undefined;
@@ -391,7 +509,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it.
- const regular = try self.getFaceFromEntry(entry);
+ const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticItalic(opts.faceOptions());
var buf: [256]u8 = undefined;
@@ -420,9 +538,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
while (it.next()) |array| {
var entry_it = array.value.iterator(0);
while (entry_it.next()) |entry| switch (entry.*) {
- .loaded, .fallback_loaded => |*f| try f.setSize(
- opts.faceOptions(),
- ),
+ .loaded,
+ .fallback_loaded,
+ => |*f| {
+ const new_opts = try self.adjustedSize(f) orelse opts.*;
+ try f.setSize(new_opts.faceOptions());
+ },
// Deferred aren't loaded so we don't need to set their size.
// The size for when they're loaded is set since `opts` changed.
@@ -549,6 +670,16 @@ pub const Entry = union(enum) {
}
}
+ /// If this face is loaded, or is an alias to a loaded face,
+ /// then this returns the `Face`, otherwise returns null.
+ pub fn getLoaded(self: *Entry) ?*Face {
+ return switch (self.*) {
+ .deferred, .fallback_deferred => null,
+ .loaded, .fallback_loaded => |*face| face,
+ .alias => |v| v.getLoaded(),
+ };
+ }
+
/// True if the entry is deferred.
fn isDeferred(self: Entry) bool {
return switch (self) {
@@ -906,12 +1037,13 @@ test "metrics" {
var c = init();
defer c.deinit(alloc);
- c.load_options = .{ .library = lib };
+ const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
+ c.load_options = .{ .library = lib, .size = size };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
lib,
testFont,
- .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
+ .{ .size = size },
) });
try c.updateMetrics();
@@ -958,3 +1090,62 @@ test "metrics" {
.cursor_height = 34,
}, c.metrics);
}
+
+// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
+test "adjusted sizes" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.inconsolata;
+ const fallback = font.embedded.monaspace_neon;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
+ c.load_options = .{ .library = lib, .size = size };
+
+ // Add our primary face.
+ _ = try c.add(alloc, .regular, .{ .loaded = try .init(
+ lib,
+ testFont,
+ .{ .size = size },
+ ) });
+
+ try c.updateMetrics();
+
+ // Add the fallback face.
+ const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init(
+ lib,
+ fallback,
+ .{ .size = size },
+ ) });
+
+ // The ex heights should match.
+ {
+ const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
+ const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
+
+ try std.testing.expectApproxEqAbs(
+ primary_metrics.ex_height.?,
+ fallback_metrics.ex_height.?,
+ // We accept anything within half a pixel.
+ 0.5,
+ );
+ }
+
+ // Resize should keep that relationship.
+ try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
+ {
+ const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
+ const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
+
+ try std.testing.expectApproxEqAbs(
+ primary_metrics.ex_height.?,
+ fallback_metrics.ex_height.?,
+ // We accept anything within half a pixel.
+ 0.5,
+ );
+ }
+}
diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig
index fa29e44fa..f99370271 100644
--- a/src/font/Glyph.zig
+++ b/src/font/Glyph.zig
@@ -17,9 +17,3 @@ offset_y: i32,
/// be normalized to be between 0 and 1 prior to use in shaders.
atlas_x: u32,
atlas_y: u32,
-
-/// horizontal position to increase drawing position for strings
-advance_x: f32,
-
-/// Whether we drew this glyph ourselves with the sprite font.
-sprite: bool = false,
diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig
index bf527a021..f96d753b3 100644
--- a/src/font/Metrics.zig
+++ b/src/font/Metrics.zig
@@ -107,6 +107,19 @@ pub const FaceMetrics = struct {
/// a provided ex height metric or measured from the height of the
/// lowercase x glyph.
ex_height: ?f64 = null,
+
+ /// The width of the character "水" (CJK water ideograph, U+6C34),
+ /// if present. This is used for font size adjustment, to normalize
+ /// the width of CJK fonts mixed with latin fonts.
+ ///
+ /// NOTE: IC = Ideograph Character
+ ic_width: ?f64 = null,
+
+ /// Convenience function for getting the line height
+ /// (ascent - descent + line_gap).
+ pub inline fn lineHeight(self: FaceMetrics) f64 {
+ return self.ascent - self.descent + self.line_gap;
+ }
};
/// Calculate our metrics based on values extracted from a font.
diff --git a/src/font/face.zig b/src/font/face.zig
index 363576ff0..245edcf4b 100644
--- a/src/font/face.zig
+++ b/src/font/face.zig
@@ -147,6 +147,9 @@ pub const RenderOptions = struct {
/// Maximum ratio of width to height when resizing.
max_xy_ratio: ?f64 = null,
+ /// Maximum number of cells horizontally to use.
+ max_constraint_width: u2 = 2,
+
pub const Size = enum {
/// Don't change the size of this glyph.
none,
@@ -186,16 +189,26 @@ pub const RenderOptions = struct {
pub fn constrain(
self: Constraint,
glyph: GlyphSize,
- /// Available width
+ /// Width of one cell.
cell_width: f64,
- /// Available height
+ /// Height of one cell.
cell_height: f64,
+ /// Number of cells horizontally available for this glyph.
+ constraint_width: u2,
) GlyphSize {
var g = glyph;
- const w = cell_width -
- self.pad_left * cell_width -
- self.pad_right * cell_width;
+ const available_width =
+ cell_width * @as(f64, @floatFromInt(
+ @min(
+ self.max_constraint_width,
+ constraint_width,
+ ),
+ ));
+
+ const w = available_width -
+ self.pad_left * available_width -
+ self.pad_right * available_width;
const h = cell_height -
self.pad_top * cell_height -
self.pad_bottom * cell_height;
@@ -203,7 +216,7 @@ pub const RenderOptions = struct {
// Subtract padding from the bearings so that our
// alignment and sizing code works correctly. We
// re-add before returning.
- g.x -= self.pad_left * cell_width;
+ g.x -= self.pad_left * available_width;
g.y -= self.pad_bottom * cell_height;
switch (self.size_horizontal) {
@@ -305,7 +318,7 @@ pub const RenderOptions = struct {
}
// Re-add our padding before returning.
- g.x += self.pad_left * cell_width;
+ g.x += self.pad_left * available_width;
g.y += self.pad_bottom * cell_height;
return g;
diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig
index 35f094848..83f993715 100644
--- a/src/font/face/coretext.zig
+++ b/src/font/face/coretext.zig
@@ -31,6 +31,9 @@ pub const Face = struct {
/// tables).
color: ?ColorState = null,
+ /// The current size this font is set to.
+ size: font.face.DesiredSize,
+
/// True if our build is using Harfbuzz. If we're not, we can avoid
/// some Harfbuzz-specific code paths.
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
@@ -106,6 +109,7 @@ pub const Face = struct {
.font = ct_font,
.hb_font = hb_font,
.color = color,
+ .size = opts.size,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@@ -333,11 +337,10 @@ pub const Face = struct {
.offset_y = 0,
.atlas_x = 0,
.atlas_y = 0,
- .advance_x = 0,
};
const metrics = opts.grid_metrics;
- const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
+ const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_size = opts.constraint.constrain(
@@ -349,6 +352,7 @@ pub const Face = struct {
},
cell_width,
cell_height,
+ opts.constraint_width,
);
const width = glyph_size.width;
@@ -356,8 +360,16 @@ pub const Face = struct {
const x = glyph_size.x;
const y = glyph_size.y;
- const px_width: u32 = @intFromFloat(@ceil(width));
- const px_height: u32 = @intFromFloat(@ceil(height));
+ // We have to include the fractional pixels that we won't be offsetting
+ // in our width and height calculations, that is, we offset by the floor
+ // of the bearings when we render the glyph, meaning there's still a bit
+ // of extra width to the area that's drawn in beyond just the width of
+ // the glyph itself, so we include that extra fraction of a pixel when
+ // calculating the width and height here.
+ const frac_x = rect.origin.x - @floor(rect.origin.x);
+ const frac_y = rect.origin.y - @floor(rect.origin.y);
+ const px_width: u32 = @intFromFloat(@ceil(width + frac_x));
+ const px_height: u32 = @intFromFloat(@ceil(height + frac_y));
// Settings that are specific to if we are rendering text or emoji.
const color: struct {
@@ -475,26 +487,44 @@ pub const Face = struct {
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
- var result: i32 = @intFromFloat(@round(x));
-
- // If our cell was resized then we adjust our glyph's
- // position relative to the new center. This keeps glyphs
- // centered in the cell whether it was made wider or narrower.
- if (metrics.original_cell_width) |original_width| {
- const before: i32 = @intCast(original_width);
- const after: i32 = @intCast(metrics.cell_width);
- // Increase the offset by half of the difference
- // between the widths to keep things centered.
- result += @divTrunc(after - before, 2);
+ // If the glyph's advance is narrower than the cell width then we
+ // center the advance of the glyph within the cell width. At first
+ // I implemented this to proportionally scale the center position
+ // of the glyph but that messes up glyphs that are meant to align
+ // vertically with others, so this is a compromise.
+ //
+ // This makes it so that when the `adjust-cell-width` config is
+ // used, or when a fallback font with a different advance width
+ // is used, we don't get weirdly aligned glyphs.
+ //
+ // We don't do this if the constraint has a horizontal alignment,
+ // since in that case the position was already calculated with the
+ // new cell width in mind.
+ if (opts.constraint.align_horizontal == .none) {
+ var advances: [glyphs.len]macos.graphics.Size = undefined;
+ _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
+ const advance = advances[0].width;
+ const new_advance =
+ cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
+ // If the original advance is greater than the cell width then
+ // it's possible that this is a ligature or other glyph that is
+ // intended to overflow the cell to one side or the other, and
+ // adjusting the bearings could mess that up, so we just leave
+ // it alone if that's the case.
+ //
+ // We also don't want to do anything if the advance is zero or
+ // less, since this is used for stuff like combining characters.
+ if (advance > new_advance or advance <= 0.0) {
+ break :offset_x @intFromFloat(@ceil(x - frac_x));
+ }
+ break :offset_x @intFromFloat(
+ @ceil(x - frac_x + (new_advance - advance) / 2),
+ );
+ } else {
+ break :offset_x @intFromFloat(@ceil(x - frac_x));
}
-
- break :offset_x result;
};
- // Get our advance
- var advances: [glyphs.len]macos.graphics.Size = undefined;
- _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
-
return .{
.width = px_width,
.height = px_height,
@@ -502,7 +532,6 @@ pub const Face = struct {
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
- .advance_x = @floatCast(advances[0].width),
};
}
@@ -734,6 +763,20 @@ pub const Face = struct {
break :cell_width max;
};
+ // Measure "水" (CJK water ideograph, U+6C34) for our ic width.
+ const ic_width: ?f64 = ic_width: {
+ const glyph = self.glyphIndex('水') orelse break :ic_width null;
+
+ var advances: [1]macos.graphics.Size = undefined;
+ _ = ct_font.getAdvancesForGlyphs(
+ .horizontal,
+ &.{@intCast(glyph)},
+ &advances,
+ );
+
+ break :ic_width advances[0].width;
+ };
+
return .{
.cell_width = cell_width,
.ascent = ascent,
@@ -745,6 +788,7 @@ pub const Face = struct {
.strikethrough_thickness = strikethrough_thickness,
.cap_height = cap_height,
.ex_height = ex_height,
+ .ic_width = ic_width,
};
}
diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig
index c23ede04a..ae3bd0968 100644
--- a/src/font/face/freetype.zig
+++ b/src/font/face/freetype.zig
@@ -59,6 +59,9 @@ pub const Face = struct {
bold: bool = false,
} = .{},
+ /// The current size this font is set to.
+ size: font.face.DesiredSize,
+
/// Initialize a new font face with the given source in-memory.
pub fn initFile(
lib: Library,
@@ -107,6 +110,7 @@ pub const Face = struct {
.hb_font = hb_font,
.ft_mutex = ft_mutex,
.load_flags = opts.freetype_load_flags,
+ .size = opts.size,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@@ -203,6 +207,7 @@ pub const Face = struct {
/// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, opts: font.face.Options) !void {
try setSize_(self.face, opts.size);
+ self.size = opts.size;
}
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
@@ -348,7 +353,7 @@ pub const Face = struct {
// use options from config
.no_hinting = !do_hinting,
- .force_autohint = !self.load_flags.@"force-autohint",
+ .force_autohint = self.load_flags.@"force-autohint",
.no_autohint = !self.load_flags.autohint,
// NO_SVG set to true because we don't currently support rendering
@@ -373,7 +378,6 @@ pub const Face = struct {
.offset_y = 0,
.atlas_x = 0,
.atlas_y = 0,
- .advance_x = 0,
};
// For synthetic bold, we embolden the glyph.
@@ -390,7 +394,7 @@ pub const Face = struct {
// Next we need to apply any constraints.
const metrics = opts.grid_metrics;
- const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
+ const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
@@ -405,6 +409,7 @@ pub const Face = struct {
},
cell_width,
cell_height,
+ opts.constraint_width,
);
const width = glyph_size.width;
@@ -638,20 +643,40 @@ pub const Face = struct {
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
- var result: i32 = @intFromFloat(@floor(x));
-
- // If our cell was resized then we adjust our glyph's
- // position relative to the new center. This keeps glyphs
- // centered in the cell whether it was made wider or narrower.
- if (metrics.original_cell_width) |original_width| {
- const before: i32 = @intCast(original_width);
- const after: i32 = @intCast(metrics.cell_width);
- // Increase the offset by half of the difference
- // between the widths to keep things centered.
- result += @divTrunc(after - before, 2);
+ // If the glyph's advance is narrower than the cell width then we
+ // center the advance of the glyph within the cell width. At first
+ // I implemented this to proportionally scale the center position
+ // of the glyph but that messes up glyphs that are meant to align
+ // vertically with others, so this is a compromise.
+ //
+ // This makes it so that when the `adjust-cell-width` config is
+ // used, or when a fallback font with a different advance width
+ // is used, we don't get weirdly aligned glyphs.
+ //
+ // We don't do this if the constraint has a horizontal alignment,
+ // since in that case the position was already calculated with the
+ // new cell width in mind.
+ if (opts.constraint.align_horizontal == .none) {
+ const advance = f26dot6ToFloat(glyph.*.advance.x);
+ const new_advance =
+ cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
+ // If the original advance is greater than the cell width then
+ // it's possible that this is a ligature or other glyph that is
+ // intended to overflow the cell to one side or the other, and
+ // adjusting the bearings could mess that up, so we just leave
+ // it alone if that's the case.
+ //
+ // We also don't want to do anything if the advance is zero or
+ // less, since this is used for stuff like combining characters.
+ if (advance > new_advance or advance <= 0.0) {
+ break :offset_x @intFromFloat(@floor(x));
+ }
+ break :offset_x @intFromFloat(
+ @floor(x + (new_advance - advance) / 2),
+ );
+ } else {
+ break :offset_x @intFromFloat(@floor(x));
}
-
- break :offset_x result;
};
return Glyph{
@@ -661,7 +686,6 @@ pub const Face = struct {
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
- .advance_x = f26dot6ToFloat(glyph.*.advance.x),
};
}
@@ -832,7 +856,7 @@ pub const Face = struct {
while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
- .render = true,
+ .render = false,
.no_svg = true,
})) {
max = @max(
@@ -870,7 +894,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock();
if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
- .render = true,
+ .render = false,
.no_svg = true,
})) {
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
@@ -883,7 +907,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock();
if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
- .render = true,
+ .render = false,
.no_svg = true,
})) {
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
@@ -894,6 +918,21 @@ pub const Face = struct {
};
};
+ // Measure "水" (CJK water ideograph, U+6C34) for our ic width.
+ const ic_width: ?f64 = ic_width: {
+ self.ft_mutex.lock();
+ defer self.ft_mutex.unlock();
+
+ const glyph = face.getCharIndex('水') orelse break :ic_width null;
+
+ face.loadGlyph(glyph, .{
+ .render = false,
+ .no_svg = true,
+ }) catch break :ic_width null;
+
+ break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x);
+ };
+
return .{
.cell_width = cell_width,
@@ -909,6 +948,7 @@ pub const Face = struct {
.cap_height = cap_height,
.ex_height = ex_height,
+ .ic_width = ic_width,
};
}
diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig
index 30540191d..7ea2f0426 100644
--- a/src/font/face/web_canvas.zig
+++ b/src/font/face/web_canvas.zig
@@ -235,7 +235,6 @@ pub const Face = struct {
.offset_y = 0,
.atlas_x = region.x,
.atlas_y = region.y,
- .advance_x = 0,
};
}
diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig
index 1465a8466..70920bb0a 100644
--- a/src/font/nerd_font_attributes.zig
+++ b/src/font/nerd_font_attributes.zig
@@ -1,4 +1,4 @@
-//! This is a generate file, produced by nerd_font_codegen.py
+//! This is a generated file, produced by nerd_font_codegen.py
//! DO NOT EDIT BY HAND!
//!
//! This file provides info extracted from the nerd fonts patcher script,
@@ -13,6 +13,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .center,
.align_vertical = .center,
.pad_left = -0.02,
@@ -24,24 +25,29 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .cover,
.size_vertical = .fit,
+ .max_constraint_width = 1,
.align_horizontal = .center,
.align_vertical = .center,
.pad_left = 0.1,
.pad_right = 0.1,
- .pad_top = 0.01,
- .pad_bottom = 0.01,
+ .pad_top = 0.1,
+ .pad_bottom = 0.1,
},
0x276c...0x2771,
=> .{
.size_horizontal = .cover,
.size_vertical = .fit,
+ .max_constraint_width = 1,
.align_horizontal = .center,
.align_vertical = .center,
+ .pad_top = 0.15,
+ .pad_bottom = 0.15,
},
0xe0b0,
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.06,
@@ -54,6 +60,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.max_xy_ratio = 0.7,
@@ -62,6 +69,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.06,
@@ -74,6 +82,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.max_xy_ratio = 0.7,
@@ -82,6 +91,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.06,
@@ -94,6 +104,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.max_xy_ratio = 0.5,
@@ -102,6 +113,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.06,
@@ -114,6 +126,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.max_xy_ratio = 0.5,
@@ -123,6 +136,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.05,
@@ -135,6 +149,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
},
@@ -143,6 +158,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.05,
@@ -155,6 +171,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
},
@@ -204,8 +221,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
- .pad_top = 0.01,
- .pad_bottom = 0.01,
+ .pad_top = 0.03,
+ .pad_bottom = 0.03,
.max_xy_ratio = 0.86,
},
0xe0c5,
@@ -216,8 +233,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
- .pad_top = 0.01,
- .pad_bottom = 0.01,
+ .pad_top = 0.03,
+ .pad_bottom = 0.03,
.max_xy_ratio = 0.86,
},
0xe0c6,
@@ -228,8 +245,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
- .pad_top = 0.01,
- .pad_bottom = 0.01,
+ .pad_top = 0.03,
+ .pad_bottom = 0.03,
.max_xy_ratio = 0.78,
},
0xe0c7,
@@ -240,8 +257,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center,
.pad_left = 0.03,
.pad_right = 0.03,
- .pad_top = 0.01,
- .pad_bottom = 0.01,
+ .pad_top = 0.03,
+ .pad_bottom = 0.03,
.max_xy_ratio = 0.78,
},
0xe0cc,
@@ -285,6 +302,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.02,
@@ -297,6 +315,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.02,
@@ -309,6 +328,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .start,
.align_vertical = .center,
.pad_left = -0.05,
@@ -321,6 +341,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .stretch,
.size_vertical = .stretch,
+ .max_constraint_width = 1,
.align_horizontal = .end,
.align_vertical = .center,
.pad_left = -0.05,
diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py
index c2dd7314f..e74b2ead1 100644
--- a/src/font/nerd_font_codegen.py
+++ b/src/font/nerd_font_codegen.py
@@ -1,102 +1,124 @@
"""
-This file is mostly vibe coded because I don't like Python. It extracts the
-patch sets from the nerd fonts font patcher file in order to extract scaling
-rules and attributes for different codepoint ranges which it then codegens
-in to a Zig file with a function that switches over codepoints and returns
-the attributes and scaling rules.
+This file extracts the patch sets from the nerd fonts font patcher file in order to
+extract scaling rules and attributes for different codepoint ranges which it then
+codegens in to a Zig file with a function that switches over codepoints and returns the
+attributes and scaling rules.
-This does include an `eval` call! This is spooky, but we trust
-the nerd fonts code to be safe and not malicious or anything.
+This does include an `eval` call! This is spooky, but we trust the nerd fonts code to
+be safe and not malicious or anything.
+
+This script requires Python 3.12 or greater.
"""
import ast
import math
-from pathlib import Path
from collections import defaultdict
+from contextlib import suppress
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Literal, TypedDict, cast
+
+type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry]
+type AttributeHash = tuple[str | None, str | None, str, float, float, float]
+type ResolvedSymbol = PatchSetAttributes | PatchSetScaleRules | int | None
+
+
+class PatchSetScaleRules(TypedDict):
+ ShiftMode: str
+ ScaleGroups: list[list[int] | range]
+
+
+class PatchSetAttributeEntry(TypedDict):
+ align: str
+ valign: str
+ stretch: str
+ params: dict[str, float | bool]
+
+
+class PatchSet(TypedDict):
+ SymStart: int
+ SymEnd: int
+ SrcStart: int | None
+ ScaleRules: PatchSetScaleRules | None
+ Attributes: PatchSetAttributes
class PatchSetExtractor(ast.NodeVisitor):
- def __init__(self):
- self.symbol_table = {}
- self.patch_set_values = []
+ def __init__(self) -> None:
+ self.symbol_table: dict[str, ast.expr] = {}
+ self.patch_set_values: list[PatchSet] = []
- def visit_ClassDef(self, node):
- if node.name == "font_patcher":
- for item in node.body:
- if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set":
- self.visit_setup_patch_set(item)
+ def visit_ClassDef(self, node: ast.ClassDef) -> None:
+ if node.name != "font_patcher":
+ return
+ for item in node.body:
+ if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set":
+ self.visit_setup_patch_set(item)
- def visit_setup_patch_set(self, node):
+ def visit_setup_patch_set(self, node: ast.FunctionDef) -> None:
# First pass: gather variable assignments
for stmt in node.body:
- if isinstance(stmt, ast.Assign):
- # Store simple variable assignments in the symbol table
- if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
- var_name = stmt.targets[0].id
- self.symbol_table[var_name] = stmt.value
+ match stmt:
+ case ast.Assign(targets=[ast.Name(id=symbol)]):
+ # Store simple variable assignments in the symbol table
+ self.symbol_table[symbol] = stmt.value
# Second pass: process self.patch_set
for stmt in node.body:
- if isinstance(stmt, ast.Assign):
- for target in stmt.targets:
- if isinstance(target, ast.Attribute) and target.attr == "patch_set":
- if isinstance(stmt.value, ast.List):
- for elt in stmt.value.elts:
- if isinstance(elt, ast.Dict):
- self.process_patch_entry(elt)
+ if not isinstance(stmt, ast.Assign):
+ continue
+ for target in stmt.targets:
+ if (
+ isinstance(target, ast.Attribute)
+ and target.attr == "patch_set"
+ and isinstance(stmt.value, ast.List)
+ ):
+ for elt in stmt.value.elts:
+ if isinstance(elt, ast.Dict):
+ self.process_patch_entry(elt)
- def resolve_symbol(self, node):
+ def resolve_symbol(self, node: ast.expr) -> ResolvedSymbol:
"""Resolve named variables to their actual values from the symbol table."""
if isinstance(node, ast.Name) and node.id in self.symbol_table:
return self.safe_literal_eval(self.symbol_table[node.id])
return self.safe_literal_eval(node)
- def safe_literal_eval(self, node):
+ def safe_literal_eval(self, node: ast.expr) -> ResolvedSymbol:
"""Try to evaluate or stringify an AST node."""
try:
return ast.literal_eval(node)
- except Exception:
+ except ValueError:
# Spooky eval! But we trust nerd fonts to be safe...
if hasattr(ast, "unparse"):
return eval(
- ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()}
+ ast.unparse(node),
+ {"box_keep": True},
+ {"self": SimpleNamespace(args=SimpleNamespace(careful=True))},
)
- else:
- return f""
+ msg = f""
+ raise ValueError(msg) from None
- def process_patch_entry(self, dict_node):
+ def process_patch_entry(self, dict_node: ast.Dict) -> None:
entry = {}
+ disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"})
for key_node, value_node in zip(dict_node.keys, dict_node.values):
- if isinstance(key_node, ast.Constant) and key_node.value in (
- "Enabled",
- "Name",
- "Filename",
- "Exact",
+ if (
+ isinstance(key_node, ast.Constant)
+ and key_node.value not in disallowed_key_nodes
):
- continue
- key = ast.literal_eval(key_node)
- value = self.resolve_symbol(value_node)
- entry[key] = value
- self.patch_set_values.append(entry)
+ key = ast.literal_eval(cast("ast.Constant", key_node))
+ entry[key] = self.resolve_symbol(value_node)
+ self.patch_set_values.append(cast("PatchSet", entry))
-def extract_patch_set_values(source_code):
+def extract_patch_set_values(source_code: str) -> list[PatchSet]:
tree = ast.parse(source_code)
extractor = PatchSetExtractor()
extractor.visit(tree)
return extractor.patch_set_values
-# We have to spoof `self` and `self.args` for the eval.
-class SpoofArgs:
- careful = True
-
-
-class SpoofSelf:
- args = SpoofArgs()
-
-
-def parse_alignment(val):
+def parse_alignment(val: str) -> str | None:
return {
"l": ".start",
"r": ".end",
@@ -105,28 +127,24 @@ def parse_alignment(val):
}.get(val, ".none")
-def get_param(d, key, default):
- return float(d.get(key, default))
-
-
-def attr_key(attr):
+def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash:
"""Convert attributes to a hashable key for grouping."""
- stretch = attr.get("stretch", "")
+ params = attr.get("params", {})
return (
parse_alignment(attr.get("align", "")),
parse_alignment(attr.get("valign", "")),
- stretch,
- float(attr.get("params", {}).get("overlap", 0.0)),
- float(attr.get("params", {}).get("xy-ratio", -1.0)),
- float(attr.get("params", {}).get("ypadding", 0.0)),
+ attr.get("stretch", ""),
+ float(params.get("overlap", 0.0)),
+ float(params.get("xy-ratio", -1.0)),
+ float(params.get("ypadding", 0.0)),
)
-def coalesce_codepoints_to_ranges(codepoints):
+def coalesce_codepoints_to_ranges(codepoints: list[int]) -> list[tuple[int, int]]:
"""Convert a sorted list of integers to a list of single values and ranges."""
- ranges = []
+ ranges: list[tuple[int, int]] = []
cp_iter = iter(sorted(codepoints))
- try:
+ with suppress(StopIteration):
start = prev = next(cp_iter)
for cp in cp_iter:
if cp == prev + 1:
@@ -135,88 +153,96 @@ def coalesce_codepoints_to_ranges(codepoints):
ranges.append((start, prev))
start = prev = cp
ranges.append((start, prev))
- except StopIteration:
- pass
return ranges
-def emit_zig_entry_multikey(codepoints, attr):
+def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) -> str:
align = parse_alignment(attr.get("align", ""))
valign = parse_alignment(attr.get("valign", ""))
stretch = attr.get("stretch", "")
params = attr.get("params", {})
- overlap = get_param(params, "overlap", 0.0)
- xy_ratio = get_param(params, "xy-ratio", -1.0)
- y_padding = get_param(params, "ypadding", 0.0)
+ overlap = params.get("overlap", 0.0)
+ xy_ratio = params.get("xy-ratio", -1.0)
+ y_padding = params.get("ypadding", 0.0)
ranges = coalesce_codepoints_to_ranges(codepoints)
keys = "\n".join(
- f" 0x{start:x}...0x{end:x}," if start != end else f" 0x{start:x},"
+ f" {start:#x}...{end:#x}," if start != end else f" {start:#x},"
for start, end in ranges
)
- s = f"""{keys}
- => .{{\n"""
+ s = f"{keys}\n => .{{\n"
# These translations don't quite capture the way
# the actual patcher does scaling, but they're a
# good enough compromise.
- if ("xy" in stretch):
+ if "xy" in stretch:
s += " .size_horizontal = .stretch,\n"
s += " .size_vertical = .stretch,\n"
- elif ("!" in stretch):
+ elif "!" in stretch:
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .fit,\n"
- elif ("^" in stretch):
+ elif "^" in stretch:
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .cover,\n"
else:
s += " .size_horizontal = .fit,\n"
s += " .size_vertical = .fit,\n"
- if (align is not None):
+ # There are two cases where we want to limit the constraint width to 1:
+ # - If there's a `1` in the stretch mode string.
+ # - If the stretch mode is `xy` and there's not an explicit `2`.
+ if "1" in stretch or ("xy" in stretch and "2" not in stretch):
+ s += " .max_constraint_width = 1,\n"
+
+ if align is not None:
s += f" .align_horizontal = {align},\n"
- if (valign is not None):
+ if valign is not None:
s += f" .align_vertical = {valign},\n"
- if (overlap != 0.0):
+ # `overlap` and `ypadding` are mutually exclusive,
+ # this is asserted in the nerd fonts patcher itself.
+ if overlap:
pad = -overlap
s += f" .pad_left = {pad},\n"
s += f" .pad_right = {pad},\n"
- v_pad = y_padding - math.copysign(min(0.01, abs(overlap)), overlap)
+ # In the nerd fonts patcher, overlap values
+ # are capped at 0.01 in the vertical direction.
+ v_pad = -min(0.01, overlap)
s += f" .pad_top = {v_pad},\n"
s += f" .pad_bottom = {v_pad},\n"
+ elif y_padding:
+ s += f" .pad_top = {y_padding / 2},\n"
+ s += f" .pad_bottom = {y_padding / 2},\n"
- if (xy_ratio > 0):
+ if xy_ratio > 0:
s += f" .max_xy_ratio = {xy_ratio},\n"
s += " },"
-
return s
-def generate_zig_switch_arms(patch_set):
- entries = {}
- for entry in patch_set:
+
+def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str:
+ entries: dict[int, PatchSetAttributeEntry] = {}
+ for entry in patch_sets:
attributes = entry["Attributes"]
for cp in range(entry["SymStart"], entry["SymEnd"] + 1):
entries[cp] = attributes["default"]
- for k, v in attributes.items():
- if isinstance(k, int):
- entries[k] = v
+ entries |= {k: v for k, v in attributes.items() if isinstance(k, int)}
del entries[0]
# Group codepoints by attribute key
- grouped = defaultdict(list)
+ grouped = defaultdict[AttributeHash, list[int]](list)
for cp, attr in entries.items():
grouped[attr_key(attr)].append(cp)
# Emit zig switch arms
- result = []
- for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]):
+ result: list[str] = []
+ for codepoints in sorted(grouped.values()):
# Use one of the attrs in the group to emit the value
attr = entries[codepoints[0]]
result.append(emit_zig_entry_multikey(codepoints, attr))
@@ -225,23 +251,16 @@ def generate_zig_switch_arms(patch_set):
if __name__ == "__main__":
- path = (
- Path(__file__).resolve().parent
- / ".."
- / ".."
- / "vendor"
- / "nerd-fonts"
- / "font-patcher.py"
- )
- with open(path, "r", encoding="utf-8") as f:
- source = f.read()
+ project_root = Path(__file__).resolve().parents[2]
+ patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py"
+ source = patcher_path.read_text(encoding="utf-8")
patch_set = extract_patch_set_values(source)
- out_path = Path(__file__).resolve().parent / "nerd_font_attributes.zig"
+ out_path = project_root / "src" / "font" / "nerd_font_attributes.zig"
- with open(out_path, "w", encoding="utf-8") as f:
- f.write("""//! This is a generate file, produced by nerd_font_codegen.py
+ with out_path.open("w", encoding="utf-8") as f:
+ f.write("""//! This is a generated file, produced by nerd_font_codegen.py
//! DO NOT EDIT BY HAND!
//!
//! This file provides info extracted from the nerd fonts patcher script,
@@ -254,6 +273,4 @@ pub fn getConstraint(cp: u21) Constraint {
return switch (cp) {
""")
f.write(generate_zig_switch_arms(patch_set))
- f.write("\n")
-
- f.write(" else => .none,\n };\n}\n")
+ f.write("\n else => .none,\n };\n}\n")
diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig
index 1463fb38b..dfff8fa75 100644
--- a/src/font/sprite/Face.zig
+++ b/src/font/sprite/Face.zig
@@ -195,7 +195,6 @@ pub fn renderGlyph(
.offset_y = 0,
.atlas_x = 0,
.atlas_y = 0,
- .advance_x = 0,
};
const metrics = self.metrics;
@@ -227,8 +226,6 @@ pub fn renderGlyph(
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
.atlas_x = region.x,
.atlas_y = region.y,
- .advance_x = @floatFromInt(width),
- .sprite = true,
};
}
diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig
index b981449bc..a77b90a56 100644
--- a/src/font/sprite/canvas.zig
+++ b/src/font/sprite/canvas.zig
@@ -140,24 +140,7 @@ pub const Canvas = struct {
const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
// Allocate our texture atlas region
- const region = region: {
- // Reserve a region with a 1px margin on the bottom and right edges
- // so that we can avoid interpolation between adjacent glyphs during
- // texture sampling.
- var region = try atlas.reserve(
- alloc,
- region_width + 1,
- region_height + 1,
- );
-
- // Modify the region to remove the margin so that we write to the
- // non-zero location. The data in an Altlas is always initialized
- // to zero (Atlas.clear) so we don't need to worry about zero-ing
- // that.
- region.width -= 1;
- region.height -= 1;
- break :region region;
- };
+ const region = try atlas.reserve(alloc, region_width, region_height);
if (region.width > 0 and region.height > 0) {
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
diff --git a/src/input/Binding.zig b/src/input/Binding.zig
index 7cdb8047c..f76da360a 100644
--- a/src/input/Binding.zig
+++ b/src/input/Binding.zig
@@ -281,6 +281,10 @@ pub const Action = union(enum) {
/// If there is a URL under the cursor, copy it to the default clipboard.
copy_url_to_clipboard,
+ /// Copy the terminal title to the clipboard. If the terminal title is not
+ /// set or is empty this has no effect.
+ copy_title_to_clipboard,
+
/// Increase the font size by the specified amount in points (pt).
///
/// For example, `increase_font_size:1.5` will increase the font size
@@ -296,6 +300,12 @@ pub const Action = union(enum) {
/// Reset the font size to the original configured size.
reset_font_size,
+ /// Set the font size to the specified size in points (pt).
+ ///
+ /// For example, `set_font_size:14.5` will set the font size
+ /// to 14.5 points.
+ set_font_size: f32,
+
/// Clear the screen and all scrollback.
clear_screen,
@@ -999,11 +1009,13 @@ pub const Action = union(enum) {
.reset,
.copy_to_clipboard,
.copy_url_to_clipboard,
+ .copy_title_to_clipboard,
.paste_from_clipboard,
.paste_from_selection,
.increase_font_size,
.decrease_font_size,
.reset_font_size,
+ .set_font_size,
.prompt_surface_title,
.clear_screen,
.select_all,
@@ -3065,6 +3077,7 @@ test "set: getEvent codepoint case folding" {
try testing.expect(action == null);
}
}
+
test "Action: clone" {
const testing = std.testing;
var arena = std.heap.ArenaAllocator.init(testing.allocator);
@@ -3083,3 +3096,42 @@ test "Action: clone" {
try testing.expect(b == .text);
}
}
+
+test "parse: increase_font_size" {
+ const testing = std.testing;
+
+ {
+ const binding = try parseSingle("a=increase_font_size:1.5");
+ try testing.expect(binding.action == .increase_font_size);
+ try testing.expectEqual(1.5, binding.action.increase_font_size);
+ }
+}
+
+test "parse: decrease_font_size" {
+ const testing = std.testing;
+
+ {
+ const binding = try parseSingle("a=decrease_font_size:2.5");
+ try testing.expect(binding.action == .decrease_font_size);
+ try testing.expectEqual(2.5, binding.action.decrease_font_size);
+ }
+}
+
+test "parse: reset_font_size" {
+ const testing = std.testing;
+
+ {
+ const binding = try parseSingle("a=reset_font_size");
+ try testing.expect(binding.action == .reset_font_size);
+ }
+}
+
+test "parse: set_font_size" {
+ const testing = std.testing;
+
+ {
+ const binding = try parseSingle("a=set_font_size:13.5");
+ try testing.expect(binding.action == .set_font_size);
+ try testing.expectEqual(13.5, binding.action.set_font_size);
+ }
+}
diff --git a/src/input/command.zig b/src/input/command.zig
index 693d5c8d4..84e9afc79 100644
--- a/src/input/command.zig
+++ b/src/input/command.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Action = @import("Binding.zig").Action;
@@ -131,6 +132,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Copy the URL under the cursor to the clipboard.",
}},
+ .copy_title_to_clipboard => comptime &.{.{
+ .action = .copy_title_to_clipboard,
+ .title = "Copy Terminal Title to Clipboard",
+ .description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
+ }},
+
.paste_from_clipboard => comptime &.{.{
.action = .paste_from_clipboard,
.title = "Paste from Clipboard",
@@ -460,6 +467,7 @@ fn actionCommands(action: Action.Key) []const Command {
.esc,
.text,
.cursor_key,
+ .set_font_size,
.scroll_page_fractional,
.scroll_page_lines,
.adjust_selection,
diff --git a/src/main_c.zig b/src/main_c.zig
index 1b73d7327..2c266cfb5 100644
--- a/src/main_c.zig
+++ b/src/main_c.zig
@@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig");
// Some comptime assertions that our C API depends on.
comptime {
- assert(apprt.runtime == apprt.embedded);
+ // We allow tests to reference this file because we unit test
+ // some of the C API. At runtime though we should never get these
+ // functions unless we are building libghostty.
+ if (!builtin.is_test) {
+ assert(apprt.runtime == apprt.embedded);
+ }
}
/// Global options so we can log. This is identical to main.
@@ -29,7 +34,9 @@ comptime {
// These structs need to be referenced so the `export` functions
// are truly exported by the C API lib.
_ = @import("config.zig").CAPI;
- _ = apprt.runtime.CAPI;
+ if (@hasDecl(apprt.runtime, "CAPI")) {
+ _ = apprt.runtime.CAPI;
+ }
}
/// ghostty_info_s
@@ -46,17 +53,29 @@ const Info = extern struct {
};
};
-/// Initialize ghostty global state. It is possible to have more than
-/// one global state but it has zero practical benefit.
-export fn ghostty_init() c_int {
+/// ghostty_string_s
+pub const String = extern struct {
+ ptr: ?[*]const u8,
+ len: usize,
+
+ pub const empty: String = .{
+ .ptr = null,
+ .len = 0,
+ };
+
+ pub fn fromSlice(slice: []const u8) String {
+ return .{
+ .ptr = slice.ptr,
+ .len = slice.len,
+ };
+ }
+};
+
+/// Initialize ghostty global state.
+export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
assert(builtin.link_libc);
- // Since in the lib we don't go through start.zig, we need
- // to populate argv so that inspecting std.os.argv doesn't
- // touch uninitialized memory.
- var argv: [0][*:0]u8 = .{};
- std.os.argv = &argv;
-
+ std.os.argv = argv[0..argc];
state.init() catch |err| {
std.log.err("failed to initialize ghostty error={}", .{err});
return 1;
@@ -65,15 +84,17 @@ export fn ghostty_init() c_int {
return 0;
}
-/// This is the entrypoint for the CLI version of Ghostty. This
-/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init
-/// if you are going to run this. This will not return.
-export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn {
- std.os.argv = argv[0..argc];
- main.main() catch |err| {
- std.log.err("failed to run ghostty error={}", .{err});
+/// Runs an action if it is specified. If there is no action this returns
+/// false. If there is an action then this doesn't return.
+export fn ghostty_cli_try_action() void {
+ const action = state.action orelse return;
+ std.log.info("executing CLI action={}", .{action});
+ posix.exit(action.run(state.alloc) catch |err| {
+ std.log.err("CLI action failed error={}", .{err});
posix.exit(1);
- };
+ });
+
+ posix.exit(0);
}
/// Return metadata about Ghostty, such as version, build mode, etc.
@@ -99,3 +120,8 @@ export fn ghostty_info() Info {
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
return internal_os.i18n._(msgid);
}
+
+/// Free a string allocated by Ghostty.
+export fn ghostty_string_free(str: String) void {
+ state.alloc.free(str.ptr.?[0..str.len]);
+}
diff --git a/src/os/desktop.zig b/src/os/desktop.zig
index 3bc843e5c..93bfb74bc 100644
--- a/src/os/desktop.zig
+++ b/src/os/desktop.zig
@@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
// This special case is so that if we launch the app via the
// app bundle (i.e. via open) then we still treat it as if it
// was launched from the desktop.
- if (build_config.artifact == .lib and
- posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true;
+ if (build_config.artifact == .lib) lib: {
+ const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
+ const source = posix.getenv(env) orelse break :lib;
+
+ // Source can be "app", "cli", or "zig_run". We assume
+ // its the desktop only if its "app". We may want to do
+ // "zig_run" but at the moment there's no reason.
+ if (std.mem.eql(u8, source, "app")) break :macos true;
+ }
break :macos c.getppid() == 1;
},
diff --git a/src/os/i18n.zig b/src/os/i18n.zig
index a4d6c1577..2ecae27ac 100644
--- a/src/os/i18n.zig
+++ b/src/os/i18n.zig
@@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{
"ca_ES.UTF-8",
"bg_BG.UTF-8",
"ga_IE.UTF-8",
+ "he_IL.UTF-8",
};
/// Set for faster membership lookup of locales.
diff --git a/src/os/kernel_info.zig b/src/os/kernel_info.zig
new file mode 100644
index 000000000..9e3933dde
--- /dev/null
+++ b/src/os/kernel_info.zig
@@ -0,0 +1,27 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+pub fn getKernelInfo(alloc: std.mem.Allocator) ?[]const u8 {
+ if (comptime builtin.os.tag != .linux) return null;
+ const path = "/proc/sys/kernel/osrelease";
+ var file = std.fs.openFileAbsolute(path, .{}) catch return null;
+ defer file.close();
+
+ // 128 bytes should be enough to hold the kernel information
+ const kernel_info = file.readToEndAlloc(alloc, 128) catch return null;
+ defer alloc.free(kernel_info);
+ return alloc.dupe(u8, std.mem.trim(u8, kernel_info, &std.ascii.whitespace)) catch return null;
+}
+
+test "read /proc/sys/kernel/osrelease" {
+ if (comptime builtin.os.tag != .linux) return null;
+ const allocator = std.testing.allocator;
+
+ const kernel_info = try getKernelInfo(allocator);
+ defer allocator.free(kernel_info);
+
+ // Since we can't hardcode the info in tests, just check
+ // if something was read from the file
+ try std.testing.expect(kernel_info.len > 0);
+ try std.testing.expect(!std.mem.eql(u8, kernel_info, ""));
+}
diff --git a/src/os/main.zig b/src/os/main.zig
index 906e3d150..7398fc779 100644
--- a/src/os/main.zig
+++ b/src/os/main.zig
@@ -14,6 +14,7 @@ const openpkg = @import("open.zig");
const pipepkg = @import("pipe.zig");
const resourcesdir = @import("resourcesdir.zig");
const systemd = @import("systemd.zig");
+const kernelInfo = @import("kernel_info.zig");
// Namespaces
pub const args = @import("args.zig");
@@ -58,6 +59,7 @@ pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;
pub const ResourcesDir = resourcesdir.ResourcesDir;
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
+pub const getKernelInfo = kernelInfo.getKernelInfo;
test {
_ = i18n;
diff --git a/src/os/open.zig b/src/os/open.zig
index ce62a7e0b..9b069c80f 100644
--- a/src/os/open.zig
+++ b/src/os/open.zig
@@ -1,24 +1,23 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
+const apprt = @import("../apprt.zig");
const log = std.log.scoped(.@"os-open");
-/// The type of the data at the URL to open. This is used as a hint
-/// to potentially open the URL in a different way.
-pub const Type = enum {
- text,
- unknown,
-};
-
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
/// Output on stdout is ignored. The allocator is used to buffer the
/// log output and may allocate from another thread.
+///
+/// This function is purposely simple for the sake of providing
+/// some portable way to open URLs. If you are implementing an
+/// apprt for Ghostty, you should consider doing something special-cased
+/// for your platform.
pub fn open(
alloc: Allocator,
- typ: Type,
+ kind: apprt.action.OpenUrl.Kind,
url: []const u8,
) !void {
var exe: std.process.Child = switch (builtin.os.tag) {
@@ -33,7 +32,7 @@ pub fn open(
),
.macos => .init(
- switch (typ) {
+ switch (kind) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig
index 00df8e273..882d6fc03 100644
--- a/src/renderer/OpenGL.zig
+++ b/src/renderer/OpenGL.zig
@@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
.format = .rgba,
.internal_format = .srgba,
.target = .@"2D",
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
};
}
@@ -388,6 +392,16 @@ pub inline fn imageTextureOptions(
.format = format.toPixelFormat(),
.internal_format = if (srgb) .srgba else .rgba,
.target = .@"2D",
+ // TODO: Generate mipmaps for image textures and use
+ // linear_mipmap_linear filtering so that they
+ // look good even when scaled way down.
+ .min_filter = .linear,
+ .mag_filter = .linear,
+ // TODO: Separate out background image options, use
+ // repeating coordinate modes so we don't have
+ // to do the modulus in the shader.
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
};
}
@@ -409,6 +423,10 @@ pub fn initAtlasTexture(
.format = format,
.internal_format = internal_format,
.target = .Rectangle,
+ .min_filter = .nearest,
+ .mag_filter = .nearest,
+ .wrap_s = .clamp_to_edge,
+ .wrap_t = .clamp_to_edge,
},
atlas.size,
atlas.size,
diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig
index 829563075..3965d302a 100644
--- a/src/renderer/generic.zig
+++ b/src/renderer/generic.zig
@@ -519,7 +519,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
foreground: terminal.color.RGB,
selection_background: ?configpkg.Config.TerminalColor,
selection_foreground: ?configpkg.Config.TerminalColor,
- bold_is_bright: bool,
+ bold_color: ?configpkg.BoldColor,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
@@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
- .bold_is_bright = config.@"bold-is-bright",
+ .bold_color = config.@"bold-color",
+
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",
@@ -2540,10 +2541,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette);
- const fg_style = style.fg(
- color_palette,
- self.config.bold_is_bright,
- ) orelse self.foreground_color orelse self.default_foreground_color;
+ const fg_style = style.fg(.{
+ .default = self.foreground_color orelse self.default_foreground_color,
+ .palette = color_palette,
+ .bold = self.config.bold_color,
+ });
// The final background color for the cell.
const bg = bg: {
@@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background",
=> |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- const fg_style = sty.fg(
- color_palette,
- self.config.bold_is_bright,
- ) orelse self.foreground_color orelse self.default_foreground_color;
+ const fg_style = sty.fg(.{
+ .default = self.foreground_color orelse self.default_foreground_color,
+ .palette = color_palette,
+ .bold = self.config.bold_color,
+ });
const bg_style = sty.bg(
screen.cursor.page_cell,
color_palette,
@@ -2852,7 +2855,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
- const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
+ const fg_style = sty.fg(.{
+ .default = self.foreground_color orelse self.default_foreground_color,
+ .palette = color_palette,
+ .bold = self.config.bold_color,
+ });
const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
break :blk switch (txt) {
diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig
index 9be2b7078..2f3e7f46a 100644
--- a/src/renderer/opengl/Texture.zig
+++ b/src/renderer/opengl/Texture.zig
@@ -16,6 +16,10 @@ pub const Options = struct {
format: gl.Texture.Format,
internal_format: gl.Texture.InternalFormat,
target: gl.Texture.Target,
+ min_filter: gl.Texture.MinFilter,
+ mag_filter: gl.Texture.MagFilter,
+ wrap_s: gl.Texture.Wrap,
+ wrap_t: gl.Texture.Wrap,
};
texture: gl.Texture,
@@ -48,10 +52,10 @@ pub fn init(
{
const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
defer texbind.unbind();
- texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
- texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed;
- texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
- texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed;
+ texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
+ texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
+ texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
+ texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
texbind.image2D(
0,
opts.internal_format,
diff --git a/src/terminal/style.zig b/src/terminal/style.zig
index 865e15f64..78afcdf39 100644
--- a/src/terminal/style.zig
+++ b/src/terminal/style.zig
@@ -1,5 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
+const configpkg = @import("../config.zig");
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const page = @import("page.zig");
@@ -115,24 +116,68 @@ pub const Style = struct {
};
}
- /// Returns the fg color for a cell with this style given the palette.
+ pub const Fg = struct {
+ /// The default color to use if the style doesn't specify a
+ /// foreground color and no configuration options override
+ /// it.
+ default: color.RGB,
+
+ /// The current color palette. Required to map palette indices to
+ /// real color values.
+ palette: *const color.Palette,
+
+ /// If specified, the color to use for bold text.
+ bold: ?configpkg.BoldColor = null,
+ };
+
+ /// Returns the fg color for a cell with this style given the palette
+ /// and various configuration options.
pub fn fg(
self: Style,
- palette: *const color.Palette,
- bold_is_bright: bool,
- ) ?color.RGB {
+ opts: Fg,
+ ) color.RGB {
+ // Note we don't pull the bold check to the top-level here because
+ // we don't want to duplicate the conditional multiple times since
+ // certain colors require more checks (e.g. `bold_is_bright`).
+
return switch (self.fg_color) {
- .none => null,
- .palette => |idx| palette: {
- if (bold_is_bright and self.flags.bold) {
- const bright_offset = @intFromEnum(color.Name.bright_black);
- if (idx < bright_offset)
- break :palette palette[idx + bright_offset];
+ .none => default: {
+ if (self.flags.bold) {
+ if (opts.bold) |bold| switch (bold) {
+ .bright => {},
+ .color => |v| break :default v.toTerminalRGB(),
+ };
}
- break :palette palette[idx];
+ break :default opts.default;
+ },
+
+ .palette => |idx| palette: {
+ if (self.flags.bold) {
+ if (opts.bold) |bold| switch (bold) {
+ .color => |v| break :palette v.toTerminalRGB(),
+ .bright => {
+ const bright_offset = @intFromEnum(color.Name.bright_black);
+ if (idx < bright_offset) {
+ break :palette opts.palette[idx + bright_offset];
+ }
+ },
+ };
+ }
+
+ break :palette opts.palette[idx];
+ },
+
+ .rgb => |rgb| rgb: {
+ if (self.flags.bold and rgb.eql(opts.default)) {
+ if (opts.bold) |bold| switch (bold) {
+ .color => |v| break :rgb v.toTerminalRGB(),
+ .bright => {},
+ };
+ }
+
+ break :rgb rgb;
},
- .rgb => |rgb| rgb,
};
}