Merge branch 'main' into ssh-integration

This commit is contained in:
Jason Rayne
2025-07-07 09:07:14 -07:00
60 changed files with 1888 additions and 528 deletions

4
.github/pinact.yml vendored Normal file
View File

@ -0,0 +1,4 @@
version: 3
ignore_actions:
- name: "DeterminateSystems/nix-installer-action"
ref: "main"

View File

@ -10,7 +10,7 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- name: Remove old artifacts - name: Remove old artifacts
uses: c-hive/gha-remove-artifacts@v1 uses: c-hive/gha-remove-artifacts@44fc7acaf1b3d0987da0e8d4707a989d80e9554b # v1.4.0
with: with:
age: "1 week" age: "1 week"
skip-tags: true skip-tags: true

View File

@ -15,7 +15,7 @@ jobs:
name: Milestone Update name: Milestone Update
steps: steps:
- name: Set Milestone for PR - name: Set Milestone for PR
uses: hustcer/milestone-action@v2 uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
with: with:
action: bind-pr # `bind-pr` is the default action action: bind-pr # `bind-pr` is the default action
@ -24,7 +24,7 @@ jobs:
# Bind milestone to closed issue that has a merged PR fix # Bind milestone to closed issue that has a merged PR fix
- name: Set Milestone for Issue - name: Set Milestone for Issue
uses: hustcer/milestone-action@v2 uses: hustcer/milestone-action@09bdc6fda0f43a4df28cda5815cc47df74cfdba7 # v2.8
if: github.event.issue.state == 'closed' if: github.event.issue.state == 'closed'
with: with:
action: bind-issue action: bind-issue

View File

@ -34,18 +34,18 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- name: Setup Nix - name: Setup Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"

View File

@ -64,7 +64,7 @@ jobs:
mkdir blob mkdir blob
mv appcast.xml blob/appcast.xml mv appcast.xml blob/appcast.xml
- name: Upload Appcast to R2 - name: Upload Appcast to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1
with: with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}

View File

@ -8,7 +8,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug] needs: [build-macos-debug]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -29,7 +29,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos] needs: [build-macos]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -51,16 +51,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 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.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}
@ -203,16 +203,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 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.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 cp ghostty-macos-universal-debug-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-dsym.zip
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}

View File

@ -56,7 +56,7 @@ jobs:
fi fi
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
@ -80,20 +80,20 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 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 nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: source-tarball name: source-tarball
path: |- path: |-
@ -128,12 +128,12 @@ jobs:
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps: steps:
- name: Checkout code - 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: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -260,7 +260,7 @@ jobs:
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: macos name: macos
path: |- path: |-
@ -277,7 +277,7 @@ jobs:
curl -sL https://sentry.io/get-cli/ | bash curl -sL https://sentry.io/get-cli/ | bash
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: macos name: macos
@ -297,10 +297,10 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: macos name: macos
@ -331,7 +331,7 @@ jobs:
mv appcast_new.xml appcast.xml mv appcast_new.xml appcast.xml
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: sparkle name: sparkle
path: |- path: |-
@ -348,17 +348,17 @@ jobs:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: macos name: macos
- name: Download Sparkle Artifacts - name: Download Sparkle Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: sparkle name: sparkle
- name: Download Source Tarball Artifacts - name: Download Source Tarball Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: source-tarball name: source-tarball
@ -378,7 +378,7 @@ jobs:
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml mv appcast.xml blob/${GHOSTTY_VERSION}/appcast-staged.xml
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}

View File

@ -19,7 +19,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos] needs: [build-macos]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Tip Tag - name: Tip Tag
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
@ -31,7 +31,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-slow] needs: [build-macos-debug-slow]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -52,7 +52,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-fast] needs: [build-macos-debug-fast]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -73,7 +73,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: [build-macos] needs: [build-macos]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -105,17 +105,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 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 nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
- name: Update Release - name: Update Release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true
@ -158,16 +158,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -299,7 +299,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true
@ -331,7 +331,7 @@ jobs:
cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@ -349,7 +349,7 @@ jobs:
cp appcast_new.xml blob/appcast.xml cp appcast_new.xml blob/appcast.xml
- name: Upload Appcast to R2 - name: Upload Appcast to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@ -373,16 +373,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -507,7 +507,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true 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.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 cp ghostty-macos-universal-debug-slow-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
@ -548,16 +548,16 @@ jobs:
timeout-minutes: 90 timeout-minutes: 90
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -682,7 +682,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@v2.3.2 uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true 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.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 cp ghostty-macos-universal-debug-fast-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with: with:
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }} r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }} r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}

View File

@ -27,6 +27,7 @@ jobs:
- test-gtk - test-gtk
- test-sentry-linux - test-sentry-linux
- test-macos - test-macos
- pinact
- prettier - prettier
- alejandra - alejandra
- typos - typos
@ -64,20 +65,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -95,20 +96,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -131,20 +132,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -160,20 +161,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -193,20 +194,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -237,20 +238,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -262,7 +263,7 @@ jobs:
cp zig-out/dist/*.tar.gz ghostty-source.tar.gz cp zig-out/dist/*.tar.gz ghostty-source.tar.gz
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: source-tarball name: source-tarball
path: |- path: |-
@ -273,13 +274,13 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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. # 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: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -313,13 +314,13 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
with: with:
determinate: true determinate: true
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -353,13 +354,13 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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. # 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: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -400,7 +401,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Download Source Tarball Artifacts - name: Download Source Tarball Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: source-tarball name: source-tarball
- name: Extract tarball - name: Extract tarball
@ -408,7 +409,7 @@ jobs:
mkdir dist mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
@ -420,7 +421,7 @@ jobs:
_LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device"
sudo mkdir -p /var/lib/snapd/cgroup sudo mkdir -p /var/lib/snapd/cgroup
echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}"
- uses: snapcore/action-build@v1 - uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0
with: with:
path: dist path: dist
@ -431,7 +432,7 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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 # This could be from a script if we wanted to but inlining here for now
# in one place. # in one place.
@ -500,20 +501,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -542,20 +543,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -581,20 +582,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -608,13 +609,13 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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. # 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: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -637,17 +638,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -656,6 +657,34 @@ jobs:
- name: zig fmt - name: zig fmt
run: nix develop -c zig fmt --check . 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: prettier:
if: github.repository == 'ghostty-org/ghostty' if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-xsm runs-on: namespace-profile-ghostty-xsm
@ -664,17 +693,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -691,17 +720,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -718,17 +747,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -745,17 +774,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -772,17 +801,17 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@v4 # Check out repo so we can lint it - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -806,20 +835,20 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -834,13 +863,13 @@ jobs:
needs: [test, build-dist] needs: [test, build-dist]
steps: steps:
- name: Install and configure Namespace CLI - name: Install and configure Namespace CLI
uses: namespacelabs/nscloud-setup@v0 uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
- name: Configure Namespace powered Buildx - 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 - name: Download Source Tarball Artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: source-tarball name: source-tarball
@ -850,7 +879,7 @@ jobs:
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
context: dist context: dist
file: dist/src/build/docker/debian/Dockerfile file: dist/src/build/docker/debian/Dockerfile
@ -865,18 +894,18 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- name: Setup Nix - name: Setup Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -901,8 +930,8 @@ jobs:
runs-on: ${{ matrix.variant.runner }} runs-on: ${{ matrix.variant.runner }}
needs: [flatpak-check-zig-cache, test] needs: [flatpak-check-zig-cache, test]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: flatpak/flatpak-github-actions/flatpak-builder@v6 - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
with: with:
bundle: com.mitchellh.ghostty bundle: com.mitchellh.ghostty
manifest-path: flatpak/com.mitchellh.ghostty.yml manifest-path: flatpak/com.mitchellh.ghostty.yml

View File

@ -17,22 +17,22 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.8 uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
with: with:
path: | path: |
/nix /nix
/zig /zig
- name: Setup Nix - name: Setup Nix
uses: cachix/install-nix-action@v31 uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with: with:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
@ -60,7 +60,7 @@ jobs:
run: nix build .#ghostty run: nix build .#ghostty
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
title: Update iTerm2 colorschemes title: Update iTerm2 colorschemes
base: main base: main

View File

@ -155,6 +155,7 @@
/src/input/KeyEncoder.zig @ghostty-org/terminal /src/input/KeyEncoder.zig @ghostty-org/terminal
/src/terminal/ @ghostty-org/terminal /src/terminal/ @ghostty-org/terminal
/src/terminfo/ @ghostty-org/terminal /src/terminfo/ @ghostty-org/terminal
/src/termio/ @ghostty-org/terminal
/src/unicode/ @ghostty-org/terminal /src/unicode/ @ghostty-org/terminal
/src/Surface.zig @ghostty-org/terminal /src/Surface.zig @ghostty-org/terminal
/src/surface_mouse.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/zh_CN.UTF-8.po @ghostty-org/zh_CN
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE /po/ga_IE.UTF-8.po @ghostty-org/ga_IE
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR /po/ko_KR.UTF-8.po @ghostty-org/ko_KR
/po/he_IL.UTF-8.po @ghostty-org/he_IL
# Packaging - Snap # Packaging - Snap
/snap/ @ghostty-org/snap /snap/ @ghostty-org/snap

View File

@ -19,6 +19,7 @@ X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory= X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command X-TerminalArgHold=--wait-after-command
DBusActivatable=true DBusActivatable=true
X-KDE-Shortcuts=Ctrl+Alt+T
[Desktop Action new-window] [Desktop Action new-window]
Name=New Window Name=New Window

View File

@ -350,6 +350,11 @@ typedef struct {
const char* message; const char* message;
} ghostty_diagnostic_s; } ghostty_diagnostic_s;
typedef struct {
const char* ptr;
uintptr_t len;
} ghostty_string_s;
typedef struct { typedef struct {
double tl_px_x; double tl_px_x;
double tl_px_y; double tl_px_y;
@ -662,6 +667,19 @@ typedef struct {
bool soft; bool soft;
} ghostty_action_reload_config_s; } 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 // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_QUIT, GHOSTTY_ACTION_QUIT,
@ -711,7 +729,8 @@ typedef enum {
GHOSTTY_ACTION_RING_BELL, GHOSTTY_ACTION_RING_BELL,
GHOSTTY_ACTION_UNDO, GHOSTTY_ACTION_UNDO,
GHOSTTY_ACTION_REDO, GHOSTTY_ACTION_REDO,
GHOSTTY_ACTION_CHECK_FOR_UPDATES GHOSTTY_ACTION_CHECK_FOR_UPDATES,
GHOSTTY_ACTION_OPEN_URL,
} ghostty_action_tag_e; } ghostty_action_tag_e;
typedef union { typedef union {
@ -739,6 +758,7 @@ typedef union {
ghostty_action_color_change_s color_change; ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config; ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change; ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {
@ -778,10 +798,11 @@ typedef struct {
//------------------------------------------------------------------- //-------------------------------------------------------------------
// Published API // Published API
int ghostty_init(void); int ghostty_init(uintptr_t, char**);
void ghostty_cli_main(uintptr_t, char**); void ghostty_cli_try_action(void);
ghostty_info_s ghostty_info(void); ghostty_info_s ghostty_info(void);
const char* ghostty_translate(const char*); const char* ghostty_translate(const char*);
void ghostty_string_free(ghostty_string_s);
ghostty_config_t ghostty_config_new(); ghostty_config_t ghostty_config_new();
void ghostty_config_free(ghostty_config_t); void ghostty_config_free(ghostty_config_t);
@ -796,7 +817,7 @@ ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t,
uintptr_t); uintptr_t);
uint32_t ghostty_config_diagnostics_count(ghostty_config_t); uint32_t ghostty_config_diagnostics_count(ghostty_config_t);
ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_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_app_t ghostty_app_new(const ghostty_runtime_config_s*,
ghostty_config_t); ghostty_config_t);

View File

@ -48,8 +48,8 @@
<string></string> <string></string>
<key>LSEnvironment</key> <key>LSEnvironment</key>
<dict> <dict>
<key>GHOSTTY_MAC_APP</key> <key>GHOSTTY_MAC_LAUNCH_SOURCE</key>
<string>1</string> <string>app</string>
</dict> </dict>
<key>MDItemKeywords</key> <key>MDItemKeywords</key>
<string>Terminal</string> <string>Terminal</string>

View File

@ -13,6 +13,8 @@
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; 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 */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.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 */; }; 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 = "<group>"; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = "<group>"; };
A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = "<group>"; };
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; };
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; }; A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; };
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; }; A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; };
@ -516,6 +520,7 @@
A586366A2DF0A98900E04A10 /* Array+Extension.swift */, A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
A50297342DFA0F3300B4E924 /* Double+Extension.swift */, A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */,
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
A51194122E05D003007258CC /* Optional+Extension.swift */, A51194122E05D003007258CC /* Optional+Extension.swift */,
@ -528,6 +533,7 @@
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */,
A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */,
A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
A58636722DF4813000E04A10 /* UndoManager+Extension.swift */, A58636722DF4813000E04A10 /* UndoManager+Extension.swift */,
A5CC36142C9CDA03004D6760 /* View+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
@ -799,6 +805,7 @@
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */, A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */,
A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
@ -815,6 +822,7 @@
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */,
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */, A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */,

