Merge and fix conflict on README

This commit is contained in:
David
2025-12-03 22:26:07 +01:00
285 changed files with 9550 additions and 3433 deletions

View File

@@ -34,7 +34,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
@@ -42,7 +42,7 @@ jobs:
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16

View File

@@ -56,7 +56,7 @@ jobs:
fi
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Important so that build number generation works
fetch-depth: 0
@@ -80,7 +80,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -89,7 +89,7 @@ jobs:
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
@@ -132,7 +132,7 @@ jobs:
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: DeterminateSystems/nix-installer-action@main
with:
@@ -306,7 +306,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download macOS Artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0

View File

@@ -29,11 +29,11 @@ jobs:
commit: ${{ steps.extract_build_info.outputs.commit }}
commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Important so that build number generation works
fetch-depth: 0
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -66,7 +66,7 @@ jobs:
needs: [setup, build-macos]
if: needs.setup.outputs.should_skip != 'true'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Tip Tag
run: |
git config user.name "github-actions[bot]"
@@ -81,7 +81,7 @@ jobs:
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli
run: |
@@ -104,7 +104,7 @@ jobs:
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli
run: |
@@ -127,7 +127,7 @@ jobs:
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli
run: |
@@ -159,14 +159,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -186,7 +186,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@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -217,7 +217,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Important so that build number generation works
fetch-depth: 0
@@ -356,7 +356,7 @@ jobs:
# Update Release
- name: Release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -451,7 +451,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Important so that build number generation works
fetch-depth: 0
@@ -583,7 +583,7 @@ jobs:
# Update Release
- name: Release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true
@@ -635,7 +635,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Important so that build number generation works
fetch-depth: 0
@@ -767,7 +767,7 @@ jobs:
# Update Release
- name: Release
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
name: 'Ghostty Tip ("Nightly")'
prerelease: true

View File

@@ -69,7 +69,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -79,7 +79,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -112,7 +112,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -122,7 +122,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -145,7 +145,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -155,7 +155,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -179,7 +179,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -189,7 +189,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -222,7 +222,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -232,7 +232,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -258,7 +258,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -268,7 +268,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -287,7 +287,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -297,7 +297,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -320,7 +320,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -330,7 +330,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -366,7 +366,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -376,7 +376,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -404,7 +404,7 @@ jobs:
needs: [build-dist, build-snap]
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Trigger Snap workflow
run: |
@@ -421,7 +421,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
@@ -464,7 +464,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
@@ -509,7 +509,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# This could be from a script if we wanted to but inlining here for now
# in one place.
@@ -580,7 +580,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get required Zig version
id: zig
@@ -595,7 +595,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -627,7 +627,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -637,7 +637,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -675,7 +675,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -685,7 +685,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -710,7 +710,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -720,7 +720,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -737,7 +737,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
@@ -774,7 +774,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -784,7 +784,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -804,14 +804,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -832,14 +832,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -859,14 +859,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -886,14 +886,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -913,14 +913,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -940,14 +940,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -974,14 +974,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -1001,14 +1001,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -1035,7 +1035,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -1045,7 +1045,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -1104,7 +1104,7 @@ jobs:
runs-on: ${{ matrix.variant.runner }}
needs: test
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
with:
bundle: com.mitchellh.ghostty
@@ -1123,7 +1123,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
@@ -1133,7 +1133,7 @@ jobs:
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -1162,7 +1162,7 @@ jobs:
# timeout-minutes: 10
# steps:
# - name: Checkout Ghostty
# uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
#
# - name: Start SSH
# run: |

View File

@@ -17,7 +17,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
@@ -29,7 +29,7 @@ jobs:
/zig
- name: Setup Nix
uses: cachix/install-nix-action@7ec16f2c061ab07b235a7245e06ed46fe9a1cab6 # v31.8.3
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -62,7 +62,7 @@ jobs:
run: nix build .#ghostty
- name: Create pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
title: Update iTerm2 colorschemes
base: main

View File

@@ -55,7 +55,7 @@ pub fn build(b: *std.Build) !void {
);
// Ghostty resources like terminfo, shell integration, themes, etc.
const resources = try buildpkg.GhosttyResources.init(b, &config);
const resources = try buildpkg.GhosttyResources.init(b, &config, &deps);
const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
// Ghostty executable, the actual runnable Ghostty program.

View File

@@ -38,9 +38,9 @@
.lazy = true,
},
.uucode = .{
// TODO: currently the use-llvm branch because its broken on self-hosted
.url = "https://github.com/jacobsandlund/uucode/archive/b309dfb4e25a38201d7b300b201a698e02283862.tar.gz",
.hash = "uucode-0.1.0-ZZjBPl_mQwDHQowHOzXwgTPhAqcFekfYpAeUfeHZNCl3",
// jacobsandlund/uucode
.url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
},
.zig_wayland = .{
// codeberg ifreund/zig-wayland
@@ -116,7 +116,7 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251110-150531-d5f3d53/ghostty-themes.tgz",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251124-150533-2b326a8/ghostty-themes.tgz",
.hash = "N-V-__8AAPZCAwDJ0OsIn2nbr3FMvBw68oiv-hC2pFuY1eLN",
.lazy = true,
},

10
build.zig.zon.json generated
View File

@@ -51,8 +51,8 @@
},
"N-V-__8AAPZCAwDJ0OsIn2nbr3FMvBw68oiv-hC2pFuY1eLN": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251110-150531-d5f3d53/ghostty-themes.tgz",
"hash": "sha256-VZq3L/cAAu7kLA5oqJYNjAZApoblfBtAzfdKVOuJPQI="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251124-150533-2b326a8/ghostty-themes.tgz",
"hash": "sha256-5mmXW7d9SkesHyIwUBlWmyGtOWf6wu0S6zkHe93FVLM="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
@@ -114,10 +114,10 @@
"url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
"hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="
},
"uucode-0.1.0-ZZjBPl_mQwDHQowHOzXwgTPhAqcFekfYpAeUfeHZNCl3": {
"uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": {
"name": "uucode",
"url": "https://github.com/jacobsandlund/uucode/archive/b309dfb4e25a38201d7b300b201a698e02283862.tar.gz",
"hash": "sha256-jvko1MdWr1OG4P58KjdB1JMnWy4EbrO3xIkV8fiQkC0="
"url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="
},
"vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
"name": "vaxis",

10
build.zig.zon.nix generated
View File

@@ -166,8 +166,8 @@ in
name = "N-V-__8AAPZCAwDJ0OsIn2nbr3FMvBw68oiv-hC2pFuY1eLN";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251110-150531-d5f3d53/ghostty-themes.tgz";
hash = "sha256-VZq3L/cAAu7kLA5oqJYNjAZApoblfBtAzfdKVOuJPQI=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251124-150533-2b326a8/ghostty-themes.tgz";
hash = "sha256-5mmXW7d9SkesHyIwUBlWmyGtOWf6wu0S6zkHe93FVLM=";
};
}
{
@@ -267,11 +267,11 @@ in
};
}
{
name = "uucode-0.1.0-ZZjBPl_mQwDHQowHOzXwgTPhAqcFekfYpAeUfeHZNCl3";
name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E";
path = fetchZigArtifact {
name = "uucode";
url = "https://github.com/jacobsandlund/uucode/archive/b309dfb4e25a38201d7b300b201a698e02283862.tar.gz";
hash = "sha256-jvko1MdWr1OG4P58KjdB1JMnWy4EbrO3xIkV8fiQkC0=";
url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=";
};
}
{

4
build.zig.zon.txt generated
View File

@@ -28,8 +28,8 @@ https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
https://github.com/jacobsandlund/uucode/archive/b309dfb4e25a38201d7b300b201a698e02283862.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251110-150531-d5f3d53/ghostty-themes.tgz
https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251124-150533-2b326a8/ghostty-themes.tgz
https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz

View File

@@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251110-150531-d5f3d53/ghostty-themes.tgz",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251124-150533-2b326a8/ghostty-themes.tgz",
"dest": "vendor/p/N-V-__8AAPZCAwDJ0OsIn2nbr3FMvBw68oiv-hC2pFuY1eLN",
"sha256": "559ab72ff70002eee42c0e68a8960d8c0640a686e57c1b40cdf74a54eb893d02"
"sha256": "e669975bb77d4a47ac1f22305019569b21ad3967fac2ed12eb39077bddc554b3"
},
{
"type": "archive",
@@ -139,9 +139,9 @@
},
{
"type": "archive",
"url": "https://github.com/jacobsandlund/uucode/archive/b309dfb4e25a38201d7b300b201a698e02283862.tar.gz",
"dest": "vendor/p/uucode-0.1.0-ZZjBPl_mQwDHQowHOzXwgTPhAqcFekfYpAeUfeHZNCl3",
"sha256": "8ef928d4c756af5386e0fe7c2a3741d493275b2e046eb3b7c48915f1f890902d"
"url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
"sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e"
},
{
"type": "archive",

View File

@@ -747,6 +747,21 @@ typedef struct {
uint64_t duration;
} ghostty_action_command_finished_s;
// apprt.action.StartSearch.C
typedef struct {
const char* needle;
} ghostty_action_start_search_s;
// apprt.action.SearchTotal
typedef struct {
ssize_t total;
} ghostty_action_search_total_s;
// apprt.action.SearchSelected
typedef struct {
ssize_t selected;
} ghostty_action_search_selected_s;
// terminal.Scrollbar
typedef struct {
uint64_t total;
@@ -811,6 +826,10 @@ typedef enum {
GHOSTTY_ACTION_PROGRESS_REPORT,
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
GHOSTTY_ACTION_COMMAND_FINISHED,
GHOSTTY_ACTION_START_SEARCH,
GHOSTTY_ACTION_END_SEARCH,
GHOSTTY_ACTION_SEARCH_TOTAL,
GHOSTTY_ACTION_SEARCH_SELECTED,
} ghostty_action_tag_e;
typedef union {
@@ -844,6 +863,9 @@ typedef union {
ghostty_surface_message_childexited_s child_exited;
ghostty_action_progress_report_s progress_report;
ghostty_action_command_finished_s command_finished;
ghostty_action_start_search_s start_search;
ghostty_action_search_total_s search_total;
ghostty_action_search_selected_s search_selected;
} ghostty_action_u;
typedef struct {

View File

@@ -44,6 +44,11 @@ class AppDelegate: NSObject,
@IBOutlet private var menuPaste: NSMenuItem?
@IBOutlet private var menuPasteSelection: NSMenuItem?
@IBOutlet private var menuSelectAll: NSMenuItem?
@IBOutlet private var menuFindParent: NSMenuItem?
@IBOutlet private var menuFind: NSMenuItem?
@IBOutlet private var menuFindNext: NSMenuItem?
@IBOutlet private var menuFindPrevious: NSMenuItem?
@IBOutlet private var menuHideFindBar: NSMenuItem?
@IBOutlet private var menuToggleVisibility: NSMenuItem?
@IBOutlet private var menuToggleFullScreen: NSMenuItem?
@@ -553,6 +558,7 @@ class AppDelegate: NSObject,
self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line")
self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line")
self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square")
self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass")
}
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
@@ -581,6 +587,9 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind)
syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext)
syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious)
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
@@ -885,12 +894,17 @@ class AppDelegate: NSObject,
NSApplication.shared.appearance = .init(ghosttyConfig: config)
}
@concurrent
// Using AppIconActor to ensure this work
// happens synchronously in the background
@AppIconActor
private func updateAppIcon(from config: Ghostty.Config) async {
var appIcon: NSImage?
var appIconName: String? = config.macosIcon.rawValue
switch (config.macosIcon) {
case .official:
// Discard saved icon name
appIconName = nil
break
case .blueprint:
appIcon = NSImage(named: "BlueprintImage")!
@@ -919,10 +933,15 @@ class AppDelegate: NSObject,
case .custom:
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
appIcon = userIcon
appIconName = config.macosCustomIcon
} else {
appIcon = nil // Revert back to official icon if invalid location
appIconName = nil // Discard saved icon name
}
case .customStyle:
// Discard saved icon name
// if no valid colours were found
appIconName = nil
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
guard let icon = ColorizedGhosttyIcon(
@@ -931,6 +950,24 @@ class AppDelegate: NSObject,
frame: config.macosIconFrame
).makeImage() else { break }
appIcon = icon
let colorStrings = ([ghostColor] + screenColors).compactMap(\.hexString)
appIconName = (colorStrings + [config.macosIconFrame.rawValue])
.joined(separator: "_")
}
// Only change the icon if it has actually changed
// from the current one
guard UserDefaults.standard.string(forKey: "CustomGhosttyIcon") != appIconName else {
#if DEBUG
if appIcon == nil {
await MainActor.run {
// Changing the app bundle's icon will corrupt code signing.
// We only use the default blueprint icon for the dock,
// so developers don't need to clean and re-build every time.
NSApplication.shared.applicationIconImage = NSImage(named: "BlueprintImage")
}
}
#endif
return
}
// make it immutable, so Swift 6 won't complain
let newIcon = appIcon
@@ -941,16 +978,9 @@ class AppDelegate: NSObject,
await MainActor.run {
self.appIcon = newIcon
#if DEBUG
// if no custom icon specified, we use blueprint to distinguish from release app
NSApplication.shared.applicationIconImage = newIcon ?? NSImage(named: "BlueprintImage")
// Changing the app bundle's icon will corrupt code signing.
// We only use the default blueprint icon for the dock,
// so developers don't need to clean and re-build every time.
#else
NSApplication.shared.applicationIconImage = newIcon
#endif
}
UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon")
}
//MARK: - Restorable State
@@ -1154,10 +1184,19 @@ class AppDelegate: NSObject,
// want to bring back these windows if we remove the toggle.
//
// We also ignore fullscreen windows because they don't hide anyways.
self.hiddenWindows = NSApp.windows.filter {
var visibleWindows = [Weak<NSWindow>]()
NSApp.windows.filter {
$0.isVisible &&
!$0.styleMask.contains(.fullScreen)
}.map { Weak($0) }
}.forEach { window in
// We only keep track of selectedWindow if it's in a tabGroup,
// so we can keep its selection state when restoring
let windowToHide = window.tabGroup?.selectedWindow ?? window
if !visibleWindows.contains(where: { $0.value === windowToHide }) {
visibleWindows.append(Weak(windowToHide))
}
}
self.hiddenWindows = visibleWindows
}
func restore() {
@@ -1229,3 +1268,8 @@ extension AppDelegate: NSMenuItemValidation {
}
}
}
@globalActor
fileprivate actor AppIconActor: GlobalActor {
static let shared = AppIconActor()
}

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@@ -26,7 +26,12 @@
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/>
<outlet property="menuFind" destination="nwE-0w-30h" id="idg-Nc-apE"/>
<outlet property="menuFindNext" destination="XqU-X8-q32" id="vNh-AH-6gZ"/>
<outlet property="menuFindParent" destination="cE3-Bt-FcH" id="2dc-ok-hgH"/>
<outlet property="menuFindPrevious" destination="1hd-2Z-wVm" id="sSo-wO-2MW"/>
<outlet property="menuFloatOnTop" destination="uRj-7z-1Nh" id="94n-o9-Jol"/>
<outlet property="menuHideFindBar" destination="xzC-AG-HAc" id="HCo-o6-VWv"/>
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
@@ -245,6 +250,39 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VYS-RG-uZD"/>
<menuItem title="Find" id="cE3-Bt-FcH">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Find" id="vPo-Sd-cTP">
<items>
<menuItem title="Find..." id="nwE-0w-30h">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="find:" target="-1" id="PeY-3u-IxC"/>
</connections>
</menuItem>
<menuItem title="Find Next" id="XqU-X8-q32">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="findNext:" target="-1" id="Dka-ng-aSs"/>
</connections>
</menuItem>
<menuItem title="Find Previous" id="1hd-2Z-wVm">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="findPrevious:" target="-1" id="Zvs-bs-ZR4"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="KlV-2C-wYr"/>
<menuItem title="Hide Find Bar" id="xzC-AG-HAc">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="findHide:" target="-1" id="hGP-K9-yN9"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="Xbz-ms-irt"/>
</items>
</menu>
</menuItem>

