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
steps:
- name: Remove old artifacts
uses: c-hive/gha-remove-artifacts@v1
uses: c-hive/gha-remove-artifacts@44fc7acaf1b3d0987da0e8d4707a989d80e9554b # v1.4.0
with:
age: "1 week"
skip-tags: true

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,32 @@ import AppKit
import Cocoa
import GhosttyKit
// We put the GHOSTTY_MAC_APP env var into the Info.plist to detect
// whether we launch from the app or not. A user can fake this if
// they want but they're doing so at their own detriment...
let process = ProcessInfo.processInfo
if ((process.environment["GHOSTTY_MAC_APP"] ?? "") == "") {
ghostty_cli_main(UInt(CommandLine.argc), CommandLine.unsafeArgv)
// Initialize Ghostty global state. We do this once right away because the
// CLI APIs require it and it lets us ensure it is done immediately for the
// rest of the app.
if ghostty_init(UInt(CommandLine.argc), CommandLine.unsafeArgv) != GHOSTTY_SUCCESS {
Ghostty.logger.critical("ghostty_init failed")
// We also write to stderr if this is executed from the CLI or zig run
switch Ghostty.launchSource {
case .cli, .zig_run:
let stderrHandle = FileHandle.standardError
stderrHandle.write(
"Ghostty failed to initialize! If you're executing Ghostty from the command line\n" +
"then this is usually because an invalid action or multiple actions were specified.\n" +
"Actions start with the `+` character.\n\n" +
"View all available actions by running `ghostty +help`.\n")
exit(1)
case .app:
// For the app we exit immediately. We should handle this case more
// gracefully in the future.
exit(1)
}
}
// This will run the CLI action and exit if one was specified. A CLI
// action is a command starting with a `+`, such as `ghostty +boo`.
ghostty_cli_try_action();
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)

View File

@ -40,4 +40,34 @@ extension Ghostty.Action {
self.amount = c.amount
}
}
struct OpenURL {
enum Kind {
case unknown
case text
init(_ c: ghostty_action_open_url_kind_e) {
switch c {
case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT:
self = .text
default:
self = .unknown
}
}
}
let kind: Kind
let url: String
init(c: ghostty_action_open_url_s) {
self.kind = Kind(c.kind)
if let urlCString = c.url {
let data = Data(bytes: urlCString, count: Int(c.len))
self.url = String(data: data, encoding: .utf8) ?? ""
} else {
self.url = ""
}
}
}
}

View File