View File

@ -256,9 +256,8 @@ class AppDelegate: NSObject,
// Setup signal handlers // Setup signal handlers
setupSignals() setupSignals()
// This is a hack used by our build scripts, specifically `zig build run`, // If we launched via zig run then we need to force foreground.
// to force our app to the foreground. if Ghostty.launchSource == .zig_run {
if ProcessInfo.processInfo.environment["GHOSTTY_MAC_ACTIVATE"] == "1" {
// This never gets called until we click the dock icon. This forces it // This never gets called until we click the dock icon. This forces it
// activate immediately. // activate immediately.
applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification)) applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification))
@ -933,7 +932,7 @@ class AppDelegate: NSObject,
//MARK: - IB Actions //MARK: - IB Actions
@IBAction func openConfig(_ sender: Any?) { @IBAction func openConfig(_ sender: Any?) {
ghostty.openConfig() Ghostty.App.openConfig()
} }
@IBAction func reloadConfig(_ sender: Any?) { @IBAction func reloadConfig(_ sender: Any?) {

View File

@ -2,13 +2,32 @@ import AppKit
import Cocoa import Cocoa
import GhosttyKit import GhosttyKit
// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect // Initialize Ghostty global state. We do this once right away because the
// whether we launch from the app or not. A user can fake this if // CLI APIs require it and it lets us ensure it is done immediately for the
// they want but they're doing so at their own detriment... // rest of the app.
let process = ProcessInfo.processInfo if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS {
if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") { Ghostty.logger.critical("ghostty_init failed")
ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv)
exit(1) // 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) _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

View File

@ -40,4 +40,34 @@ extension Ghostty.Action {
self.amount = c.amount 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 = ""
}
}
}
} }

View File