View File

@@ -44,6 +44,7 @@ struct CommandPaletteView: View {
@State private var query = ""
@State private var selectedIndex: UInt?
@State private var hoveredOptionID: UUID?
@FocusState private var isTextFieldFocused: Bool
// The options that we should show, taking into account any filtering from
// the query.
@@ -72,7 +73,7 @@ struct CommandPaletteView: View {
}
VStack(alignment: .leading, spacing: 0) {
CommandPaletteQuery(query: $query) { event in
CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in
switch (event) {
case .exit:
isPresented = false
@@ -144,6 +145,28 @@ struct CommandPaletteView: View {
.shadow(radius: 32, x: 0, y: 12)
.padding()
.environment(\.colorScheme, scheme)
.onChange(of: isPresented) { newValue in
// Reset focus when quickly showing and hiding.
// macOS will destroy this view after a while,
// so task/onAppear will not be called again.
// If you toggle it rather quickly, we reset
// it here when dismissing.
isTextFieldFocused = newValue
if !isPresented {
// This is optional, since most of the time
// there will be a delay before the next use.
// To keep behavior the same as before, we reset it.
query = ""
}
}
.task {
// Grab focus on the first appearance.
// This happens right after onAppear,
// so we dont need to dispatch it again.
// Fixes: https://github.com/ghostty-org/ghostty/issues/8497
// Also fixes initial focus while animating.
isTextFieldFocused = isPresented
}
}
}
@@ -153,6 +176,12 @@ fileprivate struct CommandPaletteQuery: View {
var onEvent: ((KeyboardEvent) -> Void)? = nil
@FocusState private var isTextFieldFocused: Bool
init(query: Binding<String>, isTextFieldFocused: FocusState<Bool>, onEvent: ((KeyboardEvent) -> Void)? = nil) {
_query = query
self.onEvent = onEvent
_isTextFieldFocused = isTextFieldFocused
}
enum KeyboardEvent {
case exit
case submit
@@ -185,14 +214,6 @@ fileprivate struct CommandPaletteQuery: View {
.frame(height: 48)
.textFieldStyle(.plain)
.focused($isTextFieldFocused)
.onAppear {
// We want to grab focus on appearance. We have to do this after a tick
// on macOS Tahoe otherwise this doesn't work. See:
// https://github.com/ghostty-org/ghostty/issues/8497
DispatchQueue.main.async {
isTextFieldFocused = true
}
}
.onChange(of: isTextFieldFocused) { focused in
if !focused {
onEvent?(.exit)

View File

@@ -90,19 +90,19 @@ struct TerminalCommandPaletteView: View {
backgroundColor: ghosttyConfig.backgroundColor,
options: commandOptions
)
.transition(
.move(edge: .top)
.combined(with: .opacity)
.animation(.spring(response: 0.4, dampingFraction: 0.8))
) // Spring animation
.zIndex(1) // Ensure it's on top
Spacer()
}
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
}
.transition(
.move(edge: .top)
.combined(with: .opacity)
)
}
}
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: isPresented)
.onChange(of: isPresented) { newValue in
// When the command palette disappears we need to send focus back to the
// surface view we were overlaid on top of. There's probably a better way

View File

@@ -342,7 +342,10 @@ class QuickTerminalController: BaseTerminalController {
// animate out.
if surfaceTree.isEmpty,
let ghostty_app = ghostty.app {
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil)
var config = Ghostty.SurfaceConfiguration()
config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1"
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
surfaceTree = SplitTree(view: view)
focusedSurface = view
}

View File

@@ -1112,6 +1112,22 @@ class BaseTerminalController: NSWindowController,
@IBAction func toggleCommandPalette(_ sender: Any?) {
commandPaletteIsShowing.toggle()
}
@IBAction func find(_ sender: Any) {
focusedSurface?.find(sender)
}
@IBAction func findNext(_ sender: Any) {
focusedSurface?.findNext(sender)
}
@IBAction func findPrevious(_ sender: Any) {
focusedSurface?.findNext(sender)
}
@IBAction func findHide(_ sender: Any) {
focusedSurface?.findHide(sender)
}
@objc func resetTerminal(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
@@ -1136,3 +1152,15 @@ class BaseTerminalController: NSWindowController,
}
}
}
extension BaseTerminalController: NSMenuItemValidation {
func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action {
case #selector(findHide):
return focusedSurface?.searchState != nil
default:
return true
}
}
}

View File

@@ -508,55 +508,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
window.syncAppearance(surfaceConfig)
}
/// Returns the default size of the window. This is contextual based on the focused surface because
/// the focused surface may specify a different default size than others.
private var defaultSize: NSRect? {
guard let screen = window?.screen ?? NSScreen.main else { return nil }
if derivedConfig.maximize {
return screen.visibleFrame
} else if let focusedSurface,
let initialSize = focusedSurface.initialSize {
// Get the current frame of the window
guard var frame = window?.frame else { return nil }
// Calculate the chrome size (window size minus view size)
let chromeWidth = frame.size.width - focusedSurface.frame.size.width
let chromeHeight = frame.size.height - focusedSurface.frame.size.height
// Calculate the new width and height, clamping to the screen's size
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
// Update the frame size while keeping the window's position intact
frame.size.width = newWidth
frame.size.height = newHeight
// Ensure the window doesn't go outside the screen boundaries
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
return adjustForWindowPosition(frame: frame, on: screen)
}
guard let initialFrame else { return nil }
guard var frame = window?.frame else { return nil }
// Calculate the new width and height, clamping to the screen's size
let newWidth = min(initialFrame.size.width, screen.visibleFrame.width)
let newHeight = min(initialFrame.size.height, screen.visibleFrame.height)
// Update the frame size while keeping the window's position intact
frame.size.width = newWidth
frame.size.height = newHeight
// Ensure the window doesn't go outside the screen boundaries
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
return adjustForWindowPosition(frame: frame, on: screen)
}
/// Adjusts the given frame for the configured window position.
func adjustForWindowPosition(frame: NSRect, on screen: NSScreen) -> NSRect {
guard let x = derivedConfig.windowPositionX else { return frame }
@@ -922,9 +873,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
super.windowDidLoad()
guard let window else { return }
// Store our initial frame so we can know our default later.
initialFrame = window.frame
// I copy this because we may change the source in the future but also because
// I regularly audit our codebase for "ghostty.config" access because generally
// you shouldn't use it. Its safe in this case because for a new window we should
@@ -944,19 +892,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// If this is our first surface then our focused surface will be nil
// so we force the focused surface to the leaf.
focusedSurface = view
if let defaultSize {
window.setFrame(defaultSize, display: true)
}
}
// Initialize our content view to the SwiftUI root
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
delegate: self,
))
// If we have a default size, we want to apply it.
if let defaultSize {
switch (defaultSize) {
case .frame:
// Frames can be applied immediately
defaultSize.apply(to: window)
case .contentIntrinsicSize:
// Content intrinsic size requires a short delay so that AppKit
// can layout our SwiftUI views.
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak window] in
guard let window else { return }
defaultSize.apply(to: window)
}
}
}
// Store our initial frame so we can know our default later. This MUST
// be after the defaultSize call above so that we don't re-apply our frame.
// Note: we probably want to set this on the first frame change or something
// so it respects cascade.
initialFrame = window.frame
// In various situations, macOS automatically tabs new windows. Ghostty handles
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it.
@@ -1144,8 +1111,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
@IBAction func returnToDefaultSize(_ sender: Any?) {
guard let defaultSize else { return }
window?.setFrame(defaultSize, display: true)
guard let window, let defaultSize else { return }
defaultSize.apply(to: window)
}
@IBAction override func closeWindow(_ sender: Any?) {
@@ -1403,8 +1370,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// MARK: NSMenuItemValidation
extension TerminalController: NSMenuItemValidation {
func validateMenuItem(_ item: NSMenuItem) -> Bool {
extension TerminalController {
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action {
case #selector(returnToDefaultSize):
guard let window else { return false }
@@ -1421,19 +1388,68 @@ extension TerminalController: NSMenuItemValidation {
// If our window is already the default size or we don't have a
// default size, then disable.
guard let defaultSize,
window.frame.size != .init(
width: defaultSize.size.width,
height: defaultSize.size.height
)
else {
return false
}
return true
return defaultSize?.isChanged(for: window) ?? false
default:
return true
return super.validateMenuItem(item)
}
}
}
// MARK: Default Size
extension TerminalController {
/// The possible default sizes for a terminal. The size can't purely be known as a
/// window frame because if we set `window-width/height` then it is based
/// on content size.
enum DefaultSize {
/// A frame, set with `window.setFrame`
case frame(NSRect)
/// A content size, set with `window.setContentSize`
case contentIntrinsicSize
func isChanged(for window: NSWindow) -> Bool {
switch self {
case .frame(let rect):
return window.frame != rect
case .contentIntrinsicSize:
guard let view = window.contentView else {
return false
}
return view.frame.size != view.intrinsicContentSize
}
}
func apply(to window: NSWindow) {
switch self {
case .frame(let rect):
window.setFrame(rect, display: true)
case .contentIntrinsicSize:
guard let size = window.contentView?.intrinsicContentSize else {
return
}
window.setContentSize(size)
window.constrainToScreen()
}
}
}
private var defaultSize: DefaultSize? {
if derivedConfig.maximize, let screen = window?.screen ?? NSScreen.main {
// Maximize takes priority, we take up the full screen we're on.
return .frame(screen.visibleFrame)
} else if focusedSurface?.initialSize != nil {
// Initial size as requested by the configuration (e.g. `window-width`)
// takes next priority.
return .contentIntrinsicSize
} else if let initialFrame {
// The initial frame we had when we started otherwise.
return .frame(initialFrame)
} else {
return nil
}
}
}

View File

@@ -45,7 +45,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// An optional delegate to receive information about terminal changes.
weak var delegate: (any TerminalViewDelegate)? = nil
// The most recently focused surface, equal to focusedSurface when
// it is non-nil.
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init()
@@ -100,6 +100,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
guard let size = newValue else { return }
self.delegate?.cellSizeDidChange(to: size)
}
.frame(idealWidth: lastFocusedSurface.value?.initialSize?.width,
idealHeight: lastFocusedSurface.value?.initialSize?.height)
}
// Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style
.ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])

View File

@@ -115,6 +115,18 @@ extension Ghostty.Action {
len = c.len
}
}
struct StartSearch {
let needle: String?
init(c: ghostty_action_start_search_s) {
if let needleCString = c.needle {
self.needle = String(cString: needleCString)
} else {
self.needle = nil
}
}
}
}
// Putting the initializer in an extension preserves the automatic one.

View File