@ -45,12 +45,6 @@ extension Ghostty {
}
init() {
// Initialize ghostty global state. This happens once per process.
if ghostty_init() != GHOSTTY_SUCCESS {
logger.critical("ghostty_init failed, weird things may happen")
readiness = .error
}
// Initialize the global configuration.
self.config = Config()
if self.config.config == nil {
@ -120,9 +114,21 @@ extension Ghostty {
ghostty_app_tick(app)
}
func openConfig() {
guard let app = self.app else { return }
ghostty_app_open_config(app)
static func openConfig() {
let str = Ghostty.AllocatedString(ghostty_config_open_path()).string
guard !str.isEmpty else { return }
#if os(macOS)
let fileURL = URL(fileURLWithPath: str).absoluteString
var action = ghostty_action_open_url_s()
action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT
fileURL.withCString { cStr in
action.url = cStr
action.len = UInt(fileURL.count)
_ = openURL(action)
}
#else
fatalError("Unsupported platform for opening config file")
#endif
}
/// Reload the configuration.
@ -494,7 +500,7 @@ extension Ghostty {
pwdChanged(app, target: target, v: action.action.pwd)
case GHOSTTY_ACTION_OPEN_CONFIG:
ghostty_config_open()
openConfig()
case GHOSTTY_ACTION_FLOAT_WINDOW:
toggleFloatWindow(app, target: target, mode: action.action.float_window)
@ -553,6 +559,9 @@ extension Ghostty {
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
checkForUpdates(app)
case GHOSTTY_ACTION_OPEN_URL:
return openURL(action.action.open_url)
case GHOSTTY_ACTION_UNDO:
return undo(app, target: target)
@ -605,6 +614,34 @@ extension Ghostty {
}
}
private static func openURL(
_ v: ghostty_action_open_url_s
) -> Bool {
let action = Ghostty.Action.OpenURL(c: v)
// Convert the URL string to a URL object
guard let url = URL(string: action.url) else {
Ghostty.logger.warning("invalid URL for open URL action: \(action.url)")
return false
}
switch action.kind {
case .text:
// Open with the default text editor
if let textEditor = NSWorkspace.shared.defaultTextEditor {
NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration())
return true
}
case .unknown:
break
}
// Open with the default application for the URL
NSWorkspace.shared.open(url)
return true
}
private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
let undoManager: UndoManager?
switch (target.tag) {

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
extension Ghostty {
class AllocatedString {
private let cString: ghostty_string_s
init(_ c: ghostty_string_s) {
self.cString = c
}
var string: String {
guard let ptr = cString.ptr else { return "" }
let data = Data(bytes: ptr, count: Int(cString.len))
return String(data: data, encoding: .utf8) ?? ""
}
deinit {
ghostty_string_free(cString)
}
}
}
extension Ghostty {
enum SetFloatWIndow {
case on

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,
minisign,
pandoc,
pinact,
hyperfine,
typos,
uv,
@ -98,6 +99,7 @@ in
# Linting
nodePackages.prettier
alejandra
pinact
typos
# 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.
pub const DataType = enum(c_uint) {
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_report: bool,
links: []Link,
link_previews: configpkg.LinkPreviews,
const Link = struct {
regex: oni.Regex,
@ -336,6 +337,7 @@ const DerivedConfig = struct {
.title = config.title,
.title_report = config.@"title-report",
.links = links,
.link_previews = config.@"link-previews",
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
@ -1242,7 +1244,7 @@ fn mouseRefreshLinks(
// Get our link at the current position. This returns null if there
// isn't a link OR if we shouldn't be showing links for some reason
// (see further comments for cases).
const link_: ?apprt.action.MouseOverLink = link: {
const link_: ?apprt.action.MouseOverLink, const preview: bool = link: {
// If we clicked and our mouse moved cells then we never
// highlight links until the mouse is unclicked. This follows
// standard macOS and Linux behavior where a click and drag cancels
@ -1257,18 +1259,21 @@ fn mouseRefreshLinks(
if (!click_pt.coord().eql(pos_vp)) {
log.debug("mouse moved while left click held, ignoring link hover", .{});
break :link null;
break :link .{ null, false };
}
}
const link = (try self.linkAtPos(pos)) orelse break :link null;
const link = (try self.linkAtPos(pos)) orelse break :link .{ null, false };
switch (link[0]) {
.open => {
const str = try self.io.terminal.screen.selectionString(alloc, .{
.sel = link[1],
.trim = false,
});
break :link .{ .url = str };
break :link .{
.{ .url = str },
self.config.link_previews == .true,
};
},
._open_osc8 => {
@ -1276,9 +1281,14 @@ fn mouseRefreshLinks(
const pin = link[1].start();
const uri = self.osc8URI(pin) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{});
break :link null;
break :link .{ null, false };
};
break :link .{
.{
.url = uri,
},
self.config.link_previews != .false,
};
break :link .{ .url = uri };
},
}
};
@ -1294,11 +1304,15 @@ fn mouseRefreshLinks(
.mouse_shape,
.pointer,
);
if (preview) {
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
link,
);
}
try self.queueRender();
return;
}
@ -3710,7 +3724,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false,
});
defer self.alloc.free(str);
try internal_os.open(self.alloc, .unknown, str);
try self.openUrl(.{ .kind = .unknown, .url = str });
},
._open_osc8 => {
@ -3718,13 +3732,35 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
try internal_os.open(self.alloc, .unknown, uri);
try self.openUrl(.{ .kind = .unknown, .url = uri });
},
}
return true;
}
fn openUrl(
self: *Surface,
action: apprt.action.OpenUrl,
) !void {
// If the apprt handles it then we're done.
if (try self.rt_app.performAction(
.{ .surface = self },
.open_url,
action,
)) return;
// apprt didn't handle it, fallback to our simple cross-platform
// URL opener. We log a warning because we want well-behaved
// apprts to handle this themselves.
log.warn("apprt did not handle open URL action, falling back to default opener", .{});
try internal_os.open(
self.alloc,
action.kind,
action.url,
);
}
/// Return the URI for an OSC8 hyperlink at the given position or null
/// if there is no hyperlink.
fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
@ -4443,6 +4479,18 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return false;
},
.copy_title_to_clipboard => {
const title = self.rt_surface.getTitle() orelse return false;
if (title.len == 0) return false;
self.rt_surface.setClipboardString(title, .standard, false) catch |err| {
log.err("error copying title to clipboard err={}", .{err});
return true;
};
return true;
},
.paste_from_clipboard => try self.startClipboardRequest(
.standard,
.{ .paste = {} },
@ -4484,6 +4532,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size);
},
.set_font_size => |points| {
log.debug("set font size={d}", .{points});
var size = self.font_size;
size.points = std.math.clamp(points, 1.0, 255.0);
try self.setFontSize(size);
},
.prompt_surface_title => return try self.rt_app.performAction(
.{ .surface = self },
.prompt_title,
@ -4923,7 +4979,7 @@ fn writeScreenFile(
defer self.alloc.free(pathZ);
try self.rt_surface.setClipboardString(pathZ, .standard, false);
},
.open => try internal_os.open(self.alloc, .text, path),
.open => try self.openUrl(.{ .kind = .text, .url = path }),
.paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc,
path,

View File

@ -267,6 +267,11 @@ pub const Action = union(Key) {
check_for_updates,
/// Open a URL using the native OS mechanisms. On macOS this might be `open`
/// or on Linux this might be `xdg-open`. The exact mechanism is up to the
/// apprt.
open_url: OpenUrl,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@ -317,6 +322,7 @@ pub const Action = union(Key) {
undo,
redo,
check_for_updates,
open_url,
};
/// Sync with: ghostty_action_u
@ -357,7 +363,11 @@ pub const Action = union(Key) {
// For ABI compatibility, we expect that this is our union size.
// At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it.
assert(@sizeOf(CValue) == 16);
assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
4 => 16,
8 => 24,
else => unreachable,
});
}
/// Returns the value type for the given key.
@ -614,3 +624,44 @@ pub const ConfigChange = struct {
};
}
};
/// Open a URL
pub const OpenUrl = struct {
/// The type of data that the URL refers to.
kind: Kind,
/// The URL.
url: []const u8,
/// The type of the data at the URL to open. This is used as a hint to
/// potentially open the URL in a different way.
///
/// Sync with: ghostty_action_open_url_kind_e
pub const Kind = enum(c_int) {
/// The type is unknown. This is the default and apprts should
/// open the URL in the most generic way possible. For example,
/// on macOS this would be the equivalent of `open` or on Linux
/// this would be `xdg-open`.
unknown,
/// The URL is known to be a text file. In this case, the apprt
/// should try to open the URL in a text editor or viewer or
/// some equivalent, if possible.
text,
};
// Sync with: ghostty_action_open_url_s
pub const C = extern struct {
kind: Kind,
url: [*]const u8,
len: usize,
};
pub fn cval(self: OpenUrl) C {
return .{
.kind = self.kind,
.url = self.url.ptr,
.len = self.url.len,
};
}
};

View File