@ -45,12 +45,6 @@ extension Ghostty {
} }
init() { 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. // Initialize the global configuration.
self.config = Config() self.config = Config()
if self.config.config == nil { if self.config.config == nil {
@ -120,9 +114,21 @@ extension Ghostty {
ghostty_app_tick(app) ghostty_app_tick(app)
} }
func openConfig() { static func openConfig() {
guard let app = self.app else { return } let str = Ghostty.AllocatedString(ghostty_config_open_path()).string
ghostty_app_open_config(app) 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. /// Reload the configuration.
@ -494,7 +500,7 @@ extension Ghostty {
pwdChanged(app, target: target, v: action.action.pwd) pwdChanged(app, target: target, v: action.action.pwd)
case GHOSTTY_ACTION_OPEN_CONFIG: case GHOSTTY_ACTION_OPEN_CONFIG:
ghostty_config_open() openConfig()
case GHOSTTY_ACTION_FLOAT_WINDOW: case GHOSTTY_ACTION_FLOAT_WINDOW:
toggleFloatWindow(app, target: target, mode: action.action.float_window) toggleFloatWindow(app, target: target, mode: action.action.float_window)
@ -552,6 +558,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CHECK_FOR_UPDATES: case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app) checkForUpdates(app)
case GHOSTTY_ACTION_OPEN_URL:
return openURL(action.action.open_url)
case GHOSTTY_ACTION_UNDO: case GHOSTTY_ACTION_UNDO:
return undo(app, target: target) return undo(app, target: target)
@ -604,6 +613,34 @@ extension Ghostty {
appDelegate.checkForUpdates(nil) 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 { private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
let undoManager: UndoManager? let undoManager: UndoManager?

View File

@ -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 // 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 { extension Ghostty {
enum SetFloatWIndow { enum SetFloatWIndow {
case on case on

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -58,6 +58,7 @@
jq, jq,
minisign, minisign,
pandoc, pandoc,
pinact,
hyperfine, hyperfine,
typos, typos,
uv, uv,
@ -98,6 +99,7 @@ in
# Linting # Linting
nodePackages.prettier nodePackages.prettier
alejandra alejandra
pinact
typos typos
# Testing # Testing

View File

@ -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. /// Data type for texture images.
pub const DataType = enum(c_uint) { pub const DataType = enum(c_uint) {
UnsignedByte = c.GL_UNSIGNED_BYTE, UnsignedByte = c.GL_UNSIGNED_BYTE,

298
po/he_IL.UTF-8.po Normal file
View File

@ -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 <ghostty@slsrepo.com>, 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 <ghostty@slsrepo.com>\n"
"Language-Team: Hebrew <he_IL@lists.sourceforge.net>\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 "הועתק ללוח ההעתקה"

View File

@ -270,6 +270,7 @@ const DerivedConfig = struct {
title: ?[:0]const u8, title: ?[:0]const u8,
title_report: bool, title_report: bool,
links: []Link, links: []Link,
link_previews: configpkg.LinkPreviews,
const Link = struct { const Link = struct {
regex: oni.Regex, regex: oni.Regex,
@ -336,6 +337,7 @@ const DerivedConfig = struct {
.title = config.title, .title = config.title,
.title_report = config.@"title-report", .title_report = config.@"title-report",
.links = links, .links = links,
.link_previews = config.@"link-previews",
// Assignments happen sequentially so we have to do this last // Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above. // 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 // 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 // isn't a link OR if we shouldn't be showing links for some reason
// (see further comments for cases). // (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 // If we clicked and our mouse moved cells then we never
// highlight links until the mouse is unclicked. This follows // highlight links until the mouse is unclicked. This follows
// standard macOS and Linux behavior where a click and drag cancels // standard macOS and Linux behavior where a click and drag cancels
@ -1257,18 +1259,21 @@ fn mouseRefreshLinks(
if (!click_pt.coord().eql(pos_vp)) { if (!click_pt.coord().eql(pos_vp)) {
log.debug("mouse moved while left click held, ignoring link hover", .{}); 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]) { switch (link[0]) {
.open => { .open => {
const str = try self.io.terminal.screen.selectionString(alloc, .{ const str = try self.io.terminal.screen.selectionString(alloc, .{
.sel = link[1], .sel = link[1],
.trim = false, .trim = false,
}); });
break :link .{ .url = str }; break :link .{
.{ .url = str },
self.config.link_previews == .true,
};
}, },
._open_osc8 => { ._open_osc8 => {
@ -1276,9 +1281,14 @@ fn mouseRefreshLinks(
const pin = link[1].start(); const pin = link[1].start();
const uri = self.osc8URI(pin) orelse { const uri = self.osc8URI(pin) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{}); 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, .mouse_shape,
.pointer, .pointer,
); );
_ = try self.rt_app.performAction(
.{ .surface = self }, if (preview) {
.mouse_over_link, _ = try self.rt_app.performAction(
link, .{ .surface = self },
); .mouse_over_link,
link,
);
}
try self.queueRender(); try self.queueRender();
return; return;
} }
@ -3710,7 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false, .trim = false,
}); });
defer self.alloc.free(str); defer self.alloc.free(str);
try internal_os.open(self.alloc, .unknown, str); try self.openUrl(.{ .kind = .unknown, .url = str });
}, },
._open_osc8 => { ._open_osc8 => {
@ -3718,13 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{}); log.warn("failed to get URI for OSC8 hyperlink", .{});
return false; return false;
}; };
try internal_os.open(self.alloc, .unknown, uri); try self.openUrl(.{ .kind = .unknown, .url = uri });
}, },
} }
return true; 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 /// Return the URI for an OSC8 hyperlink at the given position or null
/// if there is no hyperlink. /// if there is no hyperlink.
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 { 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; 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( .paste_from_clipboard => try self.startClipboardRequest(
.standard, .standard,
.{ .paste = {} }, .{ .paste = {} },
@ -4484,6 +4532,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size); 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( .prompt_surface_title => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.prompt_title, .prompt_title,
@ -4923,7 +4979,7 @@ fn writeScreenFile(
defer self.alloc.free(pathZ); defer self.alloc.free(pathZ);
try self.rt_surface.setClipboardString(pathZ, .standard, false); 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( .paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc, self.alloc,
path, path,

View File

@ -267,6 +267,11 @@ pub const Action = union(Key) {
check_for_updates, 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 /// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) { pub const Key = enum(c_int) {
quit, quit,
@ -317,6 +322,7 @@ pub const Action = union(Key) {
undo, undo,
redo, redo,
check_for_updates, check_for_updates,
open_url,
}; };
/// Sync with: ghostty_action_u /// 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. // For ABI compatibility, we expect that this is our union size.
// At the time of writing, we don't promise ABI compatibility // At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it. // 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. /// 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,
};
}
};

View File

@ -884,7 +884,7 @@ pub const Surface = struct {
} }
// Remove this so that running `ghostty` within Ghostty works. // 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 // If we were launched from the desktop then we want to
// remove the LANGUAGE env var so that we don't inherit // remove the LANGUAGE env var so that we don't inherit

View File

@ -496,7 +496,7 @@ pub fn performAction(
.resize_split => self.resizeSplit(target, value), .resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target), .equalize_splits => self.equalizeSplits(target),
.goto_split => return self.gotoSplit(target, value), .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), .config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value), .reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value), .inspector => self.controlInspector(target, value),
@ -519,6 +519,7 @@ pub fn performAction(
.secure_input => self.setSecureInput(target, value), .secure_input => self.setSecureInput(target, value),
.ring_bell => try self.ringBell(target), .ring_bell => try self.ringBell(target),
.toggle_command_palette => try self.toggleCommandPalette(target), .toggle_command_palette => try self.toggleCommandPalette(target),
.open_url => self.openUrl(value),
// Unimplemented // Unimplemented
.close_all_windows, .close_all_windows,
@ -1757,3 +1758,34 @@ fn initActions(self: *App) void {
action_map.addAction(action.as(gio.Action)); 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});
}

View File

@ -214,6 +214,7 @@ pub fn init(self: *Window, app: *App) !void {
{ {
const btn = gtk.MenuButton.new(); const btn = gtk.MenuButton.new();
btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu")); btn.as(gtk.Widget).setTooltipText(i18n._("Main Menu"));
btn.as(gtk.Widget).setCanFocus(0);
btn.setIconName("open-menu-symbolic"); btn.setIconName("open-menu-symbolic");
btn.setPopover(self.titlebar_menu.asWidget()); btn.setPopover(self.titlebar_menu.asWidget());
_ = gobject.Object.signals.notify.connect( _ = gobject.Object.signals.notify.connect(
@ -253,6 +254,7 @@ pub fn init(self: *Window, app: *App) !void {
}, },
}; };
btn.setCanFocus(0);
btn.setFocusOnClick(0); btn.setFocusOnClick(0);
self.headerbar.packEnd(btn); self.headerbar.packEnd(btn);
} }

View File

@ -88,6 +88,19 @@ pub fn init(
// Our step to open the resulting Ghostty app. // Our step to open the resulting Ghostty app.
const open = open: { 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"); const open = RunStep.create(b, "run Ghostty app");
open.has_side_effects = true; open.has_side_effects = true;
open.cwd = b.path(""); open.cwd = b.path("");
@ -98,22 +111,17 @@ pub fn init(
// Open depends on the app // Open depends on the app
open.step.dependOn(&build.step); open.step.dependOn(&build.step);
open.step.dependOn(&disable_save_state.step);
// This overrides our default behavior and forces logs to show // This overrides our default behavior and forces logs to show
// up on stderr (in addition to the centralized macOS log). // up on stderr (in addition to the centralized macOS log).
open.setEnvironmentVariable("GHOSTTY_LOG", "1"); open.setEnvironmentVariable("GHOSTTY_LOG", "1");
// This is hack so that we can activate the app and bring it to // Configure how we're launching
// the front forcibly even though we're executing directly open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
// via the binary and not launch services.
open.setEnvironmentVariable("GHOSTTY_MAC_ACTIVATE", "1");
if (b.args) |args| { if (b.args) |args| {
open.addArgs(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; break :open open;

View File

@ -760,6 +760,9 @@ pub fn gtkDistResources(
}); });
const resources_c = generate_c.addOutputFileArg("ghostty_resources.c"); const resources_c = generate_c.addOutputFileArg("ghostty_resources.c");
generate_c.addFileArg(gresource_xml); generate_c.addFileArg(gresource_xml);
for (gresource.dependencies) |file| {
generate_c.addFileInput(b.path(file));
}
const generate_h = b.addSystemCommand(&.{ const generate_h = b.addSystemCommand(&.{
"glib-compile-resources", "glib-compile-resources",
@ -770,6 +773,9 @@ pub fn gtkDistResources(
}); });
const resources_h = generate_h.addOutputFileArg("ghostty_resources.h"); const resources_h = generate_h.addOutputFileArg("ghostty_resources.h");
generate_h.addFileArg(gresource_xml); generate_h.addFileArg(gresource_xml);
for (gresource.dependencies) |file| {
generate_h.addFileInput(b.path(file));
}
return .{ return .{
.resources_c = .{ .resources_c = .{

View File

@ -15,8 +15,6 @@ pub const Options = struct {};
/// The `version` command is used to display information about Ghostty. Recognized as /// The `version` command is used to display information about Ghostty. Recognized as
/// either `+version` or `--version`. /// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 { pub fn run(alloc: Allocator) !u8 {
_ = alloc;
const stdout = std.io.getStdOut().writer(); const stdout = std.io.getStdOut().writer();
const tty = std.io.getStdOut().isTty(); 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(" - channel: {s}\n", .{@tagName(build_config.release_channel)});
try stdout.print("Build Config\n", .{}); try stdout.print("Build Config\n", .{});
try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string}); try stdout.print(" - Zig version : {s}\n", .{builtin.zig_version_string});
try stdout.print(" - build mode : {}\n", .{builtin.mode}); try stdout.print(" - build mode : {}\n", .{builtin.mode});
try stdout.print(" - app runtime: {}\n", .{build_config.app_runtime}); try stdout.print(" - app runtime : {}\n", .{build_config.app_runtime});
try stdout.print(" - font engine: {}\n", .{build_config.font_backend}); try stdout.print(" - font engine : {}\n", .{build_config.font_backend});
try stdout.print(" - renderer : {}\n", .{renderer.Renderer}); try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)}); try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
if (comptime build_config.app_runtime == .gtk) { if (comptime build_config.app_runtime == .gtk) {
try stdout.print(" - desktop env: {s}\n", .{@tagName(internal_os.desktopEnvironment())}); if (comptime builtin.os.tag == .linux) {
try stdout.print(" - GTK version:\n", .{}); const kernel_info = internal_os.getKernelInfo(alloc);
try stdout.print(" build : {}\n", .{gtk_version.comptime_version}); defer if (kernel_info) |k| alloc.free(k);
try stdout.print(" runtime : {}\n", .{gtk_version.getRuntimeVersion()}); try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"});
try stdout.print(" - libadwaita : enabled\n", .{}); }
try stdout.print(" build : {}\n", .{adw_version.comptime_version}); try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())});
try stdout.print(" runtime : {}\n", .{adw_version.getRuntimeVersion()}); 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) { if (comptime build_options.x11) {
try stdout.print(" - libX11 : enabled\n", .{}); try stdout.print(" - libX11 : enabled\n", .{});
} else { } else {
try stdout.print(" - libX11 : disabled\n", .{}); try stdout.print(" - libX11 : disabled\n", .{});
} }
// We say `libwayland` since it is possible to build Ghostty without // We say `libwayland` since it is possible to build Ghostty without
// Wayland integration but with Wayland-enabled GTK // Wayland integration but with Wayland-enabled GTK
if (comptime build_options.wayland) { if (comptime build_options.wayland) {
try stdout.print(" - libwayland : enabled\n", .{}); try stdout.print(" - libwayland : enabled\n", .{});
} else { } else {
try stdout.print(" - libwayland : disabled\n", .{}); try stdout.print(" - libwayland : disabled\n", .{});
} }
} }
return 0; return 0;

View File

@ -14,6 +14,7 @@ pub const entryFormatter = formatter.entryFormatter;
pub const formatEntry = formatter.formatEntry; pub const formatEntry = formatter.formatEntry;
// Field types // Field types
pub const BoldColor = Config.BoldColor;
pub const ClipboardAccess = Config.ClipboardAccess; pub const ClipboardAccess = Config.ClipboardAccess;
pub const Command = Config.Command; pub const Command = Config.Command;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
@ -37,6 +38,7 @@ pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures;
pub const WindowPaddingColor = Config.WindowPaddingColor; pub const WindowPaddingColor = Config.WindowPaddingColor;
pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImagePosition = Config.BackgroundImagePosition;
pub const BackgroundImageFit = Config.BackgroundImageFit; pub const BackgroundImageFit = Config.BackgroundImageFit;
pub const LinkPreviews = Config.LinkPreviews;
// Alternate APIs // Alternate APIs
pub const CAPI = @import("config/CAPI.zig"); pub const CAPI = @import("config/CAPI.zig");

View File

@ -1,7 +1,9 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const cli = @import("../cli.zig"); const cli = @import("../cli.zig");
const inputpkg = @import("../input.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 Config = @import("Config.zig");
const c_get = @import("c_get.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. /// Create a new configuration filled with the initial default values.
export fn ghostty_config_new() ?*Config { 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}); log.err("error allocating config err={}", .{err});
return null; return null;
}; };
result.* = Config.default(global.alloc) catch |err| { result.* = Config.default(state.alloc) catch |err| {
log.err("error creating config err={}", .{err}); log.err("error creating config err={}", .{err});
global.alloc.destroy(result); state.alloc.destroy(result);
return null; return null;
}; };
@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config {
export fn ghostty_config_free(ptr: ?*Config) void { export fn ghostty_config_free(ptr: ?*Config) void {
if (ptr) |v| { if (ptr) |v| {
v.deinit(); v.deinit();
global.alloc.destroy(v); state.alloc.destroy(v);
} }
} }
/// Deep clone the configuration. /// Deep clone the configuration.
export fn ghostty_config_clone(self: *Config) ?*Config { 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}); log.err("error allocating config err={}", .{err});
return null; return null;
}; };
result.* = self.clone(global.alloc) catch |err| { result.* = self.clone(state.alloc) catch |err| {
log.err("error cloning config err={}", .{err}); log.err("error cloning config err={}", .{err});
global.alloc.destroy(result); state.alloc.destroy(result);
return null; return null;
}; };
@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config {
/// Load the configuration from the CLI args. /// Load the configuration from the CLI args.
export fn ghostty_config_load_cli_args(self: *Config) void { 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}); 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 /// is usually done first. The default file locations are locations
/// such as the home directory. /// such as the home directory.
export fn ghostty_config_load_default_files(self: *Config) void { 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}); 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 /// file locations in the previously loaded configuration. This will
/// recursively continue to load up to a built-in limit. /// recursively continue to load up to a built-in limit.
export fn ghostty_config_load_recursive_files(self: *Config) void { 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}); 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 }; return .{ .message = message.ptr };
} }
export fn ghostty_config_open() void { export fn ghostty_config_open_path() c.String {
edit.open(global.alloc) catch |err| { const path = edit.openPath(state.alloc) catch |err| {
log.err("error opening config in editor err={}", .{err}); log.err("error opening config in editor err={}", .{err});
return .empty;
}; };
return .fromSlice(path);
} }
/// Sync with ghostty_diagnostic_s /// Sync with ghostty_diagnostic_s