@@ -180,14 +180,14 @@ extension Ghostty {
func newTab(surface: ghostty_surface_t) {
let action = "new_tab"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
func newWindow(surface: ghostty_surface_t) {
let action = "new_window"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
@@ -210,14 +210,14 @@ extension Ghostty {
func splitToggleZoom(surface: ghostty_surface_t) {
let action = "toggle_split_zoom"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
func toggleFullscreen(surface: ghostty_surface_t) {
let action = "toggle_fullscreen"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
@@ -238,21 +238,21 @@ extension Ghostty {
case .reset:
action = "reset_font_size"
}
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
func toggleTerminalInspector(surface: ghostty_surface_t) {
let action = "inspector:toggle"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
func resetTerminal(surface: ghostty_surface_t) {
let action = "reset"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
logger.warning("action failed action=\(action)")
}
}
@@ -606,6 +606,18 @@ extension Ghostty {
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
closeAllWindows(app, target: target)
case GHOSTTY_ACTION_START_SEARCH:
startSearch(app, target: target, v: action.action.start_search)
case GHOSTTY_ACTION_END_SEARCH:
endSearch(app, target: target)
case GHOSTTY_ACTION_SEARCH_TOTAL:
searchTotal(app, target: target, v: action.action.search_total)
case GHOSTTY_ACTION_SEARCH_SELECTED:
searchSelected(app, target: target, v: action.action.search_selected)
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
fallthrough
case GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS:
@@ -1641,6 +1653,100 @@ extension Ghostty {
}
}
private static func startSearch(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_start_search_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("start_search does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
let startSearch = Ghostty.Action.StartSearch(c: v)
DispatchQueue.main.async {
if surfaceView.searchState != nil {
NotificationCenter.default.post(name: .ghosttySearchFocus, object: surfaceView)
} else {
surfaceView.searchState = Ghostty.SurfaceView.SearchState(from: startSearch)
}
}
default:
assertionFailure()
}
}
private static func endSearch(
_ app: ghostty_app_t,
target: ghostty_target_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("end_search does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
DispatchQueue.main.async {
surfaceView.searchState = nil
}
default:
assertionFailure()
}
}
private static func searchTotal(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_search_total_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("search_total does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
let total: UInt? = v.total >= 0 ? UInt(v.total) : nil
DispatchQueue.main.async {
surfaceView.searchState?.total = total
}
default:
assertionFailure()
}
}
private static func searchSelected(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_search_selected_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("search_selected does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
let selected: UInt? = v.selected >= 0 ? UInt(v.selected) : nil
DispatchQueue.main.async {
surfaceView.searchState?.selected = selected
}
default:
assertionFailure()
}
}
private static func configReload(
_ app: ghostty_app_t,
target: ghostty_target_s,

View File

@@ -105,7 +105,7 @@ extension Ghostty {
func keyboardShortcut(for action: String) -> KeyboardShortcut? {
guard let cfg = self.config else { return nil }
let trigger = ghostty_config_trigger(cfg, action, UInt(action.count))
let trigger = ghostty_config_trigger(cfg, action, UInt(action.lengthOfBytes(using: .utf8)))
return Ghostty.keyboardShortcut(for: trigger)
}
#endif
@@ -120,7 +120,7 @@ extension Ghostty {
guard let config = self.config else { return .init() }
var v: CUnsignedInt = 0
let key = "bell-features"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .init() }
return .init(rawValue: v)
}
@@ -128,7 +128,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = true;
let key = "initial-window"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -136,7 +136,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false;
let key = "quit-after-last-window-closed"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -144,7 +144,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "title"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
@@ -153,7 +153,7 @@ extension Ghostty {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-save-state"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
@@ -162,21 +162,21 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: Int16 = 0
let key = "window-position-x"
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
return ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) ? v : nil
}
var windowPositionY: Int16? {
guard let config = self.config else { return nil }
var v: Int16 = 0
let key = "window-position-y"
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
return ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) ? v : nil
}
var windowNewTabPosition: String {
guard let config = self.config else { return "" }
var v: UnsafePointer<Int8>? = nil
let key = "window-new-tab-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
}
@@ -186,7 +186,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "window-decoration"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
@@ -196,7 +196,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-theme"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
@@ -205,7 +205,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false
let key = "window-step-resize"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -213,7 +213,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false
let key = "fullscreen"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -223,7 +223,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-non-native-fullscreen"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return switch str {
@@ -245,7 +245,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "window-title-font-family"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
guard let ptr = v else { return nil }
return String(cString: ptr)
}
@@ -255,7 +255,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-window-buttons"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSWindowButtons(rawValue: str) ?? defaultValue
@@ -266,7 +266,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-titlebar-style"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
return String(cString: ptr)
}
@@ -276,7 +276,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-titlebar-proxy-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSTitlebarProxyIcon(rawValue: str) ?? defaultValue
@@ -287,7 +287,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-dock-drop-behavior"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacDockDropBehavior(rawValue: str) ?? defaultValue
@@ -297,7 +297,7 @@ extension Ghostty {
guard let config = self.config else { return false }
var v = false;
let key = "macos-window-shadow"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -306,7 +306,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSIcon(rawValue: str) ?? defaultValue
@@ -318,7 +318,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-custom-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
guard let path = NSString(utf8String: ptr) else { return defaultValue }
return path.expandingTildeInPath
@@ -332,7 +332,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-icon-frame"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSIconFrame(rawValue: str) ?? defaultValue
@@ -342,7 +342,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: ghostty_config_color_s = .init()
let key = "macos-icon-ghost-color"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
return .init(ghostty: v)
}
@@ -350,7 +350,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: ghostty_config_color_list_s = .init()
let key = "macos-icon-screen-color"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
guard v.len > 0 else { return nil }
let buffer = UnsafeBufferPointer(start: v.colors, count: v.len)
return buffer.map { .init(ghostty: $0) }
@@ -360,7 +360,7 @@ extension Ghostty {
guard let config = self.config else { return .never }
var v: UnsafePointer<Int8>? = nil
let key = "macos-hidden"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .never }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .never }
guard let ptr = v else { return .never }
let str = String(cString: ptr)
return MacHidden(rawValue: str) ?? .never
@@ -370,14 +370,14 @@ extension Ghostty {
guard let config = self.config else { return false }
var v = false;
let key = "focus-follows-mouse"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
var backgroundColor: Color {
var color: ghostty_config_color_s = .init();
let bg_key = "background"
if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.count))) {
if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8)))) {
#if os(macOS)
return Color(NSColor.windowBackgroundColor)
#elseif os(iOS)
@@ -398,7 +398,7 @@ extension Ghostty {
guard let config = self.config else { return 1 }
var v: Double = 1
let key = "background-opacity"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v;
}
@@ -406,7 +406,7 @@ extension Ghostty {
guard let config = self.config else { return 1 }
var v: Int = 0
let key = "background-blur"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v;
}
@@ -414,7 +414,7 @@ extension Ghostty {
guard let config = self.config else { return 1 }
var opacity: Double = 0.85
let key = "unfocused-split-opacity"
_ = ghostty_config_get(config, &opacity, key, UInt(key.count))
_ = ghostty_config_get(config, &opacity, key, UInt(key.lengthOfBytes(using: .utf8)))
return 1 - opacity
}
@@ -423,9 +423,9 @@ extension Ghostty {
var color: ghostty_config_color_s = .init();
let key = "unfocused-split-fill"
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) {
let bg_key = "background"
_ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.count));
_ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.lengthOfBytes(using: .utf8)));
}
return .init(
@@ -444,7 +444,7 @@ extension Ghostty {
var color: ghostty_config_color_s = .init();
let key = "split-divider-color"
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
if (!ghostty_config_get(config, &color, key, UInt(key.lengthOfBytes(using: .utf8)))) {
return Color(newColor)
}
@@ -460,7 +460,7 @@ extension Ghostty {
guard let config = self.config else { return .top }
var v: UnsafePointer<Int8>? = nil
let key = "quick-terminal-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .top }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .top }
guard let ptr = v else { return .top }
let str = String(cString: ptr)
return QuickTerminalPosition(rawValue: str) ?? .top
@@ -470,7 +470,7 @@ extension Ghostty {
guard let config = self.config else { return .main }
var v: UnsafePointer<Int8>? = nil
let key = "quick-terminal-screen"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .main }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .main }
guard let ptr = v else { return .main }
let str = String(cString: ptr)
return QuickTerminalScreen(fromGhosttyConfig: str) ?? .main
@@ -480,7 +480,7 @@ extension Ghostty {
guard let config = self.config else { return 0.2 }
var v: Double = 0.2
let key = "quick-terminal-animation-duration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -488,7 +488,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = true
let key = "quick-terminal-autohide"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -496,7 +496,7 @@ extension Ghostty {
guard let config = self.config else { return .move }
var v: UnsafePointer<Int8>? = nil
let key = "quick-terminal-space-behavior"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .move }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .move }
guard let ptr = v else { return .move }
let str = String(cString: ptr)
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
@@ -506,7 +506,7 @@ extension Ghostty {
guard let config = self.config else { return QuickTerminalSize() }
var v = ghostty_config_quick_terminal_size_s()
let key = "quick-terminal-size"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return QuickTerminalSize() }
return QuickTerminalSize(from: v)
}
#endif
@@ -515,7 +515,7 @@ extension Ghostty {
guard let config = self.config else { return .after_first }
var v: UnsafePointer<Int8>? = nil
let key = "resize-overlay"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .after_first }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return .after_first }
guard let ptr = v else { return .after_first }
let str = String(cString: ptr)
return ResizeOverlay(rawValue: str) ?? .after_first
@@ -526,7 +526,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "resize-overlay-position"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return ResizeOverlayPosition(rawValue: str) ?? defaultValue
@@ -536,7 +536,7 @@ extension Ghostty {
guard let config = self.config else { return 1000 }
var v: UInt = 0
let key = "resize-overlay-duration"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v;
}
@@ -544,7 +544,7 @@ extension Ghostty {
guard let config = self.config else { return .seconds(5) }
var v: UInt = 0
let key = "undo-timeout"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return .milliseconds(v)
}
@@ -552,7 +552,7 @@ extension Ghostty {
guard let config = self.config else { return nil }
var v: UnsafePointer<Int8>? = nil
let key = "auto-update"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return nil }
guard let ptr = v else { return nil }
let str = String(cString: ptr)
return AutoUpdate(rawValue: str)
@@ -563,7 +563,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "auto-update-channel"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return AutoUpdateChannel(rawValue: str) ?? defaultValue
@@ -573,7 +573,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false;
let key = "macos-auto-secure-input"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -581,7 +581,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false;
let key = "macos-secure-input-indication"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -589,7 +589,7 @@ extension Ghostty {
guard let config = self.config else { return true }
var v = false;
let key = "maximize"
_ = ghostty_config_get(config, &v, key, UInt(key.count))
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
@@ -598,7 +598,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-shortcuts"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacShortcuts(rawValue: str) ?? defaultValue
@@ -609,7 +609,7 @@ extension Ghostty {
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "scrollbar"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8))) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return Scrollbar(rawValue: str) ?? defaultValue

View File

@@ -396,6 +396,9 @@ extension Notification.Name {
/// Notification sent when scrollbar updates
static let ghosttyDidUpdateScrollbar = Notification.Name("com.mitchellh.ghostty.didUpdateScrollbar")
static let ScrollbarKey = ghosttyDidUpdateScrollbar.rawValue + ".scrollbar"
/// Focus the search field
static let ghosttySearchFocus = Notification.Name("com.mitchellh.ghostty.searchFocus")
}
// NOTE: I am moving all of these to Notification.Name extensions over time. This

View File

@@ -172,13 +172,16 @@ class SurfaceScrollView: NSView {
}
// MARK: Scrolling
private func synchronizeAppearance() {
let scrollbarConfig = surfaceView.derivedConfig.scrollbar
scrollView.hasVerticalScroller = scrollbarConfig != .never
scrollView.verticalScroller?.controlSize = .small
let hasLightBackground = OSColor(surfaceView.derivedConfig.backgroundColor).isLightColor
// Make sure the scrollers appearance matches the surface's background color.
scrollView.appearance = NSAppearance(named: hasLightBackground ? .aqua : .darkAqua)
}
/// Positions the surface view to fill the currently visible rectangle.
///
/// This is called whenever the scroll position changes. The surface view (which does the

View File