@ -884,7 +884,7 @@ pub const Surface = struct {
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_APP");
env.remove("GHOSTTY_MAC_LAUNCH_SOURCE");
// If we were launched from the desktop then we want to
// remove the LANGUAGE env var so that we don't inherit

View File

@ -496,7 +496,7 @@ pub fn performAction(
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
.goto_split => return self.gotoSplit(target, value),
.open_config => try configpkg.edit.open(self.core_app.alloc),
.open_config => return self.openConfig(),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value),
@ -519,6 +519,7 @@ pub fn performAction(
.secure_input => self.setSecureInput(target, value),
.ring_bell => try self.ringBell(target),
.toggle_command_palette => try self.toggleCommandPalette(target),
.open_url => self.openUrl(value),
// Unimplemented
.close_all_windows,
@ -1757,3 +1758,34 @@ fn initActions(self: *App) void {
action_map.addAction(action.as(gio.Action));
}
}
fn openConfig(self: *App) !bool {
// Get the config file path
const alloc = self.core_app.alloc;
const path = configpkg.edit.openPath(alloc) catch |err| {
log.warn("error getting config file path: {}", .{err});
return false;
};
defer alloc.free(path);
// Open it using openURL. "path" isn't actually a URL but
// at the time of writing that works just fine for GTK.
self.openUrl(.{ .kind = .text, .url = path });
return true;
}
fn openUrl(
app: *App,
value: apprt.action.OpenUrl,
) void {
// TODO: use https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.OpenURI.html
// Fallback to the minimal cross-platform way of opening a URL.
// This is always a safe fallback and enables for example Windows
// to open URLs (GTK on Windows via WSL is a thing).
internal_os.open(
app.core_app.alloc,
value.kind,
value.url,
) catch |err| log.warn("unable to open url: {}", .{err});
}

View File

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

View File

@ -88,6 +88,19 @@ pub fn init(
// Our step to open the resulting Ghostty app.
const open = open: {
const disable_save_state = RunStep.create(b, "disable save state");
disable_save_state.has_side_effects = true;
disable_save_state.addArgs(&.{
"/usr/libexec/PlistBuddy",
"-c",
// We'll have to change this to `Set` if we ever put this
// into our Info.plist.
"Add :NSQuitAlwaysKeepsWindows bool false",
b.fmt("{s}/Contents/Info.plist", .{app_path}),
});
disable_save_state.expectExitCode(0);
disable_save_state.step.dependOn(&build.step);
const open = RunStep.create(b, "run Ghostty app");
open.has_side_effects = true;
open.cwd = b.path("");
@ -98,22 +111,17 @@ pub fn init(
// Open depends on the app
open.step.dependOn(&build.step);
open.step.dependOn(&disable_save_state.step);
// This overrides our default behavior and forces logs to show
// up on stderr (in addition to the centralized macOS log).
open.setEnvironmentVariable("GHOSTTY_LOG", "1");
// This is hack so that we can activate the app and bring it to
// the front forcibly even though we're executing directly
// via the binary and not launch services.
open.setEnvironmentVariable("GHOSTTY_MAC_ACTIVATE", "1");
// Configure how we're launching
open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
if (b.args) |args| {
open.addArgs(args);
} else {
// This tricks the app into thinking it's running from the
// app bundle so we don't execute our CLI mode.
open.setEnvironmentVariable("GHOSTTY_MAC_APP", "1");
}
break :open open;

View File

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

View File

@ -15,8 +15,6 @@ pub const Options = struct {};
/// The `version` command is used to display information about Ghostty. Recognized as
/// either `+version` or `--version`.
pub fn run(alloc: Allocator) !u8 {
_ = alloc;
const stdout = std.io.getStdOut().writer();
const tty = std.io.getStdOut().isTty();
@ -41,6 +39,11 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print(" - renderer : {}\n", .{renderer.Renderer});
try stdout.print(" - libxev : {s}\n", .{@tagName(xev.backend)});
if (comptime build_config.app_runtime == .gtk) {
if (comptime builtin.os.tag == .linux) {
const kernel_info = internal_os.getKernelInfo(alloc);
defer if (kernel_info) |k| alloc.free(k);
try stdout.print(" - kernel version: {s}\n", .{kernel_info orelse "Kernel information unavailable"});
}
try stdout.print(" - desktop env : {s}\n", .{@tagName(internal_os.desktopEnvironment())});
try stdout.print(" - GTK version :\n", .{});
try stdout.print(" build : {}\n", .{gtk_version.comptime_version});

View File

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

View File

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

View File

@ -69,6 +69,10 @@ pub const compatibility = std.StaticStringMap(
// this behavior. This applies to selection too.
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
// Ghostty 1.2 merged `bold-is-bright` into the new `bold-color`
// by setting the value to "bright".
.{ "bold-is-bright", compatBoldIsBright },
});
/// The font families to use.
@ -435,7 +439,7 @@ pub const compatibility = std.StaticStringMap(
/// * `hinting` - Enable or disable hinting. Enabled by default.
///
/// * `force-autohint` - Always use the freetype auto-hinter instead of
/// the font's native hinter. Enabled by default.
/// the font's native hinter. Disabled by default.
///
/// * `monochrome` - Instructs renderer to use 1-bit monochrome rendering.
/// This will disable anti-aliasing, and probably not look very good unless
@ -1046,6 +1050,14 @@ link: RepeatableLink = .{},
/// `link`). If you want to customize URL matching, use `link` and disable this.
@"link-url": bool = true,
/// Show link previews for a matched URL.
///
/// When true, link previews are shown for all matched URLs. When false, link
/// previews are never shown. When set to "osc8", link previews are only shown
/// for hyperlinks created with the OSC 8 sequence (in this case, the link text
/// can differ from the link destination).
@"link-previews": LinkPreviews = .true,
/// Whether to start the window in a maximized state. This setting applies
/// to new windows and does not apply to tabs, splits, etc. However, this setting
/// will apply to all new windows, not just the first one.
@ -2815,8 +2827,24 @@ else
/// notifications using certain escape sequences such as OSC 9 or OSC 777.
@"desktop-notifications": bool = true,
/// If `true`, the bold text will use the bright color palette.
@"bold-is-bright": bool = false,
/// Modifies the color used for bold text in the terminal.
///
/// This can be set to a specific color, using the same format as
/// `background` or `foreground` (e.g. `#RRGGBB` but other formats
/// are also supported; see the aforementioned documentation). If a
/// specific color is set, this color will always be used for all
/// bold text regardless of the terminal's color scheme.
///
/// This can also be set to `bright`, which uses the bright color palette
/// for bold text. For example, if the text is red, then the bold will
/// use the bright red color. The terminal palette is set with `palette`
/// but can also be overridden by the terminal application itself using
/// escape sequences such as OSC 4. (Since Ghostty 1.2.0, the previous
/// configuration `bold-is-bright` is deprecated and replaced by this
/// usage).
///
/// Available since Ghostty 1.2.0.
@"bold-color": ?BoldColor = null,
/// This will be used to set the `TERM` environment variable.
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
@ -3921,6 +3949,23 @@ fn compatSelectionInvertFgBg(
return true;
}
fn compatBoldIsBright(
self: *Config,
alloc: Allocator,
key: []const u8,
value_: ?[]const u8,
) bool {
_ = alloc;
assert(std.mem.eql(u8, key, "bold-is-bright"));
const set = cli.args.parseBool(value_ orelse "t") catch return false;
if (set) {
self.@"bold-color" = .bright;
}
return true;
}
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`
@ -4345,6 +4390,12 @@ pub const WindowSubtitle = enum {
@"working-directory",
};
pub const LinkPreviews = enum {
false,
true,
osc8,
};
/// Color represents a color using RGB.
///
/// This is a packed struct so that the C API to read color values just
@ -4542,6 +4593,58 @@ pub const TerminalColor = union(enum) {
}
};
/// Represents color values that can be used for bold. See `bold-color`.
pub const BoldColor = union(enum) {
color: Color,
bright,
pub fn parseCLI(input_: ?[]const u8) !BoldColor {
const input = input_ orelse return error.ValueRequired;
if (std.mem.eql(u8, input, "bright")) return .bright;
return .{ .color = try Color.parseCLI(input) };
}
/// Used by Formatter
pub fn formatEntry(self: BoldColor, formatter: anytype) !void {
switch (self) {
.color => try self.color.formatEntry(formatter),
.bright => try formatter.formatEntry(
[:0]const u8,
@tagName(self),
),
}
}
test "parseCLI" {
const testing = std.testing;
try testing.expectEqual(
BoldColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } },
try BoldColor.parseCLI("#4e2a84"),
);
try testing.expectEqual(
BoldColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } },
try BoldColor.parseCLI("black"),
);
try testing.expectEqual(
BoldColor.bright,
try BoldColor.parseCLI("bright"),
);
try testing.expectError(error.InvalidValue, BoldColor.parseCLI("a"));
}
test "formatConfig" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var sc: BoldColor = .bright;
try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try testing.expectEqualSlices(u8, "a = bright\n", buf.items);
}
};
pub const ColorList = struct {
const Self = @This();
@ -6552,8 +6655,9 @@ pub const RepeatableCommand = struct {
try list.parseCLI(alloc, "title:Foo,action:ignore");
try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle");
try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5");
try list.parseCLI(alloc, "title:Baz,description:Raspberry Pie,action:set_font_size:3.14");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
try testing.expectEqual(@as(usize, 4), list.value.items.len);
try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action);
try testing.expectEqualStrings("Foo", list.value.items[0].title);
@ -6570,6 +6674,13 @@ pub const RepeatableCommand = struct {
try testing.expectEqualStrings("Quux", list.value.items[2].title);
try testing.expectEqualStrings("boo", list.value.items[2].description);
try testing.expectEqual(
inputpkg.Binding.Action{ .set_font_size = 3.14 },
list.value.items[3].action,
);
try testing.expectEqualStrings("Baz", list.value.items[3].title);
try testing.expectEqualStrings("Raspberry Pie", list.value.items[3].description);
try list.parseCLI(alloc, "");
try testing.expectEqual(@as(usize, 0), list.value.items.len);
}
@ -7105,7 +7216,7 @@ pub const FreetypeLoadFlags = packed struct {
// for Freetype itself. Ghostty hasn't made any opinionated changes
// to these defaults.
hinting: bool = true,
@"force-autohint": bool = true,
@"force-autohint": bool = false,
monochrome: bool = false,
autohint: bool = true,
};
@ -8235,3 +8346,23 @@ test "compatibility: removed selection-invert-fg-bg" {
);
}
}
test "compatibility: removed bold-is-bright" {
const testing = std.testing;
const alloc = testing.allocator;
{
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--bold-is-bright",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
try testing.expectEqual(
BoldColor.bright,
cfg.@"bold-color",
);
}
}

View File

@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig");
/// Open the configuration in the OS default editor according to the default
/// paths the main config file could be in.
/// The path to the configuration that should be opened for editing.
///
/// On Linux, this will open the file at the XDG config path. This is the
/// On Linux, this will use the file at the XDG config path. This is the
/// only valid path for Linux so we don't need to check for other paths.
///
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
/// prioritizes AppSupport over XDG, we will use AppSupport if it exists,
/// followed by XDG if it exists, and finally AppSupport if neither exist.
/// For the existence check, we also prefer non-empty files over empty
/// files.
pub fn open(alloc_gpa: Allocator) !void {
///
/// The returned value is allocated using the provided allocator.
pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
// Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void {
}
};
try internal_os.open(alloc_gpa, .text, config_path);
return try alloc_gpa.dupeZ(u8, config_path);
}
/// Returns the config path to use for open for the current OS.

View File

@ -69,9 +69,13 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
if (self.load_options) |*v| v.deinit(alloc);
}
pub const AddError = Allocator.Error || error{
pub const AddError =
Allocator.Error ||
AdjustSizeError ||
error{
CollectionFull,
DeferredLoadingUnavailable,
SetSizeFailed,
};
/// Add a face to the collection for the given style. This face will be added
@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{
/// If no error is encountered then the collection takes ownership of the face,
/// in which case face will be deallocated when the collection is deallocated.
///
/// If a loaded face is added to the collection, it should be the same
/// size as all the other faces in the collection. This function will not
/// verify or modify the size until the size of the entire collection is
/// changed.
/// If a loaded face is added to the collection, its size will be changed to
/// match the size specified in load_options, adjusted for harmonization with
/// the primary face.
pub fn add(
self: *Collection,
alloc: Allocator,
@ -103,9 +106,107 @@ pub fn add(
return error.DeferredLoadingUnavailable;
try list.append(alloc, face);
var owned: *Entry = list.at(idx);
// If the face is already loaded, apply font size adjustment
// now, otherwise we'll apply it whenever we do load it.
if (owned.getLoaded()) |loaded| {
if (try self.adjustedSize(loaded)) |opts| {
loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed;
}
}
return .{ .style = style, .idx = @intCast(idx) };
}
pub const AdjustSizeError = font.Face.GetMetricsError;
// Calculate a size for the provided face that will match it with the primary
// font, metrically, to improve consistency with fallback fonts. Right now we
// match the font based on the ex height, or the ideograph width if the font
// has ideographs in it.
//
// This returns null if load options is null or if self.load_options is null.
//
// This is very much like the `font-size-adjust` CSS property in how it works.
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
//
// TODO: In the future, provide config options that allow the user to select
// which metric should be matched for fallback fonts, instead of hard
// coding it as ex height.
pub fn adjustedSize(
self: *Collection,
face: *Face,
) AdjustSizeError!?LoadOptions {
const load_options = self.load_options orelse return null;
// We silently do nothing if we can't get the primary
// face, because this might be the primary face itself.
const primary_face = self.getFace(.{ .idx = 0 }) catch return null;
// We do nothing if the primary face and this face are the same.
if (@intFromPtr(primary_face) == @intFromPtr(face)) return null;
const primary_metrics = try primary_face.getMetrics();
const face_metrics = try face.getMetrics();
// We use the ex height to match our font sizes, so that the height of
// lower-case letters matches between all fonts in the fallback chain.
//
// We estimate ex height as 0.75 * cap height if it's not specifically
// provided, and we estimate cap height as 0.75 * ascent in the same case.
//
// If the fallback font has an ic_width we prefer that, for normalization
// of CJK font sizes when mixed with latin fonts.
//
// We estimate the ic_width as twice the cell width if it isn't provided.
var primary_cap = primary_metrics.cap_height orelse 0.0;
if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75;
var primary_ex = primary_metrics.ex_height orelse 0.0;
if (primary_ex <= 0) primary_ex = primary_cap * 0.75;
var primary_ic = primary_metrics.ic_width orelse 0.0;
if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2;
var face_cap = face_metrics.cap_height orelse 0.0;
if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75;
var face_ex = face_metrics.ex_height orelse 0.0;
if (face_ex <= 0) face_ex = face_cap * 0.75;
var face_ic = face_metrics.ic_width orelse 0.0;
if (face_ic <= 0) face_ic = face_metrics.cell_width * 2;
// If the line height of the scaled font would be larger than
// the line height of the primary font, we don't want that, so
// we take the minimum between matching the ic/ex and the line
// height.
//
// NOTE: We actually allow the line height to be up to 1.2
// times the primary line height because empirically
// this is usually fine and is better for CJK.
//
// TODO: We should probably provide a config option that lets
// the user pick what metric to use for size adjustment.
const scale = @min(
1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(),
if (face_metrics.ic_width != null)
primary_ic / face_ic
else
primary_ex / face_ex,
);
// Make a copy of our load options, set the size to the size of
// the provided face, and then multiply that by our scaling factor.
var opts = load_options;
opts.size = face.size;
opts.size.points *= @as(f32, @floatCast(scale));
return opts;
}
/// Return the Face represented by a given Index. The returned pointer
/// is only valid as long as this collection is not modified.
///
@ -129,21 +230,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
break :item item;
};
return try self.getFaceFromEntry(item);
const face = try self.getFaceFromEntry(
item,
// We only want to adjust the size if this isn't the primary face.
index.style != .regular or index.idx > 0,
);
return face;
}
/// Get the face from an entry.
///
/// This entry must not be an alias.
fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face {
fn getFaceFromEntry(
self: *Collection,
entry: *Entry,
/// Whether to adjust the font size to match the primary face after loading.
adjust: bool,
) !*Face {
assert(entry.* != .alias);
return switch (entry.*) {
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse
return error.DeferredLoadingUnavailable;
const face = try d.load(opts.library, opts.faceOptions());
var face = try d.load(opts.library, opts.faceOptions());
d.deinit();
// If we need to adjust the size, do so.
if (adjust) if (try self.adjustedSize(&face)) |new_opts| {
try face.setSize(new_opts.faceOptions());
};
entry.* = switch (tag) {
.deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face },
@ -247,7 +365,7 @@ pub fn completeStyles(
while (it.next()) |entry| {
// Load our face. If we fail to load it, we just skip it and
// continue on to try the next one.
const face = self.getFaceFromEntry(entry) catch |err| {
const face = self.getFaceFromEntry(entry, false) catch |err| {
log.warn("error loading regular entry={d} err={}", .{
it.index - 1,
err,
@ -371,7 +489,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to bold it.
const regular = try self.getFaceFromEntry(entry);
const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticBold(opts.faceOptions());
var buf: [256]u8 = undefined;
@ -391,7 +509,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it.
const regular = try self.getFaceFromEntry(entry);
const regular = try self.getFaceFromEntry(entry, false);
const face = try regular.syntheticItalic(opts.faceOptions());
var buf: [256]u8 = undefined;
@ -420,9 +538,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
while (it.next()) |array| {
var entry_it = array.value.iterator(0);
while (entry_it.next()) |entry| switch (entry.*) {
.loaded, .fallback_loaded => |*f| try f.setSize(
opts.faceOptions(),
),
.loaded,
.fallback_loaded,
=> |*f| {
const new_opts = try self.adjustedSize(f) orelse opts.*;
try f.setSize(new_opts.faceOptions());
},
// Deferred aren't loaded so we don't need to set their size.
// The size for when they're loaded is set since `opts` changed.
@ -549,6 +670,16 @@ pub const Entry = union(enum) {
}
}
/// If this face is loaded, or is an alias to a loaded face,
/// then this returns the `Face`, otherwise returns null.
pub fn getLoaded(self: *Entry) ?*Face {
return switch (self.*) {
.deferred, .fallback_deferred => null,
.loaded, .fallback_loaded => |*face| face,
.alias => |v| v.getLoaded(),
};
}
/// True if the entry is deferred.
fn isDeferred(self: Entry) bool {
return switch (self) {
@ -906,12 +1037,13 @@ test "metrics" {
var c = init();
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
c.load_options = .{ .library = lib, .size = size };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
.{ .size = size },
) });
try c.updateMetrics();
@ -958,3 +1090,62 @@ test "metrics" {
.cursor_height = 34,
}, c.metrics);
}
// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
test "adjusted sizes" {
const testing = std.testing;
const alloc = testing.allocator;
const testFont = font.embedded.inconsolata;
const fallback = font.embedded.monaspace_neon;
var lib = try Library.init(alloc);
defer lib.deinit();
var c = init();
defer c.deinit(alloc);
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
c.load_options = .{ .library = lib, .size = size };
// Add our primary face.
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
lib,
testFont,
.{ .size = size },
) });
try c.updateMetrics();
// Add the fallback face.
const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init(
lib,
fallback,
.{ .size = size },
) });
// The ex heights should match.
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
try std.testing.expectApproxEqAbs(
primary_metrics.ex_height.?,
fallback_metrics.ex_height.?,
// We accept anything within half a pixel.
0.5,
);
}
// Resize should keep that relationship.
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
try std.testing.expectApproxEqAbs(
primary_metrics.ex_height.?,
fallback_metrics.ex_height.?,
// We accept anything within half a pixel.
0.5,
);
}
}

View File

@ -17,9 +17,3 @@ offset_y: i32,
/// be normalized to be between 0 and 1 prior to use in shaders.
atlas_x: u32,
atlas_y: u32,
/// horizontal position to increase drawing position for strings
advance_x: f32,
/// Whether we drew this glyph ourselves with the sprite font.
sprite: bool = false,

View File

@ -107,6 +107,19 @@ pub const FaceMetrics = struct {
/// a provided ex height metric or measured from the height of the
/// lowercase x glyph.
ex_height: ?f64 = null,
/// The width of the character "" (CJK water ideograph, U+6C34),
/// if present. This is used for font size adjustment, to normalize
/// the width of CJK fonts mixed with latin fonts.
///
/// NOTE: IC = Ideograph Character
ic_width: ?f64 = null,
/// Convenience function for getting the line height
/// (ascent - descent + line_gap).
pub inline fn lineHeight(self: FaceMetrics) f64 {
return self.ascent - self.descent + self.line_gap;
}
};
/// Calculate our metrics based on values extracted from a font.

View File

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

View File

@ -31,6 +31,9 @@ pub const Face = struct {
/// tables).
color: ?ColorState = null,
/// The current size this font is set to.
size: font.face.DesiredSize,
/// True if our build is using Harfbuzz. If we're not, we can avoid
/// some Harfbuzz-specific code paths.
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
@ -106,6 +109,7 @@ pub const Face = struct {
.font = ct_font,
.hb_font = hb_font,
.color = color,
.size = opts.size,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@ -333,11 +337,10 @@ pub const Face = struct {
.offset_y = 0,
.atlas_x = 0,
.atlas_y = 0,
.advance_x = 0,
};
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_size = opts.constraint.constrain(
@ -349,6 +352,7 @@ pub const Face = struct {
},
cell_width,
cell_height,
opts.constraint_width,
);
const width = glyph_size.width;
@ -356,8 +360,16 @@ pub const Face = struct {
const x = glyph_size.x;
const y = glyph_size.y;
const px_width: u32 = @intFromFloat(@ceil(width));
const px_height: u32 = @intFromFloat(@ceil(height));
// We have to include the fractional pixels that we won't be offsetting
// in our width and height calculations, that is, we offset by the floor
// of the bearings when we render the glyph, meaning there's still a bit
// of extra width to the area that's drawn in beyond just the width of
// the glyph itself, so we include that extra fraction of a pixel when
// calculating the width and height here.
const frac_x = rect.origin.x - @floor(rect.origin.x);
const frac_y = rect.origin.y - @floor(rect.origin.y);
const px_width: u32 = @intFromFloat(@ceil(width + frac_x));
const px_height: u32 = @intFromFloat(@ceil(height + frac_y));
// Settings that are specific to if we are rendering text or emoji.
const color: struct {
@ -475,25 +487,43 @@ pub const Face = struct {
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
var result: i32 = @intFromFloat(@round(x));
// If our cell was resized then we adjust our glyph's
// position relative to the new center. This keeps glyphs
// centered in the cell whether it was made wider or narrower.
if (metrics.original_cell_width) |original_width| {
const before: i32 = @intCast(original_width);
const after: i32 = @intCast(metrics.cell_width);
// Increase the offset by half of the difference
// between the widths to keep things centered.
result += @divTrunc(after - before, 2);
}
break :offset_x result;
};
// Get our advance
// If the glyph's advance is narrower than the cell width then we
// center the advance of the glyph within the cell width. At first
// I implemented this to proportionally scale the center position
// of the glyph but that messes up glyphs that are meant to align
// vertically with others, so this is a compromise.
//
// This makes it so that when the `adjust-cell-width` config is
// used, or when a fallback font with a different advance width
// is used, we don't get weirdly aligned glyphs.
//
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal == .none) {
var advances: [glyphs.len]macos.graphics.Size = undefined;
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
const advance = advances[0].width;
const new_advance =
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
// If the original advance is greater than the cell width then
// it's possible that this is a ligature or other glyph that is
// intended to overflow the cell to one side or the other, and
// adjusting the bearings could mess that up, so we just leave
// it alone if that's the case.
//
// We also don't want to do anything if the advance is zero or
// less, since this is used for stuff like combining characters.
if (advance > new_advance or advance <= 0.0) {
break :offset_x @intFromFloat(@ceil(x - frac_x));
}
break :offset_x @intFromFloat(
@ceil(x - frac_x + (new_advance - advance) / 2),
);
} else {
break :offset_x @intFromFloat(@ceil(x - frac_x));
}
};
return .{
.width = px_width,
@ -502,7 +532,6 @@ pub const Face = struct {
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = @floatCast(advances[0].width),
};
}
@ -734,6 +763,20 @@ pub const Face = struct {
break :cell_width max;
};
// Measure "" (CJK water ideograph, U+6C34) for our ic width.
const ic_width: ?f64 = ic_width: {
const glyph = self.glyphIndex('水') orelse break :ic_width null;
var advances: [1]macos.graphics.Size = undefined;
_ = ct_font.getAdvancesForGlyphs(
.horizontal,
&.{@intCast(glyph)},
&advances,
);
break :ic_width advances[0].width;
};
return .{
.cell_width = cell_width,
.ascent = ascent,
@ -745,6 +788,7 @@ pub const Face = struct {
.strikethrough_thickness = strikethrough_thickness,
.cap_height = cap_height,
.ex_height = ex_height,
.ic_width = ic_width,
};
}

View File

@ -59,6 +59,9 @@ pub const Face = struct {
bold: bool = false,
} = .{},
/// The current size this font is set to.
size: font.face.DesiredSize,
/// Initialize a new font face with the given source in-memory.
pub fn initFile(
lib: Library,
@ -107,6 +110,7 @@ pub const Face = struct {
.hb_font = hb_font,
.ft_mutex = ft_mutex,
.load_flags = opts.freetype_load_flags,
.size = opts.size,
};
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
@ -203,6 +207,7 @@ pub const Face = struct {
/// for clearing any glyph caches, font atlas data, etc.
pub fn setSize(self: *Face, opts: font.face.Options) !void {
try setSize_(self.face, opts.size);
self.size = opts.size;
}
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
@ -348,7 +353,7 @@ pub const Face = struct {
// use options from config
.no_hinting = !do_hinting,
.force_autohint = !self.load_flags.@"force-autohint",
.force_autohint = self.load_flags.@"force-autohint",
.no_autohint = !self.load_flags.autohint,
// NO_SVG set to true because we don't currently support rendering
@ -373,7 +378,6 @@ pub const Face = struct {
.offset_y = 0,
.atlas_x = 0,
.atlas_y = 0,
.advance_x = 0,
};
// For synthetic bold, we embolden the glyph.
@ -390,7 +394,7 @@ pub const Face = struct {
// Next we need to apply any constraints.
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width * opts.constraint_width);
const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
@ -405,6 +409,7 @@ pub const Face = struct {
},
cell_width,
cell_height,
opts.constraint_width,
);
const width = glyph_size.width;
@ -638,20 +643,40 @@ pub const Face = struct {
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
var result: i32 = @intFromFloat(@floor(x));
// If our cell was resized then we adjust our glyph's
// position relative to the new center. This keeps glyphs
// centered in the cell whether it was made wider or narrower.
if (metrics.original_cell_width) |original_width| {
const before: i32 = @intCast(original_width);
const after: i32 = @intCast(metrics.cell_width);
// Increase the offset by half of the difference
// between the widths to keep things centered.
result += @divTrunc(after - before, 2);
// If the glyph's advance is narrower than the cell width then we
// center the advance of the glyph within the cell width. At first
// I implemented this to proportionally scale the center position
// of the glyph but that messes up glyphs that are meant to align
// vertically with others, so this is a compromise.
//
// This makes it so that when the `adjust-cell-width` config is
// used, or when a fallback font with a different advance width
// is used, we don't get weirdly aligned glyphs.
//
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal == .none) {
const advance = f26dot6ToFloat(glyph.*.advance.x);
const new_advance =
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
// If the original advance is greater than the cell width then
// it's possible that this is a ligature or other glyph that is
// intended to overflow the cell to one side or the other, and
// adjusting the bearings could mess that up, so we just leave
// it alone if that's the case.
//
// We also don't want to do anything if the advance is zero or
// less, since this is used for stuff like combining characters.
if (advance > new_advance or advance <= 0.0) {
break :offset_x @intFromFloat(@floor(x));
}
break :offset_x @intFromFloat(
@floor(x + (new_advance - advance) / 2),
);
} else {
break :offset_x @intFromFloat(@floor(x));
}
break :offset_x result;
};
return Glyph{
@ -661,7 +686,6 @@ pub const Face = struct {
.offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
.advance_x = f26dot6ToFloat(glyph.*.advance.x),
};
}
@ -832,7 +856,7 @@ pub const Face = struct {
while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = true,
.render = false,
.no_svg = true,
})) {
max = @max(
@ -870,7 +894,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock();
if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = true,
.render = false,
.no_svg = true,
})) {
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
@ -883,7 +907,7 @@ pub const Face = struct {
defer self.ft_mutex.unlock();
if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = true,
.render = false,
.no_svg = true,
})) {
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
@ -894,6 +918,21 @@ pub const Face = struct {
};
};
// Measure "" (CJK water ideograph, U+6C34) for our ic width.
const ic_width: ?f64 = ic_width: {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
const glyph = face.getCharIndex('水') orelse break :ic_width null;
face.loadGlyph(glyph, .{
.render = false,
.no_svg = true,
}) catch break :ic_width null;
break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x);
};
return .{
.cell_width = cell_width,
@ -909,6 +948,7 @@ pub const Face = struct {
.cap_height = cap_height,
.ex_height = ex_height,
.ic_width = ic_width,
};
}

View File

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

View File

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

View File

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

View File

@ -140,24 +140,7 @@ pub const Canvas = struct {
const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
// Allocate our texture atlas region
const region = region: {
// Reserve a region with a 1px margin on the bottom and right edges
// so that we can avoid interpolation between adjacent glyphs during
// texture sampling.
var region = try atlas.reserve(
alloc,
region_width + 1,
region_height + 1,
);
// Modify the region to remove the margin so that we write to the
// non-zero location. The data in an Altlas is always initialized
// to zero (Atlas.clear) so we don't need to worry about zero-ing
// that.
region.width -= 1;
region.height -= 1;
break :region region;
};
const region = try atlas.reserve(alloc, region_width, region_height);
if (region.width > 0 and region.height > 0) {
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);

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.
copy_url_to_clipboard,
/// Copy the terminal title to the clipboard. If the terminal title is not
/// set or is empty this has no effect.
copy_title_to_clipboard,
/// Increase the font size by the specified amount in points (pt).
///
/// For example, `increase_font_size:1.5` will increase the font size
@ -296,6 +300,12 @@ pub const Action = union(enum) {
/// Reset the font size to the original configured size.
reset_font_size,
/// Set the font size to the specified size in points (pt).
///
/// For example, `set_font_size:14.5` will set the font size
/// to 14.5 points.
set_font_size: f32,
/// Clear the screen and all scrollback.
clear_screen,
@ -999,11 +1009,13 @@ pub const Action = union(enum) {
.reset,
.copy_to_clipboard,
.copy_url_to_clipboard,
.copy_title_to_clipboard,
.paste_from_clipboard,
.paste_from_selection,
.increase_font_size,
.decrease_font_size,
.reset_font_size,
.set_font_size,
.prompt_surface_title,
.clear_screen,
.select_all,
@ -3065,6 +3077,7 @@ test "set: getEvent codepoint case folding" {
try testing.expect(action == null);
}
}
test "Action: clone" {
const testing = std.testing;
var arena = std.heap.ArenaAllocator.init(testing.allocator);
@ -3083,3 +3096,42 @@ test "Action: clone" {
try testing.expect(b == .text);
}
}
test "parse: increase_font_size" {
const testing = std.testing;
{
const binding = try parseSingle("a=increase_font_size:1.5");
try testing.expect(binding.action == .increase_font_size);
try testing.expectEqual(1.5, binding.action.increase_font_size);
}
}
test "parse: decrease_font_size" {
const testing = std.testing;
{
const binding = try parseSingle("a=decrease_font_size:2.5");
try testing.expect(binding.action == .decrease_font_size);
try testing.expectEqual(2.5, binding.action.decrease_font_size);
}
}
test "parse: reset_font_size" {
const testing = std.testing;
{
const binding = try parseSingle("a=reset_font_size");
try testing.expect(binding.action == .reset_font_size);
}
}
test "parse: set_font_size" {
const testing = std.testing;
{
const binding = try parseSingle("a=set_font_size:13.5");
try testing.expect(binding.action == .set_font_size);
try testing.expectEqual(13.5, binding.action.set_font_size);
}
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Action = @import("Binding.zig").Action;
@ -131,6 +132,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Copy the URL under the cursor to the clipboard.",
}},
.copy_title_to_clipboard => comptime &.{.{
.action = .copy_title_to_clipboard,
.title = "Copy Terminal Title to Clipboard",
.description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.",
}},
.paste_from_clipboard => comptime &.{.{
.action = .paste_from_clipboard,
.title = "Paste from Clipboard",
@ -460,6 +467,7 @@ fn actionCommands(action: Action.Key) []const Command {
.esc,
.text,
.cursor_key,
.set_font_size,
.scroll_page_fractional,
.scroll_page_lines,
.adjust_selection,

View File

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

View File

@ -24,8 +24,15 @@ pub fn launchedFromDesktop() bool {
// This special case is so that if we launch the app via the
// app bundle (i.e. via open) then we still treat it as if it
// was launched from the desktop.
if (build_config.artifact == .lib and
posix.getenv("GHOSTTY_MAC_APP") != null) break :macos true;
if (build_config.artifact == .lib) lib: {
const env = "GHOSTTY_MAC_LAUNCH_SOURCE";
const source = posix.getenv(env) orelse break :lib;
// Source can be "app", "cli", or "zig_run". We assume
// its the desktop only if its "app". We may want to do
// "zig_run" but at the moment there's no reason.
if (std.mem.eql(u8, source, "app")) break :macos true;
}
break :macos c.getppid() == 1;
},

View File

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

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

View File

@ -1,24 +1,23 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const apprt = @import("../apprt.zig");
const log = std.log.scoped(.@"os-open");
/// The type of the data at the URL to open. This is used as a hint
/// to potentially open the URL in a different way.
pub const Type = enum {
text,
unknown,
};
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
/// Output on stdout is ignored. The allocator is used to buffer the
/// log output and may allocate from another thread.
///
/// This function is purposely simple for the sake of providing
/// some portable way to open URLs. If you are implementing an
/// apprt for Ghostty, you should consider doing something special-cased
/// for your platform.
pub fn open(
alloc: Allocator,
typ: Type,
kind: apprt.action.OpenUrl.Kind,
url: []const u8,
) !void {
var exe: std.process.Child = switch (builtin.os.tag) {
@ -33,7 +32,7 @@ pub fn open(
),
.macos => .init(
switch (typ) {
switch (kind) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},

View File

@ -356,6 +356,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options {
.format = .rgba,
.internal_format = .srgba,
.target = .@"2D",
.min_filter = .linear,
.mag_filter = .linear,
.wrap_s = .clamp_to_edge,
.wrap_t = .clamp_to_edge,
};
}
@ -388,6 +392,16 @@ pub inline fn imageTextureOptions(
.format = format.toPixelFormat(),
.internal_format = if (srgb) .srgba else .rgba,
.target = .@"2D",
// TODO: Generate mipmaps for image textures and use
// linear_mipmap_linear filtering so that they
// look good even when scaled way down.
.min_filter = .linear,
.mag_filter = .linear,
// TODO: Separate out background image options, use
// repeating coordinate modes so we don't have
// to do the modulus in the shader.
.wrap_s = .clamp_to_edge,
.wrap_t = .clamp_to_edge,
};
}
@ -409,6 +423,10 @@ pub fn initAtlasTexture(
.format = format,
.internal_format = internal_format,
.target = .Rectangle,
.min_filter = .nearest,
.mag_filter = .nearest,
.wrap_s = .clamp_to_edge,
.wrap_t = .clamp_to_edge,
},
atlas.size,
atlas.size,

View File

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

View File

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

View File

@ -1,5 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
const configpkg = @import("../config.zig");
const color = @import("color.zig");
const sgr = @import("sgr.zig");
const page = @import("page.zig");
@ -115,24 +116,68 @@ pub const Style = struct {
};
}
/// Returns the fg color for a cell with this style given the palette.
pub const Fg = struct {
/// The default color to use if the style doesn't specify a
/// foreground color and no configuration options override
/// it.
default: color.RGB,
/// The current color palette. Required to map palette indices to
/// real color values.
palette: *const color.Palette,
/// If specified, the color to use for bold text.
bold: ?configpkg.BoldColor = null,
};
/// Returns the fg color for a cell with this style given the palette
/// and various configuration options.
pub fn fg(
self: Style,
palette: *const color.Palette,
bold_is_bright: bool,
) ?color.RGB {
opts: Fg,
) color.RGB {
// Note we don't pull the bold check to the top-level here because
// we don't want to duplicate the conditional multiple times since
// certain colors require more checks (e.g. `bold_is_bright`).
return switch (self.fg_color) {
.none => null,
.palette => |idx| palette: {
if (bold_is_bright and self.flags.bold) {
const bright_offset = @intFromEnum(color.Name.bright_black);
if (idx < bright_offset)
break :palette palette[idx + bright_offset];
.none => default: {
if (self.flags.bold) {
if (opts.bold) |bold| switch (bold) {
.bright => {},
.color => |v| break :default v.toTerminalRGB(),
};
}
break :palette palette[idx];
break :default opts.default;
},
.palette => |idx| palette: {
if (self.flags.bold) {
if (opts.bold) |bold| switch (bold) {
.color => |v| break :palette v.toTerminalRGB(),
.bright => {
const bright_offset = @intFromEnum(color.Name.bright_black);
if (idx < bright_offset) {
break :palette opts.palette[idx + bright_offset];
}
},
};
}
break :palette opts.palette[idx];
},
.rgb => |rgb| rgb: {
if (self.flags.bold and rgb.eql(opts.default)) {
if (opts.bold) |bold| switch (bold) {
.color => |v| break :rgb v.toTerminalRGB(),
.bright => {},
};
}
break :rgb rgb;
},
.rgb => |rgb| rgb,
};
}