View File

@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
// this behavior. This applies to selection too. // this behavior. This applies to selection too.
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg }, .{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg }, .{ "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. /// The font families to use.
@ -435,7 +439,7 @@ pub const compatibility = std.StaticStringMap(
/// * `hinting` - Enable or disable hinting. Enabled by default. /// * `hinting` - Enable or disable hinting. Enabled by default.
/// ///
/// * `force-autohint` - Always use the freetype auto-hinter instead of /// * `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. /// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering.
/// This will disable anti-aliasing, and probably not look very good unless /// 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`). If you want to customize URL matching, use `link` and disable this.
@"link-url": bool = true, @"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 /// 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 /// 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. /// 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. /// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true, @"desktop-notifications": bool = true,
/// If `true`, the bold text will use the bright color palette. /// Modifies the color used for bold text in the terminal.
@"bold-is-bright": bool = false, ///
/// 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. /// 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 /// HACK: We set this with an `xterm` prefix because vim uses that to enable key
@ -3921,6 +3949,23 @@ fn compatSelectionInvertFgBg(
return true; 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 /// 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 /// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit` /// any changes or new allocations. The config should have `deinit`
@ -4345,6 +4390,12 @@ pub const WindowSubtitle = enum {
@"working-directory", @"working-directory",
}; };
pub const LinkPreviews = enum {
false,
true,
osc8,
};
/// Color represents a color using RGB. /// Color represents a color using RGB.
/// ///
/// This is a packed struct so that the C API to read color values just /// 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 { pub const ColorList = struct {
const Self = @This(); const Self = @This();
@ -6552,8 +6655,9 @@ pub const RepeatableCommand = struct {
try list.parseCLI(alloc, "title:Foo,action:ignore"); 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: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: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.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title); 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("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description); 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 list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len); 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 // for Freetype itself. Ghostty hasn't made any opinionated changes
// to these defaults. // to these defaults.
hinting: bool = true, hinting: bool = true,
@"force-autohint": bool = true, @"force-autohint": bool = false,
monochrome: bool = false, monochrome: bool = false,
autohint: bool = true, 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",
);
}
}

View File

@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig"); const internal_os = @import("../os/main.zig");
/// Open the configuration in the OS default editor according to the default /// The path to the configuration that should be opened for editing.
/// paths the main config file could be in.
/// ///
/// 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. /// 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 /// 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. /// followed by XDG if it exists, and finally AppSupport if neither exist.
/// For the existence check, we also prefer non-empty files over empty /// For the existence check, we also prefer non-empty files over empty
/// files. /// 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. // Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa); var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit(); 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. /// Returns the config path to use for open for the current OS.

View File