@@ -197,7 +197,16 @@ extension Ghostty {
SecureInputOverlay()
}
#endif
// Search overlay
if let searchState = surfaceView.searchState {
SurfaceSearchOverlay(
surfaceView: surfaceView,
searchState: searchState,
onClose: { surfaceView.searchState = nil }
)
}
// Show bell border if enabled
if (ghostty.config.bellFeatures.contains(.border)) {
BellBorderOverlay(bell: surfaceView.bell)
@@ -382,6 +391,202 @@ extension Ghostty {
}
}
/// Search overlay view that displays a search bar with input field and navigation buttons.
struct SurfaceSearchOverlay: View {
let surfaceView: SurfaceView
@ObservedObject var searchState: SurfaceView.SearchState
let onClose: () -> Void
@State private var corner: Corner = .topRight
@State private var dragOffset: CGSize = .zero
@State private var barSize: CGSize = .zero
@FocusState private var isSearchFieldFocused: Bool
private let padding: CGFloat = 8
var body: some View {
GeometryReader { geo in
HStack(spacing: 4) {
TextField("Search", text: $searchState.needle)
.textFieldStyle(.plain)
.frame(width: 180)
.padding(.leading, 8)
.padding(.trailing, 50)
.padding(.vertical, 6)
.background(Color.primary.opacity(0.1))
.cornerRadius(6)
.focused($isSearchFieldFocused)
.overlay(alignment: .trailing) {
if let selected = searchState.selected {
Text("\(selected + 1)/\(searchState.total, default: "?")")
.font(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
} else if let total = searchState.total {
Text("-/\(total)")
.font(.caption)
.foregroundColor(.secondary)
.monospacedDigit()
.padding(.trailing, 8)
}
}
#if canImport(AppKit)
.onExitCommand {
Ghostty.moveFocus(to: surfaceView)
}
#endif
.backport.onKeyPress(.return) { modifiers in
guard let surface = surfaceView.surface else { return .ignored }
let action = modifiers.contains(.shift)
? "navigate_search:previous"
: "navigate_search:next"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
return .handled
}
Button(action: {
guard let surface = surfaceView.surface else { return }
let action = "navigate_search:next"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}) {
Image(systemName: "chevron.up")
}
.buttonStyle(SearchButtonStyle())
Button(action: {
guard let surface = surfaceView.surface else { return }
let action = "navigate_search:previous"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}) {
Image(systemName: "chevron.down")
}
.buttonStyle(SearchButtonStyle())
Button(action: onClose) {
Image(systemName: "xmark")
}
.buttonStyle(SearchButtonStyle())
}
.padding(8)
.background(.background)
.clipShape(clipShape)
.shadow(radius: 4)
.onAppear {
isSearchFieldFocused = true
}
.onReceive(NotificationCenter.default.publisher(for: .ghosttySearchFocus)) { notification in
guard notification.object as? SurfaceView === surfaceView else { return }
isSearchFieldFocused = true
}
.background(
GeometryReader { barGeo in
Color.clear.onAppear {
barSize = barGeo.size
}
}
)
.padding(padding)
.offset(dragOffset)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: corner.alignment)
.gesture(
DragGesture()
.onChanged { value in
dragOffset = value.translation
}
.onEnded { value in
let centerPos = centerPosition(for: corner, in: geo.size, barSize: barSize)
let newCenter = CGPoint(
x: centerPos.x + value.translation.width,
y: centerPos.y + value.translation.height
)
let newCorner = closestCorner(to: newCenter, in: geo.size)
withAnimation(.easeOut(duration: 0.2)) {
corner = newCorner
dragOffset = .zero
}
}
)
}
}
private var clipShape: some Shape {
if #available(iOS 26.0, macOS 26.0, *) {
return ConcentricRectangle(corners: .concentric(minimum: 8), isUniform: true)
} else {
return RoundedRectangle(cornerRadius: 8)
}
}
enum Corner {
case topLeft, topRight, bottomLeft, bottomRight
var alignment: Alignment {
switch self {
case .topLeft: return .topLeading
case .topRight: return .topTrailing
case .bottomLeft: return .bottomLeading
case .bottomRight: return .bottomTrailing
}
}
}
private func centerPosition(for corner: Corner, in containerSize: CGSize, barSize: CGSize) -> CGPoint {
let halfWidth = barSize.width / 2 + padding
let halfHeight = barSize.height / 2 + padding
switch corner {
case .topLeft:
return CGPoint(x: halfWidth, y: halfHeight)
case .topRight:
return CGPoint(x: containerSize.width - halfWidth, y: halfHeight)
case .bottomLeft:
return CGPoint(x: halfWidth, y: containerSize.height - halfHeight)
case .bottomRight:
return CGPoint(x: containerSize.width - halfWidth, y: containerSize.height - halfHeight)
}
}
private func closestCorner(to point: CGPoint, in containerSize: CGSize) -> Corner {
let midX = containerSize.width / 2
let midY = containerSize.height / 2
if point.x < midX {
return point.y < midY ? .topLeft : .bottomLeft
} else {
return point.y < midY ? .topRight : .bottomRight
}
}
struct SearchButtonStyle: ButtonStyle {
@State private var isHovered = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundStyle(isHovered || configuration.isPressed ? .primary : .secondary)
.padding(.horizontal, 2)
.frame(height: 26)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(backgroundColor(isPressed: configuration.isPressed))
)
.onHover { hovering in
isHovered = hovering
}
.backport.pointerStyle(.link)
}
private func backgroundColor(isPressed: Bool) -> Color {
if isPressed {
return Color.primary.opacity(0.2)
} else if isHovered {
return Color.primary.opacity(0.1)
} else {
return Color.clear
}
}
}
}
/// A surface is terminology in Ghostty for a terminal surface, or a place where a terminal is actually drawn
/// and interacted with. The word "surface" is used because a surface may represent a window, a tab,
/// a split, a small preview pane, etc. It is ANYTHING that has a terminal drawn to it.
@@ -658,3 +863,17 @@ extension FocusedValues {
typealias Value = OSSize
}
}
// MARK: Search State
extension Ghostty.SurfaceView {
class SearchState: ObservableObject {
@Published var needle: String = ""
@Published var selected: UInt? = nil
@Published var total: UInt? = nil
init(from startSearch: Ghostty.Action.StartSearch) {
self.needle = startSearch.needle ?? ""
}
}
}

View File

