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