@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
if (self.load_options) |*v| v.deinit(alloc); if (self.load_options) |*v| v.deinit(alloc);
} }
pub const AddError = Allocator.Error || error{ pub const AddError =
CollectionFull, Allocator.Error ||
DeferredLoadingUnavailable, AdjustSizeError ||
}; error{
CollectionFull,
DeferredLoadingUnavailable,
SetSizeFailed,
};
/// Add a face to the collection for the given style. This face will be added /// 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 /// 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, /// 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. /// 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 /// If a loaded face is added to the collection, its size will be changed to
/// size as all the other faces in the collection. This function will not /// match the size specified in load_options, adjusted for harmonization with
/// verify or modify the size until the size of the entire collection is /// the primary face.
/// changed.
pub fn add( pub fn add(
self: *Collection, self: *Collection,
alloc: Allocator, alloc: Allocator,
@ -103,9 +106,107 @@ pub fn add(
return error.DeferredLoadingUnavailable; return error.DeferredLoadingUnavailable;
try list.append(alloc, face); 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) }; 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 /// Return the Face represented by a given Index. The returned pointer
/// is only valid as long as this collection is not modified. /// 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; 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. /// Get the face from an entry.
/// ///
/// This entry must not be an alias. /// 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); assert(entry.* != .alias);
return switch (entry.*) { return switch (entry.*) {
inline .deferred, .fallback_deferred => |*d, tag| deferred: { inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse const opts = self.load_options orelse
return error.DeferredLoadingUnavailable; return error.DeferredLoadingUnavailable;
const face = try d.load(opts.library, opts.faceOptions()); var face = try d.load(opts.library, opts.faceOptions());
d.deinit(); 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) { entry.* = switch (tag) {
.deferred => .{ .loaded = face }, .deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face }, .fallback_deferred => .{ .fallback_loaded = face },
@ -247,7 +365,7 @@ pub fn completeStyles(
while (it.next()) |entry| { while (it.next()) |entry| {
// Load our face. If we fail to load it, we just skip it and // Load our face. If we fail to load it, we just skip it and
// continue on to try the next one. // 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={}", .{ log.warn("error loading regular entry={d} err={}", .{
it.index - 1, it.index - 1,
err, err,
@ -371,7 +489,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable; const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to bold it. // Try to bold it.
const regular = try self.getFaceFromEntry(entry); const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticBold(opts.faceOptions()); const face = try regular.syntheticBold(opts.faceOptions());
var buf: [256]u8 = undefined; 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; const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it. // Try to italicize it.
const regular = try self.getFaceFromEntry(entry); const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticItalic(opts.faceOptions()); const face = try regular.syntheticItalic(opts.faceOptions());
var buf: [256]u8 = undefined; var buf: [256]u8 = undefined;
@ -420,9 +538,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
while (it.next()) |array| { while (it.next()) |array| {
var entry_it = array.value.iterator(0); var entry_it = array.value.iterator(0);
while (entry_it.next()) |entry| switch (entry.*) { while (entry_it.next()) |entry| switch (entry.*) {
.loaded, .fallback_loaded => |*f| try f.setSize( .loaded,
opts.faceOptions(), .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. // 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. // 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. /// True if the entry is deferred.
fn isDeferred(self: Entry) bool { fn isDeferred(self: Entry) bool {
return switch (self) { return switch (self) {
@ -906,12 +1037,13 @@ test "metrics" {
var c = init(); var c = init();
defer c.deinit(alloc); 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( _ = try c.add(alloc, .regular, .{ .loaded = try .init(
lib, lib,
testFont, testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, .{ .size = size },
) }); ) });
try c.updateMetrics(); try c.updateMetrics();
@ -958,3 +1090,62 @@ test "metrics" {
.cursor_height = 34, .cursor_height = 34,
}, c.metrics); }, 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,
);
}
}

View File

@ -17,9 +17,3 @@ offset_y: i32,
/// be normalized to be between 0 and 1 prior to use in shaders. /// be normalized to be between 0 and 1 prior to use in shaders.
atlas_x: u32, atlas_x: u32,
atlas_y: 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,

View File

@ -107,6 +107,19 @@ pub const FaceMetrics = struct {
/// a provided ex height metric or measured from the height of the /// a provided ex height metric or measured from the height of the
/// lowercase x glyph. /// lowercase x glyph.
ex_height: ?f64 = null, 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. /// Calculate our metrics based on values extracted from a font.

View File

@ -147,6 +147,9 @@ pub const RenderOptions = struct {
/// Maximum ratio of width to height when resizing. /// Maximum ratio of width to height when resizing.
max_xy_ratio: ?f64 = null, max_xy_ratio: ?f64 = null,
/// Maximum number of cells horizontally to use.
max_constraint_width: u2 = 2,
pub const Size = enum { pub const Size = enum {
/// Don't change the size of this glyph. /// Don't change the size of this glyph.
none, none,
@ -186,16 +189,26 @@ pub const RenderOptions = struct {
pub fn constrain( pub fn constrain(
self: Constraint, self: Constraint,
glyph: GlyphSize, glyph: GlyphSize,
/// Available width /// Width of one cell.
cell_width: f64, cell_width: f64,
/// Available height /// Height of one cell.
cell_height: f64, cell_height: f64,
/// Number of cells horizontally available for this glyph.
constraint_width: u2,
) GlyphSize { ) GlyphSize {
var g = glyph; var g = glyph;
const w = cell_width - const available_width =
self.pad_left * cell_width - cell_width * @as(f64, @floatFromInt(
self.pad_right * cell_width; @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 - const h = cell_height -
self.pad_top * cell_height - self.pad_top * cell_height -
self.pad_bottom * cell_height; self.pad_bottom * cell_height;
@ -203,7 +216,7 @@ pub const RenderOptions = struct {
// Subtract padding from the bearings so that our // Subtract padding from the bearings so that our
// alignment and sizing code works correctly. We // alignment and sizing code works correctly. We
// re-add before returning. // 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; g.y -= self.pad_bottom * cell_height;
switch (self.size_horizontal) { switch (self.size_horizontal) {
@ -305,7 +318,7 @@ pub const RenderOptions = struct {
} }
// Re-add our padding before returning. // 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; g.y += self.pad_bottom * cell_height;
return g; return g;

View File

@ -31,6 +31,9 @@ pub const Face = struct {
/// tables). /// tables).
color: ?ColorState = null, 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 /// True if our build is using Harfbuzz. If we're not, we can avoid
/// some Harfbuzz-specific code paths. /// some Harfbuzz-specific code paths.
const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
@ -106,6 +109,7 @@ pub const Face = struct {
.font = ct_font, .font = ct_font,
.hb_font = hb_font, .hb_font = hb_font,
.color = color, .color = color,
.size = opts.size,
}; };
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@ -333,11 +337,10 @@ pub const Face = struct {
.offset_y = 0, .offset_y = 0,
.atlas_x = 0, .atlas_x = 0,
.atlas_y = 0, .atlas_y = 0,
.advance_x = 0,
}; };
const metrics = opts.grid_metrics; 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 cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_size = opts.constraint.constrain( const glyph_size = opts.constraint.constrain(
@ -349,6 +352,7 @@ pub const Face = struct {
}, },
cell_width, cell_width,
cell_height, cell_height,
opts.constraint_width,
); );
const width = glyph_size.width; const width = glyph_size.width;
@ -356,8 +360,16 @@ pub const Face = struct {
const x = glyph_size.x; const x = glyph_size.x;
const y = glyph_size.y; const y = glyph_size.y;
const px_width: u32 = @intFromFloat(@ceil(width)); // We have to include the fractional pixels that we won't be offsetting
const px_height: u32 = @intFromFloat(@ceil(height)); // 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. // Settings that are specific to if we are rendering text or emoji.
const color: struct { const color: struct {
@ -475,26 +487,44 @@ pub const Face = struct {
// This should be the distance from the left of // This should be the distance from the left of
// the cell to the left of the glyph's bounding box. // the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: { const offset_x: i32 = offset_x: {
var result: i32 = @intFromFloat(@round(x)); // 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
// If our cell was resized then we adjust our glyph's // I implemented this to proportionally scale the center position
// position relative to the new center. This keeps glyphs // of the glyph but that messes up glyphs that are meant to align
// centered in the cell whether it was made wider or narrower. // vertically with others, so this is a compromise.
if (metrics.original_cell_width) |original_width| { //
const before: i32 = @intCast(original_width); // This makes it so that when the `adjust-cell-width` config is
const after: i32 = @intCast(metrics.cell_width); // used, or when a fallback font with a different advance width
// Increase the offset by half of the difference // is used, we don't get weirdly aligned glyphs.
// between the widths to keep things centered. //
result += @divTrunc(after - before, 2); // 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 .{ return .{
.width = px_width, .width = px_width,
.height = px_height, .height = px_height,
@ -502,7 +532,6 @@ pub const Face = struct {
.offset_y = offset_y, .offset_y = offset_y,
.atlas_x = region.x, .atlas_x = region.x,
.atlas_y = region.y, .atlas_y = region.y,
.advance_x = @floatCast(advances[0].width),
}; };
} }
@ -734,6 +763,20 @@ pub const Face = struct {
break :cell_width max; 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 .{ return .{
.cell_width = cell_width, .cell_width = cell_width,
.ascent = ascent, .ascent = ascent,
@ -745,6 +788,7 @@ pub const Face = struct {
.strikethrough_thickness = strikethrough_thickness, .strikethrough_thickness = strikethrough_thickness,
.cap_height = cap_height, .cap_height = cap_height,
.ex_height = ex_height, .ex_height = ex_height,
.ic_width = ic_width,
}; };
} }

View File

@ -59,6 +59,9 @@ pub const Face = struct {
bold: bool = false, 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. /// Initialize a new font face with the given source in-memory.
pub fn initFile( pub fn initFile(
lib: Library, lib: Library,
@ -107,6 +110,7 @@ pub const Face = struct {
.hb_font = hb_font, .hb_font = hb_font,
.ft_mutex = ft_mutex, .ft_mutex = ft_mutex,
.load_flags = opts.freetype_load_flags, .load_flags = opts.freetype_load_flags,
.size = opts.size,
}; };
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); 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. /// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, opts: font.face.Options) !void { pub fn setSize(self: *Face, opts: font.face.Options) !void {
try setSize_(self.face, opts.size); try setSize_(self.face, opts.size);
self.size = opts.size;
} }
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
@ -348,7 +353,7 @@ pub const Face = struct {
// use options from config // use options from config
.no_hinting = !do_hinting, .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_autohint = !self.load_flags.autohint,
// NO_SVG set to true because we don't currently support rendering // NO_SVG set to true because we don't currently support rendering
@ -373,7 +378,6 @@ pub const Face = struct {
.offset_y = 0, .offset_y = 0,
.atlas_x = 0, .atlas_x = 0,
.atlas_y = 0, .atlas_y = 0,
.advance_x = 0,
}; };
// For synthetic bold, we embolden the glyph. // For synthetic bold, we embolden the glyph.
@ -390,7 +394,7 @@ pub const Face = struct {
// Next we need to apply any constraints. // Next we need to apply any constraints.
const metrics = opts.grid_metrics; 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 cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX); const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
@ -405,6 +409,7 @@ pub const Face = struct {
}, },
cell_width, cell_width,
cell_height, cell_height,
opts.constraint_width,
); );
const width = glyph_size.width; const width = glyph_size.width;
@ -638,20 +643,40 @@ pub const Face = struct {
// This should be the distance from the left of // This should be the distance from the left of
// the cell to the left of the glyph's bounding box. // the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: { const offset_x: i32 = offset_x: {
var result: i32 = @intFromFloat(@floor(x)); // 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
// If our cell was resized then we adjust our glyph's // I implemented this to proportionally scale the center position
// position relative to the new center. This keeps glyphs // of the glyph but that messes up glyphs that are meant to align
// centered in the cell whether it was made wider or narrower. // vertically with others, so this is a compromise.
if (metrics.original_cell_width) |original_width| { //
const before: i32 = @intCast(original_width); // This makes it so that when the `adjust-cell-width` config is
const after: i32 = @intCast(metrics.cell_width); // used, or when a fallback font with a different advance width
// Increase the offset by half of the difference // is used, we don't get weirdly aligned glyphs.
// between the widths to keep things centered. //
result += @divTrunc(after - before, 2); // 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{ return Glyph{
@ -661,7 +686,6 @@ pub const Face = struct {
.offset_y = offset_y, .offset_y = offset_y,
.atlas_x = region.x, .atlas_x = region.x,
.atlas_y = region.y, .atlas_y = region.y,
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
}; };
} }
@ -832,7 +856,7 @@ pub const Face = struct {
while (c < 127) : (c += 1) { while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| { if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ if (face.loadGlyph(glyph_index, .{
.render = true, .render = false,
.no_svg = true, .no_svg = true,
})) { })) {
max = @max( max = @max(
@ -870,7 +894,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock(); defer self.ft_mutex.unlock();
if (face.getCharIndex('H')) |glyph_index| { if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ if (face.loadGlyph(glyph_index, .{
.render = true, .render = false,
.no_svg = true, .no_svg = true,
})) { })) {
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
@ -883,7 +907,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock(); defer self.ft_mutex.unlock();
if (face.getCharIndex('x')) |glyph_index| { if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{ if (face.loadGlyph(glyph_index, .{
.render = true, .render = false,
.no_svg = true, .no_svg = true,
})) { })) {
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); 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 .{ return .{
.cell_width = cell_width, .cell_width = cell_width,
@ -909,6 +948,7 @@ pub const Face = struct {
.cap_height = cap_height, .cap_height = cap_height,
.ex_height = ex_height, .ex_height = ex_height,
.ic_width = ic_width,
}; };
} }

View File

@ -235,7 +235,6 @@ pub const Face = struct {
.offset_y = 0, .offset_y = 0,
.atlas_x = region.x, .atlas_x = region.x,
.atlas_y = region.y, .atlas_y = region.y,
.advance_x = 0,
}; };
} }

View File

@ -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! //! DO NOT EDIT BY HAND!
//! //!
//! This file provides info extracted from the nerd fonts patcher script, //! 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_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .center, .align_horizontal = .center,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.02, .pad_left = -0.02,
@ -24,24 +25,29 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .cover, .size_horizontal = .cover,
.size_vertical = .fit, .size_vertical = .fit,
.max_constraint_width = 1,
.align_horizontal = .center, .align_horizontal = .center,
.align_vertical = .center, .align_vertical = .center,
.pad_left = 0.1, .pad_left = 0.1,
.pad_right = 0.1, .pad_right = 0.1,
.pad_top = 0.01, .pad_top = 0.1,
.pad_bottom = 0.01, .pad_bottom = 0.1,
}, },
0x276c...0x2771, 0x276c...0x2771,
=> .{ => .{
.size_horizontal = .cover, .size_horizontal = .cover,
.size_vertical = .fit, .size_vertical = .fit,
.max_constraint_width = 1,
.align_horizontal = .center, .align_horizontal = .center,
.align_vertical = .center, .align_vertical = .center,
.pad_top = 0.15,
.pad_bottom = 0.15,
}, },
0xe0b0, 0xe0b0,
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.06, .pad_left = -0.06,
@ -54,6 +60,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.max_xy_ratio = 0.7, .max_xy_ratio = 0.7,
@ -62,6 +69,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.06, .pad_left = -0.06,
@ -74,6 +82,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.max_xy_ratio = 0.7, .max_xy_ratio = 0.7,
@ -82,6 +91,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.06, .pad_left = -0.06,
@ -94,6 +104,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.max_xy_ratio = 0.5, .max_xy_ratio = 0.5,
@ -102,6 +113,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.06, .pad_left = -0.06,
@ -114,6 +126,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.max_xy_ratio = 0.5, .max_xy_ratio = 0.5,
@ -123,6 +136,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.05, .pad_left = -0.05,
@ -135,6 +149,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
}, },
@ -143,6 +158,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.05, .pad_left = -0.05,
@ -155,6 +171,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
}, },
@ -204,8 +221,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center, .align_vertical = .center,
.pad_left = 0.03, .pad_left = 0.03,
.pad_right = 0.03, .pad_right = 0.03,
.pad_top = 0.01, .pad_top = 0.03,
.pad_bottom = 0.01, .pad_bottom = 0.03,
.max_xy_ratio = 0.86, .max_xy_ratio = 0.86,
}, },
0xe0c5, 0xe0c5,
@ -216,8 +233,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center, .align_vertical = .center,
.pad_left = 0.03, .pad_left = 0.03,
.pad_right = 0.03, .pad_right = 0.03,
.pad_top = 0.01, .pad_top = 0.03,
.pad_bottom = 0.01, .pad_bottom = 0.03,
.max_xy_ratio = 0.86, .max_xy_ratio = 0.86,
}, },
0xe0c6, 0xe0c6,
@ -228,8 +245,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center, .align_vertical = .center,
.pad_left = 0.03, .pad_left = 0.03,
.pad_right = 0.03, .pad_right = 0.03,
.pad_top = 0.01, .pad_top = 0.03,
.pad_bottom = 0.01, .pad_bottom = 0.03,
.max_xy_ratio = 0.78, .max_xy_ratio = 0.78,
}, },
0xe0c7, 0xe0c7,
@ -240,8 +257,8 @@ pub fn getConstraint(cp: u21) Constraint {
.align_vertical = .center, .align_vertical = .center,
.pad_left = 0.03, .pad_left = 0.03,
.pad_right = 0.03, .pad_right = 0.03,
.pad_top = 0.01, .pad_top = 0.03,
.pad_bottom = 0.01, .pad_bottom = 0.03,
.max_xy_ratio = 0.78, .max_xy_ratio = 0.78,
}, },
0xe0cc, 0xe0cc,
@ -285,6 +302,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.02, .pad_left = -0.02,
@ -297,6 +315,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.02, .pad_left = -0.02,
@ -309,6 +328,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .start, .align_horizontal = .start,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.05, .pad_left = -0.05,
@ -321,6 +341,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{ => .{
.size_horizontal = .stretch, .size_horizontal = .stretch,
.size_vertical = .stretch, .size_vertical = .stretch,
.max_constraint_width = 1,
.align_horizontal = .end, .align_horizontal = .end,
.align_vertical = .center, .align_vertical = .center,
.pad_left = -0.05, .pad_left = -0.05,

View File

@ -1,102 +1,124 @@
""" """
This file is mostly vibe coded because I don't like Python. It extracts the This file extracts the patch sets from the nerd fonts font patcher file in order to
patch sets from the nerd fonts font patcher file in order to extract scaling extract scaling rules and attributes for different codepoint ranges which it then
rules and attributes for different codepoint ranges which it then codegens codegens in to a Zig file with a function that switches over codepoints and returns the
in to a Zig file with a function that switches over codepoints and returns attributes and scaling rules.
the attributes and scaling rules.
This does include an `eval` call! This is spooky, but we trust This does include an `eval` call! This is spooky, but we trust the nerd fonts code to
the nerd fonts code to be safe and not malicious or anything. be safe and not malicious or anything.
This script requires Python 3.12 or greater.
""" """
import ast import ast
import math import math
from pathlib import Path
from collections import defaultdict 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): class PatchSetExtractor(ast.NodeVisitor):
def __init__(self): def __init__(self) -> None:
self.symbol_table = {} self.symbol_table: dict[str, ast.expr] = {}
self.patch_set_values = [] self.patch_set_values: list[PatchSet] = []
def visit_ClassDef(self, node): def visit_ClassDef(self, node: ast.ClassDef) -> None:
if node.name == "font_patcher": if node.name != "font_patcher":
for item in node.body: return
if isinstance(item, ast.FunctionDef) and item.name == "setup_patch_set": for item in node.body:
self.visit_setup_patch_set(item) 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 # First pass: gather variable assignments
for stmt in node.body: for stmt in node.body:
if isinstance(stmt, ast.Assign): match stmt:
# Store simple variable assignments in the symbol table case ast.Assign(targets=[ast.Name(id=symbol)]):
if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name): # Store simple variable assignments in the symbol table
var_name = stmt.targets[0].id self.symbol_table[symbol] = stmt.value
self.symbol_table[var_name] = stmt.value
# Second pass: process self.patch_set # Second pass: process self.patch_set
for stmt in node.body: for stmt in node.body:
if isinstance(stmt, ast.Assign): if not isinstance(stmt, ast.Assign):
for target in stmt.targets: continue
if isinstance(target, ast.Attribute) and target.attr == "patch_set": for target in stmt.targets:
if isinstance(stmt.value, ast.List): if (
for elt in stmt.value.elts: isinstance(target, ast.Attribute)
if isinstance(elt, ast.Dict): and target.attr == "patch_set"
self.process_patch_entry(elt) 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.""" """Resolve named variables to their actual values from the symbol table."""
if isinstance(node, ast.Name) and node.id in self.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(self.symbol_table[node.id])
return self.safe_literal_eval(node) 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 to evaluate or stringify an AST node."""
try: try:
return ast.literal_eval(node) return ast.literal_eval(node)
except Exception: except ValueError:
# Spooky eval! But we trust nerd fonts to be safe... # Spooky eval! But we trust nerd fonts to be safe...
if hasattr(ast, "unparse"): if hasattr(ast, "unparse"):
return eval( return eval(
ast.unparse(node), {"box_keep": True}, {"self": SpoofSelf()} ast.unparse(node),
{"box_keep": True},
{"self": SimpleNamespace(args=SimpleNamespace(careful=True))},
) )
else: msg = f"<cannot eval: {type(node).__name__}>"
return f"<cannot eval: {type(node).__name__}>" raise ValueError(msg) from None
def process_patch_entry(self, dict_node): def process_patch_entry(self, dict_node: ast.Dict) -> None:
entry = {} entry = {}
disallowed_key_nodes = frozenset({"Enabled", "Name", "Filename", "Exact"})
for key_node, value_node in zip(dict_node.keys, dict_node.values): for key_node, value_node in zip(dict_node.keys, dict_node.values):
if isinstance(key_node, ast.Constant) and key_node.value in ( if (
"Enabled", isinstance(key_node, ast.Constant)
"Name", and key_node.value not in disallowed_key_nodes
"Filename",
"Exact",
): ):
continue key = ast.literal_eval(cast("ast.Constant", key_node))
key = ast.literal_eval(key_node) entry[key] = self.resolve_symbol(value_node)
value = self.resolve_symbol(value_node) self.patch_set_values.append(cast("PatchSet", entry))
entry[key] = value
self.patch_set_values.append(entry)
def extract_patch_set_values(source_code): def extract_patch_set_values(source_code: str) -> list[PatchSet]:
tree = ast.parse(source_code) tree = ast.parse(source_code)
extractor = PatchSetExtractor() extractor = PatchSetExtractor()
extractor.visit(tree) extractor.visit(tree)
return extractor.patch_set_values return extractor.patch_set_values
# We have to spoof `self` and `self.args` for the eval. def parse_alignment(val: str) -> str | None:
class SpoofArgs:
careful = True
class SpoofSelf:
args = SpoofArgs()
def parse_alignment(val):
return { return {
"l": ".start", "l": ".start",
"r": ".end", "r": ".end",
@ -105,28 +127,24 @@ def parse_alignment(val):
}.get(val, ".none") }.get(val, ".none")
def get_param(d, key, default): def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash:
return float(d.get(key, default))
def attr_key(attr):
"""Convert attributes to a hashable key for grouping.""" """Convert attributes to a hashable key for grouping."""
stretch = attr.get("stretch", "") params = attr.get("params", {})
return ( return (
parse_alignment(attr.get("align", "")), parse_alignment(attr.get("align", "")),
parse_alignment(attr.get("valign", "")), parse_alignment(attr.get("valign", "")),
stretch, attr.get("stretch", ""),
float(attr.get("params", {}).get("overlap", 0.0)), float(params.get("overlap", 0.0)),
float(attr.get("params", {}).get("xy-ratio", -1.0)), float(params.get("xy-ratio", -1.0)),
float(attr.get("params", {}).get("ypadding", 0.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.""" """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)) cp_iter = iter(sorted(codepoints))
try: with suppress(StopIteration):
start = prev = next(cp_iter) start = prev = next(cp_iter)
for cp in cp_iter: for cp in cp_iter:
if cp == prev + 1: if cp == prev + 1:
@ -135,88 +153,96 @@ def coalesce_codepoints_to_ranges(codepoints):
ranges.append((start, prev)) ranges.append((start, prev))
start = prev = cp start = prev = cp
ranges.append((start, prev)) ranges.append((start, prev))
except StopIteration:
pass
return ranges 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", "")) align = parse_alignment(attr.get("align", ""))
valign = parse_alignment(attr.get("valign", "")) valign = parse_alignment(attr.get("valign", ""))
stretch = attr.get("stretch", "") stretch = attr.get("stretch", "")
params = attr.get("params", {}) params = attr.get("params", {})
overlap = get_param(params, "overlap", 0.0) overlap = params.get("overlap", 0.0)
xy_ratio = get_param(params, "xy-ratio", -1.0) xy_ratio = params.get("xy-ratio", -1.0)
y_padding = get_param(params, "ypadding", 0.0) y_padding = params.get("ypadding", 0.0)
ranges = coalesce_codepoints_to_ranges(codepoints) ranges = coalesce_codepoints_to_ranges(codepoints)
keys = "\n".join( 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 for start, end in ranges
) )
s = f"""{keys} s = f"{keys}\n => .{{\n"
=> .{{\n"""
# These translations don't quite capture the way # These translations don't quite capture the way
# the actual patcher does scaling, but they're a # the actual patcher does scaling, but they're a
# good enough compromise. # good enough compromise.
if ("xy" in stretch): if "xy" in stretch:
s += " .size_horizontal = .stretch,\n" s += " .size_horizontal = .stretch,\n"
s += " .size_vertical = .stretch,\n" s += " .size_vertical = .stretch,\n"
elif ("!" in stretch): elif "!" in stretch:
s += " .size_horizontal = .cover,\n" s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .fit,\n" s += " .size_vertical = .fit,\n"
elif ("^" in stretch): elif "^" in stretch:
s += " .size_horizontal = .cover,\n" s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .cover,\n" s += " .size_vertical = .cover,\n"
else: else:
s += " .size_horizontal = .fit,\n" s += " .size_horizontal = .fit,\n"
s += " .size_vertical = .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" s += f" .align_horizontal = {align},\n"
if (valign is not None): if valign is not None:
s += f" .align_vertical = {valign},\n" 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 pad = -overlap
s += f" .pad_left = {pad},\n" s += f" .pad_left = {pad},\n"
s += f" .pad_right = {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_top = {v_pad},\n"
s += f" .pad_bottom = {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 += f" .max_xy_ratio = {xy_ratio},\n"
s += " }," s += " },"
return s return s
def generate_zig_switch_arms(patch_set):
entries = {} def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str:
for entry in patch_set: entries: dict[int, PatchSetAttributeEntry] = {}
for entry in patch_sets:
attributes = entry["Attributes"] attributes = entry["Attributes"]
for cp in range(entry["SymStart"], entry["SymEnd"] + 1): for cp in range(entry["SymStart"], entry["SymEnd"] + 1):
entries[cp] = attributes["default"] entries[cp] = attributes["default"]
for k, v in attributes.items(): entries |= {k: v for k, v in attributes.items() if isinstance(k, int)}
if isinstance(k, int):
entries[k] = v
del entries[0] del entries[0]
# Group codepoints by attribute key # Group codepoints by attribute key
grouped = defaultdict(list) grouped = defaultdict[AttributeHash, list[int]](list)
for cp, attr in entries.items(): for cp, attr in entries.items():
grouped[attr_key(attr)].append(cp) grouped[attr_key(attr)].append(cp)
# Emit zig switch arms # Emit zig switch arms
result = [] result: list[str] = []
for _, codepoints in sorted(grouped.items(), key=lambda x: x[1]): for codepoints in sorted(grouped.values()):
# Use one of the attrs in the group to emit the value # Use one of the attrs in the group to emit the value
attr = entries[codepoints[0]] attr = entries[codepoints[0]]
result.append(emit_zig_entry_multikey(codepoints, attr)) result.append(emit_zig_entry_multikey(codepoints, attr))
@ -225,23 +251,16 @@ def generate_zig_switch_arms(patch_set):
if __name__ == "__main__": if __name__ == "__main__":
path = ( project_root = Path(__file__).resolve().parents[2]
Path(__file__).resolve().parent
/ ".."
/ ".."
/ "vendor"
/ "nerd-fonts"
/ "font-patcher.py"
)
with open(path, "r", encoding="utf-8") as f:
source = f.read()
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) 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: with out_path.open("w", encoding="utf-8") as f:
f.write("""//! This is a generate file, produced by nerd_font_codegen.py f.write("""//! This is a generated file, produced by nerd_font_codegen.py
//! DO NOT EDIT BY HAND! //! DO NOT EDIT BY HAND!
//! //!
//! This file provides info extracted from the nerd fonts patcher script, //! This file provides info extracted from the nerd fonts patcher script,
@ -254,6 +273,4 @@ pub fn getConstraint(cp: u21) Constraint {
return switch (cp) { return switch (cp) {
""") """)
f.write(generate_zig_switch_arms(patch_set)) f.write(generate_zig_switch_arms(patch_set))
f.write("\n") f.write("\n else => .none,\n };\n}\n")
f.write(" else => .none,\n };\n}\n")

View File

@ -195,7 +195,6 @@ pub fn renderGlyph(
.offset_y = 0, .offset_y = 0,
.atlas_x = 0, .atlas_x = 0,
.atlas_y = 0, .atlas_y = 0,
.advance_x = 0,
}; };
const metrics = self.metrics; 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)), .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
.atlas_x = region.x, .atlas_x = region.x,
.atlas_y = region.y, .atlas_y = region.y,
.advance_x = @floatFromInt(width),
.sprite = true,
}; };
} }