@@ -1,4 +1,5 @@
import AppKit
import Combine
import SwiftUI
import CoreText
import UserNotifications
@@ -64,6 +65,43 @@ extension Ghostty {
// The currently active key sequence. The sequence is not active if this is empty.
@Published var keySequence: [KeyboardShortcut] = []
// The current search state. When non-nil, the search overlay should be shown.
@Published var searchState: SearchState? = nil {
didSet {
if let searchState {
// I'm not a Combine expert so if there is a better way to do this I'm
// all ears. What we're doing here is grabbing the latest needle. If the
// needle is less than 3 chars, we debounce it for a few hundred ms to
// avoid kicking off expensive searches.
searchNeedleCancellable = searchState.$needle
.removeDuplicates()
.map { needle -> AnyPublisher<String, Never> in
if needle.isEmpty || needle.count >= 3 {
return Just(needle).eraseToAnyPublisher()
} else {
return Just(needle)
.delay(for: .milliseconds(300), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
.switchToLatest()
.sink { [weak self] needle in
guard let surface = self?.surface else { return }
let action = "search:\(needle)"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}
} else if oldValue != nil {
searchNeedleCancellable = nil
guard let surface = self.surface else { return }
let action = "end_search"
ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))
}
}
}
// Cancellable for search state needle changes
private var searchNeedleCancellable: AnyCancellable?
// The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms.
@Published var focusInstant: ContinuousClock.Instant? = nil
@@ -1410,7 +1448,7 @@ extension Ghostty {
@IBAction func copy(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "copy_to_clipboard"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1418,7 +1456,7 @@ extension Ghostty {
@IBAction func paste(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "paste_from_clipboard"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1427,7 +1465,7 @@ extension Ghostty {
@IBAction func pasteAsPlainText(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "paste_from_clipboard"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1435,7 +1473,7 @@ extension Ghostty {
@IBAction func pasteSelection(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "paste_from_selection"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1443,7 +1481,39 @@ extension Ghostty {
@IBAction override func selectAll(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "select_all"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@IBAction func find(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "start_search"
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@IBAction func findNext(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "search:next"
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@IBAction func findPrevious(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "search:previous"
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@IBAction func findHide(_ sender: Any?) {
guard let surface = self.surface else { return }
let action = "end_search"
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1471,7 +1541,7 @@ extension Ghostty {
@objc func resetTerminal(_ sender: Any) {
guard let surface = self.surface else { return }
let action = "reset"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1479,7 +1549,7 @@ extension Ghostty {
@objc func toggleTerminalInspector(_ sender: Any) {
guard let surface = self.surface else { return }
let action = "inspector:toggle"
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
AppDelegate.logger.warning("action failed action=\(action)")
}
}
@@ -1740,7 +1810,13 @@ extension Ghostty.SurfaceView: NSTextInputClient {
} else {
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
}
if range.length == 0, width > 0 {
// This fixes #8493 while speaking
// My guess is that positive width doesn't make sense
// for the dictation microphone indicator
width = 0
x += cellSize.width * Double(range.location + range.length)
}
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
// bottom-left since that is what UIKit expects
// when there's is no characters selected,
@@ -1914,6 +1990,9 @@ extension Ghostty.SurfaceView: NSMenuItemValidation {
let pb = NSPasteboard.ghosttySelection
guard let str = pb.getOpinionatedStringContents() else { return false }
return !str.isEmpty
case #selector(findHide):
return searchState != nil
default:
return true

View File

@@ -40,6 +40,9 @@ extension Ghostty {
/// True when the bell is active. This is set inactive on focus or event.
@Published var bell: Bool = false
// The current search state. When non-nil, the search overlay should be shown.
@Published var searchState: SearchState? = nil
// Returns sizing information for the surface. This is the raw C
// structure because I'm lazy.

View File

@@ -18,6 +18,12 @@ extension Backport where Content: Scene {
// None currently
}
/// Result type for backported onKeyPress handler
enum BackportKeyPressResult {
case handled
case ignored
}
extension Backport where Content: View {
func pointerVisibility(_ v: BackportVisibility) -> some View {
#if canImport(AppKit)
@@ -42,6 +48,24 @@ extension Backport where Content: View {
return content
#endif
}
/// Backported onKeyPress that works on macOS 14+ and is a no-op on macOS 13.
func onKeyPress(_ key: KeyEquivalent, action: @escaping (EventModifiers) -> BackportKeyPressResult) -> some View {
#if canImport(AppKit)
if #available(macOS 14, *) {
return content.onKeyPress(key, phases: .down, action: { keyPress in
switch action(keyPress.modifiers) {
case .handled: return .handled
case .ignored: return .ignored
}
})
} else {
return content
}
#else
return content
#endif
}
}
enum BackportVisibility {

View File

@@ -15,4 +15,20 @@ extension NSWindow {
guard let firstWindow = tabGroup?.windows.first else { return true }
return firstWindow === self
}
/// Adjusts the window origin if necessary to ensure the window remains visible on screen.
func constrainToScreen() {
guard let screen = screen ?? NSScreen.main else { return }
let visibleFrame = screen.visibleFrame
var windowFrame = frame
windowFrame.origin.x = max(visibleFrame.minX,
min(windowFrame.origin.x, visibleFrame.maxX - windowFrame.width))
windowFrame.origin.y = max(visibleFrame.minY,
min(windowFrame.origin.y, visibleFrame.maxY - windowFrame.height))
if windowFrame.origin != frame.origin {
setFrameOrigin(windowFrame.origin)
}
}
}

View File

@@ -11,8 +11,12 @@ pub const LangSet = opaque {
c.FcLangSetDestroy(self.cval());
}
pub fn addLang(self: *LangSet, lang: [:0]const u8) bool {
return c.FcLangSetAdd(self.cval(), lang.ptr) == c.FcTrue;
}
pub fn hasLang(self: *const LangSet, lang: [:0]const u8) bool {
return c.FcLangSetHasLang(self.cvalConst(), lang.ptr) == c.FcTrue;
return c.FcLangSetHasLang(self.cvalConst(), lang.ptr) == c.FcLangEqual;
}
pub inline fn cval(self: *LangSet) *c.struct__FcLangSet {
@@ -32,3 +36,26 @@ test "create" {
try testing.expect(!fs.hasLang("und-zsye"));
}
test "hasLang exact match" {
const testing = std.testing;
// Test exact match: langset with "en-US" should return true for "en-US"
var fs = LangSet.create();
defer fs.destroy();
try testing.expect(fs.addLang("en-US"));
try testing.expect(fs.hasLang("en-US"));
// Test exact match: langset with "und-zsye" should return true for "und-zsye"
var fs_emoji = LangSet.create();
defer fs_emoji.destroy();
try testing.expect(fs_emoji.addLang("und-zsye"));
try testing.expect(fs_emoji.hasLang("und-zsye"));
// Test mismatch: langset with "en-US" should return false for "fr"
try testing.expect(!fs.hasLang("fr"));
// Test partial match: langset with "en-US" should return false for "en-GB"
// (different territory, but we only want exact matches)
try testing.expect(!fs.hasLang("en-GB"));
}

View File

@@ -252,9 +252,13 @@ pub const RenderMode = enum(c_uint) {
sdf = c.FT_RENDER_MODE_SDF,
};
/// A list of bit field constants for FT_Load_Glyph to indicate what kind of
/// operations to perform during glyph loading.
pub const LoadFlags = packed struct {
/// A collection of flags for FT_Load_Glyph that indicate
/// what kind of operations to perform during glyph loading.
///
/// Some of these flags are not included in the official FreeType
/// documentation, but are nevertheless present and named in the
/// header, so the names have been copied from there.
pub const LoadFlags = packed struct(c_int) {
no_scale: bool = false,
no_hinting: bool = false,
render: bool = false,
@@ -263,39 +267,97 @@ pub const LoadFlags = packed struct {
force_autohint: bool = false,
crop_bitmap: bool = false,
pedantic: bool = false,
ignore_global_advance_with: bool = false,
advance_only: bool = false,
ignore_global_advance_width: bool = false,
no_recurse: bool = false,
ignore_transform: bool = false,
monochrome: bool = false,
linear_design: bool = false,
sbits_only: bool = false,
no_autohint: bool = false,
_padding1: u1 = 0,
target_normal: bool = false,
target_light: bool = false,
target_mono: bool = false,
target_lcd: bool = false,
target_lcd_v: bool = false,
target: Target = .normal,
color: bool = false,
compute_metrics: bool = false,
bitmap_metrics_only: bool = false,
_padding2: u1 = 0,
svg_only: bool = false,
no_svg: bool = false,
_padding3: u7 = 0,
_padding: u7 = 0,
test {
// This must always be an i32 size so we can bitcast directly.
const testing = std.testing;
try testing.expectEqual(@sizeOf(i32), @sizeOf(LoadFlags));
}
pub const Target = enum(u4) {
normal = 0,
light = 1,
mono = 2,
lcd = 3,
lcd_v = 4,
};
test "bitcast" {
const testing = std.testing;
const cval: i32 = c.FT_LOAD_RENDER | c.FT_LOAD_PEDANTIC | c.FT_LOAD_COLOR;
const flags = @as(LoadFlags, @bitCast(cval));
try testing.expect(!flags.no_hinting);
try testing.expect(flags.render);
try testing.expect(flags.pedantic);
try testing.expect(flags.color);
// Verify bit alignment (for bit 9)
const cval2: i32 = c.FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH;
const flags2 = @as(LoadFlags, @bitCast(cval2));
try testing.expect(flags2.ignore_global_advance_width);
try testing.expect(!flags2.no_recurse);
}
test "all flags individually" {
const testing = std.testing;
try testing.expectEqual(
c.FT_LOAD_DEFAULT,
@as(c_int, @bitCast(LoadFlags{})),
);
inline for ([_]struct { c_int, []const u8 }{
.{ c.FT_LOAD_NO_SCALE, "no_scale" },
.{ c.FT_LOAD_NO_HINTING, "no_hinting" },
.{ c.FT_LOAD_RENDER, "render" },
.{ c.FT_LOAD_NO_BITMAP, "no_bitmap" },
.{ c.FT_LOAD_VERTICAL_LAYOUT, "vertical_layout" },
.{ c.FT_LOAD_FORCE_AUTOHINT, "force_autohint" },
.{ c.FT_LOAD_CROP_BITMAP, "crop_bitmap" },
.{ c.FT_LOAD_PEDANTIC, "pedantic" },
.{ c.FT_LOAD_ADVANCE_ONLY, "advance_only" },
.{ c.FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH, "ignore_global_advance_width" },
.{ c.FT_LOAD_NO_RECURSE, "no_recurse" },
.{ c.FT_LOAD_IGNORE_TRANSFORM, "ignore_transform" },
.{ c.FT_LOAD_MONOCHROME, "monochrome" },
.{ c.FT_LOAD_LINEAR_DESIGN, "linear_design" },
.{ c.FT_LOAD_SBITS_ONLY, "sbits_only" },
.{ c.FT_LOAD_NO_AUTOHINT, "no_autohint" },
.{ c.FT_LOAD_COLOR, "color" },
.{ c.FT_LOAD_COMPUTE_METRICS, "compute_metrics" },
.{ c.FT_LOAD_BITMAP_METRICS_ONLY, "bitmap_metrics_only" },
.{ c.FT_LOAD_SVG_ONLY, "svg_only" },
.{ c.FT_LOAD_NO_SVG, "no_svg" },
}) |pair| {
var flags: LoadFlags = .{};
@field(flags, pair[1]) = true;
try testing.expectEqual(pair[0], @as(c_int, @bitCast(flags)));
}
}
test "all load targets" {
const testing = std.testing;
inline for ([_]struct { c_int, Target }{
.{ c.FT_LOAD_TARGET_NORMAL, .normal },
.{ c.FT_LOAD_TARGET_LIGHT, .light },
.{ c.FT_LOAD_TARGET_MONO, .mono },
.{ c.FT_LOAD_TARGET_LCD, .lcd },
.{ c.FT_LOAD_TARGET_LCD_V, .lcd_v },
}) |pair| {
const flags: LoadFlags = .{ .target = pair[1] };
try testing.expectEqual(pair[0], @as(c_int, @bitCast(flags)));
}
}
};

Binary file not shown.

View File

@@ -1 +1 @@
pub const font_regular = @embedFile("res/JetBrainsMono-Regular.ttf");
pub const font_regular = @embedFile("res/FiraCode-Regular.ttf");

View File

@@ -67,6 +67,10 @@ pub fn build(b: *std.Build) !void {
"-fno-cxx-exceptions",
"-fno-slp-vectorize",
"-fno-vectorize",
// Fixes linker issues for release builds missing ubsanitizer symbols
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
if (target.result.os.tag != .windows) {
try flags.appendSlice(b.allocator, &.{

View File

@@ -24,7 +24,13 @@ pub fn build(b: *std.Build) !void {
defer flags.deinit(b.allocator);
// Zig 0.13 bug: https://github.com/ziglang/zig/issues/20414
// (See root Ghostty build.zig on why we do this)
try flags.appendSlice(b.allocator, &.{"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0"});
try flags.appendSlice(b.allocator, &.{
"-DSIMDUTF_IMPLEMENTATION_ICELAKE=0",
// Fixes linker issues for release builds missing ubsanitizer symbols
"-fno-sanitize=undefined",
"-fno-sanitize-trap=undefined",
});
lib.addCSourceFiles(.{
.flags = flags.items,

View File

@@ -5,21 +5,16 @@ const App = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const build_config = @import("build_config.zig");
const apprt = @import("apprt.zig");
const Surface = @import("Surface.zig");
const tracy = @import("tracy");
const input = @import("input.zig");
const configpkg = @import("config.zig");
const Config = configpkg.Config;
const BlockingQueue = @import("datastruct/main.zig").BlockingQueue;
const renderer = @import("renderer.zig");
const font = @import("font/main.zig");
const internal_os = @import("os/main.zig");
const macos = @import("macos");
const objc = @import("objc");
const log = std.log.scoped(.app);

View File

@@ -17,7 +17,7 @@ pub const Message = apprt.surface.Message;
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const global_state = &@import("global.zig").state;
@@ -26,9 +26,6 @@ const crash = @import("crash/main.zig");
const unicode = @import("unicode/main.zig");
const rendererpkg = @import("renderer.zig");
const termio = @import("termio.zig");
const objc = @import("objc");
const imgui = @import("imgui");
const Pty = @import("pty.zig").Pty;
const font = @import("font/main.zig");
const Command = @import("Command.zig");
const terminal = @import("terminal/main.zig");
@@ -155,6 +152,9 @@ selection_scroll_active: bool = false,
/// the wall clock time that has elapsed between timestamps.
command_timer: ?std.time.Instant = null,
/// Search state
search: ?Search = null,
/// The effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it
@@ -174,6 +174,26 @@ pub const InputEffect = enum {
closed,
};
/// The search state for the surface.
const Search = struct {
state: terminal.search.Thread,
thread: std.Thread,
pub fn deinit(self: *Search) void {
// Notify the thread to stop
self.state.stop.notify() catch |err| log.err(
"error notifying search thread to stop, may stall err={}",
.{err},
);
// Wait for the OS thread to quit
self.thread.join();
// Now it is safe to deinit the state
self.state.deinit();
}
};
/// Mouse state for the surface.
const Mouse = struct {
/// The last tracked mouse button state by button.
@@ -728,6 +748,9 @@ pub fn init(
}
pub fn deinit(self: *Surface) void {
// Stop search thread
if (self.search) |*s| s.deinit();
// Stop rendering thread
{
self.renderer_thread.stop.notify() catch |err|
@@ -778,6 +801,14 @@ pub fn close(self: *Surface) void {
self.rt_surface.close(self.needsConfirmQuit());
}
/// Returns a mailbox that can be used to send messages to this surface.
inline fn surfaceMailbox(self: *Surface) Mailbox {
return .{
.surface = self,
.app = .{ .rt_app = self.rt_app, .mailbox = &self.app.mailbox },
};
}
/// Forces the surface to render. This is useful for when the surface
/// is in the middle of animation (such as a resize, etc.) or when
/// the render timer is managed manually by the apprt.
@@ -1043,6 +1074,22 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
log.warn("apprt failed to notify command finish={}", .{err});
};
},
.search_total => |v| {
_ = try self.rt_app.performAction(
.{ .surface = self },
.search_total,
.{ .total = v },
);
},
.search_selected => |v| {
_ = try self.rt_app.performAction(
.{ .surface = self },
.search_selected,
.{ .selected = v },
);
},
}
}
@@ -1301,6 +1348,118 @@ fn reportColorScheme(self: *Surface, force: bool) void {
self.io.queueMessage(.{ .write_stable = output }, .unlocked);
}
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
// IMPORTANT: This function is run on the SEARCH THREAD! It is NOT SAFE
// to access anything other than values that never change on the surface.
// The surface is guaranteed to be valid for the lifetime of the search
// thread.
const self: *Surface = @ptrCast(@alignCast(ud.?));
self.searchCallback_(event) catch |err| {
log.warn("error in search callback err={}", .{err});
};
}
fn searchCallback_(
self: *Surface,
event: terminal.search.Thread.Event,
) !void {
// NOTE: This runs on the search thread.
switch (event) {
.viewport_matches => |matches_unowned| {
var arena: ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
const matches = try alloc.dupe(terminal.highlight.Flattened, matches_unowned);
for (matches) |*m| m.* = try m.clone(alloc);
_ = self.renderer_thread.mailbox.push(
.{ .search_viewport_matches = .{
.arena = arena,
.matches = matches,
} },
.forever,
);
try self.renderer_thread.wakeup.notify();
},
.selected_match => |selected_| {
if (selected_) |sel| {
// Copy the flattened match.
var arena: ArenaAllocator = .init(self.alloc);
errdefer arena.deinit();
const alloc = arena.allocator();
const match = try sel.highlight.clone(alloc);
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = .{
.arena = arena,
.match = match,
} },
.forever,
);
// Send the selected index to the surface mailbox
_ = self.surfaceMailbox().push(
.{ .search_selected = sel.idx },
.forever,
);
} else {
// Reset our selected match
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = null },
.forever,
);
// Reset the selected index
_ = self.surfaceMailbox().push(
.{ .search_selected = null },
.forever,
);
}
try self.renderer_thread.wakeup.notify();
},
.total_matches => |total| {
_ = self.surfaceMailbox().push(
.{ .search_total = total },
.forever,
);
},
// When we quit, tell our renderer to reset any search state.
.quit => {
_ = self.renderer_thread.mailbox.push(
.{ .search_selected_match = null },
.forever,
);
_ = self.renderer_thread.mailbox.push(
.{ .search_viewport_matches = .{
.arena = .init(self.alloc),
.matches = &.{},
} },
.forever,
);
try self.renderer_thread.wakeup.notify();
// Reset search totals in the surface
_ = self.surfaceMailbox().push(
.{ .search_total = null },
.forever,
);
_ = self.surfaceMailbox().push(
.{ .search_selected = null },
.forever,
);
},
// Unhandled, so far.
.complete => {},
}
}
/// Call this when modifiers change. This is safe to call even if modifiers
/// match the previous state.
///
@@ -3305,6 +3464,8 @@ fn mouseReport(
.five => 65,
.six => 66,
.seven => 67,
.eight => 128,
.nine => 129,
else => return, // unsupported
};
}
@@ -4103,7 +4264,7 @@ fn osc8URI(self: *Surface, pin: terminal.Pin) ?[]const u8 {
const cell = pin.rowAndCell().cell;
const link_id = page.lookupHyperlink(cell) orelse return null;
const entry = page.hyperlink_set.get(page.memory, link_id);
return entry.uri.offset.ptr(page.memory)[0..entry.uri.len];
return entry.uri.slice(page.memory);
}
pub fn mousePressureCallback(
@@ -4770,6 +4931,96 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
self.renderer_state.terminal.fullReset();
},
.start_search => {
// To save resources, we don't actually start a search here,
// we just notify the apprt. The real thread will start when
// the first needles are set.
return try self.rt_app.performAction(
.{ .surface = self },
.start_search,
.{ .needle = "" },
);
},
.end_search => {
// We only return that this was performed if we actually
// stopped a search, but we also send the apprt end_search so
// that GUIs can clean up stale stuff.
const performed = self.search != null;
if (self.search) |*s| {
s.deinit();
self.search = null;
}
_ = try self.rt_app.performAction(
.{ .surface = self },
.end_search,
{},
);
return performed;
},
.search => |text| search: {
const s: *Search = if (self.search) |*s| s else init: {
// If we're stopping the search and we had no prior search,
// then there is nothing to do.
if (text.len == 0) return false;
// We need to assign directly to self.search because we need
// a stable pointer back to the thread state.
self.search = .{
.state = try .init(self.alloc, .{
.mutex = self.renderer_state.mutex,
.terminal = self.renderer_state.terminal,
.event_cb = &searchCallback,
.event_userdata = self,
}),
.thread = undefined,
};
const s: *Search = &self.search.?;
errdefer s.state.deinit();
s.thread = try .spawn(
.{},
terminal.search.Thread.threadMain,
.{&s.state},
);
s.thread.setName("search") catch {};
break :init s;
};
// Zero-length text means stop searching.
if (text.len == 0) {
s.deinit();
self.search = null;
break :search;
}
_ = s.state.mailbox.push(
.{ .change_needle = try .init(
self.alloc,
text,
) },
.forever,
);
s.state.wakeup.notify() catch {};
},
.navigate_search => |nav| {
const s: *Search = if (self.search) |*s| s else return false;
_ = s.state.mailbox.push(
.{ .select = switch (nav) {
.next => .next,
.previous => .prev,
} },
.forever,
);
s.state.wakeup.notify() catch {};
},
.copy_to_clipboard => |format| {
// We can read from the renderer state without holding
// the lock because only we will write to this field.

View File

@@ -8,8 +8,6 @@
//! The goal is to have different implementations share as much of the core
//! logic as possible, and to only reach out to platform-specific implementation
//! code when absolutely necessary.
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("build_config.zig");
const structs = @import("apprt/structs.zig");

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
const input = @import("../input.zig");
@@ -301,6 +301,18 @@ pub const Action = union(Key) {
/// A command has finished,
command_finished: CommandFinished,
/// Start the search overlay with an optional initial needle.
start_search: StartSearch,
/// End the search overlay, clearing the search state and hiding it.
end_search,
/// The total number of matches found by the search.
search_total: SearchTotal,
/// The currently selected search match index (1-based).
search_selected: SearchSelected,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@@ -358,6 +370,10 @@ pub const Action = union(Key) {
progress_report,
show_on_screen_keyboard,
command_finished,
start_search,
end_search,
search_total,
search_selected,
};
/// Sync with: ghostty_action_u
@@ -770,3 +786,48 @@ pub const CommandFinished = struct {
};
}
};
pub const StartSearch = struct {
needle: [:0]const u8,
// Sync with: ghostty_action_start_search_s
pub const C = extern struct {
needle: [*:0]const u8,
};
pub fn cval(self: StartSearch) C {
return .{
.needle = self.needle.ptr,
};
}
};
pub const SearchTotal = struct {
total: ?usize,
// Sync with: ghostty_action_search_total_s
pub const C = extern struct {
total: isize,
};
pub fn cval(self: SearchTotal) C {
return .{
.total = if (self.total) |t| @intCast(t) else -1,
};
}
};
pub const SearchSelected = struct {
selected: ?usize,
// Sync with: ghostty_action_search_selected_s
pub const C = extern struct {
selected: isize,
};
pub fn cval(self: SearchSelected) C {
return .{
.selected = if (self.selected) |s| @intCast(s) else -1,
};
}
};

View File

@@ -6,7 +6,7 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const objc = @import("objc");
const apprt = @import("../apprt.zig");

View File

@@ -1,5 +1,3 @@
const internal_os = @import("../os/main.zig");
// The required comptime API for any apprt.
pub const App = @import("gtk/App.zig");
pub const Surface = @import("gtk/Surface.zig");

View File

@@ -5,18 +5,13 @@ const App = @This();
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const internal_os = @import("../../os/main.zig");
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const Application = @import("class/application.zig").Application;
const Surface = @import("Surface.zig");
const gtk_version = @import("gtk_version.zig");
const adw_version = @import("adw_version.zig");
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
const log = std.log.scoped(.gtk);

View File

@@ -11,6 +11,20 @@ pub const c = @cImport({
@cInclude("adwaita.h");
});
pub const blueprint_compiler_help =
\\
\\When building from a Git checkout, Ghostty requires
\\version {f} or newer of `blueprint-compiler` as a
\\build-time dependency. Please install it, ensure that it
\\is available on your PATH, and then retry building Ghostty.
\\See `HACKING.md` for more details.
\\
\\This message should *not* appear for normal users, who
\\should build Ghostty from official release tarballs instead.
\\Please consult https://ghostty.org/docs/install/build for
\\more information on the recommended build instructions.
;
const adwaita_version = std.SemanticVersion{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
@@ -79,13 +93,9 @@ pub fn main() !void {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {f} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
},
else => return err,
@@ -103,13 +113,9 @@ pub fn main() !void {
if (version.order(required_blueprint_version) == .lt) {
std.debug.print(
\\`blueprint-compiler` is the wrong version.
\\
\\Ghostty requires version {f} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
}
}
@@ -144,13 +150,9 @@ pub fn main() !void {
error.FileNotFound => {
std.debug.print(
\\`blueprint-compiler` not found.
\\
\\Ghostty requires version {f} or newer of
\\`blueprint-compiler` as a build-time dependency starting
\\from version 1.2. Please install it, ensure that it is
\\available on your PATH, and then retry building Ghostty.
\\
, .{required_blueprint_version});
++ blueprint_compiler_help,
.{required_blueprint_version},
);
std.posix.exit(1);
},
else => return err,

View File

@@ -43,6 +43,7 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 5, .name = "inspector-widget" },
.{ .major = 1, .minor = 5, .name = "inspector-window" },
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
.{ .major = 1, .minor = 2, .name = "search-overlay" },
.{ .major = 1, .minor = 5, .name = "split-tree" },
.{ .major = 1, .minor = 5, .name = "split-tree-split" },
.{ .major = 1, .minor = 2, .name = "surface" },

View File

@@ -1,14 +1,11 @@
/// Contains all the logic for putting the Ghostty process and
/// each individual surface into its own cgroup.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const App = @import("App.zig");
const internal_os = @import("../../os/main.zig");
const log = std.log.scoped(.gtk_systemd_cgroup);

View File

@@ -1,7 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
@@ -9,7 +8,6 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const cgroup = @import("../cgroup.zig");
@@ -729,6 +727,11 @@ pub const Application = extern struct {
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
.command_finished => return Action.commandFinished(target, value),
.start_search => Action.startSearch(target),
.end_search => Action.endSearch(target),
.search_total => Action.searchTotal(target, value),
.search_selected => Action.searchSelected(target, value),
// Unimplemented
.secure_input,
.close_all_windows,
@@ -2339,6 +2342,34 @@ const Action = struct {
}
}
pub fn startSearch(target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.setSearchActive(true),
}
}
pub fn endSearch(target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.setSearchActive(false),
}
}
pub fn searchTotal(target: apprt.Target, value: apprt.action.SearchTotal) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.setSearchTotal(value.total),
}
}
pub fn searchSelected(target: apprt.Target, value: apprt.action.SearchSelected) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.setSearchSelected(value.selected),
}
}
pub fn setTitle(
target: apprt.Target,
value: apprt.action.SetTitle,

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");

View File

@@ -1,13 +1,10 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const i18n = @import("../../../os/main.zig").i18n;
const adw_version = @import("../adw_version.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Dialog = @import("dialog.zig").Dialog;
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);

View File

@@ -1,7 +1,5 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -1,10 +1,8 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Dialog = @import("dialog.zig").Dialog;

View File

@@ -1,9 +1,7 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const build_config = @import("../../../build_config.zig");
const adw_version = @import("../adw_version.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;

View File

@@ -3,10 +3,8 @@ const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_dialog);

View File

@@ -1,14 +1,11 @@
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const Binding = @import("../../../input.zig").Binding;
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const cimgui = @import("cimgui");
const gl = @import("opengl");

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -2,15 +2,12 @@ const std = @import("std");
const build_config = @import("../../../build_config.zig");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Surface = @import("surface.zig").Surface;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");

View File

@@ -0,0 +1,486 @@
const std = @import("std");
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");
const gdk = @import("gdk");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const log = std.log.scoped(.gtk_ghostty_search_overlay);
/// The overlay that shows the current size while a surface is resizing.
/// This can be used generically to show pretty much anything with a
/// disappearing overlay, but we have no other use at this point so it
/// is named specifically for what it does.
///
/// General usage:
///
/// 1. Add it to an overlay
/// 2. Set the label with `setLabel`
/// 3. Schedule to show it with `schedule`
///
/// Set any properties to change the behavior.
pub const SearchOverlay = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttySearchOverlay",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const active = struct {
pub const name = "active";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{
.getter = getSearchActive,
.setter = setSearchActive,
},
),
},
);
};
pub const @"search-total" = struct {
pub const name = "search-total";
const impl = gobject.ext.defineProperty(
name,
Self,
u64,
.{
.default = 0,
.minimum = 0,
.maximum = std.math.maxInt(u64),
.accessor = gobject.ext.typedAccessor(
Self,
u64,
.{ .getter = getSearchTotal },
),
},
);
};
pub const @"has-search-total" = struct {
pub const name = "has-search-total";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{ .getter = getHasSearchTotal },
),
},
);
};
pub const @"search-selected" = struct {
pub const name = "search-selected";
const impl = gobject.ext.defineProperty(
name,
Self,
u64,
.{
.default = 0,
.minimum = 0,
.maximum = std.math.maxInt(u64),
.accessor = gobject.ext.typedAccessor(
Self,
u64,
.{ .getter = getSearchSelected },
),
},
);
};
pub const @"has-search-selected" = struct {
pub const name = "has-search-selected";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{ .getter = getHasSearchSelected },
),
},
);
};
pub const @"halign-target" = struct {
pub const name = "halign-target";
const impl = gobject.ext.defineProperty(
name,
Self,
gtk.Align,
.{
.default = .end,
.accessor = C.privateShallowFieldAccessor("halign_target"),
},
);
};
pub const @"valign-target" = struct {
pub const name = "valign-target";
const impl = gobject.ext.defineProperty(
name,
Self,
gtk.Align,
.{
.default = .start,
.accessor = C.privateShallowFieldAccessor("valign_target"),
},
);
};
};
pub const signals = struct {
/// Emitted when the search is stopped (e.g., Escape pressed).
pub const @"stop-search" = struct {
pub const name = "stop-search";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when the search text changes (debounced).
pub const @"search-changed" = struct {
pub const name = "search-changed";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{?[*:0]const u8},
void,
);
};
/// Emitted when navigating to the next match.
pub const @"next-match" = struct {
pub const name = "next-match";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when navigating to the previous match.
pub const @"previous-match" = struct {
pub const name = "previous-match";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
/// The search entry widget.
search_entry: *gtk.SearchEntry,
/// True when a search is active, meaning we should show the overlay.
active: bool = false,
/// Total number of search matches (null means unknown/none).
search_total: ?usize = null,
/// Currently selected match index (null means none selected).
search_selected: ?usize = null,
/// Target horizontal alignment for the overlay.
halign_target: gtk.Align = .end,
/// Target vertical alignment for the overlay.
valign_target: gtk.Align = .start,
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
/// Grab focus on the search entry and select all text.
pub fn grabFocus(self: *Self) void {
const priv = self.private();
_ = priv.search_entry.as(gtk.Widget).grabFocus();
// Select all text in the search entry field. -1 is distance from
// the end, causing the entire text to be selected.
priv.search_entry.as(gtk.Editable).selectRegion(0, -1);
}
// Set active status, and update search on activation
fn setSearchActive(self: *Self, active: bool) void {
const priv = self.private();
if (!priv.active and active) {
const text = priv.search_entry.as(gtk.Editable).getText();
signals.@"search-changed".impl.emit(self, null, .{text}, null);
}
priv.active = active;
}
/// Set the total number of search matches.
pub fn setSearchTotal(self: *Self, total: ?usize) void {
const priv = self.private();
const had_total = priv.search_total != null;
if (priv.search_total == total) return;
priv.search_total = total;
self.as(gobject.Object).notifyByPspec(properties.@"search-total".impl.param_spec);
if (had_total != (total != null)) {
self.as(gobject.Object).notifyByPspec(properties.@"has-search-total".impl.param_spec);
}
}
/// Set the currently selected match index.
pub fn setSearchSelected(self: *Self, selected: ?usize) void {
const priv = self.private();
const had_selected = priv.search_selected != null;
if (priv.search_selected == selected) return;
priv.search_selected = selected;
self.as(gobject.Object).notifyByPspec(properties.@"search-selected".impl.param_spec);
if (had_selected != (selected != null)) {
self.as(gobject.Object).notifyByPspec(properties.@"has-search-selected".impl.param_spec);
}
}
fn getSearchActive(self: *Self) bool {
return self.private().active;
}
fn getSearchTotal(self: *Self) u64 {
return self.private().search_total orelse 0;
}
fn getHasSearchTotal(self: *Self) bool {
return self.private().search_total != null;
}
fn getSearchSelected(self: *Self) u64 {
return self.private().search_selected orelse 0;
}
fn getHasSearchSelected(self: *Self) bool {
return self.private().search_selected != null;
}
fn closureMatchLabel(
_: *Self,
has_selected: bool,
selected: u64,
has_total: bool,
total: u64,
) callconv(.c) ?[*:0]const u8 {
if (!has_total or total == 0) return glib.ext.dupeZ(u8, "0/0");
var buf: [32]u8 = undefined;
const label = std.fmt.bufPrintZ(&buf, "{}/{}", .{
if (has_selected) selected + 1 else 0,
total,
}) catch return null;
return glib.ext.dupeZ(u8, label);
}
//---------------------------------------------------------------
// Template callbacks
fn searchChanged(entry: *gtk.SearchEntry, self: *Self) callconv(.c) void {
const text = entry.as(gtk.Editable).getText();
signals.@"search-changed".impl.emit(self, null, .{text}, null);
}
// NOTE: The callbacks below use anyopaque for the first parameter
// because they're shared with multiple widgets in the template.
fn stopSearch(_: *anyopaque, self: *Self) callconv(.c) void {
signals.@"stop-search".impl.emit(self, null, .{}, null);
}
fn nextMatch(_: *anyopaque, self: *Self) callconv(.c) void {
signals.@"next-match".impl.emit(self, null, .{}, null);
}
fn previousMatch(_: *anyopaque, self: *Self) callconv(.c) void {
signals.@"previous-match".impl.emit(self, null, .{}, null);
}
fn searchEntryKeyPressed(
_: *gtk.EventControllerKey,
keyval: c_uint,
_: c_uint,
gtk_mods: gdk.ModifierType,
self: *Self,
) callconv(.c) c_int {
if (keyval == gdk.KEY_Return or keyval == gdk.KEY_KP_Enter) {
if (gtk_mods.shift_mask) {
signals.@"previous-match".impl.emit(self, null, .{}, null);
} else {
signals.@"next-match".impl.emit(self, null, .{}, null);
}
return 1;
}
return 0;
}
fn onDragEnd(
_: *gtk.GestureDrag,
offset_x: f64,
offset_y: f64,
self: *Self,
) callconv(.c) void {
// On drag end, we want to move our halign/valign if we crossed
// the midpoint on either axis. This lets the search overlay be
// moved to different corners of the parent container.
const priv = self.private();
const widget = self.as(gtk.Widget);
const parent = widget.getParent() orelse return;
const parent_width: f64 = @floatFromInt(parent.getAllocatedWidth());
const parent_height: f64 = @floatFromInt(parent.getAllocatedHeight());
const self_width: f64 = @floatFromInt(widget.getAllocatedWidth());
const self_height: f64 = @floatFromInt(widget.getAllocatedHeight());
const self_x: f64 = if (priv.halign_target == .start) 0 else parent_width - self_width;
const self_y: f64 = if (priv.valign_target == .start) 0 else parent_height - self_height;
const new_x = self_x + offset_x + (self_width / 2);
const new_y = self_y + offset_y + (self_height / 2);
const new_halign: gtk.Align = if (new_x > parent_width / 2) .end else .start;
const new_valign: gtk.Align = if (new_y > parent_height / 2) .end else .start;
var changed = false;
if (new_halign != priv.halign_target) {
priv.halign_target = new_halign;
self.as(gobject.Object).notifyByPspec(properties.@"halign-target".impl.param_spec);
changed = true;
}
if (new_valign != priv.valign_target) {
priv.valign_target = new_valign;
self.as(gobject.Object).notifyByPspec(properties.@"valign-target".impl.param_spec);
changed = true;
}
if (changed) self.as(gtk.Widget).queueResize();
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
_ = priv;
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
_ = priv;
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 2,
.name = "search-overlay",
}),
);
// Bindings
class.bindTemplateChildPrivate("search_entry", .{});
// Template Callbacks
class.bindTemplateCallback("stop_search", &stopSearch);
class.bindTemplateCallback("search_changed", &searchChanged);
class.bindTemplateCallback("match_label_closure", &closureMatchLabel);
class.bindTemplateCallback("next_match", &nextMatch);
class.bindTemplateCallback("previous_match", &previousMatch);
class.bindTemplateCallback("search_entry_key_pressed", &searchEntryKeyPressed);
class.bindTemplateCallback("on_drag_end", &onDragEnd);
// Properties
gobject.ext.registerProperties(class, &.{
properties.active.impl,
properties.@"search-total".impl,
properties.@"has-search-total".impl,
properties.@"search-selected".impl,
properties.@"has-search-selected".impl,
properties.@"halign-target".impl,
properties.@"valign-target".impl,
});
// Signals
signals.@"stop-search".impl.register(.{});
signals.@"search-changed".impl.register(.{});
signals.@"next-match".impl.register(.{});
signals.@"previous-match".impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@@ -1,6 +1,5 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
@@ -8,17 +7,11 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig");
const ext = @import("../ext.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const Surface = @import("surface.zig").Surface;

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gdk = @import("gdk");
@@ -19,18 +19,17 @@ const terminal = @import("../../../terminal/main.zig");
const CoreSurface = @import("../../../Surface.zig");
const gresource = @import("../build/gresource.zig");
const ext = @import("../ext.zig");
const adw_version = @import("../adw_version.zig");
const gtk_key = @import("../key.zig");
const ApprtSurface = @import("../Surface.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Config = @import("config.zig").Config;
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
const SearchOverlay = @import("search_overlay.zig").SearchOverlay;
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
const Window = @import("window.zig").Window;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const i18n = @import("../../../os/i18n.zig");
@@ -551,6 +550,9 @@ pub const Surface = extern struct {
/// The resize overlay
resize_overlay: *ResizeOverlay,
/// The search overlay
search_overlay: *SearchOverlay,
/// The apprt Surface.
rt_surface: ApprtSurface = undefined,
@@ -1465,6 +1467,10 @@ pub const Surface = extern struct {
// EnvMap is a bit annoying so I'm punting it.
if (ext.getAncestor(Window, self.as(gtk.Widget))) |window| {
try window.winproto().addSubprocessEnv(&env);
if (window.isQuickTerminal()) {
try env.put("GHOSTTY_QUICK_TERMINAL", "1");
}
}
return env;
@@ -1949,6 +1955,29 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
}
pub fn setSearchActive(self: *Self, active: bool) void {
const priv = self.private();
var value = gobject.ext.Value.newFrom(active);
defer value.unset();
gobject.Object.setProperty(
priv.search_overlay.as(gobject.Object),
SearchOverlay.properties.active.name,
&value,
);
if (active) {
priv.search_overlay.grabFocus();
}
}
pub fn setSearchTotal(self: *Self, total: ?usize) void {
self.private().search_overlay.setSearchTotal(total);
}
pub fn setSearchSelected(self: *Self, selected: ?usize) void {
self.private().search_overlay.setSearchSelected(selected);
}
fn propConfig(
self: *Self,
_: *gobject.ParamSpec,
@@ -3168,6 +3197,35 @@ pub const Surface = extern struct {
self.setTitleOverride(if (title.len == 0) null else title);
}
fn searchStop(_: *SearchOverlay, self: *Self) callconv(.c) void {
const surface = self.core() orelse return;
_ = surface.performBindingAction(.end_search) catch |err| {
log.warn("unable to perform end_search action err={}", .{err});
};
_ = self.private().gl_area.as(gtk.Widget).grabFocus();
}
fn searchChanged(_: *SearchOverlay, needle: ?[*:0]const u8, self: *Self) callconv(.c) void {
const surface = self.core() orelse return;
_ = surface.performBindingAction(.{ .search = std.mem.sliceTo(needle orelse "", 0) }) catch |err| {
log.warn("unable to perform search action err={}", .{err});
};
}
fn searchNextMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
const surface = self.core() orelse return;
_ = surface.performBindingAction(.{ .navigate_search = .next }) catch |err| {
log.warn("unable to perform navigate_search action err={}", .{err});
};
}
fn searchPreviousMatch(_: *SearchOverlay, self: *Self) callconv(.c) void {
const surface = self.core() orelse return;
_ = surface.performBindingAction(.{ .navigate_search = .previous }) catch |err| {
log.warn("unable to perform navigate_search action err={}", .{err});
};
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@@ -3182,6 +3240,7 @@ pub const Surface = extern struct {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(ResizeOverlay);
gobject.ext.ensureType(SearchOverlay);
gobject.ext.ensureType(ChildExited);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
@@ -3201,6 +3260,7 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("error_page", .{});
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("search_overlay", .{});
class.bindTemplateChildPrivate("terminal_page", .{});
class.bindTemplateChildPrivate("drop_target", .{});
class.bindTemplateChildPrivate("im_context", .{});
@@ -3238,6 +3298,10 @@ pub const Surface = extern struct {
class.bindTemplateCallback("notify_vadjustment", &propVAdjustment);
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
class.bindTemplateCallback("should_unfocused_split_be_shown", &closureShouldUnfocusedSplitBeShown);
class.bindTemplateCallback("search_stop", &searchStop);
class.bindTemplateCallback("search_changed", &searchChanged);
class.bindTemplateCallback("search_next_match", &searchNextMatch);
class.bindTemplateCallback("search_previous_match", &searchPreviousMatch);
// Properties
gobject.ext.registerProperties(class, &.{
@@ -3369,12 +3433,16 @@ const Clipboard = struct {
// text/plain type. The default charset when there is
// none is ASCII, and lots of things look for UTF-8
// specifically.
// The specs are not clear about the order here, but
// some clients apparently pick the first match in the
// order we set here then garble up bare 'text/plain'
// with non-ASCII UTF-8 content, so offer UTF-8 first.
//
// Note that under X11, GTK automatically adds the
// UTF8_STRING atom when this is present.
const text_provider_atoms = [_][:0]const u8{
"text/plain",
"text/plain;charset=utf-8",
"text/plain",
};
var text_providers: [text_provider_atoms.len]*gdk.ContentProvider = undefined;
for (text_provider_atoms, 0..) |atom, j| {

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const glib = @import("glib");
const gobject = @import("gobject");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const assert = std.debug.assert;
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -6,7 +6,6 @@ const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const ext = @import("../ext.zig");
const Common = @import("../class.zig").Common;

View File

@@ -1,19 +1,13 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig");
const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const adw = @import("adw");
const gdk = @import("gdk");
const gio = @import("gio");
@@ -28,7 +28,6 @@ const Surface = @import("surface.zig").Surface;
const Tab = @import("tab.zig").Tab;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const CommandPalette = @import("command_palette.zig").CommandPalette;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_window);

View File

@@ -34,6 +34,18 @@ label.url-overlay.right {
border-radius: 6px 0px 0px 0px;
}
/*
* GhosttySurface search overlay
*/
.search-overlay {
padding: 6px 8px;
margin: 8px;
border-radius: 8px;
outline-style: solid;
outline-color: #555555;
outline-width: 1px;
}
/*
* GhosttySurface resize overlay
*/

View File

@@ -4,10 +4,9 @@
//! helpers.
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../quirks.zig").inlineAssert;
const testing = std.testing;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const assert = std.debug.assert;
const assert = @import("../../../quirks.zig").inlineAssert;
const testing = std.testing;
const gio = @import("gio");

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const build_options = @import("build_options");
const gdk = @import("gdk");
const glib = @import("glib");

View File

@@ -0,0 +1,94 @@
using Gtk 4.0;
using Gdk 4.0;
using Adw 1;
template $GhosttySearchOverlay: Adw.Bin {
visible: bind template.active;
halign-target: end;
valign-target: start;
halign: bind template.halign-target;
valign: bind template.valign-target;
GestureDrag {
button: 1;
propagation-phase: capture;
drag-end => $on_drag_end();
}
Adw.Bin {
Box container {
styles [
"background",
"search-overlay",
]
orientation: horizontal;
spacing: 6;
SearchEntry search_entry {
placeholder-text: _("Find…");
width-chars: 20;
hexpand: true;
stop-search => $stop_search();
search-changed => $search_changed();
next-match => $next_match();
previous-match => $previous_match();
EventControllerKey {
// We need this so we capture before the SearchEntry.
propagation-phase: capture;
key-pressed => $search_entry_key_pressed();
}
}
Label {
styles [
"dim-label",
]
label: bind $match_label_closure(template.has-search-selected, template.search-selected, template.has-search-total, template.search-total) as <string>;
width-chars: 6;
xalign: 1.0;
}
Box button_box {
orientation: horizontal;
spacing: 1;
styles [
"linked",
]
Button prev_button {
icon-name: "go-up-symbolic";
tooltip-text: _("Previous Match");
clicked => $next_match();
cursor: Gdk.Cursor {
name: "pointer";
};
}
Button next_button {
icon-name: "go-down-symbolic";
tooltip-text: _("Next Match");
clicked => $previous_match();
cursor: Gdk.Cursor {
name: "pointer";
};
}
}
Button close_button {
icon-name: "window-close-symbolic";
tooltip-text: _("Close");
clicked => $stop_search();
cursor: Gdk.Cursor {
name: "pointer";
};
}
}
}
}

View File

@@ -41,6 +41,34 @@ Overlay terminal_page {
halign: start;
has-arrow: false;
}
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
};
[overlay]
@@ -64,6 +92,10 @@ Overlay terminal_page {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
// Revealers take up the full size, we need this to not capture events.
can-focus: false;
can-target: false;
focusable: false;
Box bell_overlay {
styles [
@@ -115,12 +147,26 @@ Overlay terminal_page {
label: bind template.mouse-hover-url;
}
[overlay]
$GhosttySearchOverlay search_overlay {
stop-search => $search_stop();
search-changed => $search_changed();
next-match => $search_next_match();
previous-match => $search_previous_match();
}
[overlay]
// Apply unfocused-split-fill and unfocused-split-opacity to current surface
// this is only applied when a tab has more than one surface
Revealer {
reveal-child: bind $should_unfocused_split_be_shown(template.focused, template.is-split) as <bool>;
transition-duration: 0;
// This is all necessary so that the Revealer itself doesn't override
// any input events from the other overlays. Namely, if you don't have
// these then the search overlay won't get mouse events.
can-focus: false;
can-target: false;
focusable: false;
DrawingArea {
styles [
@@ -129,35 +175,6 @@ Overlay terminal_page {
}
}
// Event controllers for interactivity
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;

View File

@@ -1,7 +1,6 @@
//! Wayland protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const gdk = @import("gdk");
const gdk_wayland = @import("gdk_wayland");

View File

@@ -1,10 +1,8 @@
//! X11 window protocol implementation for the Ghostty GTK apprt.
const std = @import("std");
const builtin = @import("builtin");
const build_options = @import("build_options");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gdk = @import("gdk");
const gdk_x11 = @import("gdk_x11");
const glib = @import("glib");

View File

@@ -2,7 +2,7 @@
//! process.
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
pub const Errors = error{
/// The IPC failed. If a function returns this error, it's expected that

View File

@@ -6,15 +6,15 @@ const build_config = @import("../build_config.zig");
const App = @import("../App.zig");
const Surface = @import("../Surface.zig");
const renderer = @import("../renderer.zig");
const termio = @import("../termio.zig");
const terminal = @import("../terminal/main.zig");
const Config = @import("../config.zig").Config;
const MessageData = @import("../datastruct/main.zig").MessageData;
/// The message types that can be sent to a single surface.
pub const Message = union(enum) {
/// Represents a write request. Magic number comes from the max size
/// we want this union to be.
pub const WriteReq = termio.MessageData(u8, 255);
pub const WriteReq = MessageData(u8, 255);
/// Set the title of the surface.
/// TODO: we should change this to a "WriteReq" style structure in
@@ -107,6 +107,12 @@ pub const Message = union(enum) {
/// The scrollbar state changed for the surface.
scrollbar: terminal.Scrollbar,
/// Search progress update
search_total: ?usize,
/// Selected search index change
search_selected: ?usize,
pub const ReportTitleStyle = enum {
csi_21_t,

View File

@@ -107,7 +107,7 @@ fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;
@@ -134,7 +134,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;
@@ -166,7 +166,7 @@ fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;

View File

@@ -90,7 +90,7 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;
@@ -113,7 +113,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;

View File

@@ -4,7 +4,6 @@
const IsSymbol = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Benchmark = @import("Benchmark.zig");
@@ -90,7 +89,7 @@ fn stepUucode(ptr: *anyopaque) Benchmark.Error!void {
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;
@@ -117,7 +116,7 @@ fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
const self: *IsSymbol = @ptrCast(@alignCast(ptr));
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;

View File

@@ -0,0 +1,196 @@
//! This benchmark tests the performance of the Screen.clone
//! function. This is useful because it is one of the primary lock
//! holders that impact IO performance when the renderer is active.
//! We do this very frequently.
const ScreenClone = @This();
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminalpkg = @import("../terminal/main.zig");
const Benchmark = @import("Benchmark.zig");
const options = @import("options.zig");
const Terminal = terminalpkg.Terminal;
const log = std.log.scoped(.@"terminal-stream-bench");
opts: Options,
terminal: Terminal,
pub const Options = struct {
/// The type of codepoint width calculation to use.
mode: Mode = .clone,
/// The size of the terminal. This affects benchmarking when
/// dealing with soft line wrapping and the memory impact
/// of page sizes.
@"terminal-rows": u16 = 80,
@"terminal-cols": u16 = 120,
/// The data to read as a filepath. If this is "-" then
/// we will read stdin. If this is unset, then we will
/// do nothing (benchmark is a noop). It'd be more unixy to
/// use stdin by default but I find that a hanging CLI command
/// with no interaction is a bit annoying.
///
/// This will be used to initialize the terminal screen state before
/// cloning. This data can switch to alt screen if it wants. The time
/// to read this is not part of the benchmark.
data: ?[]const u8 = null,
};
pub const Mode = enum {
/// The baseline mode copies the screen by value.
noop,
/// Full clone
clone,
/// RenderState rather than a screen clone.
render,
};
pub fn create(
alloc: Allocator,
opts: Options,
) !*ScreenClone {
const ptr = try alloc.create(ScreenClone);
errdefer alloc.destroy(ptr);
ptr.* = .{
.opts = opts,
.terminal = try .init(alloc, .{
.rows = opts.@"terminal-rows",
.cols = opts.@"terminal-cols",
}),
};
return ptr;
}
pub fn destroy(self: *ScreenClone, alloc: Allocator) void {
self.terminal.deinit(alloc);
alloc.destroy(self);
}
pub fn benchmark(self: *ScreenClone) Benchmark {
return .init(self, .{
.stepFn = switch (self.opts.mode) {
.noop => stepNoop,
.clone => stepClone,
.render => stepRender,
},
.setupFn = setup,
.teardownFn = teardown,
});
}
fn setup(ptr: *anyopaque) Benchmark.Error!void {
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
// Always reset our terminal state
self.terminal.fullReset();
// Force a style on every single row, which
var s = self.terminal.vtStream();
defer s.deinit();
s.nextSlice("\x1b[48;2;20;40;60m") catch unreachable;
for (0..self.terminal.rows - 1) |_| s.nextSlice("hello\r\n") catch unreachable;
s.nextSlice("hello") catch unreachable;
// Setup our terminal state
const data_f: std.fs.File = (options.dataFile(
self.opts.data,
) catch |err| {
log.warn("error opening data file err={}", .{err});
return error.BenchmarkFailed;
}) orelse return;
var stream = self.terminal.vtStream();
defer stream.deinit();
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = data_f.reader(&read_buf);
const r = &f_reader.interface;
var buf: [4096]u8 = undefined;
while (true) {
const n = r.readSliceShort(&buf) catch {
log.warn("error reading data file err={?}", .{f_reader.err});
return error.BenchmarkFailed;
};
if (n == 0) break; // EOF reached
stream.nextSlice(buf[0..n]) catch |err| {
log.warn("error processing data file chunk err={}", .{err});
return error.BenchmarkFailed;
};
}
}
fn teardown(ptr: *anyopaque) void {
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
_ = self;
}
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
// We loop because its so fast that a single benchmark run doesn't
// properly capture our speeds.
for (0..1000) |_| {
const s: terminalpkg.Screen = self.terminal.screens.active.*;
std.mem.doNotOptimizeAway(s);
}
}
fn stepClone(ptr: *anyopaque) Benchmark.Error!void {
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
// We loop because its so fast that a single benchmark run doesn't
// properly capture our speeds.
for (0..1000) |_| {
const s: *terminalpkg.Screen = self.terminal.screens.active;
const copy = s.clone(
s.alloc,
.{ .viewport = .{} },
null,
) catch |err| {
log.warn("error cloning screen err={}", .{err});
return error.BenchmarkFailed;
};
std.mem.doNotOptimizeAway(copy);
// Note: we purposely do not free memory because we don't want
// to benchmark that. We'll free when the benchmark exits.
}
}
fn stepRender(ptr: *anyopaque) Benchmark.Error!void {
const self: *ScreenClone = @ptrCast(@alignCast(ptr));
// We do this once out of the loop because a significant slowdown
// on the first run is allocation. After that first run, even with
// a full rebuild, it is much faster. Let's ignore that first run
// slowdown.
const alloc = self.terminal.screens.active.alloc;
var state: terminalpkg.RenderState = .empty;
state.update(alloc, &self.terminal) catch |err| {
log.warn("error cloning screen err={}", .{err});
return error.BenchmarkFailed;
};
// We loop because its so fast that a single benchmark run doesn't
// properly capture our speeds.
for (0..1000) |_| {
// Forces a full rebuild because it thinks our screen changed
state.screen = .alternate;
state.update(alloc, &self.terminal) catch |err| {
log.warn("error cloning screen err={}", .{err});
return error.BenchmarkFailed;
};
std.mem.doNotOptimizeAway(state);
// Note: we purposely do not free memory because we don't want
// to benchmark that. We'll free when the benchmark exits.
}
}

View File

@@ -75,7 +75,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
// the benchmark results and... I know writing this that we
// aren't currently IO bound.
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
var r = &f_reader.interface;

View File

@@ -114,7 +114,7 @@ fn step(ptr: *anyopaque) Benchmark.Error!void {
// aren't currently IO bound.
const f = self.data_f orelse return;
var read_buf: [4096]u8 = undefined;
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
var f_reader = f.reader(&read_buf);
const r = &f_reader.interface;

View File

@@ -8,6 +8,7 @@ const cli = @import("../cli.zig");
pub const Action = enum {
@"codepoint-width",
@"grapheme-break",
@"screen-clone",
@"terminal-parser",
@"terminal-stream",
@"is-symbol",
@@ -22,6 +23,7 @@ pub const Action = enum {
/// See TerminalStream for an example.
pub fn Struct(comptime action: Action) type {
return switch (action) {
.@"screen-clone" => @import("ScreenClone.zig"),
.@"terminal-stream" => @import("TerminalStream.zig"),
.@"codepoint-width" => @import("CodepointWidth.zig"),
.@"grapheme-break" => @import("GraphemeBreak.zig"),

View File

@@ -4,6 +4,7 @@ pub const CApi = @import("CApi.zig");
pub const TerminalStream = @import("TerminalStream.zig");
pub const CodepointWidth = @import("CodepointWidth.zig");
pub const GraphemeBreak = @import("GraphemeBreak.zig");
pub const ScreenClone = @import("ScreenClone.zig");
pub const TerminalParser = @import("TerminalParser.zig");
pub const IsSymbol = @import("IsSymbol.zig");

View File

@@ -2,7 +2,6 @@
const GhosttyBench = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step.Compile,

View File

@@ -170,11 +170,11 @@ pub const Resource = struct {
/// Returns true if the dist path exists at build time.
pub fn exists(self: *const Resource, b: *std.Build) bool {
if (std.fs.accessAbsolute(b.pathFromRoot(self.dist), .{})) {
if (b.build_root.handle.access(self.dist, .{})) {
// If we have a ".git" directory then we're a git checkout
// and we never want to use the dist path. This shouldn't happen
// so show a warning to the user.
if (std.fs.accessAbsolute(b.pathFromRoot(".git"), .{})) {
if (b.build_root.handle.access(".git", .{})) {
std.log.warn(
"dist resource '{s}' should not be in a git checkout",
.{self.dist},

View File

@@ -3,8 +3,6 @@
const GhosttyFrameData = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
const DistResource = @import("GhosttyDist.zig").Resource;
/// The output path for the compressed framedata zig file

View File

@@ -3,11 +3,7 @@ const GhosttyLibVt = @This();
const std = @import("std");
const assert = std.debug.assert;
const RunStep = std.Build.Step.Run;
const Config = @import("Config.zig");
const GhosttyZig = @import("GhosttyZig.zig");
const SharedDeps = @import("SharedDeps.zig");
const LibtoolStep = @import("LibtoolStep.zig");
const LipoStep = @import("LipoStep.zig");
/// The step that generates the file.
step: *std.Build.Step,

View File

@@ -1,15 +1,14 @@
const GhosttyResources = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const buildpkg = @import("main.zig");
const Config = @import("Config.zig");
const RunStep = std.Build.Step.Run;
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step,
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !GhosttyResources {
var steps: std.ArrayList(*std.Build.Step) = .empty;
errdefer steps.deinit(b.allocator);
@@ -26,6 +25,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
});
build_data_exe.linkLibC();
deps.help_strings.addImport(build_data_exe);
// Terminfo
terminfo: {
const os_tag = cfg.target.result.os.tag;

View File

@@ -3,7 +3,6 @@
const GhosttyWebdata = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
steps: []*std.Build.Step,

View File

@@ -1,7 +1,6 @@
const UnicodeTables = @This();
const std = @import("std");
const Config = @import("Config.zig");
/// The exe.
props_exe: *std.Build.Step.Compile,

View File

@@ -19,23 +19,23 @@ fn computeWidth(
_ = backing;
_ = tracking;
// Emoji modifiers are technically width 0 because they're joining
// points. But we handle joining via grapheme break and don't use width
// there. If a emoji modifier is standalone, we want it to take up
// two columns.
if (data.is_emoji_modifier) {
assert(data.wcwidth == 0);
data.wcwidth = 2;
return;
// This condition is to get the previous behavior of uucode's `wcwidth`,
// returning the width of a code point in a grapheme cluster but with the
// exception to treat emoji modifiers as width 2 so they can be displayed
// in isolation. PRs to follow will take advantage of the new uucode
// `wcwidth_standalone` vs `wcwidth_zero_in_grapheme` split.
if (data.wcwidth_zero_in_grapheme and !data.is_emoji_modifier) {
data.width = 0;
} else {
data.width = @min(2, data.wcwidth_standalone);
}
data.width = @intCast(@min(2, @max(0, data.wcwidth)));
}
const width = config.Extension{
.inputs = &.{
"wcwidth_standalone",
"wcwidth_zero_in_grapheme",
"is_emoji_modifier",
"wcwidth",
},
.compute = &computeWidth,
.fields = &.{
@@ -90,10 +90,7 @@ pub const tables = [_]config.Table{
width.field("width"),
d.field("grapheme_break"),
is_symbol.field("is_symbol"),
d.field("is_emoji_modifier"),
d.field("is_emoji_modifier_base"),
d.field("is_emoji_vs_text"),
d.field("is_emoji_vs_emoji"),
d.field("is_emoji_vs_base"),
},
},
};

View File

@@ -1,5 +1,4 @@
const std = @import("std");
const help_strings = @import("help_strings");
const helpgen_actions = @import("../../input/helpgen_actions.zig");
pub fn main() !void {

View File

@@ -9,7 +9,6 @@ const assert = std.debug.assert;
const apprt = @import("apprt.zig");
const font = @import("font/main.zig");
const rendererpkg = @import("renderer.zig");
const WasmTarget = @import("os/wasm/target.zig").Target;
const BuildConfig = @import("build/Config.zig");
pub const ReleaseChannel = BuildConfig.ReleaseChannel;

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const mem = std.mem;
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const diags = @import("diagnostics.zig");

View File

@@ -3,7 +3,6 @@ const builtin = @import("builtin");
const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Allocator = std.mem.Allocator;
const help_strings = @import("help_strings");
const vaxis = @import("vaxis");
const framedata = @import("framedata").compressed;

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const build_config = @import("../build_config.zig");

View File

@@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("../quirks.zig").inlineAssert;
const args = @import("args.zig");
const Allocator = std.mem.Allocator;
const Action = @import("ghostty.zig").Action;

View File

@@ -1,11 +1,9 @@
const std = @import("std");
const inputpkg = @import("../input.zig");
const args = @import("args.zig");
const Action = @import("ghostty.zig").Action;
const Config = @import("../config/Config.zig");
const themepkg = @import("../config/theme.zig");
const tui = @import("tui.zig");
const internal_os = @import("../os/main.zig");
const global_state = &@import("../global.zig").state;
const vaxis = @import("vaxis");
@@ -180,7 +178,13 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
return 0;
}
var theme_config = try Config.default(gpa_alloc);
defer theme_config.deinit();
for (themes.items) |theme| {
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
if (!shouldIncludeTheme(opts.color, theme_config)) {
continue;
}
if (opts.path)
try stdout.print("{s} ({t}) {s}\n", .{ theme.theme, theme.location, theme.path })
else

View File

@@ -5,7 +5,7 @@ const DiskCache = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const assert = @import("../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const internal_os = @import("../../os/main.zig");
const xdg = internal_os.xdg;

Some files were not shown because too many files have changed in this diff Show More