View File

@ -140,24 +140,7 @@ pub const Canvas = struct {
const region_height = sfc_height -| self.clip_top -| self.clip_bottom; const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
// Allocate our texture atlas region // Allocate our texture atlas region
const region = region: { const region = try atlas.reserve(alloc, region_width, region_height);
// 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;
};
if (region.width > 0 and region.height > 0) { if (region.width > 0 and region.height > 0) {
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);

View File

@ -281,6 +281,10 @@ pub const Action = union(enum) {
/// If there is a URL under the cursor, copy it to the default clipboard. /// If there is a URL under the cursor, copy it to the default clipboard.
copy_url_to_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). /// Increase the font size by the specified amount in points (pt).
/// ///
/// For example, `increase_font_size:1.5` will increase the font size /// 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 the font size to the original configured size.
reset_font_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 the screen and all scrollback.
clear_screen, clear_screen,
@ -999,11 +1009,13 @@ pub const Action = union(enum) {
.reset, .reset,
.copy_to_clipboard, .copy_to_clipboard,
.copy_url_to_clipboard, .copy_url_to_clipboard,
.copy_title_to_clipboard,
.paste_from_clipboard, .paste_from_clipboard,
.paste_from_selection, .paste_from_selection,
.increase_font_size, .increase_font_size,
.decrease_font_size, .decrease_font_size,
.reset_font_size, .reset_font_size,
.set_font_size,
.prompt_surface_title, .prompt_surface_title,
.clear_screen, .clear_screen,
.select_all, .select_all,
@ -3065,6 +3077,7 @@ test "set: getEvent codepoint case folding" {
try testing.expect(action == null); try testing.expect(action == null);
} }
} }
test "Action: clone" { test "Action: clone" {
const testing = std.testing; const testing = std.testing;
var arena = std.heap.ArenaAllocator.init(testing.allocator); var arena = std.heap.ArenaAllocator.init(testing.allocator);
@ -3083,3 +3096,42 @@ test "Action: clone" {
try testing.expect(b == .text); 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);
}
}

View File

@ -1,4 +1,5 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Action = @import("Binding.zig").Action; 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.", .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 &.{.{ .paste_from_clipboard => comptime &.{.{
.action = .paste_from_clipboard, .action = .paste_from_clipboard,
.title = "Paste from Clipboard", .title = "Paste from Clipboard",
@ -460,6 +467,7 @@ fn actionCommands(action: Action.Key) []const Command {
.esc, .esc,
.text, .text,
.cursor_key, .cursor_key,
.set_font_size,
.scroll_page_fractional, .scroll_page_fractional,
.scroll_page_lines, .scroll_page_lines,
.adjust_selection, .adjust_selection,

View File

@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig");
// Some comptime assertions that our C API depends on. // Some comptime assertions that our C API depends on.
comptime { 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. /// 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 // These structs need to be referenced so the `export` functions
// are truly exported by the C API lib. // are truly exported by the C API lib.
_ = @import("config.zig").CAPI; _ = @import("config.zig").CAPI;
_ = apprt.runtime.CAPI; if (@hasDecl(apprt.runtime, "CAPI")) {
_ = apprt.runtime.CAPI;
}
} }
/// ghostty_info_s /// ghostty_info_s
@ -46,17 +53,29 @@ const Info = extern struct {
}; };
}; };
/// Initialize ghostty global state. It is possible to have more than /// ghostty_string_s
/// one global state but it has zero practical benefit. pub const String = extern struct {
export fn ghostty_init() c_int { 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); assert(builtin.link_libc);
// Since in the lib we don't go through start.zig, we need std.os.argv = argv[0..argc];
// to populate argv so that inspecting std.os.argv doesn't
// touch uninitialized memory.
var argv: [0][*:0]u8 = .{};
std.os.argv = &argv;
state.init() catch |err| { state.init() catch |err| {
std.log.err("failed to initialize ghostty error={}", .{err}); std.log.err("failed to initialize ghostty error={}", .{err});
return 1; return 1;
@ -65,15 +84,17 @@ export fn ghostty_init() c_int {
return 0; return 0;
} }
/// This is the entrypoint for the CLI version of Ghostty. This /// Runs an action if it is specified. If there is no action this returns
/// is mutually exclusive to ghostty_init. Do NOT run ghostty_init /// false. If there is an action then this doesn't return.
/// if you are going to run this. This will not return. export fn ghostty_cli_try_action() void {
export fn ghostty_cli_main(argc: usize, argv: [*][*:0]u8) noreturn { const action = state.action orelse return;
std.os.argv = argv[0..argc]; std.log.info("executing CLI action={}", .{action});
main.main() catch |err| { posix.exit(action.run(state.alloc) catch |err| {
std.log.err("failed to run ghostty error={}", .{err}); std.log.err("CLI action failed error={}", .{err});
posix.exit(1); posix.exit(1);
}; });
posix.exit(0);
} }
/// Return metadata about Ghostty, such as version, build mode, etc. /// 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 { export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
return internal_os.i18n._(msgid); 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]);
}

View File

@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
// This special case is so that if we launch the app via the // 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 // app bundle (i.e. via open) then we still treat it as if it
// was launched from the desktop. // was launched from the desktop.
if (build_config.artifact == .lib and if (build_config.artifact == .lib) lib: {
posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true; 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; break :macos c.getppid() == 1;
}, },

View File

@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{
"ca_ES.UTF-8", "ca_ES.UTF-8",
"bg_BG.UTF-8", "bg_BG.UTF-8",
"ga_IE.UTF-8", "ga_IE.UTF-8",
"he_IL.UTF-8",
}; };
/// Set for faster membership lookup of locales. /// Set for faster membership lookup of locales.

27
src/os/kernel_info.zig Normal file
View File

@ -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, ""));
}

View File

@ -14,6 +14,7 @@ const openpkg = @import("open.zig");
const pipepkg = @import("pipe.zig"); const pipepkg = @import("pipe.zig");
const resourcesdir = @import("resourcesdir.zig"); const resourcesdir = @import("resourcesdir.zig");
const systemd = @import("systemd.zig"); const systemd = @import("systemd.zig");
const kernelInfo = @import("kernel_info.zig");
// Namespaces // Namespaces
pub const args = @import("args.zig"); 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 ResourcesDir = resourcesdir.ResourcesDir; pub const ResourcesDir = resourcesdir.ResourcesDir;
pub const ShellEscapeWriter = shell.ShellEscapeWriter; pub const ShellEscapeWriter = shell.ShellEscapeWriter;
pub const getKernelInfo = kernelInfo.getKernelInfo;
test { test {
_ = i18n; _ = i18n;

View File

@ -1,24 +1,23 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const apprt = @import("../apprt.zig");
const log = std.log.scoped(.@"os-open"); 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. /// Open a URL in the default handling application.
/// ///
/// Any output on stderr is logged as a warning in the application logs. /// 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 /// Output on stdout is ignored. The allocator is used to buffer the
/// log output and may allocate from another thread. /// 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( pub fn open(
alloc: Allocator, alloc: Allocator,
typ: Type, kind: apprt.action.OpenUrl.Kind,
url: []const u8, url: []const u8,
) !void { ) !void {
var exe: std.process.Child = switch (builtin.os.tag) { var exe: std.process.Child = switch (builtin.os.tag) {
@ -33,7 +32,7 @@ pub fn open(
), ),
.macos => .init( .macos => .init(
switch (typ) { switch (kind) {
.text => &.{ "open", "-t", url }, .text => &.{ "open", "-t", url },
.unknown => &.{ "open", url }, .unknown => &.{ "open", url },
}, },

View File

@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
.format = .rgba, .format = .rgba,
.internal_format = .srgba, .internal_format = .srgba,
.target = .@"2D", .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(), .format = format.toPixelFormat(),
.internal_format = if (srgb) .srgba else .rgba, .internal_format = if (srgb) .srgba else .rgba,
.target = .@"2D", .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, .format = format,
.internal_format = internal_format, .internal_format = internal_format,
.target = .Rectangle, .target = .Rectangle,
.min_filter = .nearest,
.mag_filter = .nearest,
.wrap_s = .clamp_to_edge,
.wrap_t = .clamp_to_edge,
}, },
atlas.size, atlas.size,
atlas.size, atlas.size,

View File

@ -519,7 +519,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
selection_background: ?configpkg.Config.TerminalColor, selection_background: ?configpkg.Config.TerminalColor,
selection_foreground: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor,
bold_is_bright: bool, bold_color: ?configpkg.BoldColor,
min_contrast: f32, min_contrast: f32,
padding_color: configpkg.WindowPaddingColor, padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath, custom_shaders: configpkg.RepeatablePath,
@ -580,7 +580,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(), .background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(),
.bold_is_bright = config.@"bold-is-bright", .bold_color = config.@"bold-color",
.min_contrast = @floatCast(config.@"minimum-contrast"), .min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color", .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 // the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc. // configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette); const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg( const fg_style = style.fg(.{
color_palette, .default = self.foreground_color orelse self.default_foreground_color,
self.config.bold_is_bright, .palette = color_palette,
) orelse self.foreground_color orelse self.default_foreground_color; .bold = self.config.bold_color,
});
// The final background color for the cell. // The final background color for the cell.
const bg = bg: { const bg = bg: {
@ -2801,10 +2803,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.@"cell-background", .@"cell-background",
=> |_, tag| { => |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
const fg_style = sty.fg( const fg_style = sty.fg(.{
color_palette, .default = self.foreground_color orelse self.default_foreground_color,
self.config.bold_is_bright, .palette = color_palette,
) orelse self.foreground_color orelse self.default_foreground_color; .bold = self.config.bold_color,
});
const bg_style = sty.bg( const bg_style = sty.bg(
screen.cursor.page_cell, screen.cursor.page_cell,
color_palette, 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 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; const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
break :blk switch (txt) { break :blk switch (txt) {

View File

@ -16,6 +16,10 @@ pub const Options = struct {
format: gl.Texture.Format, format: gl.Texture.Format,
internal_format: gl.Texture.InternalFormat, internal_format: gl.Texture.InternalFormat,
target: gl.Texture.Target, 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, texture: gl.Texture,
@ -48,10 +52,10 @@ pub fn init(
{ {
const texbind = tex.bind(opts.target) catch return error.OpenGLFailed; const texbind = tex.bind(opts.target) catch return error.OpenGLFailed;
defer texbind.unbind(); defer texbind.unbind();
texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; texbind.parameter(.WrapS, @intFromEnum(opts.wrap_s)) catch return error.OpenGLFailed;
texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; texbind.parameter(.WrapT, @intFromEnum(opts.wrap_t)) catch return error.OpenGLFailed;
texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; texbind.parameter(.MinFilter, @intFromEnum(opts.min_filter)) catch return error.OpenGLFailed;
texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; texbind.parameter(.MagFilter, @intFromEnum(opts.mag_filter)) catch return error.OpenGLFailed;
texbind.image2D( texbind.image2D(
0, 0,
opts.internal_format, opts.internal_format,

View File

@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const configpkg = @import("../config.zig");
const color = @import("color.zig"); const color = @import("color.zig");
const sgr = @import("sgr.zig"); const sgr = @import("sgr.zig");
const page = @import("page.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( pub fn fg(
self: Style, self: Style,
palette: *const color.Palette, opts: Fg,
bold_is_bright: bool, ) color.RGB {
) ?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) { return switch (self.fg_color) {
.none => null, .none => default: {
.palette => |idx| palette: { if (self.flags.bold) {
if (bold_is_bright and self.flags.bold) { if (opts.bold) |bold| switch (bold) {
const bright_offset = @intFromEnum(color.Name.bright_black); .bright => {},
if (idx < bright_offset) .color => |v| break :default v.toTerminalRGB(),
break :palette palette[idx + bright_offset]; };
} }
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,
}; };
} }