Merge branch 'ghostty-org:main' into feat-list-themes-write-config

pull/8930/head
greathongtu 2025-12-17 00:29:24 +08:00 committed by GitHub
commit e58bbc1d3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
487 changed files with 63169 additions and 12623 deletions

2
.gitattributes vendored
View File

@ -9,4 +9,6 @@ pkg/glfw/wayland-headers/** linguist-vendored
pkg/libintl/config.h linguist-generated=true pkg/libintl/config.h linguist-generated=true
pkg/libintl/libintl.h linguist-generated=true pkg/libintl/libintl.h linguist-generated=true
pkg/simdutf/vendor/** linguist-vendored pkg/simdutf/vendor/** linguist-vendored
src/font/nerd_font_attributes.zig linguist-generated=true
src/font/nerd_font_codepoint_tables.py linguist-generated=true
src/terminal/res/** linguist-vendored src/terminal/res/** linguist-vendored

50
.github/workflows/flatpak.yml vendored Normal file
View File

@ -0,0 +1,50 @@
on:
workflow_dispatch:
inputs:
source-run-id:
description: run id of the workflow that generated the artifact
required: true
type: string
source-artifact-id:
description: source tarball built during build-dist
required: true
type: string
name: Flatpak
jobs:
build:
if: github.repository == 'ghostty-org/ghostty'
name: "Flatpak"
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47
options: --privileged
strategy:
fail-fast: false
matrix:
variant:
- arch: x86_64
runner: namespace-profile-ghostty-md
- arch: aarch64
runner: namespace-profile-ghostty-md-arm64
runs-on: ${{ matrix.variant.runner }}
steps:
- name: Download Source Tarball Artifacts
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
run-id: ${{ inputs.source-run-id }}
artifact-ids: ${{ inputs.source-artifact-id }}
github-token: ${{ github.token }}
- name: Extract tarball
run: |
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
with:
bundle: com.mitchellh.ghostty
manifest-path: dist/flatpak/com.mitchellh.ghostty.yml
cache-key: flatpak-builder-${{ github.sha }}
arch: ${{ matrix.variant.arch }}
verbose: true

View File

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

View File

@ -1,5 +1,10 @@
on: [push, pull_request] on: [push, pull_request]
name: Nix name: Nix
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
required: required:
name: "Required Checks: Nix" name: "Required Checks: Nix"
@ -34,15 +39,15 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- name: Setup Nix - name: Setup Nix
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16

View File

@ -28,7 +28,7 @@ jobs:
echo "Version is valid: ${{ github.event.inputs.version }}" echo "Version is valid: ${{ github.event.inputs.version }}"
- name: Exract the Version - name: Extract the Version
id: extract_version id: extract_version
run: | run: |
VERSION=${{ github.event.inputs.version }} VERSION=${{ github.event.inputs.version }}

View File

@ -56,7 +56,7 @@ jobs:
fi fi
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
@ -80,16 +80,16 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
@ -113,7 +113,7 @@ jobs:
nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: source-tarball name: source-tarball
path: |- path: |-
@ -132,7 +132,7 @@ jobs:
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }} GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
with: with:
@ -269,7 +269,7 @@ jobs:
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/ zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: macos name: macos
path: |- path: |-
@ -286,7 +286,7 @@ jobs:
curl -sL https://sentry.io/get-cli/ | bash curl -sL https://sentry.io/get-cli/ | bash
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: macos name: macos
@ -306,10 +306,10 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: macos name: macos
@ -340,7 +340,7 @@ jobs:
mv appcast_new.xml appcast.xml mv appcast_new.xml appcast.xml
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: sparkle name: sparkle
path: |- path: |-
@ -357,17 +357,17 @@ jobs:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Download macOS Artifacts - name: Download macOS Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: macos name: macos
- name: Download Sparkle Artifacts - name: Download Sparkle Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: sparkle name: sparkle
- name: Download Source Tarball Artifacts - name: Download Source Tarball Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: source-tarball name: source-tarball

View File

@ -19,7 +19,6 @@ jobs:
if: | if: |
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
( (
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
) )
@ -30,11 +29,11 @@ jobs:
commit: ${{ steps.extract_build_info.outputs.commit }} commit: ${{ steps.extract_build_info.outputs.commit }}
commit_long: ${{ steps.extract_build_info.outputs.commit_long }} commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -67,7 +66,7 @@ jobs:
needs: [setup, build-macos] needs: [setup, build-macos]
if: needs.setup.outputs.should_skip != 'true' if: needs.setup.outputs.should_skip != 'true'
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Tip Tag - name: Tip Tag
run: | run: |
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
@ -82,7 +81,7 @@ jobs:
env: env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -105,7 +104,7 @@ jobs:
env: env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -128,7 +127,7 @@ jobs:
env: env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install sentry-cli - name: Install sentry-cli
run: | run: |
@ -151,7 +150,6 @@ jobs:
( (
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
( (
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
) )
@ -161,14 +159,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -188,7 +186,7 @@ jobs:
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
- name: Update Release - name: Update Release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true
@ -206,7 +204,6 @@ jobs:
( (
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
( (
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
) )
@ -220,7 +217,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
@ -359,7 +356,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true
@ -373,7 +370,6 @@ jobs:
# Create our appcast for Sparkle # Create our appcast for Sparkle
- name: Generate Appcast - name: Generate Appcast
if: | if: |
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
env: env:
@ -408,7 +404,6 @@ jobs:
# gets out of sync with the binaries. # gets out of sync with the binaries.
- name: Prep R2 Storage for Appcast - name: Prep R2 Storage for Appcast
if: | if: |
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
run: | run: |
@ -418,7 +413,6 @@ jobs:
- name: Upload Appcast to R2 - name: Upload Appcast to R2
if: | if: |
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4 uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
@ -444,7 +438,6 @@ jobs:
( (
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
( (
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
) )
@ -458,7 +451,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
@ -590,7 +583,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true
@ -629,7 +622,6 @@ jobs:
( (
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
( (
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' && github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main' github.ref_name == 'main'
) )
@ -643,7 +635,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }} GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
# Important so that build number generation works # Important so that build number generation works
fetch-depth: 0 fetch-depth: 0
@ -775,7 +767,7 @@ jobs:
# Update Release # Update Release
- name: Release - name: Release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with: with:
name: 'Ghostty Tip ("Nightly")' name: 'Ghostty Tip ("Nightly")'
prerelease: true prerelease: true

View File

@ -26,7 +26,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Download Source Tarball Artifacts - name: Download Source Tarball Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
run-id: ${{ inputs.source-run-id }} run-id: ${{ inputs.source-run-id }}
artifact-ids: ${{ inputs.source-artifact-id }} artifact-ids: ${{ inputs.source-artifact-id }}
@ -38,7 +38,7 @@ jobs:
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix

View File

@ -5,6 +5,11 @@ on:
name: Test name: Test
# We only want the latest commit to test for any non-main ref.
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }}
cancel-in-progress: true
jobs: jobs:
required: required:
name: "Required Checks: Test" name: "Required Checks: Test"
@ -19,7 +24,7 @@ jobs:
- build-linux-libghostty - build-linux-libghostty
- build-nix - build-nix
- build-macos - build-macos
- build-macos-matrix - build-macos-freetype
- build-snap - build-snap
- build-windows - build-windows
- test - test
@ -39,7 +44,7 @@ jobs:
- test-debian-13 - test-debian-13
- valgrind - valgrind
- zig-fmt - zig-fmt
- flatpak
steps: steps:
- id: status - id: status
name: Determine status name: Determine status
@ -69,17 +74,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -94,7 +99,16 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
dir: [c-vt, zig-vt] dir:
[
c-vt,
c-vt-key-encode,
c-vt-paste,
c-vt-sgr,
zig-formatter,
zig-vt,
zig-vt-stream,
]
name: Example ${{ matrix.dir }} name: Example ${{ matrix.dir }}
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: test needs: test
@ -103,17 +117,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -136,17 +150,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -170,17 +184,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -204,6 +218,7 @@ jobs:
aarch64-linux, aarch64-linux,
x86_64-linux, x86_64-linux,
x86_64-windows, x86_64-windows,
wasm32-freestanding,
] ]
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: test needs: test
@ -212,17 +227,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -248,17 +263,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -277,17 +292,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -310,17 +325,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -356,17 +371,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -382,7 +397,7 @@ jobs:
- name: Upload artifact - name: Upload artifact
id: upload-artifact id: upload-artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with: with:
name: source-tarball name: source-tarball
path: |- path: |-
@ -394,7 +409,7 @@ jobs:
needs: [build-dist, build-snap] needs: [build-dist, build-snap]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Trigger Snap workflow - name: Trigger Snap workflow
run: | run: |
@ -406,12 +421,30 @@ jobs:
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-flatpak:
if: github.event_name != 'pull_request'
runs-on: namespace-profile-ghostty-xsm
needs: [build-dist, build-flatpak]
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Trigger Flatpak workflow
run: |
gh workflow run \
flatpak.yml \
--ref ${{ github.ref_name || 'main' }} \
--field source-run-id=${{ github.run_id }} \
--field source-artifact-id=${{ needs.build-dist.outputs.artifact-id }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos: build-macos:
runs-on: namespace-profile-ghostty-macos-tahoe runs-on: namespace-profile-ghostty-macos-tahoe
needs: test needs: test
steps: steps:
- name: Checkout code - 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 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
@ -449,12 +482,12 @@ jobs:
cd macos cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-macos-matrix: build-macos-freetype:
runs-on: namespace-profile-ghostty-macos-tahoe runs-on: namespace-profile-ghostty-macos-tahoe
needs: test needs: test
steps: steps:
- name: Checkout code - 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 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
@ -478,18 +511,10 @@ jobs:
- name: Test All - name: Test All
run: | run: |
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=freetype nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_noshape
- name: Build All - name: Build All
run: | run: |
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=freetype nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_noshape
build-windows: build-windows:
runs-on: windows-2022 runs-on: windows-2022
@ -499,7 +524,7 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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 # This could be from a script if we wanted to but inlining here for now
# in one place. # in one place.
@ -508,9 +533,9 @@ jobs:
- name: Install zig - name: Install zig
shell: pwsh shell: pwsh
run: | run: |
# Get the zig version from build.zig so that it only needs to be updated # Get the zig version from build.zig.zon so that it only needs to be updated
$fileContent = Get-Content -Path "build.zig" -Raw $fileContent = Get-Content -Path "build.zig.zon" -Raw
$pattern = 'buildpkg\.requireZig\("(.*?)"\);' $pattern = 'minimum_zig_version\s*=\s*"([^"]+)"'
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value $zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
$version = "zig-x86_64-windows-$zigVersion" $version = "zig-x86_64-windows-$zigVersion"
Write-Output $version Write-Output $version
@ -563,22 +588,29 @@ jobs:
test: test:
if: github.repository == 'ghostty-org/ghostty' if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-md runs-on: namespace-profile-ghostty-md
outputs:
zig_version: ${{ steps.zig.outputs.version }}
env: env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get required Zig version
id: zig
run: |
echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -610,17 +642,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -658,17 +690,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -693,17 +725,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -720,7 +752,7 @@ jobs:
needs: test needs: test
steps: steps:
- name: Checkout code - 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 # TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/nix-installer-action@main
@ -757,17 +789,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -787,14 +819,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -815,14 +847,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -842,14 +874,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -869,14 +901,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -896,14 +928,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -923,14 +955,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -957,14 +989,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -984,14 +1016,14 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -1018,17 +1050,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -1050,10 +1082,10 @@ jobs:
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10 uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
- name: Configure Namespace powered Buildx - name: Configure Namespace powered Buildx
uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18 uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20
- name: Download Source Tarball Artifacts - name: Download Source Tarball Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with: with:
name: source-tarball name: source-tarball
@ -1070,32 +1102,6 @@ jobs:
build-args: | build-args: |
DISTRO_VERSION=13 DISTRO_VERSION=13
flatpak:
if: github.repository == 'ghostty-org/ghostty'
name: "Flatpak"
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47
options: --privileged
strategy:
fail-fast: false
matrix:
variant:
- arch: x86_64
runner: namespace-profile-ghostty-md
- arch: aarch64
runner: namespace-profile-ghostty-md-arm64
runs-on: ${{ matrix.variant.runner }}
needs: test
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
with:
bundle: com.mitchellh.ghostty
manifest-path: flatpak/com.mitchellh.ghostty.yml
cache-key: flatpak-builder-${{ github.sha }}
arch: ${{ matrix.variant.arch }}
verbose: true
valgrind: valgrind:
if: github.repository == 'ghostty-org/ghostty' if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-lg runs-on: namespace-profile-ghostty-lg
@ -1106,17 +1112,17 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
# Install Nix and use that to run our tests so our environment matches exactly. # Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 - uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -1133,57 +1139,62 @@ jobs:
run: | run: |
nix develop -c zig build test-valgrind nix develop -c zig build test-valgrind
build-freebsd: # build-freebsd:
name: Build on FreeBSD # name: Build on FreeBSD
needs: test # needs: test
runs-on: namespace-profile-mitchellh-sm-systemd # runs-on: namespace-profile-mitchellh-sm-systemd
if: false # FIXME: FreeBSD does not yet ship with Zig 0.15 # strategy:
strategy: # matrix:
matrix: # release:
release: # - "14.3"
- "14.3" # - "15.0"
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 # timeout-minutes: 10
steps: # steps:
- name: Checkout Ghostty # - name: Checkout Ghostty
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
#
- name: Start SSH # - name: Start SSH
run: | # run: |
sudo systemctl start ssh # sudo systemctl start ssh
#
- name: Set up FreeBSD VM # - name: Set up FreeBSD VM
uses: vmactions/freebsd-vm@487ce35b96fae3e60d45b521735f5aa436ecfade # v1.2.4 # uses: vmactions/freebsd-vm@487ce35b96fae3e60d45b521735f5aa436ecfade # v1.2.4
with: # with:
release: ${{ matrix.release }} # release: ${{ matrix.release }}
copyback: false # copyback: false
usesh: true # usesh: true
prepare: | # prepare: |
pkg install -y \ # pkg install -y \
devel/blueprint-compiler \ # devel/blueprint-compiler \
devel/gettext \ # devel/gettext \
devel/git \ # devel/git \
devel/pkgconf \ # devel/pkgconf \
graphics/wayland \ # ftp/curl \
lang/zig \ # graphics/wayland \
security/ca_root_nss \ # security/ca_root_nss \
textproc/hs-pandoc \ # textproc/hs-pandoc \
x11-fonts/jetbrains-mono \ # x11-fonts/jetbrains-mono \
x11-toolkits/libadwaita \ # x11-toolkits/libadwaita \
x11-toolkits/gtk40 \ # x11-toolkits/gtk40 \
x11-toolkits/gtk4-layer-shell # x11-toolkits/gtk4-layer-shell
# curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/${{ needs.test.outputs.zig_version }}/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}.tar.xz" && \
run: | # mkdir /opt && \
zig env # tar -xf /tmp/zig.tar.xz -C /opt && \
# rm /tmp/zig.tar.xz && \
- name: Run tests # ln -s "/opt/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}/zig" /usr/local/bin/zig
shell: freebsd {0} #
run: | # run: |
cd $GITHUB_WORKSPACE # zig env
zig build test #
# - name: Run tests
- name: Build GTK app runtime # shell: freebsd {0}
shell: freebsd {0} # run: |
run: | # cd $GITHUB_WORKSPACE
cd $GITHUB_WORKSPACE # zig build test
zig build #
./zig-out/bin/ghostty +version # - name: Build GTK app runtime
# shell: freebsd {0}
# run: |
# cd $GITHUB_WORKSPACE
# zig build
# ./zig-out/bin/ghostty +version

View File

@ -17,19 +17,19 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Cache - name: Setup Cache
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18 uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
with: with:
path: | path: |
/nix /nix
/zig /zig
- name: Setup Nix - name: Setup Nix
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0 uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
with: with:
nix_path: nixpkgs=channel:nixos-unstable nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@ -37,16 +37,33 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Run zig fetch - name: Download colorschemes
id: zig_fetch id: download
env: env:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
run: | run: |
# Get the latest release from iTerm2-Color-Schemes # Get the latest release from iTerm2-Color-Schemes
RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest) RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest)
TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name') TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name')
nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz" FILENAME="ghostty-themes-${TAG_NAME}.tgz"
mkdir -p upload
curl -L -o "upload/${FILENAME}" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz"
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "filename=$FILENAME" >> $GITHUB_OUTPUT
- name: Upload to R2
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
with:
r2-account-id: ${{ secrets.CF_R2_DEPS_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_DEPS_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_DEPS_SECRET_KEY }}
r2-bucket: ghostty-deps
source-dir: upload
destination-dir: ./
- name: Run zig fetch
run: |
nix develop -c zig fetch --save="iterm2_themes" "https://deps.files.ghostty.org/${{ steps.download.outputs.filename }}"
- name: Update zig cache hash - name: Update zig cache hash
run: | run: |
@ -62,7 +79,7 @@ jobs:
run: nix build .#ghostty run: nix build .#ghostty
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
with: with:
title: Update iTerm2 colorschemes title: Update iTerm2 colorschemes
base: main base: main
@ -75,5 +92,5 @@ jobs:
build.zig.zon.json build.zig.zon.json
flatpak/zig-packages.json flatpak/zig-packages.json
body: | body: |
Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.zig_fetch.outputs.tag_name }} Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.download.outputs.tag_name }}
labels: dependencies labels: dependencies

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ zig-cache/
.zig-cache/ .zig-cache/
zig-out/ zig-out/
/result* /result*
/.nixos-test-history
example/*.wasm example/*.wasm
test/ghostty test/ghostty
test/cases/**/*.actual.png test/cases/**/*.actual.png

View File

@ -13,11 +13,22 @@ A file for [guiding coding agents](https://agents.md/).
## Directory Structure ## Directory Structure
- Shared Zig core: `src/` - Shared Zig core: `src/`
- C API: `include/ghostty.h` - C API: `include`
- macOS app: `macos/` - macOS app: `macos/`
- GTK (Linux and FreeBSD) app: `src/apprt/gtk` - GTK (Linux and FreeBSD) app: `src/apprt/gtk`
## libghostty-vt
- Build: `zig build lib-vt`
- Build Wasm Module: `zig build lib-vt -Dtarget=wasm32-freestanding`
- Test: `zig build test-lib-vt`
- Test filter: `zig build test-lib-vt -Dtest-filter=<test name>`
- When working on libghostty-vt, do not build the full app.
- For C only changes, don't run the Zig tests. Build all the examples.
## macOS App ## macOS App
- Do not use `xcodebuild` - Do not use `xcodebuild`
- Use `zig build` to build the macOS app and any shared Zig code - Use `zig build` to build the macOS app and any shared Zig code
- Use `zig build run` to build and run the macOS app
- Run Xcode tests using `zig build test`

View File

@ -184,6 +184,7 @@
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR /po/ko_KR.UTF-8.po @ghostty-org/ko_KR
/po/he_IL.UTF-8.po @ghostty-org/he_IL /po/he_IL.UTF-8.po @ghostty-org/he_IL
/po/it_IT.UTF-8.po @ghostty-org/it_IT /po/it_IT.UTF-8.po @ghostty-org/it_IT
/po/lt_LT.UTF-8.po @ghostty-org/lt_LT
/po/zh_TW.UTF-8.po @ghostty-org/zh_TW /po/zh_TW.UTF-8.po @ghostty-org/zh_TW
/po/hr_HR.UTF-8.po @ghostty-org/hr_HR /po/hr_HR.UTF-8.po @ghostty-org/hr_HR

View File

@ -17,15 +17,62 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
> [!IMPORTANT] > [!IMPORTANT]
> >
> If you are using **any kind of AI assistance** to contribute to Ghostty, > The Ghostty project allows AI-**assisted** _code contributions_, which
> it must be disclosed in the pull request. > must be properly disclosed in the pull request.
If you are using any kind of AI assistance while contributing to Ghostty, If you are using any kind of AI assistance while contributing to Ghostty,
**this must be disclosed in the pull request**, along with the extent to **this must be disclosed in the pull request**, along with the extent to
which AI assistance was used (e.g. docs only vs. code generation). which AI assistance was used (e.g. docs only vs. code generation).
If PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed, The submitter must have also tested the pull request on all impacted
so long as it is limited to single keywords or short phrases. platforms, and it's **highly discouraged** to code for an unfamiliar platform
with AI assistance alone: if you only have a macOS machine, do **not** ask AI
to write the equivalent GTK code, and vice versa — someone else with more
expertise will eventually get to it and do it for you.
> [!WARNING]
> **Note that AI _assistance_ does not equal AI _generation_**. We require
> a significant amount of human accountability, involvement and interaction
> even within AI-assisted contributions. Contributors are required to be able
> to understand the AI-assisted output, reason with it and answer critical
> questions about it. Should a PR see no visible human accountability and
> involvement, or it is so broken that it requires significant rework to be
> acceptable, **we reserve the right to close it without hesitation**.
**In addition, we currently restrict AI assistance to code changes only.**
No AI-generated media, e.g. artwork, icons, videos and other assets is
allowed, as it goes against the methodology and ethos behind Ghostty.
While AI-assisted code can help with productive prototyping, creative
inspiration and even automated bugfinding, we have currently found zero
benefit to AI-generated assets. Instead, we are far more interested and
invested in funding professional work done by human designers and artists.
If you intend to submit AI-generated assets to Ghostty, sorry,
we are not interested.
Likewise, all community interactions, including all comments on issues and
discussions and all PR titles and descriptions **must be composed by a human**.
Community moderators and Ghostty maintainers reserve the right to mark
AI-generated responses as spam or disruptive content, and ban users who have
been repeatedly caught relying entirely on LLMs during interactions.
> [!NOTE]
> If your English isn't the best and you are currently relying on an LLM to
> translate your responses, don't fret — usually we maintainers will be able
> to understand your messages well enough. We'd like to encourage real humans
> to interact with each other more, and the positive impact of genuine,
> responsive yet imperfect human interaction more than makes up for any
> language barrier.
>
> Please write your responses yourself, to the best of your ability.
> If you do feel the need to polish your sentences, however, please use
> dedicated translation software rather than an LLM.
>
> We greatly appreciate it. Thank you. ❤️
Minor exceptions to this policy include trivial AI-generated tab completion
functionality, as it usually does not impact the quality of the code and
do not need to be disclosed, and commit titles and messages, which are often
generated by AI coding agents.
An example disclosure: An example disclosure:
@ -36,6 +83,11 @@ Or a more detailed disclosure:
> I consulted ChatGPT to understand the codebase but the solution > I consulted ChatGPT to understand the codebase but the solution
> was fully authored manually by myself. > was fully authored manually by myself.
An example of a **problematic** disclosure (not having tested all platforms):
> I used Amp to code both macOS and GTK UIs, but I have not yet tested
> the GTK UI as I don't have a Linux setup.
Failure to disclose this is first and foremost rude to the human operators Failure to disclose this is first and foremost rude to the human operators
on the other end of the pull request, but it also makes it difficult to on the other end of the pull request, but it also makes it difficult to
determine how much scrutiny to apply to the contribution. determine how much scrutiny to apply to the contribution.
@ -45,11 +97,6 @@ work than any human. That isn't the world we live in today, and in most cases
it's generating slop. I say this despite being a fan of and using them it's generating slop. I say this despite being a fan of and using them
successfully myself (with heavy supervision)! successfully myself (with heavy supervision)!
When using AI assistance, we expect contributors to understand the code
that is produced and be able to answer critical questions about it. It
isn't a maintainers job to review a PR so broken that it requires
significant rework to be acceptable.
Please be respectful to maintainers and disclose AI assistance. Please be respectful to maintainers and disclose AI assistance.
## Quick Guide ## Quick Guide
@ -74,22 +121,47 @@ submission.
### I have a bug! / Something isn't working ### I have a bug! / Something isn't working
1. Search the issue tracker and discussions for similar issues. Tip: also First, search the issue tracker and discussions for similar issues. Tip: also
search for [closed issues] and [discussions] — your issue might have already search for [closed issues] and [discussions] — your issue might have already
been fixed! been fixed!
2. If your issue hasn't been reported already, open an ["Issue Triage" discussion]
and make sure to fill in the template **completely**. They are vital for > [!NOTE]
maintainers to figure out important details about your setup. Because of >
this, please make sure that you _only_ use the "Issue Triage" category for > If there is an _open_ issue or discussion that matches your problem,
reporting bugs — thank you! > **please do not comment on it unless you have valuable insight to add**.
>
> GitHub has a very _noisy_ set of default notification settings which
> sends an email to _every participant_ in an issue/discussion every time
> someone adds a comment. Instead, use the handy upvote button for discussions,
> and/or emoji reactions on both discussions and issues, which are a visible
> yet non-disruptive way to show your support.
If your issue hasn't been reported already, open an ["Issue Triage"] discussion
and make sure to fill in the template **completely**. They are vital for
maintainers to figure out important details about your setup.
> [!WARNING]
>
> A _very_ common mistake is to file a bug report either as a Q&A or a Feature
> Request. **Please don't do this.** Otherwise, maintainers would have to ask
> for your system information again manually, and sometimes they will even ask
> you to create a new discussion because of how few detailed information is
> required for other discussion types compared to Issue Triage.
>
> Because of this, please make sure that you _only_ use the "Issue Triage"
> category for reporting bugs — thank you!
[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed [closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed
[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed [discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed
["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage ["Issue Triage"]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage
### I have an idea for a feature ### I have an idea for a feature
Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas). Like bug reports, first search through both issues and discussions and try to
find if your feature has already been requested. Otherwise, open a discussion
in the ["Feature Requests, Ideas"] category.
["Feature Requests, Ideas"]: https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas
### I've implemented a feature ### I've implemented a feature
@ -98,10 +170,28 @@ Open a discussion in the ["Feature Requests, Ideas" category](https://github.com
3. If you want to live dangerously, open a pull request and 3. If you want to live dangerously, open a pull request and
[hope for the best](#pull-requests-implement-an-issue). [hope for the best](#pull-requests-implement-an-issue).
### I have a question ### I have a question which is neither a bug report nor a feature request
Open an [Q&A discussion], or join our [Discord Server] and ask away in the Open an [Q&A discussion], or join our [Discord Server] and ask away in the
`#help` channel. `#help` forum channel.
Do not use the `#terminals` or `#development` channels to ask for help —
those are for general discussion about terminals and Ghostty development
respectively. If you do ask a question there, you will be redirected to
`#help` instead.
> [!NOTE]
> If your question is about a missing feature, please open a discussion under
> the ["Feature Requests, Ideas"] category. If Ghostty is behaving
> unexpectedly, use the ["Issue Triage"] category.
>
> The "Q&A" category is strictly for other kinds of discussions and do not
> require detailed information unlike the two other categories, meaning that
> maintainers would have to spend the extra effort to ask for basic information
> if you submit a bug report under this category.
>
> Therefore, please **pay attention to the category** before opening
> discussions to save us all some time and energy. Thank you!
[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a [Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a
[Discord Server]: https://discord.gg/ghostty [Discord Server]: https://discord.gg/ghostty
@ -142,3 +232,266 @@ pull request will be accepted with a high degree of certainty.
> **Pull requests are NOT a place to discuss feature design.** Please do > **Pull requests are NOT a place to discuss feature design.** Please do
> not open a WIP pull request to discuss a feature. Instead, use a discussion > not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch. > and link to your branch.
# Developer Guide
> [!NOTE]
>
> **The remainder of this file is dedicated to developers actively
> working on Ghostty.** If you're a user reporting an issue, you can
> ignore the rest of this document.
## Including and Updating Translations
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
## Checking for Memory Leaks
While Zig does an amazing job of finding and preventing memory leaks,
Ghostty uses many third-party libraries that are written in C. Improper usage
of those libraries or bugs in those libraries can cause memory leaks that
Zig cannot detect by itself.
### On Linux
On Linux the recommended tool to check for memory leaks is Valgrind. The
recommended way to run Valgrind is via `zig build`:
```sh
zig build run-valgrind
```
This builds a Ghostty executable with Valgrind support and runs Valgrind
with the proper flags to ensure we're suppressing known false positives.
You can combine the same build args with `run-valgrind` that you can with
`run`, such as specifying additional configurations after a trailing `--`.
## Input Stack Testing
The input stack is the part of the codebase that starts with a
key event and ends with text encoding being sent to the pty (it
does not include _rendering_ the text, which is part of the
font or rendering stack).
If you modify any part of the input stack, you must manually verify
all the following input cases work properly. We unfortunately do
not automate this in any way, but if we can do that one day that'd
save a LOT of grief and time.
Note: this list may not be exhaustive, I'm still working on it.
### Linux IME
IME (Input Method Editors) are a common source of bugs in the input stack,
especially on Linux since there are multiple different IME systems
interacting with different windowing systems and application frameworks
all written by different organizations.
The following matrix should be tested to ensure that all IME input works
properly:
1. Wayland, X11
2. ibus, fcitx, none
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
> [!NOTE]
>
> This is a **work in progress**. I'm still working on this list and it
> is not complete. As I find more test cases, I will add them here.
#### Dead Key Input
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
1. Launch Ghostty
2. Press `'`
3. Press `a`
4. Verify that `á` is displayed
Note that the dead key may or may not show a preedit state visually.
For ibus and fcitx it does but for the "none" case it does not. Importantly,
the text should be correct when it is sent to the pty.
We should also test canceling dead key input:
1. Launch Ghostty
2. Press `'`
3. Press escape
4. Press `a`
5. Verify that `a` is displayed (no diacritic)
#### CJK Input
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
exact layout doesn't matter.
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Enter`
5. Verify that `こん` is displayed in the terminal.
We should also test switching input methods while preedit is active, which
should commit the text:
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Ctrl+Shift` to switch to another layout (any)
5. Verify that `こん` is displayed in the terminal as committed text.
## Nix Virtual Machines
Several Nix virtual machine definitions are provided by the project for testing
and developing Ghostty against multiple different Linux desktop environments.
Running these requires a working Nix installation, either Nix on your
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
requirements for macOS are detailed below.
VMs should only be run on your local desktop and then powered off when not in
use, which will discard any changes to the VM.
The VM definitions provide minimal software "out of the box" but additional
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
with `common` or `create`.
3. The VM will build and then launch. Depending on the speed of your system, this
can take a while, but eventually you should get a new VM window.
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
writable by the VM user, so be careful!
### macOS
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
blog post for more information about the Linux builder and how to tune the performance.
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
above to launch a VM.
### Custom VMs
To easily create a custom VM without modifying the Ghostty source, create a new
directory, then create a file called `flake.nix` with the following text in the
new directory.
```
{
inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
ghostty.url = "github:ghostty-org/ghostty";
};
outputs = {
nixpkgs,
ghostty,
...
}: {
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
nixpkgs = nixpkgs;
system = "x86_64-linux";
overlay = ghostty.overlays.releasefast;
# module = ./configuration.nix # also works
module = {pkgs, ...}: {
environment.systemPackages = [
pkgs.btop
];
};
};
};
}
```
The custom VM can then be run with a command like this:
```
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
```
A file named `ghostty.qcow2` will be created that is used to persist any changes
made in the VM. To "reset" the VM to default delete the file and it will be
recreated the next time you run the VM.
### Contributing new VM definitions
#### VM Acceptance Criteria
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
1. They should be different enough from existing VM definitions that they represent a distinct
user (and developer) experience.
2. There's a significant Ghostty user population that uses a similar environment.
3. The VMs can be built using only packages from the current stable NixOS release.
#### VM Definition Criteria
1. VMs should be as minimal as possible so that they build and launch quickly.
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
2. VMs should not expose any services to the network, or run any remote access
software like SSH daemons, VNC or RDP.
3. VMs should auto-login using the "ghostty" user.
## Nix VM Integration Tests
Several Nix VM tests are provided by the project for testing Ghostty in a "live"
environment rather than just unit tests.
Running these requires a working Nix installation, either Nix on your
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
requirements for macOS are detailed below.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#check.<system>.<test-name>.driver`. `<system>` should be
`x86_64-linux` or `aarch64-linux` (even on macOS, this launches a Linux
VM, not a macOS one). `<test-name>` should be one of the tests defined in
`nix/tests.nix`. The test will build and then launch. Depending on the speed
of your system, this can take a while. Eventually though the test should
complete. Hopefully successfully, but if not error messages should be printed
out that can be used to diagnose the issue.
3. To run _all_ of the tests, run `nix flake check`.
### macOS
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
blog post for more information about the Linux builder and how to tune the performance.
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
above to launch a test.
### Interactively Running Test VMs
To run a test interactively, run `nix run
.#check.<system>.<test-name>.driverInteractive`. This will load a Python console
that can be used to manage the test VMs. In this console run `start_all()` to
start the VM(s). The VMs should boot up and a window should appear showing the
VM's console.
For more information about the Nix test console, see [the NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-call-nixos-test-outside-nixos)
### SSH Access to Test VMs
Some test VMs are configured to allow outside SSH access for debugging. To
access the VM, use a command like the following:
```
ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 root@192.168.122.1
ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 ghostty@192.168.122.1
```
The SSH options are important because the SSH host keys will be regenerated
every time the test is started. Without them, your personal SSH known hosts file
will become difficult to manage. The port that is needed to access the VM may
change depending on the test.
None of the users in the VM have passwords so do not expose these VMs to the Internet.

View File

@ -2,9 +2,52 @@
DOXYFILE_ENCODING = UTF-8 DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "libghostty" PROJECT_NAME = "libghostty"
INPUT = include/ghostty/vt.h PROJECT_LOGO = images/gnome/64.png
INPUT = include/ghostty
INPUT_ENCODING = UTF-8 INPUT_ENCODING = UTF-8
RECURSIVE = NO RECURSIVE = YES
FILE_PATTERNS = *.h
EXAMPLE_PATH = example
EXAMPLE_RECURSIVE = YES
EXAMPLE_PATTERNS = *
FULL_PATH_NAMES = NO
STRIP_FROM_INC_PATH = include
SOURCE_BROWSER = YES
INLINE_SOURCES = NO
REFERENCES_RELATION = YES
REFERENCED_BY_RELATION = YES
#---------------------------------------------------------------------------
# Preprocessor
#---------------------------------------------------------------------------
# Enable preprocessing to handle #ifdef guards
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = YES
PREDEFINED = __wasm__
#---------------------------------------------------------------------------
# C API Optimization
#---------------------------------------------------------------------------
# Optimize output for C API documentation
OPTIMIZE_OUTPUT_FOR_C = YES
TYPEDEF_HIDES_STRUCT = YES
HIDE_SCOPE_NAMES = YES
# Clean path names
FULL_PATH_NAMES = NO
STRIP_FROM_PATH = .
STRIP_FROM_INC_PATH = include
# Hide undocumented and internal APIs
HIDE_UNDOC_MEMBERS = YES
HIDE_UNDOC_CLASSES = YES
EXTRACT_ALL = NO
INTERNAL_DOCS = NO
EXTRACT_PRIVATE = NO
EXTRACT_LOCAL_CLASSES = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# HTML Output # HTML Output
@ -12,6 +55,26 @@ RECURSIVE = NO
GENERATE_HTML = YES GENERATE_HTML = YES
HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
HTML_EXTRA_STYLESHEET = dist/doxygen/ghostty.css
HTML_EXTRA_FILES = dist/doxygen/favicon.png \
dist/doxygen/mobile-nav.js
HTML_COLORSTYLE = DARK
HTML_CODE_FOLDING = NO
HTML_HEADER = dist/doxygen/header.html
LAYOUT_FILE = DoxygenLayout.xml
GENERATE_TREEVIEW = YES
HTML_DYNAMIC_SECTIONS = YES
SEARCHENGINE = YES
ALPHABETICAL_INDEX = YES
HTML_TIMESTAMP = NO
DISABLE_INDEX = NO
FULL_SIDEBAR = NO
#---------------------------------------------------------------------------
# Graphs and Diagrams
#---------------------------------------------------------------------------
HAVE_DOT = NO
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Man Output # Man Output
@ -20,6 +83,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
GENERATE_MAN = YES GENERATE_MAN = YES
MAN_OUTPUT = zig-out/share/man MAN_OUTPUT = zig-out/share/man
MAN_EXTENSION = .3 MAN_EXTENSION = .3
MAN_LINKS = YES
#--------------------------------------------------------------------------- #---------------------------------------------------------------------------
# Other Output # Other Output

247
DoxygenLayout.xml Normal file
View File

@ -0,0 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<doxygenlayout version="2.0">
<!-- Generated by doxygen 1.14.0 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="topics" visible="yes" title="" intro=""/>
<tab type="modules" visible="yes" title="API Groups" intro="">
<tab type="modulelist" visible="yes" title="" intro=""/>
<tab type="modulemembers" visible="yes" title="" intro=""/>
</tab>
<tab type="structs" visible="yes" title="Data Types" intro="">
<tab type="structlist" visible="yes" title="" intro=""/>
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
</tab>
<tab type="files" visible="yes" title="C Headers" intro="">
<tab type="filelist" visible="yes" title="" intro=""/>
<tab type="globals" visible="yes" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro=""/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<inheritancegraph visible="yes"/>
<collaborationgraph visible="yes"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicmethods visible="yes" title=""/>
<publicstaticmethods visible="yes" title=""/>
<publicattributes visible="yes" title=""/>
<publicstaticattributes visible="yes" title=""/>
<protectedtypes visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<protectedmethods visible="yes" title=""/>
<protectedstaticmethods visible="yes" title=""/>
<protectedattributes visible="yes" title=""/>
<protectedstaticattributes visible="yes" title=""/>
<packagetypes visible="yes" title=""/>
<packagemethods visible="yes" title=""/>
<packagestaticmethods visible="yes" title=""/>
<packageattributes visible="yes" title=""/>
<packagestaticattributes visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
<privatetypes visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<privatemethods visible="yes" title=""/>
<privatestaticmethods visible="yes" title=""/>
<privateattributes visible="yes" title=""/>
<privatestaticattributes visible="yes" title=""/>
<friends visible="yes" title=""/>
<related visible="yes" title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<services visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<constructors visible="yes" title=""/>
<functions visible="yes" title=""/>
<related visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<events visible="yes" title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="yes"/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<concepts visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a concept page -->
<concept>
<briefdescription visible="yes"/>
<includes visible="$SHOW_HEADERFILE"/>
<definition visible="yes" title=""/>
<detaileddescription visible="yes" title=""/>
<authorsection visible="yes"/>
</concept>
<!-- Layout definition for a file page -->
<file>
<briefdescription visible="yes"/>
<includes visible="$SHOW_INCLUDE_FILES"/>
<includegraph visible="yes"/>
<includedbygraph visible="yes"/>
<sourcelink visible="yes"/>
<memberdecl>
<interfaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<structs visible="yes" title=""/>
<exceptions visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdef>
<inlineclasses visible="yes" title=""/>
<defines visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<enums visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<properties visible="yes" title=""/>
</memberdef>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="yes"/>
<detaileddescription visible="yes" title=""/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="no" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<defines visible="yes" title=""/>
<modules visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
<membergroups visible="yes"/>
</memberdecl>
<memberdef>
<pagedocs/>
<inlineclasses visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<enums visible="yes" title=""/>
<enumvalues visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<defines visible="yes" title=""/>
<sequences visible="yes" title=""/>
<dictionaries visible="yes" title=""/>
<signals visible="yes" title=""/>
<publicslots visible="yes" title=""/>
<protectedslots visible="yes" title=""/>
<privateslots visible="yes" title=""/>
<events visible="yes" title=""/>
<properties visible="yes" title=""/>
<friends visible="yes" title=""/>
</memberdef>
<groupgraph visible="yes"/>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a C++20 module page -->
<module>
<briefdescription visible="yes"/>
<exportedmodules visible="yes"/>
<memberdecl>
<concepts visible="yes" title=""/>
<classes visible="yes" title=""/>
<enums visible="yes" title=""/>
<typedefs visible="yes" title=""/>
<functions visible="yes" title=""/>
<variables visible="yes" title=""/>
<membergroups visible="yes" title=""/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
<memberdecl>
<files visible="yes"/>
</memberdecl>
</module>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription visible="yes" title=""/>
</directory>
</doxygenlayout>

View File

@ -50,24 +50,22 @@ macOS users don't require any additional dependencies.
## Xcode Version and SDKs ## Xcode Version and SDKs
Building the Ghostty macOS app requires that Xcode, the macOS SDK, Building the Ghostty macOS app requires that Xcode, the macOS SDK,
and the iOS SDK are all installed. the iOS SDK, and Metal Toolchain are all installed.
A common issue is that the incorrect version of Xcode is either A common issue is that the incorrect version of Xcode is either
installed or selected. Use the `xcode-select` command to installed or selected. Use the `xcode-select` command to
ensure that the correct version of Xcode is selected: ensure that the correct version of Xcode is selected:
```shell-session ```shell-session
sudo xcode-select --switch /Applications/Xcode-beta.app sudo xcode-select --switch /Applications/Xcode.app
``` ```
> [!IMPORTANT] > [!IMPORTANT]
> >
> Main branch development of Ghostty is preparing for the next major > Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**.
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
> **Xcode 26 and the macOS 26 SDK**.
> >
> You do not need to be running on macOS 26 to build Ghostty, you can > You do not need to be running on macOS 26 to build Ghostty, you can
> still use Xcode 26 beta on macOS 15 stable. > still use Xcode 26 on macOS 15 stable.
## AI and Agents ## AI and Agents
@ -95,6 +93,36 @@ produced.
> may ask you to fix it and close the issue. It isn't a maintainers job to > may ask you to fix it and close the issue. It isn't a maintainers job to
> review a PR so broken that it requires significant rework to be acceptable. > review a PR so broken that it requires significant rework to be acceptable.
## Logging
Ghostty can write logs to a number of destinations. On all platforms, logging to
`stderr` is available. Depending on the platform and how Ghostty was launched,
logs sent to `stderr` may be stored by the system and made available for later
retrieval.
On Linux if Ghostty is launched by the default `systemd` user service, you can use
`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`.
On macOS logging to the macOS unified log is available and enabled by default.
Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`.
Ghostty's logging can be configured in two ways. The first is by what
optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug`
optimizations debug logs will be output to `stderr`. If Ghostty is compiled with
any other optimization the debug logs will not be output to `stderr`.
Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used
to control which destinations receive logs. Ghostty currently defines two
destinations:
- `stderr` - logging to `stderr`.
- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
Combine values with a comma to enable multiple destinations. Prefix a
destination with `no-` to disable it. Enabling and disabling destinations
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
## Linting ## Linting
### Prettier ### Prettier

View File

@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h
clean: clean:
rm -rf \ rm -rf \
zig-out zig-cache \ zig-out .zig-cache \
macos/build \ macos/build \
macos/GhosttyKit.xcframework macos/GhosttyKit.xcframework
.PHONY: clean .PHONY: clean

View File

@ -144,16 +144,23 @@ In addition to being a standalone terminal emulator, Ghostty is a
C-compatible library for embedding a fast, feature-rich terminal emulator C-compatible library for embedding a fast, feature-rich terminal emulator
in any 3rd party project. This library is called `libghostty`. in any 3rd party project. This library is called `libghostty`.
This goal is not hypothetical! The macOS app is a `libghostty` consumer. Due to the scope of this project, we're breaking libghostty down into
separate actually libraries, starting with `libghostty-vt`. The goal of
this project is to focus on parsing terminal sequences and maintaining
terminal state. This is covered in more detail in this
[blog post](https://mitchellh.com/writing/libghostty-is-coming).
`libghostty-vt` is already available and usable today for Zig and C and
is compatible for macOS, Linux, Windows, and WebAssembly. At the time of
writing this, the API isn't stable yet and we haven't tagged an official
release, but the core logic is well proven (since Ghostty uses it) and
we're working hard on it now.
The ultimate goal is not hypothetical! The macOS app is a `libghostty` consumer.
The macOS app is a native Swift app developed in Xcode and `main()` is The macOS app is a native Swift app developed in Xcode and `main()` is
within Swift. The Swift app links to `libghostty` and uses the C API to within Swift. The Swift app links to `libghostty` and uses the C API to
render terminals. render terminals.
This step encompasses expanding `libghostty` support to more platforms
and more use cases. At the time of writing this, `libghostty` is very
Mac-centric -- particularly around rendering -- and we have work to do to
expand this to other platforms.
## Crash Reports ## Crash Reports
Ghostty has a built-in crash reporter that will generate and save crash Ghostty has a built-in crash reporter that will generate and save crash
@ -193,4 +200,4 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
> purposely contain sensitive information, but it does contain the full > purposely contain sensitive information, but it does contain the full
> stack memory of each thread at the time of the crash. This information > stack memory of each thread at the time of the crash. This information
> is used to rebuild the stack trace but can also contain sensitive data > is used to rebuild the stack trace but can also contain sensitive data
> depending when the crash occurred. > depending on when the crash occurred.

View File

@ -3,19 +3,19 @@ const assert = std.debug.assert;
const builtin = @import("builtin"); const builtin = @import("builtin");
const buildpkg = @import("src/build/main.zig"); const buildpkg = @import("src/build/main.zig");
const appVersion = @import("build.zig.zon").version;
const minimumZigVersion = @import("build.zig.zon").minimum_zig_version;
comptime { comptime {
buildpkg.requireZig("0.15.1"); buildpkg.requireZig(minimumZigVersion);
} }
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
// Works around a Zig but still present in 0.15.1. Remove when fixed.
// https://github.com/ghostty-org/ghostty/issues/8924
try limitCoresForZigBug();
// This defines all the available build options (e.g. `-D`). If you // This defines all the available build options (e.g. `-D`). If you
// want to know what options are available, you can run `--help` or // want to know what options are available, you can run `--help` or
// you can read `src/build/Config.zig`. // you can read `src/build/Config.zig`.
const config = try buildpkg.Config.init(b);
const config = try buildpkg.Config.init(b, appVersion);
const test_filters = b.option( const test_filters = b.option(
[][]const u8, [][]const u8,
"test-filter", "test-filter",
@ -56,7 +56,7 @@ pub fn build(b: *std.Build) !void {
); );
// Ghostty resources like terminfo, shell integration, themes, etc. // 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; const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
// Ghostty executable, the actual runnable Ghostty program. // Ghostty executable, the actual runnable Ghostty program.
@ -102,10 +102,19 @@ pub fn build(b: *std.Build) !void {
); );
// libghostty-vt // libghostty-vt
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared( const libghostty_vt_shared = shared: {
b, if (config.target.result.cpu.arch.isWasm()) {
&mod, break :shared try buildpkg.GhosttyLibVt.initWasm(
); b,
&mod,
);
}
break :shared try buildpkg.GhosttyLibVt.initShared(
b,
&mod,
);
};
libghostty_vt_shared.install(libvt_step); libghostty_vt_shared.install(libvt_step);
libghostty_vt_shared.install(b.getInstallStep()); libghostty_vt_shared.install(b.getInstallStep());
@ -309,13 +318,3 @@ pub fn build(b: *std.Build) !void {
try translations_step.addError("cannot update translations when i18n is disabled", .{}); try translations_step.addError("cannot update translations when i18n is disabled", .{});
} }
} }
// WARNING: Remove this when https://github.com/ghostty-org/ghostty/issues/8924 is resolved!
// Limit ourselves to 32 cpus on Linux because of an upstream Zig bug.
fn limitCoresForZigBug() !void {
if (comptime builtin.os.tag != .linux) return;
const pid = std.os.linux.getpid();
var set: std.bit_set.ArrayBitSet(usize, std.os.linux.CPU_SETSIZE * 8) = .initEmpty();
for (0..32) |cpu| set.set(cpu);
try std.os.linux.sched_setaffinity(pid, &set.masks);
}

View File

@ -1,9 +1,9 @@
.{ .{
.name = .ghostty, .name = .ghostty,
.version = "1.2.1", .version = "1.3.0-dev",
.paths = .{""}, .paths = .{""},
.fingerprint = 0x64407a2a0b4147e5, .fingerprint = 0x64407a2a0b4147e5,
.minimum_zig_version = "0.15.1", .minimum_zig_version = "0.15.2",
.dependencies = .{ .dependencies = .{
// Zig libs // Zig libs
@ -15,14 +15,14 @@
}, },
.vaxis = .{ .vaxis = .{
// rockorager/libvaxis // rockorager/libvaxis
.url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", .url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
.hash = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", .hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
.lazy = true, .lazy = true,
}, },
.z2d = .{ .z2d = .{
// vancluever/z2d // vancluever/z2d
.url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", .url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
.hash = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", .hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
.lazy = true, .lazy = true,
}, },
.zig_objc = .{ .zig_objc = .{
@ -38,9 +38,9 @@
.lazy = true, .lazy = true,
}, },
.uucode = .{ .uucode = .{
// TODO: currently the use-llvm branch because its broken on self-hosted // jacobsandlund/uucode
.url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", .url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
.hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", .hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
}, },
.zig_wayland = .{ .zig_wayland = .{
// codeberg ifreund/zig-wayland // codeberg ifreund/zig-wayland
@ -50,15 +50,15 @@
}, },
.zf = .{ .zf = .{
// natecraddock/zf // natecraddock/zf
.url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", .url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
.hash = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", .hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
.lazy = true, .lazy = true,
}, },
.gobject = .{ .gobject = .{
// https://github.com/jcollie/ghostty-gobject based on zig_gobject // https://github.com/ghostty-org/zig-gobject based on zig_gobject
// Temporary until we generate them at build time automatically. // Temporary until we generate them at build time automatically.
.url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", .url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
.hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", .hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
.lazy = true, .lazy = true,
}, },
@ -116,8 +116,8 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
.hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", .hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
.lazy = true, .lazy = true,
}, },
}, },

124
build.zig.zon.bak Normal file
View File

@ -0,0 +1,124 @@
.{
.name = .ghostty,
.version = "1.3.0-dev",
.paths = .{""},
.fingerprint = 0x64407a2a0b4147e5,
.minimum_zig_version = "0.15.2",
.dependencies = .{
// Zig libs
.libxev = .{
// mitchellh/libxev
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
.lazy = true,
},
.vaxis = .{
// rockorager/libvaxis
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
.lazy = true,
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
.lazy = true,
},
.zig_objc = .{
// mitchellh/zig-objc
.url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
.hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
.lazy = true,
},
.zig_js = .{
// mitchellh/zig-js
.url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
.hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
.lazy = true,
},
.uucode = .{
// 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
.url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
.hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
.lazy = true,
},
.zf = .{
// natecraddock/zf
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
.lazy = true,
},
.gobject = .{
// https://github.com/ghostty-org/zig-gobject based on zig_gobject
// Temporary until we generate them at build time automatically.
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
.lazy = true,
},
// C libs
.cimgui = .{ .path = "./pkg/cimgui", .lazy = true },
.fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true },
.freetype = .{ .path = "./pkg/freetype", .lazy = true },
.gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true },
.harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true },
.highway = .{ .path = "./pkg/highway", .lazy = true },
.libintl = .{ .path = "./pkg/libintl", .lazy = true },
.libpng = .{ .path = "./pkg/libpng", .lazy = true },
.macos = .{ .path = "./pkg/macos", .lazy = true },
.oniguruma = .{ .path = "./pkg/oniguruma", .lazy = true },
.opengl = .{ .path = "./pkg/opengl", .lazy = true },
.sentry = .{ .path = "./pkg/sentry", .lazy = true },
.simdutf = .{ .path = "./pkg/simdutf", .lazy = true },
.utfcpp = .{ .path = "./pkg/utfcpp", .lazy = true },
.wuffs = .{ .path = "./pkg/wuffs", .lazy = true },
.zlib = .{ .path = "./pkg/zlib", .lazy = true },
// Shader translation
.glslang = .{ .path = "./pkg/glslang", .lazy = true },
.spirv_cross = .{ .path = "./pkg/spirv-cross", .lazy = true },
// Wayland
.wayland = .{
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
.hash = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t",
.lazy = true,
},
.wayland_protocols = .{
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
.hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S",
.lazy = true,
},
.plasma_wayland_protocols = .{
.url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
.hash = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs",
.lazy = true,
},
// Fonts
.jetbrains_mono = .{
.url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
.hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
.lazy = true,
},
.nerd_fonts_symbols_only = .{
.url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
.hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
.lazy = true,
},
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
.hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
.lazy = true,
},
},
}

59
build.zig.zon.json generated
View File

@ -24,10 +24,10 @@
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz", "url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=" "hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
}, },
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": { "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": {
"name": "gobject", "name": "gobject",
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", "url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
"hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=" "hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="
}, },
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": { "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
"name": "gtk4_layer_shell", "name": "gtk4_layer_shell",
@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
}, },
"N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": { "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-": {
"name": "iterm2_themes", "name": "iterm2_themes",
"url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
"hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=" "hash": "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk="
}, },
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono", "name": "jetbrains_mono",
@ -109,20 +109,20 @@
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=" "hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
}, },
"uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": { "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM": {
"name": "uucode", "name": "uucode",
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
"hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=" "hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="
}, },
"vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": { "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": {
"name": "vaxis", "name": "uucode",
"url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=" "hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="
}, },
"vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA": { "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
"name": "vaxis", "name": "vaxis",
"url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", "url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
"hash": "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM=" "hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="
}, },
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": { "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
"name": "wayland", "name": "wayland",
@ -139,25 +139,15 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
}, },
"z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef": { "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": {
"name": "z2d", "name": "z2d",
"url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", "url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
"hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=" "hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="
}, },
"zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR": { "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {
"name": "zf", "name": "zf",
"url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", "url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
"hash": "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ=" "hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="
},
"zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM": {
"name": "zg",
"url": "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9",
"hash": "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU="
},
"zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9": {
"name": "zg",
"url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz",
"hash": "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI="
}, },
"zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": { "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": {
"name": "zig_js", "name": "zig_js",
@ -174,11 +164,6 @@
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz", "url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
"hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=" "hash": "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="
}, },
"zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL": {
"name": "zigimg",
"url": "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726",
"hash": "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0="
},
"zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": { "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms": {
"name": "zigimg", "name": "zigimg",
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",

72
build.zig.zon.nix generated
View File

@ -5,7 +5,7 @@
fetchurl, fetchurl,
fetchgit, fetchgit,
runCommandLocal, runCommandLocal,
zig_0_14, zig_0_15,
name ? "zig-packages", name ? "zig-packages",
}: let }: let
unpackZigArtifact = { unpackZigArtifact = {
@ -14,7 +14,7 @@
}: }:
runCommandLocal name runCommandLocal name
{ {
nativeBuildInputs = [zig_0_14]; nativeBuildInputs = [zig_0_15];
} }
'' ''
hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
@ -123,11 +123,11 @@ in
}; };
} }
{ {
name = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV"; name = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "gobject"; name = "gobject";
url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst"; url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst";
hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="; hash = "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg=";
}; };
} }
{ {
@ -163,11 +163,11 @@ in
}; };
} }
{ {
name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv"; name = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "iterm2_themes"; name = "iterm2_themes";
url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz"; url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz";
hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="; hash = "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk=";
}; };
} }
{ {
@ -259,27 +259,27 @@ in
}; };
} }
{ {
name = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT"; name = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "uucode"; name = "uucode";
url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz"; url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732";
hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="; hash = "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=";
}; };
} }
{ {
name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ"; name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "vaxis"; name = "uucode";
url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz"; url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="; hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=";
}; };
} }
{ {
name = "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA"; name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "vaxis"; name = "vaxis";
url = "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz"; url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
hash = "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM="; hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=";
}; };
} }
{ {
@ -307,35 +307,19 @@ in
}; };
} }
{ {
name = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef"; name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "z2d"; name = "z2d";
url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz"; url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz";
hash = "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg="; hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=";
}; };
} }
{ {
name = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR"; name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "zf"; name = "zf";
url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz"; url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
hash = "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ="; hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=";
};
}
{
name = "zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM";
path = fetchZigArtifact {
name = "zg";
url = "git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9";
hash = "sha256-P0ieLuOQ05wKVaMmeNKJIxCWMIdyeKkmhsj8Ps80BGU=";
};
}
{
name = "zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9";
path = fetchZigArtifact {
name = "zg";
url = "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz";
hash = "sha256-BZhz1nPqxK6hdsJQ66n7Jk4zMgFSGLXm8eU0CX/7mDI=";
}; };
} }
{ {
@ -362,14 +346,6 @@ in
hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4="; hash = "sha256-TxRrc17Q1Sf1IOO/cdPpP3LD0PpYOujt06SFH3B5Ek4=";
}; };
} }
{
name = "zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL";
path = fetchZigArtifact {
name = "zigimg";
url = "git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726";
hash = "sha256-Ko5RuxxTAvpUHCnWEdHqNl7b+PVUAxg1/OPmzGGjdt0=";
};
}
{ {
name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms"; name = "zigimg-0.1.0-8_eo2vHnEwCIVW34Q14Ec-xUlzIoVg86-7FU2ypPtxms";
path = fetchZigArtifact { path = fetchZigArtifact {

17
build.zig.zon.txt generated
View File

@ -1,6 +1,4 @@
git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9 git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732
git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726
https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz
https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz
https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz
https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
@ -8,12 +6,11 @@ https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
https://deps.files.ghostty.org/gettext-0.24.tar.gz https://deps.files.ghostty.org/gettext-0.24.tar.gz
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz
https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz
https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz
https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz
https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz
@ -23,16 +20,16 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz
https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz
https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz

BIN
dist/doxygen/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

18
dist/doxygen/footer.html vendored Normal file
View File

@ -0,0 +1,18 @@
<!-- HTML footer for doxygen 1.14.0-->
<!-- start footer part -->
<!--BEGIN GENERATE_TREEVIEW-->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
$navpath
<li class="footer">$generatedby <a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<hr class="footer"/><address class="footer"><small>
$generatedby&#160;<a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
</small></address>
</div><!-- doc-content -->
<!--END !GENERATE_TREEVIEW-->
</body>
</html>

390
dist/doxygen/ghostty.css vendored Normal file
View File

@ -0,0 +1,390 @@
/**
* Ghostty Doxygen Custom Stylesheet
* Minimal branding customizations for Ghostty colors
*/
/* Ghostty brand color for links and accents - high contrast for dark bg */
a,
a:link {
color: #99b3ff;
}
a:visited {
color: #99b3ff;
}
a:hover {
color: #c2d4ff;
}
/* High contrast text colors */
body,
div.contents,
div.header,
.title,
.summary,
td,
th,
p,
li {
color: #e8e8e8 !important;
}
h1,
h2,
h3,
h4,
h5,
h6,
.groupheader {
color: #ffffff !important;
}
.memtitle,
.memname {
color: #ffffff !important;
}
.memdoc {
color: #e8e8e8 !important;
}
/* Selection color */
::selection {
background: rgba(53, 81, 243, 0.6);
}
/* Modern scrollbar styling for WebKit browsers (Safari, Chrome) */
::-webkit-scrollbar {
width: 14px;
height: 14px;
-webkit-appearance: none;
}
::-webkit-scrollbar-track {
background: #1a1f2e;
border-radius: 8px;
}
::-webkit-scrollbar-thumb {
background: #4a5260;
border-radius: 8px;
border: 3px solid #1a1f2e;
min-height: 40px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a6270;
}
::-webkit-scrollbar-thumb:active {
background: #6a7280;
}
::-webkit-scrollbar-corner {
background: #1a1f2e;
}
/* Firefox scrollbar styling */
* {
scrollbar-width: thin;
scrollbar-color: #404754 #1a1f2e;
}
/* Tree view selected item */
#nav-tree .selected {
background-color: #3551f3 !important;
}
/* Custom syntax highlighting optimized for dark backgrounds with high contrast */
.fragment,
div.line {
color: #f0f0f0 !important;
}
/* Keywords (int, void, const, static, etc.) */
.keyword,
.keywordtype {
color: #ff8be6 !important;
font-weight: 500;
}
/* Control flow (if, else, return, for, while, etc.) */
.keywordflow {
color: #ff8be6 !important;
font-weight: 500;
}
/* Comments */
.comment {
color: #8bc34a !important;
font-style: italic;
}
/* Preprocessor directives (#include, #define, etc.) */
.preprocessor {
color: #ffcc66 !important;
}
/* String and character literals */
.stringliteral,
.charliteral {
color: #b8e986 !important;
}
/* Numbers */
span.charliteral {
color: #d4a5ff !important;
}
/* Function names */
.functionname {
color: #6fe87c !important;
font-weight: 500;
}
/* Line numbers */
span.lineno {
color: #8a8a8a !important;
background-color: transparent !important;
}
span.lineno a {
color: #8a8a8a !important;
background-color: transparent !important;
}
/* Desktop: ensure page-nav maintains default width */
@media screen and (min-width: 768px) {
#page-nav-toggle {
display: none !important;
}
#page-nav {
position: relative !important;
width: 250px !important;
height: auto !important;
right: auto !important;
top: auto !important;
box-shadow: none !important;
}
}
/* Mobile-friendly responsive styles */
@media screen and (max-width: 767px) {
body {
font-size: 14px !important;
}
/* Make navigation tree collapsible on mobile */
#side-nav {
display: none;
}
#doc-content {
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Make right sidebar (page-nav) overlay on mobile */
#page-nav {
position: fixed !important;
top: 0 !important;
right: -280px !important;
width: 280px !important;
height: 100vh !important;
z-index: 10000 !important;
background: #101826 !important;
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.5) !important;
transition: right 0.3s ease !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch !important;
}
#page-nav.mobile-open {
right: 0 !important;
}
/* Hamburger menu button for page nav */
#page-nav-toggle {
display: block !important;
position: fixed !important;
top: 10px !important;
right: 15px !important;
z-index: 10001 !important;
width: 40px !important;
height: 40px !important;
background: rgba(53, 81, 243, 0.9) !important;
border: none !important;
border-radius: 5px !important;
cursor: pointer !important;
padding: 8px !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3) !important;
}
#page-nav-toggle span {
display: block !important;
width: 24px !important;
height: 3px !important;
background: #fff !important;
margin: 4px 0 !important;
border-radius: 2px !important;
transition: 0.3s !important;
}
/* Mobile overlay backdrop */
#page-nav-backdrop {
display: none !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, 0.5) !important;
z-index: 9999 !important;
}
#page-nav-backdrop.active {
display: block !important;
}
/* Improve header and navigation */
#top {
height: auto !important;
}
#titlearea {
padding: 10px !important;
}
#projectname {
font-size: 18px !important;
}
#projectbrief,
#projectnumber {
font-size: 12px !important;
}
/* Make tabs stack better on mobile */
#navrow1,
#navrow2,
#navrow3,
#navrow4 {
overflow-x: auto !important;
white-space: nowrap !important;
-webkit-overflow-scrolling: touch !important;
}
.tablist li {
display: inline-block !important;
}
/* Content adjustments */
.contents {
padding: 10px !important;
width: 100% !important;
box-sizing: border-box !important;
}
.header {
padding: 5px !important;
}
/* Code blocks */
.fragment {
font-size: 12px !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
}
div.line {
font-size: 12px !important;
}
/* Tables */
table {
display: block !important;
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
width: 100% !important;
}
.memberdecls table,
.fieldtable {
font-size: 12px !important;
}
.memtitle {
font-size: 14px !important;
padding: 8px !important;
}
.memname {
font-size: 13px !important;
word-break: break-word !important;
}
.memitem {
margin: 5px 0 !important;
}
/* Search box */
#MSearchBox {
width: 100% !important;
right: 0 !important;
}
/* Reduce padding and margins */
h1,
h2,
h3,
h4,
h5,
h6 {
margin-top: 10px !important;
margin-bottom: 8px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
h4 {
font-size: 14px !important;
}
/* Directory/file listings */
.directory .levels span {
display: none !important;
}
.directory .arrow {
margin-right: 5px !important;
}
/* Treeview adjustments */
#nav-tree {
width: 100% !important;
}
}
/* Tablet adjustments */
@media screen and (min-width: 768px) and (max-width: 1024px) {
.contents {
padding: 15px !important;
}
#side-nav {
width: 200px !important;
}
#doc-content {
margin-left: 200px !important;
}
}

77
dist/doxygen/header.html vendored Normal file
View File

@ -0,0 +1,77 @@
<!-- HTML header for doxygen 1.14.0-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="$langISO">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=11"/>
<meta name="generator" content="Doxygen $doxygenversion"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
<!--BEGIN PROJECT_ICON-->
<link rel="icon" href="$relpath^$projecticon" type="image/x-icon" />
<!--END PROJECT_ICON-->
<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
<!--BEGIN FULL_SIDEBAR-->
<script type="text/javascript">var page_layout=1;</script>
<!--END FULL_SIDEBAR-->
<script type="text/javascript" src="$relpath^jquery.js"></script>
<script type="text/javascript" src="$relpath^dynsections.js"></script>
<!--BEGIN COPY_CLIPBOARD-->
<script type="text/javascript" src="$relpath^clipboard.js"></script>
<!--END COPY_CLIPBOARD-->
$treeview
$search
$mathjax
$darkmode
<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
$extrastylesheet
<script type="text/javascript" src="$relpath^mobile-nav.js"></script>
</head>
<body>
<!--BEGIN FULL_SIDEBAR-->
<div id="side-nav" class="ui-resizable side-nav-resizable"><!-- do not remove this div, it is closed by doxygen! -->
<!--END FULL_SIDEBAR-->
<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
<!--BEGIN TITLEAREA-->
<div id="titlearea">
<table cellspacing="0" cellpadding="0">
<tbody>
<tr id="projectrow">
<!--BEGIN PROJECT_LOGO-->
<td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"$logosize/></td>
<!--END PROJECT_LOGO-->
<!--BEGIN PROJECT_NAME-->
<td id="projectalign">
<div id="projectname">$projectname<!--BEGIN PROJECT_NUMBER--><span id="projectnumber">&#160;$projectnumber</span><!--END PROJECT_NUMBER-->
</div>
<!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
</td>
<!--END PROJECT_NAME-->
<!--BEGIN !PROJECT_NAME-->
<!--BEGIN PROJECT_BRIEF-->
<td>
<div id="projectbrief">$projectbrief</div>
</td>
<!--END PROJECT_BRIEF-->
<!--END !PROJECT_NAME-->
<!--BEGIN DISABLE_INDEX-->
<!--BEGIN SEARCHENGINE-->
<!--BEGIN !FULL_SIDEBAR-->
<td>$searchbox</td>
<!--END !FULL_SIDEBAR-->
<!--END SEARCHENGINE-->
<!--END DISABLE_INDEX-->
</tr>
<!--BEGIN SEARCHENGINE-->
<!--BEGIN FULL_SIDEBAR-->
<tr><td colspan="2">$searchbox</td></tr>
<!--END FULL_SIDEBAR-->
<!--END SEARCHENGINE-->
</tbody>
</table>
</div>
<!--END TITLEAREA-->
<!-- end header part -->

65
dist/doxygen/mobile-nav.js vendored Normal file
View File

@ -0,0 +1,65 @@
/**
* Mobile navigation toggle for Doxygen documentation
*/
(function () {
// Only run on mobile devices
function isMobile() {
return window.innerWidth <= 767;
}
function initMobileNav() {
if (!isMobile()) return;
const pageNav = document.getElementById("page-nav");
if (!pageNav) return;
// Create toggle button
const toggleBtn = document.createElement("button");
toggleBtn.id = "page-nav-toggle";
toggleBtn.setAttribute("aria-label", "Toggle page navigation");
toggleBtn.innerHTML = "<span></span><span></span><span></span>";
document.body.appendChild(toggleBtn);
// Create backdrop
const backdrop = document.createElement("div");
backdrop.id = "page-nav-backdrop";
document.body.appendChild(backdrop);
// Toggle function
function toggleNav() {
const isOpen = pageNav.classList.toggle("mobile-open");
backdrop.classList.toggle("active", isOpen);
document.body.style.overflow = isOpen ? "hidden" : "";
}
// Event listeners
toggleBtn.addEventListener("click", toggleNav);
backdrop.addEventListener("click", toggleNav);
// Close on escape key
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && pageNav.classList.contains("mobile-open")) {
toggleNav();
}
});
}
// Initialize on load and resize
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initMobileNav);
} else {
initMobileNav();
}
window.addEventListener("resize", function () {
const pageNav = document.getElementById("page-nav");
const backdrop = document.getElementById("page-nav-backdrop");
if (!isMobile() && pageNav) {
pageNav.classList.remove("mobile-open");
if (backdrop) backdrop.classList.remove("active");
document.body.style.overflow = "";
}
});
})();

2659
dist/doxygen/stylesheet.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,22 @@
# Example: `ghostty-vt` C Key Encoding
This example demonstrates how to use the `ghostty-vt` C library to encode key
events into terminal escape sequences.
This example specifically shows how to:
1. Create a key encoder with the C API
2. Configure Kitty keyboard protocol flags (this example uses KKP)
3. Create and configure a key event
4. Encode the key event into a terminal escape sequence
The example encodes a Ctrl key release event with the Ctrl modifier set,
producing the escape sequence `\x1b[57442;5:3u`.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_key_encode",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@ -0,0 +1,24 @@
.{
.name = .c_vt,
.version = "0.0.0",
.fingerprint = 0x413a8529b1255f9a,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,59 @@
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
int main() {
GhosttyKeyEncoder encoder;
GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
assert(result == GHOSTTY_SUCCESS);
// Set kitty flags with all features enabled
ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
// Create key event
GhosttyKeyEvent event;
result = ghostty_key_event_new(NULL, &event);
assert(result == GHOSTTY_SUCCESS);
ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE);
ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT);
ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
printf("Encoding event: left ctrl release with all Kitty flags enabled\n");
// Optionally, encode with null buffer to get required size. You can
// skip this step and provide a sufficiently large buffer directly.
// If there isn't enoug hspace, the function will return an out of memory
// error.
size_t required = 0;
result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
assert(result == GHOSTTY_OUT_OF_MEMORY);
printf("Required buffer size: %zu bytes\n", required);
// Encode the key event. We don't use our required size above because
// that was just an example; we know 128 bytes is enough.
char buf[128];
size_t written = 0;
result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
assert(result == GHOSTTY_SUCCESS);
printf("Encoded %zu bytes\n", written);
// Print the encoded sequence (hex and string)
printf("Hex: ");
for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]);
printf("\n");
printf("String: ");
for (size_t i = 0; i < written; i++) {
if (buf[i] == 0x1b) {
printf("\\x1b");
} else {
printf("%c", buf[i]);
}
}
printf("\n");
ghostty_key_event_free(event);
ghostty_key_encoder_free(encoder);
return 0;
}

View File

@ -0,0 +1,17 @@
# Example: `ghostty-vt` Paste Safety Check
This contains a simple example of how to use the `ghostty-vt` paste
utilities to check if paste data is safe.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_paste",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@ -0,0 +1,24 @@
.{
.name = .c_vt_paste,
.version = "0.0.0",
.fingerprint = 0xa105002abbc8cf74,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,31 @@
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
int main() {
// Test safe paste data
const char *safe_data = "hello world";
if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
printf("'%s' is safe to paste\n", safe_data);
}
// Test unsafe paste data with newline
const char *unsafe_newline = "rm -rf /\n";
if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) {
printf("'%s' is UNSAFE - contains newline\n", unsafe_newline);
}
// Test unsafe paste data with bracketed paste end sequence
const char *unsafe_escape = "evil\x1b[201~code";
if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) {
printf("Data with escape sequence is UNSAFE\n");
}
// Test empty data
const char *empty_data = "";
if (ghostty_paste_is_safe(empty_data, 0)) {
printf("Empty data is safe\n");
}
return 0;
}

View File

@ -0,0 +1,21 @@
# Example: `ghostty-vt` SGR Parser
This contains a simple example of how to use the `ghostty-vt` SGR parser
to parse terminal styling sequences and extract text attributes.
This example demonstrates parsing a complex SGR sequence from Kakoune that
includes curly underline, RGB foreground/background colors, and RGB underline
color with mixed semicolon and colon separators.
This uses a `build.zig` and `Zig` to build the C program so that we
can reuse a lot of our build logic and depend directly on our source
tree, but Ghostty emits a standard C library that can be used with any
C tooling.
## Usage
Run the program:
```shell-session
zig build run
```

View File

@ -0,0 +1,42 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const exe_mod = b.createModule(.{
.target = target,
.optimize = optimize,
});
exe_mod.addCSourceFiles(.{
.root = b.path("src"),
.files = &.{"main.c"},
});
// You'll want to use a lazy dependency here so that ghostty is only
// downloaded if you actually need it.
if (b.lazyDependency("ghostty", .{
// Setting simd to false will force a pure static build that
// doesn't even require libc, but it has a significant performance
// penalty. If your embedding app requires libc anyway, you should
// always keep simd enabled.
// .simd = false,
})) |dep| {
exe_mod.linkLibrary(dep.artifact("ghostty-vt"));
}
// Exe
const exe = b.addExecutable(.{
.name = "c_vt_sgr",
.root_module = exe_mod,
});
b.installArtifact(exe);
// Run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
}

View File

@ -0,0 +1,24 @@
.{
.name = .c_vt_sgr,
.version = "0.0.0",
.fingerprint = 0x6e9c6d318e59c268,
.minimum_zig_version = "0.15.1",
.dependencies = .{
// Ghostty dependency. In reality, you'd probably use a URL-based
// dependency like the one showed (and commented out) below this one.
// We use a path dependency here for simplicity and to ensure our
// examples always test against the source they're bundled with.
.ghostty = .{ .path = "../../" },
// Example of what a URL-based dependency looks like:
// .ghostty = .{
// .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz",
// .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s",
// },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

131
example/c-vt-sgr/src/main.c Normal file
View File

@ -0,0 +1,131 @@
#include <assert.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
// Create parser
GhosttySgrParser parser;
GhosttyResult result = ghostty_sgr_new(NULL, &parser);
assert(result == GHOSTTY_SUCCESS);
// Parse a complex SGR sequence from Kakoune
// This corresponds to the escape sequence:
// ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m
//
// Breaking down the sequence:
// - 4:3 = curly underline (colon-separated sub-parameters)
// - 38;2;51;51;51 = foreground RGB color (51, 51, 51) - dark gray
// - 48;2;170;170;170 = background RGB color (170, 170, 170) - light gray
// - 58;2;255;97;136 = underline RGB color (255, 97, 136) - pink
uint16_t params[] = {4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136};
// Separator array: ':' at position 0 (between 4 and 3), ';' elsewhere
char separators[] = ";;;;;;;;;;;;;;;;";
separators[0] = ':';
result = ghostty_sgr_set_params(parser, params, separators, sizeof(params) / sizeof(params[0]));
assert(result == GHOSTTY_SUCCESS);
printf("Parsing Kakoune SGR sequence:\n");
printf("ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m\n\n");
// Iterate through attributes
GhosttySgrAttribute attr;
int count = 0;
while (ghostty_sgr_next(parser, &attr)) {
count++;
printf("Attribute %d: ", count);
switch (attr.tag) {
case GHOSTTY_SGR_ATTR_UNDERLINE:
printf("Underline style = ");
switch (attr.value.underline) {
case GHOSTTY_SGR_UNDERLINE_NONE:
printf("none\n");
break;
case GHOSTTY_SGR_UNDERLINE_SINGLE:
printf("single\n");
break;
case GHOSTTY_SGR_UNDERLINE_DOUBLE:
printf("double\n");
break;
case GHOSTTY_SGR_UNDERLINE_CURLY:
printf("curly\n");
break;
case GHOSTTY_SGR_UNDERLINE_DOTTED:
printf("dotted\n");
break;
case GHOSTTY_SGR_UNDERLINE_DASHED:
printf("dashed\n");
break;
default:
printf("unknown (%d)\n", attr.value.underline);
break;
}
break;
case GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG:
printf("Foreground RGB = (%d, %d, %d)\n",
attr.value.direct_color_fg.r,
attr.value.direct_color_fg.g,
attr.value.direct_color_fg.b);
break;
case GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG:
printf("Background RGB = (%d, %d, %d)\n",
attr.value.direct_color_bg.r,
attr.value.direct_color_bg.g,
attr.value.direct_color_bg.b);
break;
case GHOSTTY_SGR_ATTR_UNDERLINE_COLOR:
printf("Underline color RGB = (%d, %d, %d)\n",
attr.value.underline_color.r,
attr.value.underline_color.g,
attr.value.underline_color.b);
break;
case GHOSTTY_SGR_ATTR_FG_8:
printf("Foreground 8-color = %d\n", attr.value.fg_8);
break;
case GHOSTTY_SGR_ATTR_BG_8:
printf("Background 8-color = %d\n", attr.value.bg_8);
break;
case GHOSTTY_SGR_ATTR_FG_256:
printf("Foreground 256-color = %d\n", attr.value.fg_256);
break;
case GHOSTTY_SGR_ATTR_BG_256:
printf("Background 256-color = %d\n", attr.value.bg_256);
break;
case GHOSTTY_SGR_ATTR_BOLD:
printf("Bold\n");
break;
case GHOSTTY_SGR_ATTR_ITALIC:
printf("Italic\n");
break;
case GHOSTTY_SGR_ATTR_UNSET:
printf("Reset all attributes\n");
break;
case GHOSTTY_SGR_ATTR_UNKNOWN:
printf("Unknown attribute\n");
break;
default:
printf("Other attribute (tag=%d)\n", attr.tag);
break;
}
}
printf("\nTotal attributes parsed: %d\n", count);
// Cleanup
ghostty_sgr_free(parser);
return 0;
}

View File

@ -0,0 +1,40 @@
# WebAssembly Key Encoder Example
This example demonstrates how to use the Ghostty VT library from WebAssembly
to encode key events into terminal escape sequences.
## Building
First, build the WebAssembly module:
```bash
zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
```
This will create `zig-out/bin/ghostty-vt.wasm`.
## Running
**Important:** You must serve this via HTTP, not open it as a file directly.
Browsers block loading WASM files from `file://` URLs.
From the **root of the ghostty repository**, serve with a local HTTP server:
```bash
# Using Python (recommended)
python3 -m http.server 8000
# Or using Node.js
npx serve .
# Or using PHP
php -S localhost:8000
```
Then open your browser to:
```
http://localhost:8000/example/wasm-key-encode/
```
Focus the text input field and press any key combination to see the encoded output.

View File

@ -0,0 +1,687 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghostty VT Key Encoder - WebAssembly Example</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
line-height: 1.6;
}
h1 {
color: #333;
}
.output {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-all;
}
.error {
background: #fee;
border-color: #faa;
color: #c00;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.key-input {
width: 100%;
padding: 15px;
font-size: 16px;
border: 2px solid #0066cc;
border-radius: 4px;
margin: 20px 0;
box-sizing: border-box;
}
.key-input:focus {
outline: none;
border-color: #0052a3;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.status {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.controls {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.controls h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.radio-group {
display: flex;
gap: 15px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.radio-group input[type="radio"] {
cursor: pointer;
}
.warning {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
color: #856404;
}
.warning strong {
display: block;
margin-bottom: 5px;
}
</style>
</head>
<body>
<h1>Ghostty VT Key Encoder - WebAssembly Example</h1>
<p>This example demonstrates encoding key events into terminal escape sequences using the Ghostty VT WebAssembly module.</p>
<div class="warning">
<strong>⚠️ Warning:</strong>
This is an example of the libghostty-vt WebAssembly API. The JavaScript
keyboard event mapping to the libghostty-vt API may not be perfect
and may result in encoding inaccuracies for certain keys or layouts.
Do not use this as a key encoding reference.
</div>
<div class="status" id="status">Loading WebAssembly module...</div>
<div class="controls">
<h3>Key Action</h3>
<div class="radio-group">
<label><input type="radio" name="action" value="1" checked> Press</label>
<label><input type="radio" name="action" value="0"> Release</label>
<label><input type="radio" name="action" value="2"> Repeat</label>
</div>
</div>
<div class="controls">
<h3>Kitty Keyboard Protocol Flags</h3>
<div class="checkbox-group">
<label><input type="checkbox" id="flag_disambiguate" checked> Disambiguate</label>
<label><input type="checkbox" id="flag_report_events" checked> Report Events</label>
<label><input type="checkbox" id="flag_report_alternates" checked> Report Alternates</label>
<label><input type="checkbox" id="flag_report_all_as_escapes" checked> Report All As Escapes</label>
<label><input type="checkbox" id="flag_report_text" checked> Report Text</label>
</div>
</div>
<input type="text" class="key-input" id="keyInput" placeholder="Focus here and press any key combination (e.g., Ctrl+A, Shift+Enter)..." disabled>
<div id="output" class="output">Waiting for key events...</div>
<p><strong>Note:</strong> This example must be served via HTTP (not opened directly as a file). See the README for instructions.</p>
<script>
let wasmInstance = null;
let wasmMemory = null;
let encoderPtr = null;
let lastKeyEvent = null;
async function loadWasm() {
try {
// Load the wasm module - adjust path as needed
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
const wasmBytes = await response.arrayBuffer();
// Instantiate the wasm module
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
env: {
// Logging function for wasm module
log: (ptr, len) => {
const bytes = new Uint8Array(wasmModule.instance.exports.memory.buffer, ptr, len);
const text = new TextDecoder().decode(bytes);
console.log('[wasm]', text);
}
}
});
wasmInstance = wasmModule.instance;
wasmMemory = wasmInstance.exports.memory;
return true;
} catch (e) {
console.error('Failed to load WASM:', e);
if (window.location.protocol === 'file:') {
throw new Error('Cannot load WASM from file:// protocol. Please serve via HTTP (see README)');
}
return false;
}
}
function getBuffer() {
return wasmMemory.buffer;
}
function formatHex(bytes) {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
}
function formatString(bytes) {
let result = '';
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] === 0x1b) {
result += '\\x1b';
} else {
result += String.fromCharCode(bytes[i]);
}
}
return result;
}
// Map W3C KeyboardEvent.code values to Ghostty key codes
// Based on include/ghostty/vt/key/event.h
const keyCodeMap = {
// Writing System Keys
'Backquote': 1, // GHOSTTY_KEY_BACKQUOTE
'Backslash': 2, // GHOSTTY_KEY_BACKSLASH
'BracketLeft': 3, // GHOSTTY_KEY_BRACKET_LEFT
'BracketRight': 4, // GHOSTTY_KEY_BRACKET_RIGHT
'Comma': 5, // GHOSTTY_KEY_COMMA
'Digit0': 6, // GHOSTTY_KEY_DIGIT_0
'Digit1': 7, // GHOSTTY_KEY_DIGIT_1
'Digit2': 8, // GHOSTTY_KEY_DIGIT_2
'Digit3': 9, // GHOSTTY_KEY_DIGIT_3
'Digit4': 10, // GHOSTTY_KEY_DIGIT_4
'Digit5': 11, // GHOSTTY_KEY_DIGIT_5
'Digit6': 12, // GHOSTTY_KEY_DIGIT_6
'Digit7': 13, // GHOSTTY_KEY_DIGIT_7
'Digit8': 14, // GHOSTTY_KEY_DIGIT_8
'Digit9': 15, // GHOSTTY_KEY_DIGIT_9
'Equal': 16, // GHOSTTY_KEY_EQUAL
'IntlBackslash': 17, // GHOSTTY_KEY_INTL_BACKSLASH
'IntlRo': 18, // GHOSTTY_KEY_INTL_RO
'IntlYen': 19, // GHOSTTY_KEY_INTL_YEN
'KeyA': 20, // GHOSTTY_KEY_A
'KeyB': 21, // GHOSTTY_KEY_B
'KeyC': 22, // GHOSTTY_KEY_C
'KeyD': 23, // GHOSTTY_KEY_D
'KeyE': 24, // GHOSTTY_KEY_E
'KeyF': 25, // GHOSTTY_KEY_F
'KeyG': 26, // GHOSTTY_KEY_G
'KeyH': 27, // GHOSTTY_KEY_H
'KeyI': 28, // GHOSTTY_KEY_I
'KeyJ': 29, // GHOSTTY_KEY_J
'KeyK': 30, // GHOSTTY_KEY_K
'KeyL': 31, // GHOSTTY_KEY_L
'KeyM': 32, // GHOSTTY_KEY_M
'KeyN': 33, // GHOSTTY_KEY_N
'KeyO': 34, // GHOSTTY_KEY_O
'KeyP': 35, // GHOSTTY_KEY_P
'KeyQ': 36, // GHOSTTY_KEY_Q
'KeyR': 37, // GHOSTTY_KEY_R
'KeyS': 38, // GHOSTTY_KEY_S
'KeyT': 39, // GHOSTTY_KEY_T
'KeyU': 40, // GHOSTTY_KEY_U
'KeyV': 41, // GHOSTTY_KEY_V
'KeyW': 42, // GHOSTTY_KEY_W
'KeyX': 43, // GHOSTTY_KEY_X
'KeyY': 44, // GHOSTTY_KEY_Y
'KeyZ': 45, // GHOSTTY_KEY_Z
'Minus': 46, // GHOSTTY_KEY_MINUS
'Period': 47, // GHOSTTY_KEY_PERIOD
'Quote': 48, // GHOSTTY_KEY_QUOTE
'Semicolon': 49, // GHOSTTY_KEY_SEMICOLON
'Slash': 50, // GHOSTTY_KEY_SLASH
// Functional Keys
'AltLeft': 51, // GHOSTTY_KEY_ALT_LEFT
'AltRight': 52, // GHOSTTY_KEY_ALT_RIGHT
'Backspace': 53, // GHOSTTY_KEY_BACKSPACE
'CapsLock': 54, // GHOSTTY_KEY_CAPS_LOCK
'ContextMenu': 55, // GHOSTTY_KEY_CONTEXT_MENU
'ControlLeft': 56, // GHOSTTY_KEY_CONTROL_LEFT
'ControlRight': 57, // GHOSTTY_KEY_CONTROL_RIGHT
'Enter': 58, // GHOSTTY_KEY_ENTER
'MetaLeft': 59, // GHOSTTY_KEY_META_LEFT
'MetaRight': 60, // GHOSTTY_KEY_META_RIGHT
'ShiftLeft': 61, // GHOSTTY_KEY_SHIFT_LEFT
'ShiftRight': 62, // GHOSTTY_KEY_SHIFT_RIGHT
'Space': 63, // GHOSTTY_KEY_SPACE
'Tab': 64, // GHOSTTY_KEY_TAB
'Convert': 65, // GHOSTTY_KEY_CONVERT
'KanaMode': 66, // GHOSTTY_KEY_KANA_MODE
'NonConvert': 67, // GHOSTTY_KEY_NON_CONVERT
// Control Pad Section
'Delete': 68, // GHOSTTY_KEY_DELETE
'End': 69, // GHOSTTY_KEY_END
'Help': 70, // GHOSTTY_KEY_HELP
'Home': 71, // GHOSTTY_KEY_HOME
'Insert': 72, // GHOSTTY_KEY_INSERT
'PageDown': 73, // GHOSTTY_KEY_PAGE_DOWN
'PageUp': 74, // GHOSTTY_KEY_PAGE_UP
// Arrow Pad Section
'ArrowDown': 75, // GHOSTTY_KEY_ARROW_DOWN
'ArrowLeft': 76, // GHOSTTY_KEY_ARROW_LEFT
'ArrowRight': 77, // GHOSTTY_KEY_ARROW_RIGHT
'ArrowUp': 78, // GHOSTTY_KEY_ARROW_UP
// Numpad Section
'NumLock': 79, // GHOSTTY_KEY_NUM_LOCK
'Numpad0': 80, // GHOSTTY_KEY_NUMPAD_0
'Numpad1': 81, // GHOSTTY_KEY_NUMPAD_1
'Numpad2': 82, // GHOSTTY_KEY_NUMPAD_2
'Numpad3': 83, // GHOSTTY_KEY_NUMPAD_3
'Numpad4': 84, // GHOSTTY_KEY_NUMPAD_4
'Numpad5': 85, // GHOSTTY_KEY_NUMPAD_5
'Numpad6': 86, // GHOSTTY_KEY_NUMPAD_6
'Numpad7': 87, // GHOSTTY_KEY_NUMPAD_7
'Numpad8': 88, // GHOSTTY_KEY_NUMPAD_8
'Numpad9': 89, // GHOSTTY_KEY_NUMPAD_9
'NumpadAdd': 90, // GHOSTTY_KEY_NUMPAD_ADD
'NumpadBackspace': 91, // GHOSTTY_KEY_NUMPAD_BACKSPACE
'NumpadClear': 92, // GHOSTTY_KEY_NUMPAD_CLEAR
'NumpadClearEntry': 93, // GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY
'NumpadComma': 94, // GHOSTTY_KEY_NUMPAD_COMMA
'NumpadDecimal': 95, // GHOSTTY_KEY_NUMPAD_DECIMAL
'NumpadDivide': 96, // GHOSTTY_KEY_NUMPAD_DIVIDE
'NumpadEnter': 97, // GHOSTTY_KEY_NUMPAD_ENTER
'NumpadEqual': 98, // GHOSTTY_KEY_NUMPAD_EQUAL
'NumpadMemoryAdd': 99, // GHOSTTY_KEY_NUMPAD_MEMORY_ADD
'NumpadMemoryClear': 100,// GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR
'NumpadMemoryRecall': 101,// GHOSTTY_KEY_NUMPAD_MEMORY_RECALL
'NumpadMemoryStore': 102,// GHOSTTY_KEY_NUMPAD_MEMORY_STORE
'NumpadMemorySubtract': 103,// GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT
'NumpadMultiply': 104, // GHOSTTY_KEY_NUMPAD_MULTIPLY
'NumpadParenLeft': 105, // GHOSTTY_KEY_NUMPAD_PAREN_LEFT
'NumpadParenRight': 106, // GHOSTTY_KEY_NUMPAD_PAREN_RIGHT
'NumpadSubtract': 107, // GHOSTTY_KEY_NUMPAD_SUBTRACT
'NumpadSeparator': 108, // GHOSTTY_KEY_NUMPAD_SEPARATOR
'NumpadUp': 109, // GHOSTTY_KEY_NUMPAD_UP
'NumpadDown': 110, // GHOSTTY_KEY_NUMPAD_DOWN
'NumpadRight': 111, // GHOSTTY_KEY_NUMPAD_RIGHT
'NumpadLeft': 112, // GHOSTTY_KEY_NUMPAD_LEFT
'NumpadBegin': 113, // GHOSTTY_KEY_NUMPAD_BEGIN
'NumpadHome': 114, // GHOSTTY_KEY_NUMPAD_HOME
'NumpadEnd': 115, // GHOSTTY_KEY_NUMPAD_END
'NumpadInsert': 116, // GHOSTTY_KEY_NUMPAD_INSERT
'NumpadDelete': 117, // GHOSTTY_KEY_NUMPAD_DELETE
'NumpadPageUp': 118, // GHOSTTY_KEY_NUMPAD_PAGE_UP
'NumpadPageDown': 119, // GHOSTTY_KEY_NUMPAD_PAGE_DOWN
// Function Section
'Escape': 120, // GHOSTTY_KEY_ESCAPE
'F1': 121, // GHOSTTY_KEY_F1
'F2': 122, // GHOSTTY_KEY_F2
'F3': 123, // GHOSTTY_KEY_F3
'F4': 124, // GHOSTTY_KEY_F4
'F5': 125, // GHOSTTY_KEY_F5
'F6': 126, // GHOSTTY_KEY_F6
'F7': 127, // GHOSTTY_KEY_F7
'F8': 128, // GHOSTTY_KEY_F8
'F9': 129, // GHOSTTY_KEY_F9
'F10': 130, // GHOSTTY_KEY_F10
'F11': 131, // GHOSTTY_KEY_F11
'F12': 132, // GHOSTTY_KEY_F12
'F13': 133, // GHOSTTY_KEY_F13
'F14': 134, // GHOSTTY_KEY_F14
'F15': 135, // GHOSTTY_KEY_F15
'F16': 136, // GHOSTTY_KEY_F16
'F17': 137, // GHOSTTY_KEY_F17
'F18': 138, // GHOSTTY_KEY_F18
'F19': 139, // GHOSTTY_KEY_F19
'F20': 140, // GHOSTTY_KEY_F20
'F21': 141, // GHOSTTY_KEY_F21
'F22': 142, // GHOSTTY_KEY_F22
'F23': 143, // GHOSTTY_KEY_F23
'F24': 144, // GHOSTTY_KEY_F24
'F25': 145, // GHOSTTY_KEY_F25
'Fn': 146, // GHOSTTY_KEY_FN
'FnLock': 147, // GHOSTTY_KEY_FN_LOCK
'PrintScreen': 148, // GHOSTTY_KEY_PRINT_SCREEN
'ScrollLock': 149, // GHOSTTY_KEY_SCROLL_LOCK
'Pause': 150, // GHOSTTY_KEY_PAUSE
// Media Keys
'BrowserBack': 151, // GHOSTTY_KEY_BROWSER_BACK
'BrowserFavorites': 152, // GHOSTTY_KEY_BROWSER_FAVORITES
'BrowserForward': 153, // GHOSTTY_KEY_BROWSER_FORWARD
'BrowserHome': 154, // GHOSTTY_KEY_BROWSER_HOME
'BrowserRefresh': 155, // GHOSTTY_KEY_BROWSER_REFRESH
'BrowserSearch': 156, // GHOSTTY_KEY_BROWSER_SEARCH
'BrowserStop': 157, // GHOSTTY_KEY_BROWSER_STOP
'Eject': 158, // GHOSTTY_KEY_EJECT
'LaunchApp1': 159, // GHOSTTY_KEY_LAUNCH_APP_1
'LaunchApp2': 160, // GHOSTTY_KEY_LAUNCH_APP_2
'LaunchMail': 161, // GHOSTTY_KEY_LAUNCH_MAIL
'MediaPlayPause': 162, // GHOSTTY_KEY_MEDIA_PLAY_PAUSE
'MediaSelect': 163, // GHOSTTY_KEY_MEDIA_SELECT
'MediaStop': 164, // GHOSTTY_KEY_MEDIA_STOP
'MediaTrackNext': 165, // GHOSTTY_KEY_MEDIA_TRACK_NEXT
'MediaTrackPrevious': 166,// GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS
'Power': 167, // GHOSTTY_KEY_POWER
'Sleep': 168, // GHOSTTY_KEY_SLEEP
'AudioVolumeDown': 169, // GHOSTTY_KEY_AUDIO_VOLUME_DOWN
'AudioVolumeMute': 170, // GHOSTTY_KEY_AUDIO_VOLUME_MUTE
'AudioVolumeUp': 171, // GHOSTTY_KEY_AUDIO_VOLUME_UP
'WakeUp': 172, // GHOSTTY_KEY_WAKE_UP
// Legacy, Non-standard, and Special Keys
'Copy': 173, // GHOSTTY_KEY_COPY
'Cut': 174, // GHOSTTY_KEY_CUT
'Paste': 175, // GHOSTTY_KEY_PASTE
};
function encodeKeyEvent(event) {
if (!encoderPtr) return null;
try {
// Create key event
const eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_key_event_new(0, eventPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_key_event_new failed with result ${result}`);
}
const eventPtr = new DataView(getBuffer()).getUint32(eventPtrPtr, true);
// Get action from radio buttons
const actionRadio = document.querySelector('input[name="action"]:checked');
const action = parseInt(actionRadio.value);
wasmInstance.exports.ghostty_key_event_set_action(eventPtr, action);
// Map key code from event.code (preferred, layout-independent)
let keyCode = keyCodeMap[event.code] || 0; // GHOSTTY_KEY_UNIDENTIFIED = 0
wasmInstance.exports.ghostty_key_event_set_key(eventPtr, keyCode);
// Map modifiers with left/right side information
let mods = 0;
if (event.shiftKey) {
mods |= 0x01; // GHOSTTY_MODS_SHIFT
if (event.code === 'ShiftRight') mods |= 0x40; // GHOSTTY_MODS_SHIFT_SIDE
}
if (event.ctrlKey) {
mods |= 0x02; // GHOSTTY_MODS_CTRL
if (event.code === 'ControlRight') mods |= 0x80; // GHOSTTY_MODS_CTRL_SIDE
}
if (event.altKey) {
mods |= 0x04; // GHOSTTY_MODS_ALT
if (event.code === 'AltRight') mods |= 0x100; // GHOSTTY_MODS_ALT_SIDE
}
if (event.metaKey) {
mods |= 0x08; // GHOSTTY_MODS_SUPER
if (event.code === 'MetaRight') mods |= 0x200; // GHOSTTY_MODS_SUPER_SIDE
}
wasmInstance.exports.ghostty_key_event_set_mods(eventPtr, mods);
// Set UTF-8 text from the key event (the actual character produced)
if (event.key.length === 1) {
const utf8Bytes = new TextEncoder().encode(event.key);
const utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(utf8Bytes.length);
new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr);
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length);
}
// Set unshifted codepoint
const unshiftedCodepoint = getUnshiftedCodepoint(event);
if (unshiftedCodepoint !== 0) {
wasmInstance.exports.ghostty_key_event_set_unshifted_codepoint(eventPtr, unshiftedCodepoint);
}
// Encode the key event
const requiredPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
wasmInstance.exports.ghostty_key_encoder_encode(
encoderPtr, eventPtr, 0, 0, requiredPtr
);
const required = new DataView(getBuffer()).getUint32(requiredPtr, true);
const bufPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(required);
const writtenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode(
encoderPtr, eventPtr, bufPtr, required, writtenPtr
);
if (encodeResult !== 0) {
return null; // No encoding for this key
}
const written = new DataView(getBuffer()).getUint32(writtenPtr, true);
const encoded = new Uint8Array(getBuffer()).slice(bufPtr, bufPtr + written);
return {
bytes: Array.from(encoded),
hex: formatHex(encoded),
string: formatString(encoded)
};
} catch (e) {
console.error('Encoding error:', e);
return null;
}
}
function getUnshiftedCodepoint(event) {
// Derive unshifted codepoint from the physical key code
const code = event.code;
// Letter keys (KeyA-KeyZ) -> lowercase letters
if (code.startsWith('Key')) {
const letter = code.substring(3).toLowerCase();
return letter.codePointAt(0);
}
// Digit keys (Digit0-Digit9) -> the digit itself
if (code.startsWith('Digit')) {
const digit = code.substring(5);
return digit.codePointAt(0);
}
// Space
if (code === 'Space') {
return ' '.codePointAt(0);
}
// Symbol keys -> unshifted character
const unshiftedSymbols = {
'Minus': '-', 'Equal': '=', 'BracketLeft': '[', 'BracketRight': ']',
'Backslash': '\\', 'Semicolon': ';', 'Quote': "'",
'Backquote': '`', 'Comma': ',', 'Period': '.', 'Slash': '/'
};
if (unshiftedSymbols[code]) {
return unshiftedSymbols[code].codePointAt(0);
}
// Fallback: use the produced character's codepoint
if (event.key.length > 0) {
return event.key.codePointAt(0) || 0;
}
return 0;
}
function getKittyFlags() {
let flags = 0;
if (document.getElementById('flag_disambiguate').checked) flags |= 0x01;
if (document.getElementById('flag_report_events').checked) flags |= 0x02;
if (document.getElementById('flag_report_alternates').checked) flags |= 0x04;
if (document.getElementById('flag_report_all_as_escapes').checked) flags |= 0x08;
if (document.getElementById('flag_report_text').checked) flags |= 0x10;
return flags;
}
function updateEncoderFlags() {
if (!encoderPtr) return;
const flags = getKittyFlags();
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
new DataView(getBuffer()).setUint8(flagsPtr, flags);
wasmInstance.exports.ghostty_key_encoder_setopt(
encoderPtr,
5, // GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS
flagsPtr
);
// Re-encode last key with new flags
reencodeLastKey();
}
function displayEncoding(event) {
const outputDiv = document.getElementById('output');
const encoded = encodeKeyEvent(event);
const actionRadio = document.querySelector('input[name="action"]:checked');
const actionName = actionRadio.parentElement.textContent.trim();
let output = `Action: ${actionName}\n`;
output += `Key: ${event.key} (code: ${event.code})\n`;
output += `Modifiers: `;
const mods = [];
if (event.shiftKey) mods.push('Shift');
if (event.ctrlKey) mods.push('Ctrl');
if (event.altKey) mods.push('Alt');
if (event.metaKey) mods.push('Meta');
output += mods.length ? mods.join('+') : 'none';
output += '\n';
// Show Kitty flags state
const flags = [];
if (document.getElementById('flag_disambiguate').checked) flags.push('Disambiguate');
if (document.getElementById('flag_report_events').checked) flags.push('Report Events');
if (document.getElementById('flag_report_alternates').checked) flags.push('Report Alternates');
if (document.getElementById('flag_report_all_as_escapes').checked) flags.push('Report All As Escapes');
if (document.getElementById('flag_report_text').checked) flags.push('Report Text');
output += 'Kitty Flags:\n';
if (flags.length) {
flags.forEach(flag => output += ` - ${flag}\n`);
} else {
output += ' - none\n';
}
output += '\n';
if (encoded) {
output += `Encoded ${encoded.bytes.length} bytes\n`;
output += `Hex: ${encoded.hex}\n`;
output += `String: ${encoded.string}`;
} else {
output += 'No encoding for this key event';
}
outputDiv.textContent = output;
}
function handleKeyEvent(event) {
// Allow modifier keys to be pressed without clearing input
// Only prevent default for keys we want to capture
if (event.key !== 'Tab' && event.key !== 'F5') {
event.preventDefault();
}
lastKeyEvent = event;
displayEncoding(event);
}
function reencodeLastKey() {
if (lastKeyEvent) {
displayEncoding(lastKeyEvent);
}
}
async function init() {
const statusDiv = document.getElementById('status');
const keyInput = document.getElementById('keyInput');
const outputDiv = document.getElementById('output');
try {
statusDiv.textContent = 'Loading WebAssembly module...';
const loaded = await loadWasm();
if (!loaded) {
throw new Error('Failed to load WebAssembly module');
}
// Create key encoder
const encoderPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_key_encoder_new(0, encoderPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_key_encoder_new failed with result ${result}`);
}
encoderPtr = new DataView(getBuffer()).getUint32(encoderPtrPtr, true);
// Set kitty flags based on checkboxes
updateEncoderFlags();
statusDiv.textContent = '';
keyInput.disabled = false;
keyInput.focus();
// Listen for key events (only keydown since action is selected manually)
keyInput.addEventListener('keydown', handleKeyEvent);
// Listen for flag changes
const flagCheckboxes = document.querySelectorAll('.checkbox-group input[type="checkbox"]');
flagCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateEncoderFlags);
});
// Listen for action changes
const actionRadios = document.querySelectorAll('input[name="action"]');
actionRadios.forEach(radio => {
radio.addEventListener('change', reencodeLastKey);
});
} catch (e) {
statusDiv.textContent = `Error: ${e.message}`;
statusDiv.style.color = '#c00';
outputDiv.className = 'output error';
outputDiv.textContent = `Error: ${e.message}\n\nStack trace:\n${e.stack}`;
}
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

@ -0,0 +1,39 @@
# WebAssembly SGR Parser Example
This example demonstrates how to use the Ghostty VT library from WebAssembly
to parse terminal SGR (Select Graphic Rendition) sequences and extract text
styling attributes.
## Building
First, build the WebAssembly module:
```bash
zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
```
This will create `zig-out/bin/ghostty-vt.wasm`.
## Running
**Important:** You must serve this via HTTP, not open it as a file directly.
Browsers block loading WASM files from `file://` URLs.
From the **root of the ghostty repository**, serve with a local HTTP server:
```bash
# Using Python (recommended)
python3 -m http.server 8000
# Or using Node.js
npx serve .
# Or using PHP
php -S localhost:8000
```
Then open your browser to:
```
http://localhost:8000/example/wasm-sgr/
```

457
example/wasm-sgr/index.html Normal file
View File

@ -0,0 +1,457 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghostty VT SGR Parser - WebAssembly Example</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 40px auto;
padding: 0 20px;
line-height: 1.6;
}
h1 {
color: #333;
}
.input-section {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.input-section h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
textarea {
width: 100%;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
resize: vertical;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.output {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
font-size: 14px;
}
.error {
background: #fee;
border-color: #faa;
color: #c00;
}
.status {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.attribute {
padding: 8px;
margin: 5px 0;
background: white;
border-left: 3px solid #0066cc;
}
.attribute-name {
font-weight: bold;
color: #0066cc;
}
</style>
</head>
<body>
<h1>Ghostty VT SGR Parser - WebAssembly Example</h1>
<p>This example demonstrates parsing terminal SGR (Select Graphic Rendition) sequences using the Ghostty VT WebAssembly module.</p>
<div class="status" id="status">Loading WebAssembly module...</div>
<div class="input-section">
<h3>SGR Sequence</h3>
<label for="sequence">Enter SGR sequence (numbers separated by ':' or ';'):</label>
<textarea id="sequence" rows="2" disabled>4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136</textarea>
<p style="font-size: 13px; color: #666; margin-top: 5px;">The parser runs live as you type.</p>
</div>
<div id="output" class="output">Waiting for input...</div>
<p><strong>Note:</strong> This example must be served via HTTP (not opened directly as a file). See the README for instructions.</p>
<script>
let wasmInstance = null;
let wasmMemory = null;
async function loadWasm() {
try {
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
const wasmBytes = await response.arrayBuffer();
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
env: {
log: (ptr, len) => {
const bytes = new Uint8Array(wasmModule.instance.exports.memory.buffer, ptr, len);
const text = new TextDecoder().decode(bytes);
console.log('[wasm]', text);
}
}
});
wasmInstance = wasmModule.instance;
wasmMemory = wasmInstance.exports.memory;
return true;
} catch (e) {
console.error('Failed to load WASM:', e);
if (window.location.protocol === 'file:') {
throw new Error('Cannot load WASM from file:// protocol. Please serve via HTTP (see README)');
}
return false;
}
}
function getBuffer() {
return wasmMemory.buffer;
}
// SGR attribute tag values from include/ghostty/vt/sgr.h
const SGR_ATTR_TAGS = {
UNSET: 0,
UNKNOWN: 1,
BOLD: 2,
RESET_BOLD: 3,
ITALIC: 4,
RESET_ITALIC: 5,
FAINT: 6,
UNDERLINE: 7,
RESET_UNDERLINE: 8,
UNDERLINE_COLOR: 9,
UNDERLINE_COLOR_256: 10,
RESET_UNDERLINE_COLOR: 11,
OVERLINE: 12,
RESET_OVERLINE: 13,
BLINK: 14,
RESET_BLINK: 15,
INVERSE: 16,
RESET_INVERSE: 17,
INVISIBLE: 18,
RESET_INVISIBLE: 19,
STRIKETHROUGH: 20,
RESET_STRIKETHROUGH: 21,
DIRECT_COLOR_FG: 22,
DIRECT_COLOR_BG: 23,
BG_8: 24,
FG_8: 25,
RESET_FG: 26,
RESET_BG: 27,
BRIGHT_BG_8: 28,
BRIGHT_FG_8: 29,
BG_256: 30,
FG_256: 31
};
// Underline style values
const UNDERLINE_STYLES = {
0: 'none',
1: 'single',
2: 'double',
3: 'curly',
4: 'dotted',
5: 'dashed'
};
function getTagName(tag) {
for (const [name, value] of Object.entries(SGR_ATTR_TAGS)) {
if (value === tag) return name;
}
return `UNKNOWN(${tag})`;
}
function parseSGR() {
const outputDiv = document.getElementById('output');
try {
const sequenceText = document.getElementById('sequence').value.trim();
if (!sequenceText) {
outputDiv.className = 'output';
outputDiv.textContent = 'Enter an SGR sequence to parse...';
return;
}
// Parse the raw sequence into parameters and separators
const params = [];
const separators = [];
let currentNum = '';
for (let i = 0; i < sequenceText.length; i++) {
const char = sequenceText[i];
if (char === ':' || char === ';') {
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
separators.push(char);
currentNum = '';
}
} else if (char >= '0' && char <= '9') {
currentNum += char;
} else if (char !== ' ' && char !== '\t' && char !== '\n') {
throw new Error(`Invalid character in sequence: '${char}'`);
}
}
// Don't forget the last number
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
}
if (params.length === 0) {
outputDiv.className = 'output error';
outputDiv.textContent = 'Error: No parameters found in sequence';
return;
}
// Create SGR parser
const parserPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_sgr_new(0, parserPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_sgr_new failed with result ${result}`);
}
const parserPtr = new DataView(getBuffer()).getUint32(parserPtrPtr, true);
// Allocate and set parameters
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc_u16_array(params.length);
const paramsView = new Uint16Array(getBuffer(), paramsPtr, params.length);
params.forEach((p, i) => paramsView[i] = p);
// Allocate and set separators (or use null if empty)
let sepsPtr = 0;
if (separators.length > 0) {
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(separators.length);
const sepsView = new Uint8Array(getBuffer(), sepsPtr, separators.length);
separators.forEach((s, i) => sepsView[i] = s.charCodeAt(0));
}
// Set parameters in parser
const setResult = wasmInstance.exports.ghostty_sgr_set_params(
parserPtr,
paramsPtr,
sepsPtr,
params.length
);
if (setResult !== 0) {
throw new Error(`ghostty_sgr_set_params failed with result ${setResult}`);
}
// Build output
let output = 'Parsing SGR sequence:\n';
output += 'ESC[';
params.forEach((p, i) => {
if (i > 0) output += separators[i - 1];
output += p;
});
output += 'm\n\n';
// Iterate through attributes
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc_sgr_attribute();
let count = 0;
while (wasmInstance.exports.ghostty_sgr_next(parserPtr, attrPtr)) {
count++;
// Use the new ghostty_sgr_attribute_tag getter function
const tag = wasmInstance.exports.ghostty_sgr_attribute_tag(attrPtr);
// Use ghostty_sgr_attribute_value to get a pointer to the value union
const valuePtr = wasmInstance.exports.ghostty_sgr_attribute_value(attrPtr);
output += `Attribute ${count}: `;
switch (tag) {
case SGR_ATTR_TAGS.UNDERLINE: {
const view = new DataView(getBuffer(), valuePtr, 4);
const style = view.getUint32(0, true);
output += `Underline style = ${UNDERLINE_STYLES[style] || `unknown(${style})`}\n`;
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_FG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Foreground RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_BG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Background RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.UNDERLINE_COLOR: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Underline color RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.FG_8:
case SGR_ATTR_TAGS.BG_8:
case SGR_ATTR_TAGS.FG_256:
case SGR_ATTR_TAGS.BG_256:
case SGR_ATTR_TAGS.UNDERLINE_COLOR_256: {
const view = new DataView(getBuffer(), valuePtr, 1);
const color = view.getUint8(0);
const colorType = tag === SGR_ATTR_TAGS.FG_8 ? 'Foreground 8-color' :
tag === SGR_ATTR_TAGS.BG_8 ? 'Background 8-color' :
tag === SGR_ATTR_TAGS.FG_256 ? 'Foreground 256-color' :
tag === SGR_ATTR_TAGS.BG_256 ? 'Background 256-color' :
'Underline 256-color';
output += `${colorType} = ${color}\n`;
break;
}
case SGR_ATTR_TAGS.BOLD:
output += 'Bold\n';
break;
case SGR_ATTR_TAGS.ITALIC:
output += 'Italic\n';
break;
case SGR_ATTR_TAGS.UNSET:
output += 'Reset all attributes\n';
break;
case SGR_ATTR_TAGS.UNKNOWN:
output += 'Unknown attribute\n';
break;
default:
output += `Other attribute (tag=${getTagName(tag)})\n`;
break;
}
}
output += `\nTotal attributes parsed: ${count}`;
outputDiv.className = 'output';
outputDiv.textContent = output;
// Cleanup
wasmInstance.exports.ghostty_wasm_free_sgr_attribute(attrPtr);
wasmInstance.exports.ghostty_sgr_free(parserPtr);
} catch (e) {
console.error('Parse error:', e);
outputDiv.className = 'output error';
outputDiv.textContent = `Error: ${e.message}\n\nStack trace:\n${e.stack}`;
}
}
async function init() {
const statusDiv = document.getElementById('status');
const sequenceInput = document.getElementById('sequence');
try {
statusDiv.textContent = 'Loading WebAssembly module...';
const loaded = await loadWasm();
if (!loaded) {
throw new Error('Failed to load WebAssembly module');
}
statusDiv.textContent = '';
sequenceInput.disabled = false;
// Parse live as user types
sequenceInput.addEventListener('input', parseSGR);
// Parse the default example on load
parseSGR();
} catch (e) {
statusDiv.textContent = `Error: ${e.message}`;
statusDiv.style.color = '#c00';
}
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

@ -0,0 +1,24 @@
# Example: stdin to HTML using `vtStream` and `TerminalFormatter`
This example demonstrates how to read VT sequences from stdin, parse them
using `vtStream`, and output styled HTML using `TerminalFormatter`. The
purpose of this example is primarily to show how to use formatters with
terminals.
Requires the Zig version stated in the `build.zig.zon` file.
## Usage
Basic usage:
```shell-session
echo -e "Hello \033[1;32mGreen\033[0m World" | zig build run
```
This will output HTML with inline styles and CSS palette variables.
You can also pipe complex terminal output:
```shell-session
ls --color=always | zig build run > output.html
```

View File

@ -0,0 +1,39 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const test_step = b.step("test", "Run unit tests");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
if (b.lazyDependency("ghostty", .{})) |dep| {
exe_mod.addImport(
"ghostty-vt",
dep.module("ghostty-vt"),
);
}
const exe = b.addExecutable(.{
.name = "zig_formatter",
.root_module = exe_mod,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_module = exe_mod,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
test_step.dependOn(&run_exe_unit_tests.step);
}

View File

@ -0,0 +1,13 @@
.{
.name = .zig_formatter,
.version = "0.0.0",
.fingerprint = 0x578de530797eafe6,
.dependencies = .{
.ghostty = .{ .path = "../../" },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,42 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const alloc = gpa.allocator();
// Create a terminal
var t: ghostty_vt.Terminal = try .init(alloc, .{ .cols = 150, .rows = 80 });
defer t.deinit(alloc);
// Create a read-only VT stream for parsing terminal sequences
var stream = t.vtStream();
defer stream.deinit();
// Read from stdin
const stdin = std.fs.File.stdin();
var buf: [4096]u8 = undefined;
while (true) {
const n = try stdin.readAll(&buf);
if (n == 0) break;
// Replace \n with \r\n
for (buf[0..n]) |byte| {
if (byte == '\n') try stream.next('\r');
try stream.next(byte);
}
}
// Use TerminalFormatter to emit HTML
const formatter: ghostty_vt.formatter.TerminalFormatter = .init(&t, .{
.emit = .html,
.palette = &t.colors.palette.current,
});
// Write to stdout
var stdout_writer = std.fs.File.stdout().writer(&buf);
const stdout = &stdout_writer.interface;
try stdout.print("{f}", .{formatter});
try stdout.flush();
}

View File

@ -0,0 +1,33 @@
# Example: `vtStream` API for Parsing Terminal Streams
This example demonstrates how to use the `vtStream` API to parse and process
VT sequences. The `vtStream` API is ideal for read-only terminal applications
that need to parse terminal output without responding to queries, such as:
- Replay tooling
- CI log viewers
- PaaS builder output
- etc.
The stream processes VT escape sequences and updates terminal state, while
ignoring sequences that require responses (like device status queries).
Requires the Zig version stated in the `build.zig.zon` file.
## Usage
Run the program:
```shell-session
zig build run
```
The example will process various VT sequences including:
- Plain text output
- ANSI color codes
- Cursor positioning
- Line clearing
- Multiple line handling
And display the final terminal state after processing all sequences.

View File

@ -0,0 +1,39 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the app");
const test_step = b.step("test", "Run unit tests");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
if (b.lazyDependency("ghostty", .{})) |dep| {
exe_mod.addImport(
"ghostty-vt",
dep.module("ghostty-vt"),
);
}
const exe = b.addExecutable(.{
.name = "zig_vt_stream",
.root_module = exe_mod,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
run_step.dependOn(&run_cmd.step);
const exe_unit_tests = b.addTest(.{
.root_module = exe_mod,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
test_step.dependOn(&run_exe_unit_tests.step);
}

View File

@ -0,0 +1,14 @@
.{
.name = .zig_vt_stream,
.version = "0.0.0",
.fingerprint = 0x34c1f71303690b3f,
.minimum_zig_version = "0.15.1",
.dependencies = .{
.ghostty = .{ .path = "../../" },
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}

View File

@ -0,0 +1,40 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
pub fn main() !void {
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer _ = gpa.deinit();
const alloc = gpa.allocator();
var t: ghostty_vt.Terminal = try .init(alloc, .{ .cols = 80, .rows = 24 });
defer t.deinit(alloc);
// Create a read-only VT stream for parsing terminal sequences
var stream = t.vtStream();
defer stream.deinit();
// Basic text with newline
try stream.nextSlice("Hello, World!\r\n");
// ANSI color codes: ESC[1;32m = bold green, ESC[0m = reset
try stream.nextSlice("\x1b[1;32mGreen Text\x1b[0m\r\n");
// Cursor positioning: ESC[1;1H = move to row 1, column 1
try stream.nextSlice("\x1b[1;1HTop-left corner\r\n");
// Cursor movement: ESC[5B = move down 5 lines
try stream.nextSlice("\x1b[5B");
try stream.nextSlice("Moved down!\r\n");
// Erase line: ESC[2K = clear entire line
try stream.nextSlice("\x1b[2K");
try stream.nextSlice("New content\r\n");
// Multiple lines
try stream.nextSlice("Line A\r\nLine B\r\nLine C\r\n");
// Get the final terminal state as a plain string
const str = try t.plainString(alloc);
defer alloc.free(str);
std.debug.print("{s}\n", .{str});
}

View File

@ -3,11 +3,11 @@
"flake-compat": { "flake-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1747046372, "lastModified": 1761588595,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -34,36 +34,45 @@
"type": "github" "type": "github"
} }
}, },
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1755776884,
"narHash": "sha256-CPM7zm6csUx7vSfKvzMDIjepEJv1u/usmaT7zydzbuI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "4fb695d10890e9fc6a19deadf85ff79ffb78da86",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 315532800, "lastModified": 1763191728,
"narHash": "sha256-YwoXN6fthkakCFD7nXPcUK+rkNr6ZTNTuF8zdGaxZo0=", "narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
"rev": "dc704e6102e76aad573f63b74c742cd96f8f1e6c", "rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre870318.dc704e6102e7/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre896415.1d4c88323ac3/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" "url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
} }
}, },
"nixpkgs_2": {
"locked": {
"lastModified": 1758360447,
"narHash": "sha256-XDY3A83bclygHDtesRoaRTafUd80Q30D/Daf9KSG6bs=",
"rev": "8eaee110344796db060382e15d3af0a9fc396e0e",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-25.11pre864002.8eaee1103447/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"home-manager": "home-manager",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"zig": "zig", "zig": "zig",
"zon2nix": "zon2nix" "zon2nix": "zon2nix"
@ -97,11 +106,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1759192380, "lastModified": 1763295135,
"narHash": "sha256-0BWJgt4OSzxCESij5oo8WLWrPZ+1qLp8KUQe32QeV4Q=", "narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=",
"owner": "mitchellh", "owner": "mitchellh",
"repo": "zig-overlay", "repo": "zig-overlay",
"rev": "0bcd1401ed43d10f10cbded49624206553e92f57", "rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -112,7 +121,9 @@
}, },
"zon2nix": { "zon2nix": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs_2" "nixpkgs": [
"nixpkgs"
]
}, },
"locked": { "locked": {
"lastModified": 1758405547, "lastModified": 1758405547,

View File

@ -6,7 +6,9 @@
# glibc versions used by our dependencies from Nix are compatible with the # glibc versions used by our dependencies from Nix are compatible with the
# system glibc that the user is building for. # system glibc that the user is building for.
# #
# We are currently on unstable to get Zig 0.15 for our package.nix # We are currently on nixpkgs-unstable to get Zig 0.15 for our package.nix and
# Gnome 49/Gtk 4.20.
#
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
@ -28,10 +30,14 @@
zon2nix = { zon2nix = {
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245"; url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
inputs = { inputs = {
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch nixpkgs.follows = "nixpkgs";
# we are using for "normal" builds. };
# };
# nixpkgs.follows = "nixpkgs";
home-manager = {
url = "github:nix-community/home-manager?ref=release-25.05";
inputs = {
nixpkgs.follows = "nixpkgs";
}; };
}; };
}; };
@ -41,6 +47,7 @@
nixpkgs, nixpkgs,
zig, zig,
zon2nix, zon2nix,
home-manager,
... ...
}: }:
builtins.foldl' nixpkgs.lib.recursiveUpdate {} ( builtins.foldl' nixpkgs.lib.recursiveUpdate {} (
@ -48,10 +55,18 @@
system: let system: let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in { in {
devShell.${system} = pkgs.callPackage ./nix/devShell.nix { devShells.${system}.default = pkgs.callPackage ./nix/devShell.nix {
zig = zig.packages.${system}."0.15.1"; zig = zig.packages.${system}."0.15.2";
wraptest = pkgs.callPackage ./nix/wraptest.nix {}; wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
zon2nix = zon2nix; zon2nix = zon2nix;
python3 = pkgs.python3.override {
self = pkgs.python3;
packageOverrides = pyfinal: pyprev: {
blessed = pyfinal.callPackage ./nix/pkgs/blessed.nix {};
ucs-detect = pyfinal.callPackage ./nix/pkgs/ucs-detect.nix {};
};
};
}; };
packages.${system} = let packages.${system} = let
@ -72,6 +87,10 @@
formatter.${system} = pkgs.alejandra; formatter.${system} = pkgs.alejandra;
checks.${system} = import ./nix/tests.nix {
inherit home-manager nixpkgs self system;
};
apps.${system} = let apps.${system} = let
runVM = ( runVM = (
module: let module: let
@ -88,6 +107,9 @@
in { in {
type = "app"; type = "app";
program = "${program}"; program = "${program}";
meta = {
description = "start a vm from ${toString module}";
};
} }
); );
in { in {
@ -107,17 +129,12 @@
overlays = { overlays = {
default = self.overlays.releasefast; default = self.overlays.releasefast;
releasefast = final: prev: { releasefast = final: prev: {
ghostty = self.packages.${prev.system}.ghostty-releasefast; ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-releasefast;
}; };
debug = final: prev: { debug = final: prev: {
ghostty = self.packages.${prev.system}.ghostty-debug; ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-debug;
}; };
}; };
create-vm = import ./nix/vm/create.nix;
create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
create-gnome-vm = import ./nix/vm/create-gnome.nix;
create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
create-xfce-vm = import ./nix/vm/create-xfce.nix;
}; };
nixConfig = { nixConfig = {

View File

@ -13,12 +13,12 @@ modules:
- chmod a+x /app/zig/zig - chmod a+x /app/zig/zig
sources: sources:
- type: archive - type: archive
sha256: c61c5da6edeea14ca51ecd5e4520c6f4189ef5250383db33d01848293bfafe05 sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239
url: https://ziglang.org/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz
only-arches: [x86_64] only-arches: [x86_64]
- type: archive - type: archive
sha256: bb4a8d2ad735e7fba764c497ddf4243cb129fece4148da3222a7046d3f1f19fe sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f
url: https://ziglang.org/download/0.15.1/zig-aarch64-linux-0.15.1.tar.xz url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz
only-arches: [aarch64] only-arches: [aarch64]
- name: bzip2-redirect - name: bzip2-redirect

View File

@ -31,9 +31,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst", "url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV", "dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a" "sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48"
}, },
{ {
"type": "archive", "type": "archive",
@ -61,9 +61,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
"dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv", "dest": "vendor/p/N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
"sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4" "sha256": "e5078f050952b33f56dabe334d2dd00fe70301ec8e6e1479d44d80909dd92149"
}, },
{ {
"type": "archive", "type": "archive",
@ -132,22 +132,22 @@
"sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf" "sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf"
}, },
{ {
"type": "archive", "type": "git",
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz", "url": "https://github.com/jacobsandlund/uucode",
"dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT", "commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
"sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07" "dest": "vendor/p/uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM"
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz", "url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
"dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ", "dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
"sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2" "sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e"
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz", "url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
"dest": "vendor/p/vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA", "dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
"sha256": "7aae580b6e8e6348b671d409d195cc67ea36bc740b10534d1b342de59bb3e013" "sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6"
}, },
{ {
"type": "archive", "type": "archive",
@ -169,27 +169,15 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz", "url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
"dest": "vendor/p/z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef", "dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
"sha256": "e7fa91640221d54e36bfb8ea97d5b48ebdb3cd066dbb7f43c493cb56b4b26c98" "sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10"
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz", "url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
"dest": "vendor/p/zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR", "dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
"sha256": "f018a76da9d27d978103c481028a55c7024e6cddfafc14e9c551c004a89cb0c4" "sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568"
},
{
"type": "git",
"url": "https://codeberg.org/ivanstepanovftw/zg",
"commit": "4fe689e56ce2ed5a8f59308b471bccd7da89fac9",
"dest": "vendor/p/zg-0.14.1-oGqU3J4_tAKBfyes3AWleKDjo-IcYvnEwaB8qxOqFMwM"
},
{
"type": "archive",
"url": "https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz",
"dest": "vendor/p/zg-0.15.1-oGqU3M0-tALZCy7boQS86znlBloyKx6--JriGlY0Paa9",
"sha256": "059873d673eac4aea176c250eba9fb264e3332015218b5e6f1e534097ffb9832"
}, },
{ {
"type": "archive", "type": "archive",
@ -209,12 +197,6 @@
"dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe", "dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
"sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e" "sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e"
}, },
{
"type": "git",
"url": "https://github.com/ivanstepanovftw/zigimg",
"commit": "aa4c31db872612c39edbb79f753b3cd9a79fe726",
"dest": "vendor/p/zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL"
},
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz", "url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",

View File

@ -45,6 +45,11 @@ typedef enum {
GHOSTTY_CLIPBOARD_SELECTION, GHOSTTY_CLIPBOARD_SELECTION,
} ghostty_clipboard_e; } ghostty_clipboard_e;
typedef struct {
const char *mime;
const char *data;
} ghostty_clipboard_content_s;
typedef enum { typedef enum {
GHOSTTY_CLIPBOARD_REQUEST_PASTE, GHOSTTY_CLIPBOARD_REQUEST_PASTE,
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ, GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
@ -507,6 +512,12 @@ typedef enum {
GHOSTTY_GOTO_SPLIT_RIGHT, GHOSTTY_GOTO_SPLIT_RIGHT,
} ghostty_action_goto_split_e; } ghostty_action_goto_split_e;
// apprt.action.GotoWindow
typedef enum {
GHOSTTY_GOTO_WINDOW_PREVIOUS,
GHOSTTY_GOTO_WINDOW_NEXT,
} ghostty_action_goto_window_e;
// apprt.action.ResizeSplit.Direction // apprt.action.ResizeSplit.Direction
typedef enum { typedef enum {
GHOSTTY_RESIZE_SPLIT_UP, GHOSTTY_RESIZE_SPLIT_UP,
@ -568,6 +579,12 @@ typedef enum {
GHOSTTY_QUIT_TIMER_STOP, GHOSTTY_QUIT_TIMER_STOP,
} ghostty_action_quit_timer_e; } ghostty_action_quit_timer_e;
// apprt.action.Readonly
typedef enum {
GHOSTTY_READONLY_OFF,
GHOSTTY_READONLY_ON,
} ghostty_action_readonly_e;
// apprt.action.DesktopNotification.C // apprt.action.DesktopNotification.C
typedef struct { typedef struct {
const char* title; const char* title;
@ -579,6 +596,12 @@ typedef struct {
const char* title; const char* title;
} ghostty_action_set_title_s; } ghostty_action_set_title_s;
// apprt.action.PromptTitle
typedef enum {
GHOSTTY_PROMPT_TITLE_SURFACE,
GHOSTTY_PROMPT_TITLE_TAB,
} ghostty_action_prompt_title_e;
// apprt.action.Pwd.C // apprt.action.Pwd.C
typedef struct { typedef struct {
const char* pwd; const char* pwd;
@ -695,6 +718,7 @@ typedef struct {
typedef enum { typedef enum {
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN, GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT, GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
} ghostty_action_open_url_kind_e; } ghostty_action_open_url_kind_e;
// apprt.action.OpenUrl.C // apprt.action.OpenUrl.C
@ -708,6 +732,7 @@ typedef struct {
typedef enum { typedef enum {
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
} ghostty_action_close_tab_mode_e; } ghostty_action_close_tab_mode_e;
// apprt.surface.Message.ChildExited // apprt.surface.Message.ChildExited
@ -741,6 +766,28 @@ typedef struct {
uint64_t duration; uint64_t duration;
} ghostty_action_command_finished_s; } 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;
uint64_t offset;
uint64_t len;
} ghostty_action_scrollbar_s;
// apprt.Action.Key // apprt.Action.Key
typedef enum { typedef enum {
GHOSTTY_ACTION_QUIT, GHOSTTY_ACTION_QUIT,
@ -759,6 +806,7 @@ typedef enum {
GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_MOVE_TAB,
GHOSTTY_ACTION_GOTO_TAB, GHOSTTY_ACTION_GOTO_TAB,
GHOSTTY_ACTION_GOTO_SPLIT, GHOSTTY_ACTION_GOTO_SPLIT,
GHOSTTY_ACTION_GOTO_WINDOW,
GHOSTTY_ACTION_RESIZE_SPLIT, GHOSTTY_ACTION_RESIZE_SPLIT,
GHOSTTY_ACTION_EQUALIZE_SPLITS, GHOSTTY_ACTION_EQUALIZE_SPLITS,
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM, GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
@ -767,6 +815,7 @@ typedef enum {
GHOSTTY_ACTION_RESET_WINDOW_SIZE, GHOSTTY_ACTION_RESET_WINDOW_SIZE,
GHOSTTY_ACTION_INITIAL_SIZE, GHOSTTY_ACTION_INITIAL_SIZE,
GHOSTTY_ACTION_CELL_SIZE, GHOSTTY_ACTION_CELL_SIZE,
GHOSTTY_ACTION_SCROLLBAR,
GHOSTTY_ACTION_RENDER, GHOSTTY_ACTION_RENDER,
GHOSTTY_ACTION_INSPECTOR, GHOSTTY_ACTION_INSPECTOR,
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
@ -797,7 +846,12 @@ typedef enum {
GHOSTTY_ACTION_PROGRESS_REPORT, GHOSTTY_ACTION_PROGRESS_REPORT,
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
GHOSTTY_ACTION_COMMAND_FINISHED, GHOSTTY_ACTION_COMMAND_FINISHED,
} ghostty_action_tag_e; GHOSTTY_ACTION_START_SEARCH,
GHOSTTY_ACTION_END_SEARCH,
GHOSTTY_ACTION_SEARCH_TOTAL,
GHOSTTY_ACTION_SEARCH_SELECTED,
GHOSTTY_ACTION_READONLY,
} ghostty_action_tag_e;
typedef union { typedef union {
ghostty_action_split_direction_e new_split; ghostty_action_split_direction_e new_split;
@ -805,13 +859,16 @@ typedef union {
ghostty_action_move_tab_s move_tab; ghostty_action_move_tab_s move_tab;
ghostty_action_goto_tab_e goto_tab; ghostty_action_goto_tab_e goto_tab;
ghostty_action_goto_split_e goto_split; ghostty_action_goto_split_e goto_split;
ghostty_action_goto_window_e goto_window;
ghostty_action_resize_split_s resize_split; ghostty_action_resize_split_s resize_split;
ghostty_action_size_limit_s size_limit; ghostty_action_size_limit_s size_limit;
ghostty_action_initial_size_s initial_size; ghostty_action_initial_size_s initial_size;
ghostty_action_cell_size_s cell_size; ghostty_action_cell_size_s cell_size;
ghostty_action_scrollbar_s scrollbar;
ghostty_action_inspector_e inspector; ghostty_action_inspector_e inspector;
ghostty_action_desktop_notification_s desktop_notification; ghostty_action_desktop_notification_s desktop_notification;
ghostty_action_set_title_s set_title; ghostty_action_set_title_s set_title;
ghostty_action_prompt_title_e prompt_title;
ghostty_action_pwd_s pwd; ghostty_action_pwd_s pwd;
ghostty_action_mouse_shape_e mouse_shape; ghostty_action_mouse_shape_e mouse_shape;
ghostty_action_mouse_visibility_e mouse_visibility; ghostty_action_mouse_visibility_e mouse_visibility;
@ -829,6 +886,10 @@ typedef union {
ghostty_surface_message_childexited_s child_exited; ghostty_surface_message_childexited_s child_exited;
ghostty_action_progress_report_s progress_report; ghostty_action_progress_report_s progress_report;
ghostty_action_command_finished_s command_finished; 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_readonly_e readonly;
} ghostty_action_u; } ghostty_action_u;
typedef struct { typedef struct {
@ -846,8 +907,9 @@ typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
void*, void*,
ghostty_clipboard_request_e); ghostty_clipboard_request_e);
typedef void (*ghostty_runtime_write_clipboard_cb)(void*, typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
const char*,
ghostty_clipboard_e, ghostty_clipboard_e,
const ghostty_clipboard_content_s*,
size_t,
bool); bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool); typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t, typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,

View File

@ -1,7 +1,7 @@
/** /**
* @file vt.h * @file vt.h
* *
* libghostty-vt - Virtual terminal sequence parsing library * libghostty-vt - Virtual terminal emulator library
* *
* This library provides functionality for parsing and handling terminal * This library provides functionality for parsing and handling terminal
* escape sequences as well as maintaining terminal state such as styles, * escape sequences as well as maintaining terminal state such as styles,
@ -12,14 +12,15 @@
*/ */
/** /**
* @mainpage libghostty-vt - Virtual Terminal Sequence Parser * @mainpage libghostty-vt - Virtual Terminal Emulator Library
* *
* libghostty-vt is a C library which implements a modern terminal emulator, * libghostty-vt is a C library which implements a modern terminal emulator,
* extracted from the [Ghostty](https://ghostty.org) terminal emulator. * extracted from the [Ghostty](https://ghostty.org) terminal emulator.
* *
* libghostty-vt contains the logic for handling the core parts of a terminal * libghostty-vt contains the logic for handling the core parts of a terminal
* emulator: parsing terminal escape sequences and maintaining terminal state. * emulator: parsing terminal escape sequences, maintaining terminal state,
* It can handle scrollback, line wrapping, reflow on resize, and more. * encoding input events, etc. It can handle scrollback, line wrapping,
* reflow on resize, and more.
* *
* @warning This library is currently in development and the API is not yet stable. * @warning This library is currently in development and the API is not yet stable.
* Breaking changes are expected in future versions. Use with caution in production code. * Breaking changes are expected in future versions. Use with caution in production code.
@ -27,9 +28,41 @@
* @section groups_sec API Reference * @section groups_sec API Reference
* *
* The API is organized into the following groups: * The API is organized into the following groups:
* - @ref key "Key Encoding" - Encode key events into terminal sequences
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
* - @ref paste "Paste Utilities" - Validate paste data safety
* - @ref allocator "Memory Management" - Memory management and custom allocators * - @ref allocator "Memory Management" - Memory management and custom allocators
* - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions
* *
* @section examples_sec Examples
*
* Complete working examples:
* - @ref c-vt/src/main.c - OSC parser example
* - @ref c-vt-key-encode/src/main.c - Key encoding example
* - @ref c-vt-paste/src/main.c - Paste safety check example
* - @ref c-vt-sgr/src/main.c - SGR parser example
*
*/
/** @example c-vt/src/main.c
* This example demonstrates how to use the OSC parser to parse an OSC sequence,
* extract command information, and retrieve command-specific data like window titles.
*/
/** @example c-vt-key-encode/src/main.c
* This example demonstrates how to use the key encoder to convert key events
* into terminal escape sequences using the Kitty keyboard protocol.
*/
/** @example c-vt-paste/src/main.c
* This example demonstrates how to use the paste utilities to check if
* paste data is safe before sending it to the terminal.
*/
/** @example c-vt-sgr/src/main.c
* This example demonstrates how to use the SGR parser to parse terminal
* styling sequences and extract text attributes like colors and underline styles.
*/ */
#ifndef GHOSTTY_VT_H #ifndef GHOSTTY_VT_H
@ -39,414 +72,13 @@
extern "C" { extern "C" {
#endif #endif
#include <stdbool.h> #include <ghostty/vt/result.h>
#include <stddef.h> #include <ghostty/vt/allocator.h>
#include <stdint.h> #include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
//------------------------------------------------------------------- #include <ghostty/vt/key.h>
// Types #include <ghostty/vt/paste.h>
#include <ghostty/vt/wasm.h>
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences. This isn't a full VT
* parser; it is only the OSC parser component. This is useful if you have
* a parser already and want to only extract and handle OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParser *GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data using
* `ghostty_osc_command_type` and `ghostty_osc_command_data`.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommand *GhosttyOscCommand;
/**
* Result codes for libghostty-vt operations.
*/
typedef enum {
/** Operation completed successfully */
GHOSTTY_SUCCESS = 0,
/** Operation failed due to failed allocation */
GHOSTTY_OUT_OF_MEMORY = -1,
} GhosttyResult;
/**
* OSC command types.
*
* @ingroup osc
*/
typedef enum {
GHOSTTY_OSC_COMMAND_INVALID = 0,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
GHOSTTY_OSC_COMMAND_PROMPT_START = 3,
GHOSTTY_OSC_COMMAND_PROMPT_END = 4,
GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5,
GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6,
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7,
GHOSTTY_OSC_COMMAND_REPORT_PWD = 8,
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9,
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10,
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11,
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12,
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13,
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14,
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15,
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16,
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17,
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18,
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19,
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20,
} GhosttyOscCommandType;
/**
* OSC command data types.
*
* These values specify what type of data to extract from an OSC command
* using `ghostty_osc_command_data`.
*
* @ingroup osc
*/
typedef enum {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_OSC_DATA_INVALID = 0,
/**
* Window title string data.
*
* Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
*
* Output type: const char ** (pointer to null-terminated string)
*
* Lifetime: Valid until the next call to any ghostty_osc_* function with
* the same parser instance. Memory is owned by the parser.
*/
GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
} GhosttyOscCommandData;
//-------------------------------------------------------------------
// Allocator Interface
/** @defgroup allocator Memory Management
*
* libghostty-vt does require memory allocation for various operations,
* but is resilient to allocation failures and will gracefully handle
* out-of-memory situations by returning error codes.
*
* The exact memory management semantics are documented in the relevant
* functions and data structures.
*
* libghostty-vt uses explicit memory allocation via an allocator
* interface provided by GhosttyAllocator. The interface is based on the
* [Zig](https://ziglang.org) allocator interface, since this has been
* shown to be a flexible and powerful interface in practice and enables
* a wide variety of allocation strategies.
*
* **For the common case, you can pass NULL as the allocator for any
* function that accepts one,** and libghostty will use a default allocator.
* The default allocator will be libc malloc/free if libc is linked.
* Otherwise, a custom allocator is used (currently Zig's SMP allocator)
* that doesn't require any external dependencies.
*
* ## Basic Usage
*
* For simple use cases, you can ignore this interface entirely by passing NULL
* as the allocator parameter to functions that accept one. This will use the
* default allocator (typically libc malloc/free, if libc is linked, but
* we provide our own default allocator if libc isn't linked).
*
* To use a custom allocator:
* 1. Implement the GhosttyAllocatorVtable function pointers
* 2. Create a GhosttyAllocator struct with your vtable and context
* 3. Pass the allocator to functions that accept one
*
* @{
*/
/**
* Function table for custom memory allocator operations.
*
* This vtable defines the interface for a custom memory allocator. All
* function pointers must be valid and non-NULL.
*
* @ingroup allocator
*
* If you're not going to use a custom allocator, you can ignore all of
* this. All functions that take an allocator pointer allow NULL to use a
* default allocator.
*
* The interface is based on the Zig allocator interface. I'll say up front
* that it is easy to look at this interface and think "wow, this is really
* overcomplicated". The reason for this complexity is well thought out by
* the Zig folks, and it enables a diverse set of allocation strategies
* as shown by the Zig ecosystem. As a consolation, please note that many
* of the arguments are only needed for advanced use cases and can be
* safely ignored in simple implementations. For example, if you look at
* the Zig implementation of the libc allocator in `lib/std/heap.zig`
* (search for CAllocator), you'll see it is very simple.
*
* We chose to align with the Zig allocator interface because:
*
* 1. It is a proven interface that serves a wide variety of use cases
* in the real world via the Zig ecosystem. It's shown to work.
*
* 2. Our core implementation itself is Zig, and this lets us very
* cheaply and easily convert between C and Zig allocators.
*
* NOTE(mitchellh): In the future, we can have default implementations of
* resize/remap and allow those to be null.
*/
typedef struct {
/**
* Return a pointer to `len` bytes with specified `alignment`, or return
* `NULL` indicating the allocation failed.
*
* @param ctx The allocator context
* @param len Number of bytes to allocate
* @param alignment Required alignment for the allocation. Guaranteed to
* be a power of two between 1 and 16 inclusive.
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return Pointer to allocated memory, or NULL if allocation failed
*/
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
/**
* Attempt to expand or shrink memory in place.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* `new_len` must be greater than zero.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to resize
* @param memory_len Current size of the memory block
* @param alignment Alignment (must match original allocation)
* @param new_len New requested size
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return true if resize was successful in-place, false if relocation would be required
*/
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
/**
* Attempt to expand or shrink memory, allowing relocation.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* A non-`NULL` return value indicates the resize was successful. The
* allocation may have same address, or may have been relocated. In either
* case, the allocation now has size of `new_len`. A `NULL` return value
* indicates that the resize would be equivalent to allocating new memory,
* copying the bytes from the old memory, and then freeing the old memory.
* In such case, it is more efficient for the caller to perform the copy.
*
* `new_len` must be greater than zero.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to remap
* @param memory_len Current size of the memory block
* @param alignment Alignment (must match original allocation)
* @param new_len New requested size
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
*/
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
/**
* Free and invalidate a region of memory.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to free
* @param memory_len Size of the memory block
* @param alignment Alignment (must match original allocation)
* @param ret_addr First return address of the allocation call stack (0 if not provided)
*/
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
} GhosttyAllocatorVtable;
/**
* Custom memory allocator.
*
* For functions that take an allocator pointer, a NULL pointer indicates
* that the default allocator should be used. The default allocator will
* be libc malloc/free if we're linking to libc. If libc isn't linked,
* a custom allocator is used (currently Zig's SMP allocator).
*
* @ingroup allocator
*
* Usage example:
* @code
* GhosttyAllocator allocator = {
* .vtable = &my_allocator_vtable,
* .ctx = my_allocator_state
* };
* @endcode
*/
typedef struct {
/**
* Opaque context pointer passed to all vtable functions.
* This allows the allocator implementation to maintain state
* or reference external resources needed for memory management.
*/
void *ctx;
/**
* Pointer to the allocator's vtable containing function pointers
* for memory operations (alloc, resize, remap, free).
*/
const GhosttyAllocatorVtable *vtable;
} GhosttyAllocator;
/** @} */ // end of allocator group
//-------------------------------------------------------------------
// Functions
/** @defgroup osc OSC Parser
*
* OSC (Operating System Command) sequence parser and command handling.
*
* The parser operates in a streaming fashion, processing input byte-by-byte
* to handle OSC sequences that may arrive in fragments across multiple reads.
* This interface makes it easy to integrate into most environments and avoids
* over-allocating buffers.
*
* ## Basic Usage
*
* 1. Create a parser instance with ghostty_osc_new()
* 2. Feed bytes to the parser using ghostty_osc_next()
* 3. Finalize parsing with ghostty_osc_end() to get the command
* 4. Query command type and extract data using ghostty_osc_command_type()
* and ghostty_osc_command_data()
* 5. Free the parser with ghostty_osc_free() when done
*
* @{
*/
/**
* Create a new OSC parser instance.
*
* Creates a new OSC (Operating System Command) parser using the provided
* allocator. The parser must be freed using ghostty_vt_osc_free() when
* no longer needed.
*
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
* @param parser Pointer to store the created parser handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*/
GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
/**
* Free an OSC parser instance.
*
* Releases all resources associated with the OSC parser. After this call,
* the parser handle becomes invalid and must not be used.
*
* @param parser The parser handle to free (may be NULL)
*/
void ghostty_osc_free(GhosttyOscParser parser);
/**
* Reset an OSC parser instance to its initial state.
*
* Resets the parser state, clearing any partially parsed OSC sequences
* and returning the parser to its initial state. This is useful for
* reusing a parser instance or recovering from parse errors.
*
* @param parser The parser handle to reset, must not be null.
*/
void ghostty_osc_reset(GhosttyOscParser parser);
/**
* Parse the next byte in an OSC sequence.
*
* Processes a single byte as part of an OSC sequence. The parser maintains
* internal state to track the progress through the sequence. Call this
* function for each byte in the sequence data.
*
* When finished pumping the parser with bytes, call ghostty_osc_end
* to get the final result.
*
* @param parser The parser handle, must not be null.
* @param byte The next byte to parse
*/
void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
/**
* Finalize OSC parsing and retrieve the parsed command.
*
* Call this function after feeding all bytes of an OSC sequence to the parser
* using ghostty_osc_next() with the exception of the terminating character
* (ESC or ST). This function finalizes the parsing process and returns the
* parsed OSC command.
*
* The return value is never NULL. Invalid commands will return a command
* with type GHOSTTY_OSC_COMMAND_INVALID.
*
* The terminator parameter specifies the byte that terminated the OSC sequence
* (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
* preserved in the parsed command so that responses can use the same terminator
* format for better compatibility with the calling program. For commands that
* do not require a response, this parameter is ignored and the resulting
* command will not retain the terminator information.
*
* The returned command handle is valid until the next call to any
* `ghostty_osc_*` function with the same parser instance with the exception
* of command introspection functions such as `ghostty_osc_command_type`.
*
* @param parser The parser handle, must not be null.
* @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
* @return Handle to the parsed OSC command
*/
GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
/**
* Get the type of an OSC command.
*
* Returns the type identifier for the given OSC command. This can be used
* to determine what kind of command was parsed and what data might be
* available from it.
*
* @param command The OSC command handle to query (may be NULL)
* @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
*/
GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
/**
* Extract data from an OSC command.
*
* Extracts typed data from the given OSC command based on the specified
* data type. The output pointer must be of the appropriate type for the
* requested data kind. Valid command types, output types, and memory
* safety information are documented in the `GhosttyOscCommandData` enum.
*
* @param command The OSC command handle to query (may be NULL)
* @param data The type of data to extract
* @param out Pointer to store the extracted data (type depends on data parameter)
* @return true if data extraction was successful, false otherwise
*/
bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
/** @} */ // end of osc group
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -0,0 +1,196 @@
/**
* @file allocator.h
*
* Memory management interface for libghostty-vt.
*/
#ifndef GHOSTTY_VT_ALLOCATOR_H
#define GHOSTTY_VT_ALLOCATOR_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/** @defgroup allocator Memory Management
*
* libghostty-vt does require memory allocation for various operations,
* but is resilient to allocation failures and will gracefully handle
* out-of-memory situations by returning error codes.
*
* The exact memory management semantics are documented in the relevant
* functions and data structures.
*
* libghostty-vt uses explicit memory allocation via an allocator
* interface provided by GhosttyAllocator. The interface is based on the
* [Zig](https://ziglang.org) allocator interface, since this has been
* shown to be a flexible and powerful interface in practice and enables
* a wide variety of allocation strategies.
*
* **For the common case, you can pass NULL as the allocator for any
* function that accepts one,** and libghostty will use a default allocator.
* The default allocator will be libc malloc/free if libc is linked.
* Otherwise, a custom allocator is used (currently Zig's SMP allocator)
* that doesn't require any external dependencies.
*
* ## Basic Usage
*
* For simple use cases, you can ignore this interface entirely by passing NULL
* as the allocator parameter to functions that accept one. This will use the
* default allocator (typically libc malloc/free, if libc is linked, but
* we provide our own default allocator if libc isn't linked).
*
* To use a custom allocator:
* 1. Implement the GhosttyAllocatorVtable function pointers
* 2. Create a GhosttyAllocator struct with your vtable and context
* 3. Pass the allocator to functions that accept one
*
* @{
*/
/**
* Function table for custom memory allocator operations.
*
* This vtable defines the interface for a custom memory allocator. All
* function pointers must be valid and non-NULL.
*
* @ingroup allocator
*
* If you're not going to use a custom allocator, you can ignore all of
* this. All functions that take an allocator pointer allow NULL to use a
* default allocator.
*
* The interface is based on the Zig allocator interface. I'll say up front
* that it is easy to look at this interface and think "wow, this is really
* overcomplicated". The reason for this complexity is well thought out by
* the Zig folks, and it enables a diverse set of allocation strategies
* as shown by the Zig ecosystem. As a consolation, please note that many
* of the arguments are only needed for advanced use cases and can be
* safely ignored in simple implementations. For example, if you look at
* the Zig implementation of the libc allocator in `lib/std/heap.zig`
* (search for CAllocator), you'll see it is very simple.
*
* We chose to align with the Zig allocator interface because:
*
* 1. It is a proven interface that serves a wide variety of use cases
* in the real world via the Zig ecosystem. It's shown to work.
*
* 2. Our core implementation itself is Zig, and this lets us very
* cheaply and easily convert between C and Zig allocators.
*
* NOTE(mitchellh): In the future, we can have default implementations of
* resize/remap and allow those to be null.
*/
typedef struct {
/**
* Return a pointer to `len` bytes with specified `alignment`, or return
* `NULL` indicating the allocation failed.
*
* @param ctx The allocator context
* @param len Number of bytes to allocate
* @param alignment Required alignment for the allocation. Guaranteed to
* be a power of two between 1 and 16 inclusive.
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return Pointer to allocated memory, or NULL if allocation failed
*/
void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr);
/**
* Attempt to expand or shrink memory in place.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* `new_len` must be greater than zero.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to resize
* @param memory_len Current size of the memory block
* @param alignment Alignment (must match original allocation)
* @param new_len New requested size
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return true if resize was successful in-place, false if relocation would be required
*/
bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
/**
* Attempt to expand or shrink memory, allowing relocation.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* A non-`NULL` return value indicates the resize was successful. The
* allocation may have same address, or may have been relocated. In either
* case, the allocation now has size of `new_len`. A `NULL` return value
* indicates that the resize would be equivalent to allocating new memory,
* copying the bytes from the old memory, and then freeing the old memory.
* In such case, it is more efficient for the caller to perform the copy.
*
* `new_len` must be greater than zero.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to remap
* @param memory_len Current size of the memory block
* @param alignment Alignment (must match original allocation)
* @param new_len New requested size
* @param ret_addr First return address of the allocation call stack (0 if not provided)
* @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed
*/
void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr);
/**
* Free and invalidate a region of memory.
*
* `memory_len` must equal the length requested from the most recent
* successful call to `alloc`, `resize`, or `remap`. `alignment` must
* equal the same value that was passed as the `alignment` parameter to
* the original `alloc` call.
*
* @param ctx The allocator context
* @param memory Pointer to the memory block to free
* @param memory_len Size of the memory block
* @param alignment Alignment (must match original allocation)
* @param ret_addr First return address of the allocation call stack (0 if not provided)
*/
void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr);
} GhosttyAllocatorVtable;
/**
* Custom memory allocator.
*
* For functions that take an allocator pointer, a NULL pointer indicates
* that the default allocator should be used. The default allocator will
* be libc malloc/free if we're linking to libc. If libc isn't linked,
* a custom allocator is used (currently Zig's SMP allocator).
*
* @ingroup allocator
*
* Usage example:
* @code
* GhosttyAllocator allocator = {
* .vtable = &my_allocator_vtable,
* .ctx = my_allocator_state
* };
* @endcode
*/
typedef struct GhosttyAllocator {
/**
* Opaque context pointer passed to all vtable functions.
* This allows the allocator implementation to maintain state
* or reference external resources needed for memory management.
*/
void *ctx;
/**
* Pointer to the allocator's vtable containing function pointers
* for memory operations (alloc, resize, remap, free).
*/
const GhosttyAllocatorVtable *vtable;
} GhosttyAllocator;
/** @} */
#endif /* GHOSTTY_VT_ALLOCATOR_H */

View File

@ -0,0 +1,96 @@
/**
* @file color.h
*
* Color types and utilities.
*/
#ifndef GHOSTTY_VT_COLOR_H
#define GHOSTTY_VT_COLOR_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* RGB color value.
*
* @ingroup sgr
*/
typedef struct {
uint8_t r; /**< Red component (0-255) */
uint8_t g; /**< Green component (0-255) */
uint8_t b; /**< Blue component (0-255) */
} GhosttyColorRgb;
/**
* Palette color index (0-255).
*
* @ingroup sgr
*/
typedef uint8_t GhosttyColorPaletteIndex;
/** @addtogroup sgr
* @{
*/
/** Black color (0) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BLACK 0
/** Red color (1) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_RED 1
/** Green color (2) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_GREEN 2
/** Yellow color (3) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_YELLOW 3
/** Blue color (4) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BLUE 4
/** Magenta color (5) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_MAGENTA 5
/** Cyan color (6) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_CYAN 6
/** White color (7) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_WHITE 7
/** Bright black color (8) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8
/** Bright red color (9) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9
/** Bright green color (10) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10
/** Bright yellow color (11) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11
/** Bright blue color (12) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12
/** Bright magenta color (13) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13
/** Bright cyan color (14) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14
/** Bright white color (15) @ingroup sgr */
#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15
/** @} */
/**
* Get the RGB color components.
*
* This function extracts the individual red, green, and blue components
* from a GhosttyColorRgb value. Primarily useful in WebAssembly environments
* where accessing struct fields directly is difficult.
*
* @param color The RGB color value
* @param r Pointer to store the red component (0-255)
* @param g Pointer to store the green component (0-255)
* @param b Pointer to store the blue component (0-255)
*
* @ingroup sgr
*/
void ghostty_color_rgb_get(GhosttyColorRgb color,
uint8_t* r,
uint8_t* g,
uint8_t* b);
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_COLOR_H */

80
include/ghostty/vt/key.h Normal file
View File

@ -0,0 +1,80 @@
/**
* @file key.h
*
* Key encoding module - encode key events into terminal escape sequences.
*/
#ifndef GHOSTTY_VT_KEY_H
#define GHOSTTY_VT_KEY_H
/** @defgroup key Key Encoding
*
* Utilities for encoding key events into terminal escape sequences,
* supporting both legacy encoding as well as Kitty Keyboard Protocol.
*
* ## Basic Usage
*
* 1. Create an encoder instance with ghostty_key_encoder_new()
* 2. Configure encoder options with ghostty_key_encoder_setopt().
* 3. For each key event:
* - Create a key event with ghostty_key_event_new()
* - Set event properties (action, key, modifiers, etc.)
* - Encode with ghostty_key_encoder_encode()
* - Free the event with ghostty_key_event_free()
* - Note: You can also reuse the same key event multiple times by
* changing its properties.
* 4. Free the encoder with ghostty_key_encoder_free() when done
*
* ## Example
*
* @code{.c}
* #include <assert.h>
* #include <stdio.h>
* #include <ghostty/vt.h>
*
* int main() {
* // Create encoder
* GhosttyKeyEncoder encoder;
* GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder);
* assert(result == GHOSTTY_SUCCESS);
*
* // Enable Kitty keyboard protocol with all features
* ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS,
* &(uint8_t){GHOSTTY_KITTY_KEY_ALL});
*
* // Create and configure key event for Ctrl+C press
* GhosttyKeyEvent event;
* result = ghostty_key_event_new(NULL, &event);
* assert(result == GHOSTTY_SUCCESS);
* ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS);
* ghostty_key_event_set_key(event, GHOSTTY_KEY_C);
* ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL);
*
* // Encode the key event
* char buf[128];
* size_t written = 0;
* result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
* assert(result == GHOSTTY_SUCCESS);
*
* // Use the encoded sequence (e.g., write to terminal)
* fwrite(buf, 1, written, stdout);
*
* // Cleanup
* ghostty_key_event_free(event);
* ghostty_key_encoder_free(encoder);
* return 0;
* }
* @endcode
*
* For a complete working example, see example/c-vt-key-encode in the
* repository.
*
* @{
*/
#include <ghostty/vt/key/event.h>
#include <ghostty/vt/key/encoder.h>
/** @} */
#endif /* GHOSTTY_VT_KEY_H */

View File

@ -0,0 +1,221 @@
/**
* @file encoder.h
*
* Key event encoding to terminal escape sequences.
*/
#ifndef GHOSTTY_VT_KEY_ENCODER_H
#define GHOSTTY_VT_KEY_ENCODER_H
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/key/event.h>
/**
* Opaque handle to a key encoder instance.
*
* This handle represents a key encoder that converts key events into terminal
* escape sequences.
*
* @ingroup key
*/
typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder;
/**
* Kitty keyboard protocol flags.
*
* Bitflags representing the various modes of the Kitty keyboard protocol.
* These can be combined using bitwise OR operations. Valid values all
* start with `GHOSTTY_KITTY_KEY_`.
*
* @ingroup key
*/
typedef uint8_t GhosttyKittyKeyFlags;
/** Kitty keyboard protocol disabled (all flags off) */
#define GHOSTTY_KITTY_KEY_DISABLED 0
/** Disambiguate escape codes */
#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0)
/** Report key press and release events */
#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1)
/** Report alternate key codes */
#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2)
/** Report all key events including those normally handled by the terminal */
#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3)
/** Report associated text with key events */
#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4)
/** All Kitty keyboard protocol flags enabled */
#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED)
/**
* macOS option key behavior.
*
* Determines whether the "option" key on macOS is treated as "alt" or not.
* See the Ghostty `macos-option-as-alt` configuration option for more details.
*
* @ingroup key
*/
typedef enum {
/** Option key is not treated as alt */
GHOSTTY_OPTION_AS_ALT_FALSE = 0,
/** Option key is treated as alt */
GHOSTTY_OPTION_AS_ALT_TRUE = 1,
/** Only left option key is treated as alt */
GHOSTTY_OPTION_AS_ALT_LEFT = 2,
/** Only right option key is treated as alt */
GHOSTTY_OPTION_AS_ALT_RIGHT = 3,
} GhosttyOptionAsAlt;
/**
* Key encoder option identifiers.
*
* These values are used with ghostty_key_encoder_setopt() to configure
* the behavior of the key encoder.
*
* @ingroup key
*/
typedef enum {
/** Terminal DEC mode 1: cursor key application mode (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0,
/** Terminal DEC mode 66: keypad key application mode (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1,
/** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2,
/** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3,
/** xterm modifyOtherKeys mode 2 (value: bool) */
GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4,
/** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */
GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5,
/** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */
GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6,
} GhosttyKeyEncoderOption;
/**
* Create a new key encoder instance.
*
* Creates a new key encoder with default options. The encoder can be configured
* using ghostty_key_encoder_setopt() and must be freed using
* ghostty_key_encoder_free() when no longer needed.
*
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
* @param encoder Pointer to store the created encoder handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup key
*/
GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder);
/**
* Free a key encoder instance.
*
* Releases all resources associated with the key encoder. After this call,
* the encoder handle becomes invalid and must not be used.
*
* @param encoder The encoder handle to free (may be NULL)
*
* @ingroup key
*/
void ghostty_key_encoder_free(GhosttyKeyEncoder encoder);
/**
* Set an option on the key encoder.
*
* Configures the behavior of the key encoder. Options control various aspects
* of encoding such as terminal modes (cursor key application mode, keypad mode),
* protocol selection (Kitty keyboard protocol flags), and platform-specific
* behaviors (macOS option-as-alt).
*
* A null pointer value does nothing. It does not reset the value to the
* default. The setopt call will do nothing.
*
* @param encoder The encoder handle, must not be NULL
* @param option The option to set
* @param value Pointer to the value to set (type depends on the option)
*
* @ingroup key
*/
void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value);
/**
* Encode a key event into a terminal escape sequence.
*
* Converts a key event into the appropriate terminal escape sequence based on
* the encoder's current options. The sequence is written to the provided buffer.
*
* Not all key events produce output. For example, unmodified modifier keys
* typically don't generate escape sequences. Check the out_len parameter to
* determine if any data was written.
*
* If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY
* and out_len will contain the required buffer size. The caller can then
* allocate a larger buffer and call the function again.
*
* @param encoder The encoder handle, must not be NULL
* @param event The key event to encode, must not be NULL
* @param out_buf Buffer to write the encoded sequence to
* @param out_buf_size Size of the output buffer in bytes
* @param out_len Pointer to store the number of bytes written (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code
*
* ## Example: Calculate required buffer size
*
* @code{.c}
* // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY)
* size_t required = 0;
* GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required);
* assert(result == GHOSTTY_OUT_OF_MEMORY);
*
* // Allocate buffer of required size
* char *buf = malloc(required);
*
* // Encode with properly sized buffer
* size_t written = 0;
* result = ghostty_key_encoder_encode(encoder, event, buf, required, &written);
* assert(result == GHOSTTY_SUCCESS);
*
* // Use the encoded sequence...
*
* free(buf);
* @endcode
*
* ## Example: Direct encoding with static buffer
*
* @code{.c}
* // Most escape sequences are short, so a static buffer often suffices
* char buf[128];
* size_t written = 0;
* GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written);
*
* if (result == GHOSTTY_SUCCESS) {
* // Write the encoded sequence to the terminal
* write(pty_fd, buf, written);
* } else if (result == GHOSTTY_OUT_OF_MEMORY) {
* // Buffer too small, written contains required size
* char *dynamic_buf = malloc(written);
* result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written);
* assert(result == GHOSTTY_SUCCESS);
* write(pty_fd, dynamic_buf, written);
* free(dynamic_buf);
* }
* @endcode
*
* @ingroup key
*/
GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len);
#endif /* GHOSTTY_VT_KEY_ENCODER_H */

View File

@ -0,0 +1,474 @@
/**
* @file event.h
*
* Key event representation and manipulation.
*/
#ifndef GHOSTTY_VT_KEY_EVENT_H
#define GHOSTTY_VT_KEY_EVENT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
/**
* Opaque handle to a key event.
*
* This handle represents a keyboard input event containing information about
* the physical key pressed, modifiers, and generated text.
*
* @ingroup key
*/
typedef struct GhosttyKeyEvent *GhosttyKeyEvent;
/**
* Keyboard input event types.
*
* @ingroup key
*/
typedef enum {
/** Key was released */
GHOSTTY_KEY_ACTION_RELEASE = 0,
/** Key was pressed */
GHOSTTY_KEY_ACTION_PRESS = 1,
/** Key is being repeated (held down) */
GHOSTTY_KEY_ACTION_REPEAT = 2,
} GhosttyKeyAction;
/**
* Keyboard modifier keys bitmask.
*
* A bitmask representing all keyboard modifiers. This tracks which modifier keys
* are pressed and, where supported by the platform, which side (left or right)
* of each modifier is active.
*
* Use the GHOSTTY_MODS_* constants to test and set individual modifiers.
*
* Modifier side bits are only meaningful when the corresponding modifier bit is set.
* Not all platforms support distinguishing between left and right modifier
* keys and Ghostty is built to expect that some platforms may not provide this
* information.
*
* @ingroup key
*/
typedef uint16_t GhosttyMods;
/** Shift key is pressed */
#define GHOSTTY_MODS_SHIFT (1 << 0)
/** Control key is pressed */
#define GHOSTTY_MODS_CTRL (1 << 1)
/** Alt/Option key is pressed */
#define GHOSTTY_MODS_ALT (1 << 2)
/** Super/Command/Windows key is pressed */
#define GHOSTTY_MODS_SUPER (1 << 3)
/** Caps Lock is active */
#define GHOSTTY_MODS_CAPS_LOCK (1 << 4)
/** Num Lock is active */
#define GHOSTTY_MODS_NUM_LOCK (1 << 5)
/**
* Right shift is pressed (0 = left, 1 = right).
* Only meaningful when GHOSTTY_MODS_SHIFT is set.
*/
#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6)
/**
* Right ctrl is pressed (0 = left, 1 = right).
* Only meaningful when GHOSTTY_MODS_CTRL is set.
*/
#define GHOSTTY_MODS_CTRL_SIDE (1 << 7)
/**
* Right alt is pressed (0 = left, 1 = right).
* Only meaningful when GHOSTTY_MODS_ALT is set.
*/
#define GHOSTTY_MODS_ALT_SIDE (1 << 8)
/**
* Right super is pressed (0 = left, 1 = right).
* Only meaningful when GHOSTTY_MODS_SUPER is set.
*/
#define GHOSTTY_MODS_SUPER_SIDE (1 << 9)
/**
* Physical key codes.
*
* The set of key codes that Ghostty is aware of. These represent physical keys
* on the keyboard and are layout-independent. For example, the "a" key on a US
* keyboard is the same as the "ф" key on a Russian keyboard, but both will
* report the same key_a value.
*
* Layout-dependent strings are provided separately as UTF-8 text and are produced
* by the platform. These values are based on the W3C UI Events KeyboardEvent code
* standard. See: https://www.w3.org/TR/uievents-code
*
* @ingroup key
*/
typedef enum {
GHOSTTY_KEY_UNIDENTIFIED = 0,
// Writing System Keys (W3C § 3.1.1)
GHOSTTY_KEY_BACKQUOTE,
GHOSTTY_KEY_BACKSLASH,
GHOSTTY_KEY_BRACKET_LEFT,
GHOSTTY_KEY_BRACKET_RIGHT,
GHOSTTY_KEY_COMMA,
GHOSTTY_KEY_DIGIT_0,
GHOSTTY_KEY_DIGIT_1,
GHOSTTY_KEY_DIGIT_2,
GHOSTTY_KEY_DIGIT_3,
GHOSTTY_KEY_DIGIT_4,
GHOSTTY_KEY_DIGIT_5,
GHOSTTY_KEY_DIGIT_6,
GHOSTTY_KEY_DIGIT_7,
GHOSTTY_KEY_DIGIT_8,
GHOSTTY_KEY_DIGIT_9,
GHOSTTY_KEY_EQUAL,
GHOSTTY_KEY_INTL_BACKSLASH,
GHOSTTY_KEY_INTL_RO,
GHOSTTY_KEY_INTL_YEN,
GHOSTTY_KEY_A,
GHOSTTY_KEY_B,
GHOSTTY_KEY_C,
GHOSTTY_KEY_D,
GHOSTTY_KEY_E,
GHOSTTY_KEY_F,
GHOSTTY_KEY_G,
GHOSTTY_KEY_H,
GHOSTTY_KEY_I,
GHOSTTY_KEY_J,
GHOSTTY_KEY_K,
GHOSTTY_KEY_L,
GHOSTTY_KEY_M,
GHOSTTY_KEY_N,
GHOSTTY_KEY_O,
GHOSTTY_KEY_P,
GHOSTTY_KEY_Q,
GHOSTTY_KEY_R,
GHOSTTY_KEY_S,
GHOSTTY_KEY_T,
GHOSTTY_KEY_U,
GHOSTTY_KEY_V,
GHOSTTY_KEY_W,
GHOSTTY_KEY_X,
GHOSTTY_KEY_Y,
GHOSTTY_KEY_Z,
GHOSTTY_KEY_MINUS,
GHOSTTY_KEY_PERIOD,
GHOSTTY_KEY_QUOTE,
GHOSTTY_KEY_SEMICOLON,
GHOSTTY_KEY_SLASH,
// Functional Keys (W3C § 3.1.2)
GHOSTTY_KEY_ALT_LEFT,
GHOSTTY_KEY_ALT_RIGHT,
GHOSTTY_KEY_BACKSPACE,
GHOSTTY_KEY_CAPS_LOCK,
GHOSTTY_KEY_CONTEXT_MENU,
GHOSTTY_KEY_CONTROL_LEFT,
GHOSTTY_KEY_CONTROL_RIGHT,
GHOSTTY_KEY_ENTER,
GHOSTTY_KEY_META_LEFT,
GHOSTTY_KEY_META_RIGHT,
GHOSTTY_KEY_SHIFT_LEFT,
GHOSTTY_KEY_SHIFT_RIGHT,
GHOSTTY_KEY_SPACE,
GHOSTTY_KEY_TAB,
GHOSTTY_KEY_CONVERT,
GHOSTTY_KEY_KANA_MODE,
GHOSTTY_KEY_NON_CONVERT,
// Control Pad Section (W3C § 3.2)
GHOSTTY_KEY_DELETE,
GHOSTTY_KEY_END,
GHOSTTY_KEY_HELP,
GHOSTTY_KEY_HOME,
GHOSTTY_KEY_INSERT,
GHOSTTY_KEY_PAGE_DOWN,
GHOSTTY_KEY_PAGE_UP,
// Arrow Pad Section (W3C § 3.3)
GHOSTTY_KEY_ARROW_DOWN,
GHOSTTY_KEY_ARROW_LEFT,
GHOSTTY_KEY_ARROW_RIGHT,
GHOSTTY_KEY_ARROW_UP,
// Numpad Section (W3C § 3.4)
GHOSTTY_KEY_NUM_LOCK,
GHOSTTY_KEY_NUMPAD_0,
GHOSTTY_KEY_NUMPAD_1,
GHOSTTY_KEY_NUMPAD_2,
GHOSTTY_KEY_NUMPAD_3,
GHOSTTY_KEY_NUMPAD_4,
GHOSTTY_KEY_NUMPAD_5,
GHOSTTY_KEY_NUMPAD_6,
GHOSTTY_KEY_NUMPAD_7,
GHOSTTY_KEY_NUMPAD_8,
GHOSTTY_KEY_NUMPAD_9,
GHOSTTY_KEY_NUMPAD_ADD,
GHOSTTY_KEY_NUMPAD_BACKSPACE,
GHOSTTY_KEY_NUMPAD_CLEAR,
GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY,
GHOSTTY_KEY_NUMPAD_COMMA,
GHOSTTY_KEY_NUMPAD_DECIMAL,
GHOSTTY_KEY_NUMPAD_DIVIDE,
GHOSTTY_KEY_NUMPAD_ENTER,
GHOSTTY_KEY_NUMPAD_EQUAL,
GHOSTTY_KEY_NUMPAD_MEMORY_ADD,
GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR,
GHOSTTY_KEY_NUMPAD_MEMORY_RECALL,
GHOSTTY_KEY_NUMPAD_MEMORY_STORE,
GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT,
GHOSTTY_KEY_NUMPAD_MULTIPLY,
GHOSTTY_KEY_NUMPAD_PAREN_LEFT,
GHOSTTY_KEY_NUMPAD_PAREN_RIGHT,
GHOSTTY_KEY_NUMPAD_SUBTRACT,
GHOSTTY_KEY_NUMPAD_SEPARATOR,
GHOSTTY_KEY_NUMPAD_UP,
GHOSTTY_KEY_NUMPAD_DOWN,
GHOSTTY_KEY_NUMPAD_RIGHT,
GHOSTTY_KEY_NUMPAD_LEFT,
GHOSTTY_KEY_NUMPAD_BEGIN,
GHOSTTY_KEY_NUMPAD_HOME,
GHOSTTY_KEY_NUMPAD_END,
GHOSTTY_KEY_NUMPAD_INSERT,
GHOSTTY_KEY_NUMPAD_DELETE,
GHOSTTY_KEY_NUMPAD_PAGE_UP,
GHOSTTY_KEY_NUMPAD_PAGE_DOWN,
// Function Section (W3C § 3.5)
GHOSTTY_KEY_ESCAPE,
GHOSTTY_KEY_F1,
GHOSTTY_KEY_F2,
GHOSTTY_KEY_F3,
GHOSTTY_KEY_F4,
GHOSTTY_KEY_F5,
GHOSTTY_KEY_F6,
GHOSTTY_KEY_F7,
GHOSTTY_KEY_F8,
GHOSTTY_KEY_F9,
GHOSTTY_KEY_F10,
GHOSTTY_KEY_F11,
GHOSTTY_KEY_F12,
GHOSTTY_KEY_F13,
GHOSTTY_KEY_F14,
GHOSTTY_KEY_F15,
GHOSTTY_KEY_F16,
GHOSTTY_KEY_F17,
GHOSTTY_KEY_F18,
GHOSTTY_KEY_F19,
GHOSTTY_KEY_F20,
GHOSTTY_KEY_F21,
GHOSTTY_KEY_F22,
GHOSTTY_KEY_F23,
GHOSTTY_KEY_F24,
GHOSTTY_KEY_F25,
GHOSTTY_KEY_FN,
GHOSTTY_KEY_FN_LOCK,
GHOSTTY_KEY_PRINT_SCREEN,
GHOSTTY_KEY_SCROLL_LOCK,
GHOSTTY_KEY_PAUSE,
// Media Keys (W3C § 3.6)
GHOSTTY_KEY_BROWSER_BACK,
GHOSTTY_KEY_BROWSER_FAVORITES,
GHOSTTY_KEY_BROWSER_FORWARD,
GHOSTTY_KEY_BROWSER_HOME,
GHOSTTY_KEY_BROWSER_REFRESH,
GHOSTTY_KEY_BROWSER_SEARCH,
GHOSTTY_KEY_BROWSER_STOP,
GHOSTTY_KEY_EJECT,
GHOSTTY_KEY_LAUNCH_APP_1,
GHOSTTY_KEY_LAUNCH_APP_2,
GHOSTTY_KEY_LAUNCH_MAIL,
GHOSTTY_KEY_MEDIA_PLAY_PAUSE,
GHOSTTY_KEY_MEDIA_SELECT,
GHOSTTY_KEY_MEDIA_STOP,
GHOSTTY_KEY_MEDIA_TRACK_NEXT,
GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS,
GHOSTTY_KEY_POWER,
GHOSTTY_KEY_SLEEP,
GHOSTTY_KEY_AUDIO_VOLUME_DOWN,
GHOSTTY_KEY_AUDIO_VOLUME_MUTE,
GHOSTTY_KEY_AUDIO_VOLUME_UP,
GHOSTTY_KEY_WAKE_UP,
// Legacy, Non-standard, and Special Keys (W3C § 3.7)
GHOSTTY_KEY_COPY,
GHOSTTY_KEY_CUT,
GHOSTTY_KEY_PASTE,
} GhosttyKey;
/**
* Create a new key event instance.
*
* Creates a new key event with default values. The event must be freed using
* ghostty_key_event_free() when no longer needed.
*
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
* @param event Pointer to store the created key event handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup key
*/
GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event);
/**
* Free a key event instance.
*
* Releases all resources associated with the key event. After this call,
* the event handle becomes invalid and must not be used.
*
* @param event The key event handle to free (may be NULL)
*
* @ingroup key
*/
void ghostty_key_event_free(GhosttyKeyEvent event);
/**
* Set the key action (press, release, repeat).
*
* @param event The key event handle, must not be NULL
* @param action The action to set
*
* @ingroup key
*/
void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action);
/**
* Get the key action (press, release, repeat).
*
* @param event The key event handle, must not be NULL
* @return The key action
*
* @ingroup key
*/
GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event);
/**
* Set the physical key code.
*
* @param event The key event handle, must not be NULL
* @param key The physical key code to set
*
* @ingroup key
*/
void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key);
/**
* Get the physical key code.
*
* @param event The key event handle, must not be NULL
* @return The physical key code
*
* @ingroup key
*/
GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event);
/**
* Set the modifier keys bitmask.
*
* @param event The key event handle, must not be NULL
* @param mods The modifier keys bitmask to set
*
* @ingroup key
*/
void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods);
/**
* Get the modifier keys bitmask.
*
* @param event The key event handle, must not be NULL
* @return The modifier keys bitmask
*
* @ingroup key
*/
GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event);
/**
* Set the consumed modifiers bitmask.
*
* @param event The key event handle, must not be NULL
* @param consumed_mods The consumed modifiers bitmask to set
*
* @ingroup key
*/
void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods);
/**
* Get the consumed modifiers bitmask.
*
* @param event The key event handle, must not be NULL
* @return The consumed modifiers bitmask
*
* @ingroup key
*/
GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event);
/**
* Set whether the key event is part of a composition sequence.
*
* @param event The key event handle, must not be NULL
* @param composing Whether the key event is part of a composition sequence
*
* @ingroup key
*/
void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing);
/**
* Get whether the key event is part of a composition sequence.
*
* @param event The key event handle, must not be NULL
* @return Whether the key event is part of a composition sequence
*
* @ingroup key
*/
bool ghostty_key_event_get_composing(GhosttyKeyEvent event);
/**
* Set the UTF-8 text generated by the key event.
*
* The key event does NOT take ownership of the text pointer. The caller
* must ensure the string remains valid for the lifetime needed by the event.
*
* @param event The key event handle, must not be NULL
* @param utf8 The UTF-8 text to set (or NULL for empty)
* @param len Length of the UTF-8 text in bytes
*
* @ingroup key
*/
void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len);
/**
* Get the UTF-8 text generated by the key event.
*
* The returned pointer is valid until the event is freed or the UTF-8 text is modified.
*
* @param event The key event handle, must not be NULL
* @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL)
* @return The UTF-8 text (or NULL for empty)
*
* @ingroup key
*/
const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len);
/**
* Set the unshifted Unicode codepoint.
*
* @param event The key event handle, must not be NULL
* @param codepoint The unshifted Unicode codepoint to set
*
* @ingroup key
*/
void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint);
/**
* Get the unshifted Unicode codepoint.
*
* @param event The key event handle, must not be NULL
* @return The unshifted Unicode codepoint
*
* @ingroup key
*/
uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event);
#endif /* GHOSTTY_VT_KEY_EVENT_H */

231
include/ghostty/vt/osc.h Normal file
View File

@ -0,0 +1,231 @@
/**
* @file osc.h
*
* OSC (Operating System Command) sequence parser and command handling.
*/
#ifndef GHOSTTY_VT_OSC_H
#define GHOSTTY_VT_OSC_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
/**
* Opaque handle to an OSC parser instance.
*
* This handle represents an OSC (Operating System Command) parser that can
* be used to parse the contents of OSC sequences.
*
* @ingroup osc
*/
typedef struct GhosttyOscParser *GhosttyOscParser;
/**
* Opaque handle to a single OSC command.
*
* This handle represents a parsed OSC (Operating System Command) command.
* The command can be queried for its type and associated data.
*
* @ingroup osc
*/
typedef struct GhosttyOscCommand *GhosttyOscCommand;
/** @defgroup osc OSC Parser
*
* OSC (Operating System Command) sequence parser and command handling.
*
* The parser operates in a streaming fashion, processing input byte-by-byte
* to handle OSC sequences that may arrive in fragments across multiple reads.
* This interface makes it easy to integrate into most environments and avoids
* over-allocating buffers.
*
* ## Basic Usage
*
* 1. Create a parser instance with ghostty_osc_new()
* 2. Feed bytes to the parser using ghostty_osc_next()
* 3. Finalize parsing with ghostty_osc_end() to get the command
* 4. Query command type and extract data using ghostty_osc_command_type()
* and ghostty_osc_command_data()
* 5. Free the parser with ghostty_osc_free() when done
*
* @{
*/
/**
* OSC command types.
*
* @ingroup osc
*/
typedef enum {
GHOSTTY_OSC_COMMAND_INVALID = 0,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1,
GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2,
GHOSTTY_OSC_COMMAND_PROMPT_START = 3,
GHOSTTY_OSC_COMMAND_PROMPT_END = 4,
GHOSTTY_OSC_COMMAND_END_OF_INPUT = 5,
GHOSTTY_OSC_COMMAND_END_OF_COMMAND = 6,
GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 7,
GHOSTTY_OSC_COMMAND_REPORT_PWD = 8,
GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 9,
GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 10,
GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 11,
GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 12,
GHOSTTY_OSC_COMMAND_HYPERLINK_START = 13,
GHOSTTY_OSC_COMMAND_HYPERLINK_END = 14,
GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 15,
GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 16,
GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 17,
GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 18,
GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 19,
GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 20,
} GhosttyOscCommandType;
/**
* OSC command data types.
*
* These values specify what type of data to extract from an OSC command
* using `ghostty_osc_command_data`.
*
* @ingroup osc
*/
typedef enum {
/** Invalid data type. Never results in any data extraction. */
GHOSTTY_OSC_DATA_INVALID = 0,
/**
* Window title string data.
*
* Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE
*
* Output type: const char ** (pointer to null-terminated string)
*
* Lifetime: Valid until the next call to any ghostty_osc_* function with
* the same parser instance. Memory is owned by the parser.
*/
GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1,
} GhosttyOscCommandData;
/**
* Create a new OSC parser instance.
*
* Creates a new OSC (Operating System Command) parser using the provided
* allocator. The parser must be freed using ghostty_vt_osc_free() when
* no longer needed.
*
* @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator
* @param parser Pointer to store the created parser handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup osc
*/
GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser);
/**
* Free an OSC parser instance.
*
* Releases all resources associated with the OSC parser. After this call,
* the parser handle becomes invalid and must not be used.
*
* @param parser The parser handle to free (may be NULL)
*
* @ingroup osc
*/
void ghostty_osc_free(GhosttyOscParser parser);
/**
* Reset an OSC parser instance to its initial state.
*
* Resets the parser state, clearing any partially parsed OSC sequences
* and returning the parser to its initial state. This is useful for
* reusing a parser instance or recovering from parse errors.
*
* @param parser The parser handle to reset, must not be null.
*
* @ingroup osc
*/
void ghostty_osc_reset(GhosttyOscParser parser);
/**
* Parse the next byte in an OSC sequence.
*
* Processes a single byte as part of an OSC sequence. The parser maintains
* internal state to track the progress through the sequence. Call this
* function for each byte in the sequence data.
*
* When finished pumping the parser with bytes, call ghostty_osc_end
* to get the final result.
*
* @param parser The parser handle, must not be null.
* @param byte The next byte to parse
*
* @ingroup osc
*/
void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte);
/**
* Finalize OSC parsing and retrieve the parsed command.
*
* Call this function after feeding all bytes of an OSC sequence to the parser
* using ghostty_osc_next() with the exception of the terminating character
* (ESC or ST). This function finalizes the parsing process and returns the
* parsed OSC command.
*
* The return value is never NULL. Invalid commands will return a command
* with type GHOSTTY_OSC_COMMAND_INVALID.
*
* The terminator parameter specifies the byte that terminated the OSC sequence
* (typically 0x07 for BEL or 0x5C for ST after ESC). This information is
* preserved in the parsed command so that responses can use the same terminator
* format for better compatibility with the calling program. For commands that
* do not require a response, this parameter is ignored and the resulting
* command will not retain the terminator information.
*
* The returned command handle is valid until the next call to any
* `ghostty_osc_*` function with the same parser instance with the exception
* of command introspection functions such as `ghostty_osc_command_type`.
*
* @param parser The parser handle, must not be null.
* @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST)
* @return Handle to the parsed OSC command
*
* @ingroup osc
*/
GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator);
/**
* Get the type of an OSC command.
*
* Returns the type identifier for the given OSC command. This can be used
* to determine what kind of command was parsed and what data might be
* available from it.
*
* @param command The OSC command handle to query (may be NULL)
* @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL
*
* @ingroup osc
*/
GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command);
/**
* Extract data from an OSC command.
*
* Extracts typed data from the given OSC command based on the specified
* data type. The output pointer must be of the appropriate type for the
* requested data kind. Valid command types, output types, and memory
* safety information are documented in the `GhosttyOscCommandData` enum.
*
* @param command The OSC command handle to query (may be NULL)
* @param data The type of data to extract
* @param out Pointer to store the extracted data (type depends on data parameter)
* @return true if data extraction was successful, false otherwise
*
* @ingroup osc
*/
bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out);
/** @} */
#endif /* GHOSTTY_VT_OSC_H */

View File

@ -0,0 +1,75 @@
/**
* @file paste.h
*
* Paste utilities - validate and encode paste data for terminal input.
*/
#ifndef GHOSTTY_VT_PASTE_H
#define GHOSTTY_VT_PASTE_H
/** @defgroup paste Paste Utilities
*
* Utilities for validating paste data safety.
*
* ## Basic Usage
*
* Use ghostty_paste_is_safe() to check if paste data contains potentially
* dangerous sequences before sending it to the terminal.
*
* ## Example
*
* @code{.c}
* #include <stdio.h>
* #include <string.h>
* #include <ghostty/vt.h>
*
* int main() {
* const char* safe_data = "hello world";
* const char* unsafe_data = "rm -rf /\n";
*
* if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) {
* printf("Safe to paste\n");
* }
*
* if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) {
* printf("Unsafe! Contains newline\n");
* }
*
* return 0;
* }
* @endcode
*
* @{
*/
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Check if paste data is safe to paste into the terminal.
*
* Data is considered unsafe if it contains:
* - Newlines (`\n`) which can inject commands
* - The bracketed paste end sequence (`\x1b[201~`) which can be used
* to exit bracketed paste mode and inject commands
*
* This check is conservative and considers data unsafe regardless of
* current terminal state.
*
* @param data The paste data to check (must not be NULL)
* @param len The length of the data in bytes
* @return true if the data is safe to paste, false otherwise
*/
bool ghostty_paste_is_safe(const char* data, size_t len);
#ifdef __cplusplus
}
#endif
/** @} */
#endif /* GHOSTTY_VT_PASTE_H */

View File

@ -0,0 +1,22 @@
/**
* @file result.h
*
* Result codes for libghostty-vt operations.
*/
#ifndef GHOSTTY_VT_RESULT_H
#define GHOSTTY_VT_RESULT_H
/**
* Result codes for libghostty-vt operations.
*/
typedef enum {
/** Operation completed successfully */
GHOSTTY_SUCCESS = 0,
/** Operation failed due to failed allocation */
GHOSTTY_OUT_OF_MEMORY = -1,
/** Operation failed due to invalid value */
GHOSTTY_INVALID_VALUE = -2,
} GhosttyResult;
#endif /* GHOSTTY_VT_RESULT_H */

394
include/ghostty/vt/sgr.h Normal file
View File

@ -0,0 +1,394 @@
/**
* @file sgr.h
*
* SGR (Select Graphic Rendition) attribute parsing and handling.
*/
#ifndef GHOSTTY_VT_SGR_H
#define GHOSTTY_VT_SGR_H
/** @defgroup sgr SGR Parser
*
* SGR (Select Graphic Rendition) attribute parser.
*
* SGR sequences are the syntax used to set styling attributes such as
* bold, italic, underline, and colors for text in terminal emulators.
* For example, you may be familiar with sequences like `ESC[1;31m`. The
* `1;31` is the SGR attribute list.
*
* The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`)
* and returns individual text attributes like bold, italic, colors, etc.
* It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed,
* and handles various color formats including 8-color, 16-color, 256-color,
* X11 named colors, and RGB in multiple formats.
*
* ## Basic Usage
*
* 1. Create a parser instance with ghostty_sgr_new()
* 2. Set SGR parameters with ghostty_sgr_set_params()
* 3. Iterate through attributes using ghostty_sgr_next()
* 4. Free the parser with ghostty_sgr_free() when done
*
* ## Example
*
* @code{.c}
* #include <assert.h>
* #include <stdio.h>
* #include <ghostty/vt.h>
*
* int main() {
* // Create parser
* GhosttySgrParser parser;
* GhosttyResult result = ghostty_sgr_new(NULL, &parser);
* assert(result == GHOSTTY_SUCCESS);
*
* // Parse "bold, red foreground" sequence: ESC[1;31m
* uint16_t params[] = {1, 31};
* result = ghostty_sgr_set_params(parser, params, NULL, 2);
* assert(result == GHOSTTY_SUCCESS);
*
* // Iterate through attributes
* GhosttySgrAttribute attr;
* while (ghostty_sgr_next(parser, &attr)) {
* switch (attr.tag) {
* case GHOSTTY_SGR_ATTR_BOLD:
* printf("Bold enabled\n");
* break;
* case GHOSTTY_SGR_ATTR_FG_8:
* printf("Foreground color: %d\n", attr.value.fg_8);
* break;
* default:
* break;
* }
* }
*
* // Cleanup
* ghostty_sgr_free(parser);
* return 0;
* }
* @endcode
*
* @{
*/
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/result.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* Opaque handle to an SGR parser instance.
*
* This handle represents an SGR (Select Graphic Rendition) parser that can
* be used to parse SGR sequences and extract individual text attributes.
*
* @ingroup sgr
*/
typedef struct GhosttySgrParser* GhosttySgrParser;
/**
* SGR attribute tags.
*
* These values identify the type of an SGR attribute in a tagged union.
* Use the tag to determine which field in the attribute value union to access.
*
* @ingroup sgr
*/
typedef enum {
GHOSTTY_SGR_ATTR_UNSET = 0,
GHOSTTY_SGR_ATTR_UNKNOWN = 1,
GHOSTTY_SGR_ATTR_BOLD = 2,
GHOSTTY_SGR_ATTR_RESET_BOLD = 3,
GHOSTTY_SGR_ATTR_ITALIC = 4,
GHOSTTY_SGR_ATTR_RESET_ITALIC = 5,
GHOSTTY_SGR_ATTR_FAINT = 6,
GHOSTTY_SGR_ATTR_UNDERLINE = 7,
GHOSTTY_SGR_ATTR_RESET_UNDERLINE = 8,
GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 9,
GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 10,
GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 11,
GHOSTTY_SGR_ATTR_OVERLINE = 12,
GHOSTTY_SGR_ATTR_RESET_OVERLINE = 13,
GHOSTTY_SGR_ATTR_BLINK = 14,
GHOSTTY_SGR_ATTR_RESET_BLINK = 15,
GHOSTTY_SGR_ATTR_INVERSE = 16,
GHOSTTY_SGR_ATTR_RESET_INVERSE = 17,
GHOSTTY_SGR_ATTR_INVISIBLE = 18,
GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 19,
GHOSTTY_SGR_ATTR_STRIKETHROUGH = 20,
GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 21,
GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 22,
GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 23,
GHOSTTY_SGR_ATTR_BG_8 = 24,
GHOSTTY_SGR_ATTR_FG_8 = 25,
GHOSTTY_SGR_ATTR_RESET_FG = 26,
GHOSTTY_SGR_ATTR_RESET_BG = 27,
GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 28,
GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 29,
GHOSTTY_SGR_ATTR_BG_256 = 30,
GHOSTTY_SGR_ATTR_FG_256 = 31,
} GhosttySgrAttributeTag;
/**
* Underline style types.
*
* @ingroup sgr
*/
typedef enum {
GHOSTTY_SGR_UNDERLINE_NONE = 0,
GHOSTTY_SGR_UNDERLINE_SINGLE = 1,
GHOSTTY_SGR_UNDERLINE_DOUBLE = 2,
GHOSTTY_SGR_UNDERLINE_CURLY = 3,
GHOSTTY_SGR_UNDERLINE_DOTTED = 4,
GHOSTTY_SGR_UNDERLINE_DASHED = 5,
} GhosttySgrUnderline;
/**
* Unknown SGR attribute data.
*
* Contains the full parameter list and the partial list where parsing
* encountered an unknown or invalid sequence.
*
* @ingroup sgr
*/
typedef struct {
const uint16_t* full_ptr;
size_t full_len;
const uint16_t* partial_ptr;
size_t partial_len;
} GhosttySgrUnknown;
/**
* SGR attribute value union.
*
* This union contains all possible attribute values. Use the tag field
* to determine which union member is active. Attributes without associated
* data (like bold, italic) don't use the union value.
*
* @ingroup sgr
*/
typedef union {
GhosttySgrUnknown unknown;
GhosttySgrUnderline underline;
GhosttyColorRgb underline_color;
GhosttyColorPaletteIndex underline_color_256;
GhosttyColorRgb direct_color_fg;
GhosttyColorRgb direct_color_bg;
GhosttyColorPaletteIndex bg_8;
GhosttyColorPaletteIndex fg_8;
GhosttyColorPaletteIndex bright_bg_8;
GhosttyColorPaletteIndex bright_fg_8;
GhosttyColorPaletteIndex bg_256;
GhosttyColorPaletteIndex fg_256;
uint64_t _padding[8];
} GhosttySgrAttributeValue;
/**
* SGR attribute (tagged union).
*
* A complete SGR attribute with both its type tag and associated value.
* Always check the tag field to determine which value union member is valid.
*
* Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be
* identified by tag alone; the value union is not used for these and
* the memory in the value field is undefined.
*
* @ingroup sgr
*/
typedef struct {
GhosttySgrAttributeTag tag;
GhosttySgrAttributeValue value;
} GhosttySgrAttribute;
/**
* Create a new SGR parser instance.
*
* Creates a new SGR (Select Graphic Rendition) parser using the provided
* allocator. The parser must be freed using ghostty_sgr_free() when
* no longer needed.
*
* @param allocator Pointer to the allocator to use for memory management, or
* NULL to use the default allocator
* @param parser Pointer to store the created parser handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup sgr
*/
GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator,
GhosttySgrParser* parser);
/**
* Free an SGR parser instance.
*
* Releases all resources associated with the SGR parser. After this call,
* the parser handle becomes invalid and must not be used. This includes
* any attributes previously returned by ghostty_sgr_next().
*
* @param parser The parser handle to free (may be NULL)
*
* @ingroup sgr
*/
void ghostty_sgr_free(GhosttySgrParser parser);
/**
* Reset an SGR parser instance to the beginning of the parameter list.
*
* Resets the parser's iteration state without clearing the parameters.
* After calling this, ghostty_sgr_next() will start from the beginning
* of the parameter list again.
*
* @param parser The parser handle to reset, must not be NULL
*
* @ingroup sgr
*/
void ghostty_sgr_reset(GhosttySgrParser parser);
/**
* Set SGR parameters for parsing.
*
* Sets the SGR parameter list to parse. Parameters are the numeric values
* from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}).
*
* The separators array optionally specifies the separator type for each
* parameter position. Each byte should be either ';' for semicolon or ':'
* for colon. This is needed for certain color formats that use colon
* separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator
* values are treated as semicolons. The separators array must have the same
* length as the params array, if it is not NULL.
*
* If separators is NULL, all parameters are assumed to be semicolon-separated.
*
* This function makes an internal copy of the parameter and separator data,
* so the caller can safely free or modify the input arrays after this call.
*
* After calling this function, the parser is automatically reset and ready
* to iterate from the beginning.
*
* @param parser The parser handle, must not be NULL
* @param params Array of SGR parameter values
* @param separators Optional array of separator characters (';' or ':'), or
* NULL
* @param len Number of parameters (and separators if provided)
* @return GHOSTTY_SUCCESS on success, or an error code on failure
*
* @ingroup sgr
*/
GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser,
const uint16_t* params,
const char* separators,
size_t len);
/**
* Get the next SGR attribute.
*
* Parses and returns the next attribute from the parameter list.
* Call this function repeatedly until it returns false to process
* all attributes in the sequence.
*
* @param parser The parser handle, must not be NULL
* @param attr Pointer to store the next attribute
* @return true if an attribute was returned, false if no more attributes
*
* @ingroup sgr
*/
bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr);
/**
* Get the full parameter list from an unknown SGR attribute.
*
* This function retrieves the full parameter list that was provided to the
* parser when an unknown attribute was encountered. Primarily useful in
* WebAssembly environments where accessing struct fields directly is difficult.
*
* @param unknown The unknown attribute data
* @param ptr Pointer to store the pointer to the parameter array (may be NULL)
* @return The length of the full parameter array
*
* @ingroup sgr
*/
size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown,
const uint16_t** ptr);
/**
* Get the partial parameter list from an unknown SGR attribute.
*
* This function retrieves the partial parameter list where parsing stopped
* when an unknown attribute was encountered. Primarily useful in WebAssembly
* environments where accessing struct fields directly is difficult.
*
* @param unknown The unknown attribute data
* @param ptr Pointer to store the pointer to the parameter array (may be NULL)
* @return The length of the partial parameter array
*
* @ingroup sgr
*/
size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown,
const uint16_t** ptr);
/**
* Get the tag from an SGR attribute.
*
* This function extracts the tag that identifies which type of attribute
* this is. Primarily useful in WebAssembly environments where accessing
* struct fields directly is difficult.
*
* @param attr The SGR attribute
* @return The attribute tag
*
* @ingroup sgr
*/
GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr);
/**
* Get the value from an SGR attribute.
*
* This function returns a pointer to the value union from an SGR attribute. Use
* the tag to determine which field of the union is valid. Primarily useful in
* WebAssembly environments where accessing struct fields directly is difficult.
*
* @param attr Pointer to the SGR attribute
* @return Pointer to the attribute value union
*
* @ingroup sgr
*/
GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
GhosttySgrAttribute* attr);
#ifdef __wasm__
/**
* Allocate memory for an SGR attribute (WebAssembly only).
*
* This is a convenience function for WebAssembly environments to allocate
* memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
*
* @return Pointer to the allocated attribute structure
*
* @ingroup wasm
*/
GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
/**
* Free memory for an SGR attribute (WebAssembly only).
*
* Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
*
* @param attr Pointer to the attribute structure to free
*
* @ingroup wasm
*/
void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
#endif
#ifdef __cplusplus
}
#endif
/** @} */
#endif /* GHOSTTY_VT_SGR_H */

159
include/ghostty/vt/wasm.h Normal file
View File

@ -0,0 +1,159 @@
/**
* @file wasm.h
*
* WebAssembly utility functions for libghostty-vt.
*/
#ifndef GHOSTTY_VT_WASM_H
#define GHOSTTY_VT_WASM_H
#ifdef __wasm__
#include <stddef.h>
#include <stdint.h>
/** @defgroup wasm WebAssembly Utilities
*
* Convenience functions for allocating various types in WebAssembly builds.
* **These are only available the libghostty-vt wasm module.**
*
* Ghostty relies on pointers to various types for ABI compatibility, and
* creating those pointers in Wasm can be tedious. These functions provide
* a purely additive set of utilities that simplify memory management in
* Wasm environments without changing the core C library API.
*
* @note These functions always use the default allocator. If you need
* custom allocation strategies, you should allocate types manually using
* your custom allocator. This is a very rare use case in the WebAssembly
* world so these are optimized for simplicity.
*
* ## Example Usage
*
* Here's a simple example of using the Wasm utilities with the key encoder:
*
* @code
* const { exports } = wasmInstance;
* const view = new DataView(wasmMemory.buffer);
*
* // Create key encoder
* const encoderPtr = exports.ghostty_wasm_alloc_opaque();
* exports.ghostty_key_encoder_new(null, encoderPtr);
* const encoder = view.getUint32(encoder, true);
*
* // Configure encoder with Kitty protocol flags
* const flagsPtr = exports.ghostty_wasm_alloc_u8();
* view.setUint8(flagsPtr, 0x1F);
* exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
*
* // Allocate output buffer and size pointer
* const bufferSize = 32;
* const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
* const writtenPtr = exports.ghostty_wasm_alloc_usize();
*
* // Encode the key event
* exports.ghostty_key_encoder_encode(
* encoder, eventPtr, bufPtr, bufferSize, writtenPtr
* );
*
* // Read encoded output
* const bytesWritten = view.getUint32(writtenPtr, true);
* const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
* @endcode
*
* @remark The code above is pretty ugly! This is the lowest level interface
* to the libghostty-vt Wasm module. In practice, this should be wrapped
* in a higher-level API that abstracts away all this.
*
* @{
*/
/**
* Allocate an opaque pointer. This can be used for any opaque pointer
* types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
*
* @return Pointer to allocated opaque pointer, or NULL if allocation failed
* @ingroup wasm
*/
void** ghostty_wasm_alloc_opaque(void);
/**
* Free an opaque pointer allocated by ghostty_wasm_alloc_opaque().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @ingroup wasm
*/
void ghostty_wasm_free_opaque(void **ptr);
/**
* Allocate an array of uint8_t values.
*
* @param len Number of uint8_t elements to allocate
* @return Pointer to allocated array, or NULL if allocation failed
* @ingroup wasm
*/
uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
/**
* Free an array allocated by ghostty_wasm_alloc_u8_array().
*
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
* @param len Length of the array (must match the length passed to alloc)
* @ingroup wasm
*/
void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
/**
* Allocate an array of uint16_t values.
*
* @param len Number of uint16_t elements to allocate
* @return Pointer to allocated array, or NULL if allocation failed
* @ingroup wasm
*/
uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
/**
* Free an array allocated by ghostty_wasm_alloc_u16_array().
*
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
* @param len Length of the array (must match the length passed to alloc)
* @ingroup wasm
*/
void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
/**
* Allocate a single uint8_t value.
*
* @return Pointer to allocated uint8_t, or NULL if allocation failed
* @ingroup wasm
*/
uint8_t* ghostty_wasm_alloc_u8(void);
/**
* Free a uint8_t allocated by ghostty_wasm_alloc_u8().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @ingroup wasm
*/
void ghostty_wasm_free_u8(uint8_t *ptr);
/**
* Allocate a single size_t value.
*
* @return Pointer to allocated size_t, or NULL if allocation failed
* @ingroup wasm
*/
size_t* ghostty_wasm_alloc_usize(void);
/**
* Free a size_t allocated by ghostty_wasm_alloc_usize().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @ingroup wasm
*/
void ghostty_wasm_free_usize(size_t *ptr);
/** @} */
#endif /* __wasm__ */
#endif /* GHOSTTY_VT_WASM_H */

View File

@ -61,7 +61,7 @@
<key>NSMenuItem</key> <key>NSMenuItem</key>
<dict> <dict>
<key>default</key> <key>default</key>
<string>New Ghostty Tab Here</string> <string>New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here</string>
</dict> </dict>
<key>NSMessage</key> <key>NSMessage</key>
<string>openTab</string> <string>openTab</string>
@ -80,7 +80,7 @@
<key>NSMenuItem</key> <key>NSMenuItem</key>
<dict> <dict>
<key>default</key> <key>default</key>
<string>New Ghostty Window Here</string> <string>New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here</string>
</dict> </dict>
<key>NSMessage</key> <key>NSMessage</key>
<string>openWindow</string> <string>openWindow</string>

View File

@ -96,6 +96,7 @@
Features/QuickTerminal/QuickTerminalController.swift, Features/QuickTerminal/QuickTerminalController.swift,
Features/QuickTerminal/QuickTerminalPosition.swift, Features/QuickTerminal/QuickTerminalPosition.swift,
Features/QuickTerminal/QuickTerminalScreen.swift, Features/QuickTerminal/QuickTerminalScreen.swift,
Features/QuickTerminal/QuickTerminalScreenStateCache.swift,
Features/QuickTerminal/QuickTerminalSize.swift, Features/QuickTerminal/QuickTerminalSize.swift,
Features/QuickTerminal/QuickTerminalSpaceBehavior.swift, Features/QuickTerminal/QuickTerminalSpaceBehavior.swift,
Features/QuickTerminal/QuickTerminalWindow.swift, Features/QuickTerminal/QuickTerminalWindow.swift,
@ -114,6 +115,7 @@
Features/Terminal/ErrorView.swift, Features/Terminal/ErrorView.swift,
Features/Terminal/TerminalController.swift, Features/Terminal/TerminalController.swift,
Features/Terminal/TerminalRestorable.swift, Features/Terminal/TerminalRestorable.swift,
Features/Terminal/TerminalTabColor.swift,
Features/Terminal/TerminalView.swift, Features/Terminal/TerminalView.swift,
"Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift",
"Features/Terminal/Window Styles/Terminal.xib", "Features/Terminal/Window Styles/Terminal.xib",
@ -125,7 +127,14 @@
"Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift", "Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift",
"Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift", "Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift",
"Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift", "Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift",
Features/Update/UpdateBadge.swift,
Features/Update/UpdateController.swift,
Features/Update/UpdateDelegate.swift, Features/Update/UpdateDelegate.swift,
Features/Update/UpdateDriver.swift,
Features/Update/UpdatePill.swift,
Features/Update/UpdatePopoverView.swift,
Features/Update/UpdateSimulator.swift,
Features/Update/UpdateViewModel.swift,
"Ghostty/FullscreenMode+Extension.swift", "Ghostty/FullscreenMode+Extension.swift",
Ghostty/Ghostty.Command.swift, Ghostty/Ghostty.Command.swift,
Ghostty/Ghostty.Error.swift, Ghostty/Ghostty.Error.swift,
@ -134,6 +143,7 @@
Ghostty/Ghostty.Surface.swift, Ghostty/Ghostty.Surface.swift,
Ghostty/InspectorView.swift, Ghostty/InspectorView.swift,
"Ghostty/NSEvent+Extension.swift", "Ghostty/NSEvent+Extension.swift",
Ghostty/SurfaceScrollView.swift,
Ghostty/SurfaceView_AppKit.swift, Ghostty/SurfaceView_AppKit.swift,
Helpers/AppInfo.swift, Helpers/AppInfo.swift,
Helpers/CodableBridge.swift, Helpers/CodableBridge.swift,
@ -147,6 +157,7 @@
"Helpers/Extensions/NSAppearance+Extension.swift", "Helpers/Extensions/NSAppearance+Extension.swift",
"Helpers/Extensions/NSApplication+Extension.swift", "Helpers/Extensions/NSApplication+Extension.swift",
"Helpers/Extensions/NSImage+Extension.swift", "Helpers/Extensions/NSImage+Extension.swift",
"Helpers/Extensions/NSMenu+Extension.swift",
"Helpers/Extensions/NSMenuItem+Extension.swift", "Helpers/Extensions/NSMenuItem+Extension.swift",
"Helpers/Extensions/NSPasteboard+Extension.swift", "Helpers/Extensions/NSPasteboard+Extension.swift",
"Helpers/Extensions/NSScreen+Extension.swift", "Helpers/Extensions/NSScreen+Extension.swift",
@ -160,6 +171,7 @@
Helpers/KeyboardLayout.swift, Helpers/KeyboardLayout.swift,
Helpers/LastWindowPosition.swift, Helpers/LastWindowPosition.swift,
Helpers/MetalView.swift, Helpers/MetalView.swift,
Helpers/NonDraggableHostingView.swift,
Helpers/PermissionRequest.swift, Helpers/PermissionRequest.swift,
Helpers/Private/CGS.swift, Helpers/Private/CGS.swift,
Helpers/Private/Dock.swift, Helpers/Private/Dock.swift,
@ -545,6 +557,7 @@
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
INFOPLIST_PREPROCESS = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@ -767,7 +780,7 @@
EXECUTABLE_NAME = ghostty; EXECUTABLE_NAME = ghostty;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Ghostty-Info.plist"; INFOPLIST_FILE = "Ghostty-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Ghostty; INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript."; INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
@ -785,6 +798,7 @@
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
INFOPLIST_PREPROCESS = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
@ -839,6 +853,7 @@
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders."; INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition."; INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges."; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
INFOPLIST_PREPROCESS = YES;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",

View File

@ -6,8 +6,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle", "location" : "https://github.com/sparkle-project/Sparkle",
"state" : { "state" : {
"revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d", "revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb",
"version" : "2.7.3" "version" : "2.8.0"
} }
} }
], ],

View File

@ -1,4 +1,5 @@
import AppKit import AppKit
import SwiftUI
import UserNotifications import UserNotifications
import OSLog import OSLog
import Sparkle import Sparkle
@ -43,6 +44,11 @@ class AppDelegate: NSObject,
@IBOutlet private var menuPaste: NSMenuItem? @IBOutlet private var menuPaste: NSMenuItem?
@IBOutlet private var menuPasteSelection: NSMenuItem? @IBOutlet private var menuPasteSelection: NSMenuItem?
@IBOutlet private var menuSelectAll: 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 menuToggleVisibility: NSMenuItem?
@IBOutlet private var menuToggleFullScreen: NSMenuItem? @IBOutlet private var menuToggleFullScreen: NSMenuItem?
@ -62,6 +68,8 @@ class AppDelegate: NSObject,
@IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@IBOutlet private var menuResetFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem?
@IBOutlet private var menuChangeTitle: NSMenuItem? @IBOutlet private var menuChangeTitle: NSMenuItem?
@IBOutlet private var menuChangeTabTitle: NSMenuItem?
@IBOutlet private var menuReadonly: NSMenuItem?
@IBOutlet private var menuQuickTerminal: NSMenuItem? @IBOutlet private var menuQuickTerminal: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem?
@IBOutlet private var menuCommandPalette: NSMenuItem? @IBOutlet private var menuCommandPalette: NSMenuItem?
@ -98,8 +106,10 @@ class AppDelegate: NSObject,
) )
/// Manages updates /// Manages updates
let updaterController: SPUStandardUpdaterController let updateController = UpdateController()
let updaterDelegate: UpdaterDelegate = UpdaterDelegate() var updateViewModel: UpdateViewModel {
updateController.viewModel
}
/// The elapsed time since the process was started /// The elapsed time since the process was started
var timeSinceLaunch: TimeInterval { var timeSinceLaunch: TimeInterval {
@ -107,7 +117,7 @@ class AppDelegate: NSObject,
} }
/// Tracks the windows that we hid for toggleVisibility. /// Tracks the windows that we hid for toggleVisibility.
private var hiddenState: ToggleVisibilityState? = nil private(set) var hiddenState: ToggleVisibilityState? = nil
/// The observer for the app appearance. /// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
@ -116,25 +126,9 @@ class AppDelegate: NSObject,
private var signals: [DispatchSourceSignal] = [] private var signals: [DispatchSourceSignal] = []
/// The custom app icon image that is currently in use. /// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage? = nil { @Published private(set) var appIcon: NSImage? = nil
didSet {
NSApplication.shared.applicationIconImage = appIcon
let appPath = Bundle.main.bundlePath
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
NSWorkspace.shared.noteFileSystemChanged(appPath)
}
}
override init() { override init() {
updaterController = SPUStandardUpdaterController(
// Important: we must not start the updater here because we need to read our configuration
// first to determine whether we're automatically checking, downloading, etc. The updater
// is started later in applicationDidFinishLaunching
startingUpdater: false,
updaterDelegate: updaterDelegate,
userDriverDelegate: nil
)
super.init() super.init()
ghostty.delegate = self ghostty.delegate = self
@ -179,7 +173,7 @@ class AppDelegate: NSObject,
ghosttyConfigDidChange(config: ghostty.config) ghosttyConfigDidChange(config: ghostty.config)
// Start our update checker. // Start our update checker.
updaterController.startUpdater() updateController.startUpdater()
// Register our service provider. This must happen after everything is initialized. // Register our service provider. This must happen after everything is initialized.
NSApp.servicesProvider = ServiceProvider() NSApp.servicesProvider = ServiceProvider()
@ -293,6 +287,11 @@ class AppDelegate: NSObject,
} }
} }
func applicationDidHide(_ notification: Notification) {
// Keep track of our hidden state to restore properly
self.hiddenState = .init()
}
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
// If we're back manually then clear the hidden state because macOS handles it. // If we're back manually then clear the hidden state because macOS handles it.
self.hiddenState = nil self.hiddenState = nil
@ -323,6 +322,12 @@ class AppDelegate: NSObject,
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
let windows = NSApplication.shared.windows let windows = NSApplication.shared.windows
if (windows.isEmpty) { return .terminateNow } if (windows.isEmpty) { return .terminateNow }
// If we've already accepted to install an update, then we don't need to
// confirm quit. The user is already expecting the update to happen.
if updateController.isInstalling {
return .terminateNow
}
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't // This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
// quite work with SwiftUI because windows are retained on close. So instead we check // quite work with SwiftUI because windows are retained on close. So instead we check
@ -471,7 +476,12 @@ class AppDelegate: NSObject,
} }
switch ghostty.config.macosDockDropBehavior { switch ghostty.config.macosDockDropBehavior {
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config) case .new_tab:
_ = TerminalController.newTab(
ghostty,
from: TerminalController.preferredParent?.window,
withBaseConfig: config
)
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
} }
@ -533,8 +543,9 @@ class AppDelegate: NSObject,
self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller")
self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection")
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line") self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill")
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right")
@ -550,6 +561,7 @@ class AppDelegate: NSObject,
self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line")
self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line")
self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square") 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. /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
@ -578,6 +590,9 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll) 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: "toggle_split_zoom", menuItem: self.menuZoomSplit)
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit) syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
@ -597,6 +612,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle)
syncMenuShortcut(config, action: "prompt_tab_title", menuItem: self.menuChangeTabTitle)
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop) syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
@ -714,6 +730,10 @@ class AppDelegate: NSObject,
} }
@objc private func ghosttyBellDidRing(_ notification: Notification) { @objc private func ghosttyBellDidRing(_ notification: Notification) {
if (ghostty.config.bellFeatures.contains(.system)) {
NSSound.beep()
}
if (ghostty.config.bellFeatures.contains(.attention)) { if (ghostty.config.bellFeatures.contains(.attention)) {
// Bounce the dock icon if we're not focused. // Bounce the dock icon if we're not focused.
NSApp.requestUserAttention(.informationalRequest) NSApp.requestUserAttention(.informationalRequest)
@ -806,13 +826,21 @@ class AppDelegate: NSObject,
// defined by our "auto-update" configuration (if set) or fall back to Sparkle // defined by our "auto-update" configuration (if set) or fall back to Sparkle
// user-based defaults. // user-based defaults.
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false { if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
updaterController.updater.automaticallyChecksForUpdates = false updateController.updater.automaticallyChecksForUpdates = false
updaterController.updater.automaticallyDownloadsUpdates = false updateController.updater.automaticallyDownloadsUpdates = false
} else if let autoUpdate = config.autoUpdate { } else if let autoUpdate = config.autoUpdate {
updaterController.updater.automaticallyChecksForUpdates = updateController.updater.automaticallyChecksForUpdates =
autoUpdate == .check || autoUpdate == .download autoUpdate == .check || autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates = updateController.updater.automaticallyDownloadsUpdates =
autoUpdate == .download autoUpdate == .download
/**
To test `auto-update` easily, uncomment the line below and
delete `SUEnableAutomaticChecks` in Ghostty-Info.plist.
Note: When `auto-update = download`, you may need to
`Clean Build Folder` if a background install has already begun.
*/
//updateController.updater.checkForUpdatesInBackground()
} }
// Config could change keybindings, so update everything that depends on that // Config could change keybindings, so update everything that depends on that
@ -860,49 +888,64 @@ class AppDelegate: NSObject,
} else { } else {
GlobalEventTap.shared.disable() GlobalEventTap.shared.disable()
} }
Task {
await updateAppIcon(from: config)
}
} }
/// Sync the appearance of our app with the theme specified in the config. /// Sync the appearance of our app with the theme specified in the config.
private func syncAppearance(config: Ghostty.Config) { private func syncAppearance(config: Ghostty.Config) {
NSApplication.shared.appearance = .init(ghosttyConfig: config) NSApplication.shared.appearance = .init(ghosttyConfig: config)
}
// 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) { switch (config.macosIcon) {
case .official: case .official:
self.appIcon = nil // Discard saved icon name
appIconName = nil
break break
case .blueprint: case .blueprint:
self.appIcon = NSImage(named: "BlueprintImage")! appIcon = NSImage(named: "BlueprintImage")!
case .chalkboard: case .chalkboard:
self.appIcon = NSImage(named: "ChalkboardImage")! appIcon = NSImage(named: "ChalkboardImage")!
case .glass: case .glass:
self.appIcon = NSImage(named: "GlassImage")! appIcon = NSImage(named: "GlassImage")!
case .holographic: case .holographic:
self.appIcon = NSImage(named: "HolographicImage")! appIcon = NSImage(named: "HolographicImage")!
case .microchip: case .microchip:
self.appIcon = NSImage(named: "MicrochipImage")! appIcon = NSImage(named: "MicrochipImage")!
case .paper: case .paper:
self.appIcon = NSImage(named: "PaperImage")! appIcon = NSImage(named: "PaperImage")!
case .retro: case .retro:
self.appIcon = NSImage(named: "RetroImage")! appIcon = NSImage(named: "RetroImage")!
case .xray: case .xray:
self.appIcon = NSImage(named: "XrayImage")! appIcon = NSImage(named: "XrayImage")!
case .custom: case .custom:
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) { if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
self.appIcon = userIcon appIcon = userIcon
appIconName = config.macosCustomIcon
} else { } else {
self.appIcon = nil // Revert back to official icon if invalid location appIcon = nil // Revert back to official icon if invalid location
appIconName = nil // Discard saved icon name
} }
case .customStyle: case .customStyle:
// Discard saved icon name
// if no valid colours were found
appIconName = nil
guard let ghostColor = config.macosIconGhostColor else { break } guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break } guard let screenColors = config.macosIconScreenColor else { break }
guard let icon = ColorizedGhosttyIcon( guard let icon = ColorizedGhosttyIcon(
@ -910,8 +953,38 @@ class AppDelegate: NSObject,
ghostColor: ghostColor, ghostColor: ghostColor,
frame: config.macosIconFrame frame: config.macosIconFrame
).makeImage() else { break } ).makeImage() else { break }
self.appIcon = icon 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
let appPath = Bundle.main.bundlePath
NSWorkspace.shared.setIcon(newIcon, forFile: appPath, options: [])
NSWorkspace.shared.noteFileSystemChanged(appPath)
await MainActor.run {
self.appIcon = newIcon
NSApplication.shared.applicationIconImage = newIcon
}
UserDefaults.standard.set(appIconName, forKey: "CustomGhosttyIcon")
} }
//MARK: - Restorable State //MARK: - Restorable State
@ -1004,7 +1077,8 @@ class AppDelegate: NSObject,
} }
@IBAction func checkForUpdates(_ sender: Any?) { @IBAction func checkForUpdates(_ sender: Any?) {
updaterController.checkForUpdates(sender) updateController.checkForUpdates()
//UpdateSimulator.happyPath.simulate(with: updateViewModel)
} }
@IBAction func newWindow(_ sender: Any?) { @IBAction func newWindow(_ sender: Any?) {
@ -1012,7 +1086,10 @@ class AppDelegate: NSObject,
} }
@IBAction func newTab(_ sender: Any?) { @IBAction func newTab(_ sender: Any?) {
_ = TerminalController.newTab(ghostty) _ = TerminalController.newTab(
ghostty,
from: TerminalController.preferredParent?.window
)
} }
@IBAction func closeAllWindows(_ sender: Any?) { @IBAction func closeAllWindows(_ sender: Any?) {
@ -1046,8 +1123,6 @@ class AppDelegate: NSObject,
guard let keyWindow = NSApp.keyWindow, guard let keyWindow = NSApp.keyWindow,
!keyWindow.styleMask.contains(.fullScreen) else { return } !keyWindow.styleMask.contains(.fullScreen) else { return }
// Keep track of our hidden state to restore properly
self.hiddenState = .init()
NSApp.hide(nil) NSApp.hide(nil)
return return
} }
@ -1096,11 +1171,11 @@ class AppDelegate: NSObject,
} }
} }
private struct ToggleVisibilityState { struct ToggleVisibilityState {
let hiddenWindows: [Weak<NSWindow>] let hiddenWindows: [Weak<NSWindow>]
let keyWindow: Weak<NSWindow>? let keyWindow: Weak<NSWindow>?
init() { fileprivate init() {
// We need to know the key window so that we can bring focus back to the // We need to know the key window so that we can bring focus back to the
// right window if it was hidden. // right window if it was hidden.
self.keyWindow = if let keyWindow = NSApp.keyWindow { self.keyWindow = if let keyWindow = NSApp.keyWindow {
@ -1113,10 +1188,19 @@ class AppDelegate: NSObject,
// want to bring back these windows if we remove the toggle. // want to bring back these windows if we remove the toggle.
// //
// We also ignore fullscreen windows because they don't hide anyways. // 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.isVisible &&
!$0.styleMask.contains(.fullScreen) !$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() { func restore() {
@ -1188,3 +1272,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"?> <?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> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
</dependencies> </dependencies>
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -16,6 +16,7 @@
<connections> <connections>
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/> <outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/> <outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
<outlet property="menuChangeTabTitle" destination="iac-lh-Cl7" id="tId-v0-a3E"/>
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/> <outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/> <outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/> <outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
@ -26,7 +27,12 @@
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/> <outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/> <outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/> <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="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="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/> <outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/> <outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
@ -41,6 +47,7 @@
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/> <outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/> <outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/> <outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
<outlet property="menuReadonly" destination="xpe-ia-Yjw" id="MMT-Sl-AfD"/>
<outlet property="menuRedo" destination="EX8-lB-4s7" id="wON-2J-yT1"/> <outlet property="menuRedo" destination="EX8-lB-4s7" id="wON-2J-yT1"/>
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/> <outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/> <outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
@ -245,6 +252,39 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="VYS-RG-uZD"/> <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> </items>
</menu> </menu>
</menuItem> </menuItem>
@ -277,12 +317,24 @@
<action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/> <action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem title="Change Title..." id="24I-xg-qIq"> <menuItem title="Change Tab Title..." id="iac-lh-Cl7">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="changeTabTitle:" target="-1" id="Jhl-9P-bMj"/>
</connections>
</menuItem>
<menuItem title="Change Terminal Title..." id="24I-xg-qIq">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/> <action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
</connections> </connections>
</menuItem> </menuItem>
<menuItem title="Terminal Read-only" id="xpe-ia-Yjw">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="toggleReadonly:" target="-1" id="Gqx-wT-K9v"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Vkj-tP-dMZ"/> <menuItem isSeparatorItem="YES" id="Vkj-tP-dMZ"/>
<menuItem title="Quick Terminal" id="1pv-LF-NBJ"> <menuItem title="Quick Terminal" id="1pv-LF-NBJ">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>

View File

@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {

View File

@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent {
) )
var command: CommandEntity var command: CommandEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult & ReturnsValue<Bool> { func perform() async throws -> some IntentResult & ReturnsValue<Bool> {

View File

@ -12,8 +12,10 @@ struct FocusTerminalIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {

View File

@ -17,8 +17,10 @@ struct GetTerminalDetailsIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
static var parameterSummary: some ParameterSummary { static var parameterSummary: some ParameterSummary {
Summary("Get \(\.$detail) from \(\.$terminal)") Summary("Get \(\.$detail) from \(\.$terminal)")

View File

@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent {
) )
var terminal: TerminalEntity var terminal: TerminalEntity
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {

View File

@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent {
) )
var action: String var action: String
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = [.background, .foreground] static var supportedModes: IntentModes = [.background, .foreground]
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult & ReturnsValue<Bool> { func perform() async throws -> some IntentResult & ReturnsValue<Bool> {

View File

@ -45,8 +45,10 @@ struct NewTerminalIntent: AppIntent {
// Performing in the background can avoid opening multiple windows at the same time // Performing in the background can avoid opening multiple windows at the same time
// using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time // using `foreground` will cause `perform` and `AppDelegate.applicationDidBecomeActive(_:)`/`AppDelegate.applicationShouldHandleReopen(_:hasVisibleWindows:)` running at the 'same' time
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
@available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
static var openAppWhenRun = false static var openAppWhenRun = false

View File

@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent {
static var title: LocalizedStringResource = "Open the Quick Terminal" static var title: LocalizedStringResource = "Open the Quick Terminal"
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.") static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
#if compiler(>=6.2)
@available(macOS 26.0, *) @available(macOS 26.0, *)
static var supportedModes: IntentModes = .background static var supportedModes: IntentModes = .background
#endif
@MainActor @MainActor
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> { func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {

View File

@ -45,19 +45,16 @@ struct ClipboardConfirmationView: View {
.font(.system(size: 42)) .font(.system(size: 42))
.padding() .padding()
.frame(alignment: .center) .frame(alignment: .center)
Text(request.text()) Text(request.text())
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding() .padding()
} }
ScrollView { TextEditor(text: .constant(contents))
Text(contents) .focusable(false)
.textSelection(.enabled) .font(.system(.body, design: .monospaced))
.font(.system(.body, design: .monospaced))
.padding(.all, 4)
}
HStack { HStack {
Spacer() Spacer()
Button(Action.text(.cancel, request)) { onCancel() } Button(Action.text(.cancel, request)) { onCancel() }

View File

@ -5,7 +5,28 @@ struct CommandOption: Identifiable, Hashable {
let title: String let title: String
let description: String? let description: String?
let symbols: [String]? let symbols: [String]?
let leadingIcon: String?
let badge: String?
let emphasis: Bool
let action: () -> Void let action: () -> Void
init(
title: String,
description: String? = nil,
symbols: [String]? = nil,
leadingIcon: String? = nil,
badge: String? = nil,
emphasis: Bool = false,
action: @escaping () -> Void
) {
self.title = title
self.description = description
self.symbols = symbols
self.leadingIcon = leadingIcon
self.badge = badge
self.emphasis = emphasis
self.action = action
}
static func == (lhs: CommandOption, rhs: CommandOption) -> Bool { static func == (lhs: CommandOption, rhs: CommandOption) -> Bool {
lhs.id == rhs.id lhs.id == rhs.id
@ -23,6 +44,7 @@ struct CommandPaletteView: View {
@State private var query = "" @State private var query = ""
@State private var selectedIndex: UInt? @State private var selectedIndex: UInt?
@State private var hoveredOptionID: UUID? @State private var hoveredOptionID: UUID?
@FocusState private var isTextFieldFocused: Bool
// The options that we should show, taking into account any filtering from // The options that we should show, taking into account any filtering from
// the query. // the query.
@ -51,7 +73,7 @@ struct CommandPaletteView: View {
} }
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
CommandPaletteQuery(query: $query) { event in CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in
switch (event) { switch (event) {
case .exit: case .exit:
isPresented = false isPresented = false
@ -123,6 +145,28 @@ struct CommandPaletteView: View {
.shadow(radius: 32, x: 0, y: 12) .shadow(radius: 32, x: 0, y: 12)
.padding() .padding()
.environment(\.colorScheme, scheme) .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
}
} }
} }
@ -132,6 +176,12 @@ fileprivate struct CommandPaletteQuery: View {
var onEvent: ((KeyboardEvent) -> Void)? = nil var onEvent: ((KeyboardEvent) -> Void)? = nil
@FocusState private var isTextFieldFocused: Bool @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 { enum KeyboardEvent {
case exit case exit
case submit case submit
@ -164,14 +214,6 @@ fileprivate struct CommandPaletteQuery: View {
.frame(height: 48) .frame(height: 48)
.textFieldStyle(.plain) .textFieldStyle(.plain)
.focused($isTextFieldFocused) .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 .onChange(of: isTextFieldFocused) { focused in
if !focused { if !focused {
onEvent?(.exit) onEvent?(.exit)
@ -198,7 +240,7 @@ fileprivate struct CommandTable: View {
} else { } else {
ScrollViewReader { proxy in ScrollViewReader { proxy in
ScrollView { ScrollView {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 4) {
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
CommandRow( CommandRow(
option: option, option: option,
@ -240,15 +282,36 @@ fileprivate struct CommandRow: View {
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
HStack { HStack(spacing: 8) {
if let icon = option.leadingIcon {
Image(systemName: icon)
.foregroundStyle(option.emphasis ? Color.accentColor : .secondary)
.font(.system(size: 14, weight: .medium))
}
Text(option.title) Text(option.title)
.fontWeight(option.emphasis ? .medium : .regular)
Spacer() Spacer()
if let badge = option.badge, !badge.isEmpty {
Text(badge)
.font(.caption2.weight(.medium))
.padding(.horizontal, 7)
.padding(.vertical, 3)
.background(
Capsule().fill(Color.accentColor.opacity(0.15))
)
.foregroundStyle(Color.accentColor)
}
if let symbols = option.symbols { if let symbols = option.symbols {
ShortcutSymbolsView(symbols: symbols) ShortcutSymbolsView(symbols: symbols)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
.padding(8) .padding(8)
.contentShape(Rectangle())
.background( .background(
isSelected isSelected
? Color.accentColor.opacity(0.2) ? Color.accentColor.opacity(0.2)
@ -256,6 +319,10 @@ fileprivate struct CommandRow: View {
? Color.secondary.opacity(0.2) ? Color.secondary.opacity(0.2)
: Color.clear) : Color.clear)
) )
.overlay(
RoundedRectangle(cornerRadius: 5)
.strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5)
)
.cornerRadius(5) .cornerRadius(5)
} }
.help(option.description ?? "") .help(option.description ?? "")

View File

@ -11,15 +11,54 @@ struct TerminalCommandPaletteView: View {
/// The configuration so we can lookup keyboard shortcuts. /// The configuration so we can lookup keyboard shortcuts.
@ObservedObject var ghosttyConfig: Ghostty.Config @ObservedObject var ghosttyConfig: Ghostty.Config
/// The update view model for showing update commands.
var updateViewModel: UpdateViewModel?
/// The callback when an action is submitted. /// The callback when an action is submitted.
var onAction: ((String) -> Void) var onAction: ((String) -> Void)
// The commands available to the command palette. // The commands available to the command palette.
private var commandOptions: [CommandOption] { private var commandOptions: [CommandOption] {
guard let surface = surfaceView.surfaceModel else { return [] } var options: [CommandOption] = []
// Add update command if an update is installable. This must always be the first so
// it is at the top.
if let updateViewModel, updateViewModel.state.isInstallable {
// We override the update available one only because we want to properly
// convey it'll go all the way through.
let title: String
if case .updateAvailable = updateViewModel.state {
title = "Update Ghostty and Restart"
} else {
title = updateViewModel.text
}
options.append(CommandOption(
title: title,
description: updateViewModel.description,
leadingIcon: updateViewModel.iconName ?? "shippingbox.fill",
badge: updateViewModel.badge,
emphasis: true
) {
(NSApp.delegate as? AppDelegate)?.updateController.installUpdate()
})
}
// Add cancel/skip update command if the update is installable
if let updateViewModel, updateViewModel.state.isInstallable {
options.append(CommandOption(
title: "Cancel or Skip Update",
description: "Dismiss the current update process"
) {
updateViewModel.state.cancel()
})
}
// Add terminal commands
guard let surface = surfaceView.surfaceModel else { return options }
do { do {
return try surface.commands().map { c in let terminalCommands = try surface.commands().map { c in
return CommandOption( return CommandOption(
title: c.title, title: c.title,
description: c.description, description: c.description,
@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View {
onAction(c.action) onAction(c.action)
} }
} }
options.append(contentsOf: terminalCommands)
} catch { } catch {
return [] return options
} }
return options
} }
var body: some View { var body: some View {
@ -48,19 +90,19 @@ struct TerminalCommandPaletteView: View {
backgroundColor: ghosttyConfig.backgroundColor, backgroundColor: ghosttyConfig.backgroundColor,
options: commandOptions 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 .zIndex(1) // Ensure it's on top
Spacer() Spacer()
} }
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) .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 .onChange(of: isPresented) { newValue in
// When the command palette disappears we need to send focus back to the // 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 // surface view we were overlaid on top of. There's probably a better way

View File

@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController {
// The active space when the quick terminal was last shown. // The active space when the quick terminal was last shown.
private var previousActiveSpace: CGSSpace? = nil private var previousActiveSpace: CGSSpace? = nil
/// The saved state when the quick terminal's surface tree becomes empty. /// Cache for per-screen window state.
/// private let screenStateCache = QuickTerminalScreenStateCache()
/// This preserves the user's window size and position when all terminal surfaces
/// are closed (e.g., via the `exit` command). When a new surface is created,
/// the window will be restored to this frame, preventing SwiftUI from resetting
/// the window to its default minimum size.
private var lastClosedFrames: NSMapTable<NSScreen, LastClosedState>
/// Non-nil if we have hidden dock state. /// Non-nil if we have hidden dock state.
private var hiddenDock: HiddenDock? = nil private var hiddenDock: HiddenDock? = nil
@ -37,7 +32,7 @@ class QuickTerminalController: BaseTerminalController {
/// Tracks if we're currently handling a manual resize to prevent recursion /// Tracks if we're currently handling a manual resize to prevent recursion
private var isHandlingResize: Bool = false private var isHandlingResize: Bool = false
init(_ ghostty: Ghostty.App, init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top, position: QuickTerminalPosition = .top,
baseConfig base: Ghostty.SurfaceConfiguration? = nil, baseConfig base: Ghostty.SurfaceConfiguration? = nil,
@ -45,10 +40,6 @@ class QuickTerminalController: BaseTerminalController {
) { ) {
self.position = position self.position = position
self.derivedConfig = DerivedConfig(ghostty.config) self.derivedConfig = DerivedConfig(ghostty.config)
// This is a weak to strong mapping, so that our keys being NSScreens
// can remove themselves when they disappear.
self.lastClosedFrames = .weakToStrongObjects()
// Important detail here: we initialize with an empty surface tree so // Important detail here: we initialize with an empty surface tree so
// that we don't start a terminal process. This gets started when the // that we don't start a terminal process. This gets started when the
@ -351,7 +342,10 @@ class QuickTerminalController: BaseTerminalController {
// animate out. // animate out.
if surfaceTree.isEmpty, if surfaceTree.isEmpty,
let ghostty_app = ghostty.app { 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) surfaceTree = SplitTree(view: view)
focusedSurface = view focusedSurface = view
} }
@ -379,17 +373,15 @@ class QuickTerminalController: BaseTerminalController {
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return } guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
// Grab our last closed frame to use, and clear our state since we're animating in. // Grab our last closed frame to use from the cache.
// We only use the last closed frame if we're opening on the same screen. let closedFrame = screenStateCache.frame(for: screen)
let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame
lastClosedFrames.removeObject(forKey: screen)
// Move our window off screen to the initial animation position. // Move our window off screen to the initial animation position.
position.setInitial( position.setInitial(
in: window, in: window,
on: screen, on: screen,
terminalSize: derivedConfig.quickTerminalSize, terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame) closedFrame: closedFrame)
// We need to set our window level to a high value. In testing, only // We need to set our window level to a high value. In testing, only
// popUpMenu and above do what we want. This gets it above the menu bar // popUpMenu and above do what we want. This gets it above the menu bar
@ -424,7 +416,7 @@ class QuickTerminalController: BaseTerminalController {
in: window.animator(), in: window.animator(),
on: screen, on: screen,
terminalSize: derivedConfig.quickTerminalSize, terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame) closedFrame: closedFrame)
}, completionHandler: { }, completionHandler: {
// There is a very minor delay here so waiting at least an event loop tick // There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window. // keeps us safe from the view not being on the window.
@ -513,7 +505,7 @@ class QuickTerminalController: BaseTerminalController {
// terminal is reactivated with a new surface. Without this, SwiftUI // terminal is reactivated with a new surface. Without this, SwiftUI
// would reset the window to its minimum content size. // would reset the window to its minimum content size.
if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen { if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen {
lastClosedFrames.setObject(.init(frame: window.frame), forKey: screen) screenStateCache.save(frame: window.frame, for: screen)
} }
// If we hid the dock then we unhide it. // If we hid the dock then we unhide it.
@ -524,6 +516,10 @@ class QuickTerminalController: BaseTerminalController {
if !window.isOnActiveSpace { if !window.isOnActiveSpace {
self.previousApp = nil self.previousApp = nil
window.orderOut(self) window.orderOut(self)
// If our application is hidden previously, we hide it again
if (NSApp.delegate as? AppDelegate)?.hiddenState != nil {
NSApp.hide(nil)
}
return return
} }
@ -560,12 +556,17 @@ class QuickTerminalController: BaseTerminalController {
// This causes the window to be removed from the screen list and macOS // This causes the window to be removed from the screen list and macOS
// handles what should be focused next. // handles what should be focused next.
window.orderOut(self) window.orderOut(self)
// If our application is hidden previously, we hide it again
if (NSApp.delegate as? AppDelegate)?.hiddenState != nil {
NSApp.hide(nil)
}
}) })
} }
private func syncAppearance() { private func syncAppearance() {
guard let window else { return } guard let window else { return }
defer { updateColorSchemeForSurfaceTree() }
// Change the collection behavior of the window depending on the configuration. // Change the collection behavior of the window depending on the configuration.
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
@ -598,7 +599,6 @@ class QuickTerminalController: BaseTerminalController {
alert.alertStyle = .warning alert.alertStyle = .warning
alert.beginSheetModal(for: window) alert.beginSheetModal(for: window)
} }
// MARK: First Responder // MARK: First Responder
@IBAction override func closeWindow(_ sender: Any) { @IBAction override func closeWindow(_ sender: Any) {
@ -736,14 +736,6 @@ class QuickTerminalController: BaseTerminalController {
hidden = false hidden = false
} }
} }
private class LastClosedState {
let frame: NSRect
init(frame: NSRect) {
self.frame = frame
}
}
} }
extension Notification.Name { extension Notification.Name {

View File

@ -0,0 +1,113 @@
import Foundation
import Cocoa
/// Manages cached window state per screen for the quick terminal.
///
/// This cache tracks the last closed window frame for each screen, allowing the quick terminal
/// to restore to its previous size and position when reopened. It uses stable display UUIDs
/// to survive NSScreen garbage collection and automatically prunes stale entries.
class QuickTerminalScreenStateCache {
/// The maximum number of saved screen states we retain. This is to avoid some kind of
/// pathological memory growth in case we get our screen state serializing wrong. I don't
/// know anyone with more than 10 screens, so let's just arbitrarily go with that.
private static let maxSavedScreens = 10
/// Time-to-live for screen entries that are no longer present (14 days).
private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60
/// Keyed by display UUID to survive NSScreen garbage collection.
private var stateByDisplay: [UUID: DisplayEntry] = [:]
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(onScreensChanged(_:)),
name: NSApplication.didChangeScreenParametersNotification,
object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/// Save the window frame for a screen.
func save(frame: NSRect, for screen: NSScreen) {
guard let key = screen.displayUUID else { return }
let entry = DisplayEntry(
frame: frame,
screenSize: screen.frame.size,
scale: screen.backingScaleFactor,
lastSeen: Date()
)
stateByDisplay[key] = entry
pruneCapacity()
}
/// Retrieve the last closed frame for a screen, if valid.
func frame(for screen: NSScreen) -> NSRect? {
guard let key = screen.displayUUID, var entry = stateByDisplay[key] else { return nil }
// Drop on dimension/scale change that makes the entry invalid
if !entry.isValid(for: screen) {
stateByDisplay.removeValue(forKey: key)
return nil
}
entry.lastSeen = Date()
stateByDisplay[key] = entry
return entry.frame
}
@objc private func onScreensChanged(_ note: Notification) {
let screens = NSScreen.screens
let now = Date()
let currentIDs = Set(screens.compactMap { $0.displayUUID })
for screen in screens {
guard let key = screen.displayUUID else { continue }
if var entry = stateByDisplay[key] {
// Drop on dimension/scale change that makes the entry invalid
if !entry.isValid(for: screen) {
stateByDisplay.removeValue(forKey: key)
} else {
// Update the screen size if it grew (keep entry valid for larger screens)
entry.screenSize = screen.frame.size
entry.lastSeen = now
stateByDisplay[key] = entry
}
}
}
// TTL prune for non-present screens
stateByDisplay = stateByDisplay.filter { key, entry in
currentIDs.contains(key) || now.timeIntervalSince(entry.lastSeen) < Self.screenStaleTTL
}
pruneCapacity()
}
private func pruneCapacity() {
guard stateByDisplay.count > Self.maxSavedScreens else { return }
let toRemove = stateByDisplay
.sorted { $0.value.lastSeen < $1.value.lastSeen }
.prefix(stateByDisplay.count - Self.maxSavedScreens)
for (key, _) in toRemove {
stateByDisplay.removeValue(forKey: key)
}
}
private struct DisplayEntry {
var frame: NSRect
var screenSize: CGSize
var scale: CGFloat
var lastSeen: Date
/// Returns true if this entry is still valid for the given screen.
/// Valid if the scale matches and the cached size is not larger than the current screen size.
/// This allows entries to persist when screens grow, but invalidates them when screens shrink.
func isValid(for screen: NSScreen) -> Bool {
guard scale == screen.backingScaleFactor else { return false }
return screenSize.width <= screen.frame.size.width && screenSize.height <= screen.frame.size.height
}
}
}

View File

@ -33,6 +33,7 @@ struct QuickTerminalSize {
case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS:
self = .pixels(cStruct.value.pixels) self = .pixels(cStruct.value.pixels)
default: default:
assertionFailure()
return nil return nil
} }
} }

View File

@ -14,7 +14,7 @@ struct SettingsView: View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Coming Soon. 🚧").font(.title) Text("Coming Soon. 🚧").font(.title)
Text("You can't configure settings in the GUI yet. To modify settings, " + Text("You can't configure settings in the GUI yet. To modify settings, " +
"edit the file at $HOME/.config/ghostty/config and restart Ghostty.") "edit the file at $HOME/.config/ghostty/config.ghostty and restart Ghostty.")
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
.lineLimit(nil) .lineLimit(nil)
} }

View File

@ -21,6 +21,9 @@ struct SplitView<L: View, R: View>: View {
let left: L let left: L
let right: R let right: R
/// Called when the divider is double-tapped to equalize splits.
let onEqualize: () -> Void
/// The minimum size (in points) of a split /// The minimum size (in points) of a split
let minSize: CGFloat = 10 let minSize: CGFloat = 10
@ -56,6 +59,9 @@ struct SplitView<L: View, R: View>: View {
split: $split) split: $split)
.position(splitterPoint) .position(splitterPoint)
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint)) .gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
.onTapGesture(count: 2) {
onEqualize()
}
} }
.accessibilityElement(children: .contain) .accessibilityElement(children: .contain)
.accessibilityLabel(splitViewLabel) .accessibilityLabel(splitViewLabel)
@ -69,7 +75,8 @@ struct SplitView<L: View, R: View>: View {
dividerColor: Color, dividerColor: Color,
resizeIncrements: NSSize = .init(width: 1, height: 1), resizeIncrements: NSSize = .init(width: 1, height: 1),
@ViewBuilder left: (() -> L), @ViewBuilder left: (() -> L),
@ViewBuilder right: (() -> R) @ViewBuilder right: (() -> R),
onEqualize: @escaping () -> Void
) { ) {
self.direction = direction self.direction = direction
self._split = split self._split = split
@ -77,6 +84,7 @@ struct SplitView<L: View, R: View>: View {
self.resizeIncrements = resizeIncrements self.resizeIncrements = resizeIncrements
self.left = left() self.left = left()
self.right = right() self.right = right()
self.onEqualize = onEqualize
} }
private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture {

View File

@ -55,6 +55,10 @@ struct TerminalSplitSubtreeView: View {
}, },
right: { right: {
TerminalSplitSubtreeView(node: split.right, onResize: onResize) TerminalSplitSubtreeView(node: split.right, onResize: onResize)
},
onEqualize: {
guard let surface = node.leftmostLeaf().surface else { return }
ghostty.splitEqualize(surface: surface)
} }
) )
} }

View File

@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController,
/// This can be set to show/hide the command palette. /// This can be set to show/hide the command palette.
@Published var commandPaletteIsShowing: Bool = false @Published var commandPaletteIsShowing: Bool = false
/// Set if the terminal view should show the update overlay.
@Published var updateOverlayIsVisible: Bool = false
/// Whether the terminal surface should focus when the mouse is over it. /// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool { var focusFollowsMouse: Bool {
@ -69,12 +72,24 @@ class BaseTerminalController: NSWindowController,
/// The previous frame information from the window /// The previous frame information from the window
private var savedFrame: SavedFrame? = nil private var savedFrame: SavedFrame? = nil
/// Cache previously applied appearance to avoid unnecessary updates
private var appliedColorScheme: ghostty_color_scheme_e?
/// The configuration derived from the Ghostty config so we don't need to rely on references. /// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig private var derivedConfig: DerivedConfig
/// The cancellables related to our focused surface. /// The cancellables related to our focused surface.
private var focusedSurfaceCancellables: Set<AnyCancellable> = [] private var focusedSurfaceCancellables: Set<AnyCancellable> = []
/// An override title for the tab/window set by the user via prompt_tab_title.
/// When set, this takes precedence over the computed title from the terminal.
var titleOverride: String? = nil {
didSet { applyTitleToWindow() }
}
/// The last computed title from the focused surface (without the override).
private var lastComputedTitle: String = "👻"
/// The time that undo/redo operations that contain running ptys are valid for. /// The time that undo/redo operations that contain running ptys are valid for.
var undoExpiration: Duration { var undoExpiration: Duration {
ghostty.config.undoTimeout ghostty.config.undoTimeout
@ -319,6 +334,37 @@ class BaseTerminalController: NSWindowController,
self.alert = alert self.alert = alert
} }
/// Prompt the user to change the tab/window title.
func promptTabTitle() {
guard let window else { return }
let alert = NSAlert()
alert.messageText = "Change Tab Title"
alert.informativeText = "Leave blank to restore the default."
alert.alertStyle = .informational
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24))
textField.stringValue = titleOverride ?? window.title
alert.accessoryView = textField
alert.addButton(withTitle: "OK")
alert.addButton(withTitle: "Cancel")
alert.window.initialFirstResponder = textField
alert.beginSheetModal(for: window) { [weak self] response in
guard let self else { return }
guard response == .alertFirstButtonReturn else { return }
let newTitle = textField.stringValue
if newTitle.isEmpty {
self.titleOverride = nil
} else {
self.titleOverride = newTitle
}
}
}
/// Close a surface from a view. /// Close a surface from a view.
func closeSurface( func closeSurface(
_ view: Ghostty.SurfaceView, _ view: Ghostty.SurfaceView,
@ -566,23 +612,12 @@ class BaseTerminalController: NSWindowController,
// Get the direction from the notification // Get the direction from the notification
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return }
guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return } guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return }
// Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection
let focusDirection: SplitTree<Ghostty.SurfaceView>.FocusDirection
switch direction {
case .previous: focusDirection = .previous
case .next: focusDirection = .next
case .up: focusDirection = .spatial(.up)
case .down: focusDirection = .spatial(.down)
case .left: focusDirection = .spatial(.left)
case .right: focusDirection = .spatial(.right)
}
// Find the node for the target surface // Find the node for the target surface
guard let targetNode = surfaceTree.root?.node(view: target) else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return }
// Find the next surface to focus // Find the next surface to focus
guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else { guard let nextSurface = surfaceTree.focusTarget(for: direction.toSplitTreeFocusDirection(), from: targetNode) else {
return return
} }
@ -723,10 +758,13 @@ class BaseTerminalController: NSWindowController,
} }
private func titleDidChange(to: String) { private func titleDidChange(to: String) {
lastComputedTitle = to
applyTitleToWindow()
}
private func applyTitleToWindow() {
guard let window else { return } guard let window else { return }
window.title = titleOverride ?? lastComputedTitle
// Set the main window title
window.title = to
} }
func pwdDidChange(to: URL?) { func pwdDidChange(to: URL?) {
@ -818,7 +856,18 @@ class BaseTerminalController: NSWindowController,
} }
} }
func fullscreenDidChange() {} func fullscreenDidChange() {
guard let fullscreenStyle else { return }
// When we enter fullscreen, we want to show the update overlay so that it
// is easily visible. For native fullscreen this is visible by showing the
// menubar but we don't want to rely on that.
if fullscreenStyle.isFullscreen {
updateOverlayIsVisible = true
} else {
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
}
}
// MARK: Clipboard Confirmation // MARK: Clipboard Confirmation
@ -900,6 +949,28 @@ class BaseTerminalController: NSWindowController,
fullscreenStyle = NativeFullscreen(window) fullscreenStyle = NativeFullscreen(window)
fullscreenStyle?.delegate = self fullscreenStyle?.delegate = self
} }
// Set our update overlay state
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
}
func defaultUpdateOverlayVisibility() -> Bool {
guard let window else { return true }
// No titlebar we always show the update overlay because it can't support
// updates in the titlebar
guard window.styleMask.contains(.titled) else {
return true
}
// If it's a non terminal window we can't trust it has an update accessory,
// so we always want to show the overlay.
guard let window = window as? TerminalWindow else {
return true
}
// Show the overlay if the window isn't.
return !window.supportsUpdateAccessory
} }
// MARK: NSWindowDelegate // MARK: NSWindowDelegate
@ -989,6 +1060,10 @@ class BaseTerminalController: NSWindowController,
window.performClose(sender) window.performClose(sender)
} }
@IBAction func changeTabTitle(_ sender: Any) {
promptTabTitle()
}
@IBAction func splitRight(_ sender: Any) { @IBAction func splitRight(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
@ -1087,6 +1162,22 @@ class BaseTerminalController: NSWindowController,
@IBAction func toggleCommandPalette(_ sender: Any?) { @IBAction func toggleCommandPalette(_ sender: Any?) {
commandPaletteIsShowing.toggle() 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) { @objc func resetTerminal(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
@ -1111,3 +1202,46 @@ class BaseTerminalController: NSWindowController,
} }
} }
} }
extension BaseTerminalController: NSMenuItemValidation {
func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action {
case #selector(findHide):
return focusedSurface?.searchState != nil
default:
return true
}
}
// MARK: - Surface Color Scheme
/// Update the surface tree's color scheme only when it actually changes.
///
/// Calling ``ghostty_surface_set_color_scheme`` triggers
/// ``syncAppearance(_:)`` via notification,
/// so we avoid redundant calls.
func updateColorSchemeForSurfaceTree() {
/// Derive the target scheme from `window-theme` or system appearance.
/// We set the scheme on surfaces so they pick the correct theme
/// and let ``syncAppearance(_:)`` update the window accordingly.
///
/// Using App's effectiveAppearance here to prevent incorrect updates.
let themeAppearance = NSApplication.shared.effectiveAppearance
let scheme: ghostty_color_scheme_e
if themeAppearance.isDark {
scheme = GHOSTTY_COLOR_SCHEME_DARK
} else {
scheme = GHOSTTY_COLOR_SCHEME_LIGHT
}
guard scheme != appliedColorScheme else {
return
}
for surfaceView in surfaceTree {
if let surface = surfaceView.surface {
ghostty_surface_set_color_scheme(surface, scheme)
}
}
appliedColorScheme = scheme
}
}

View File

@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
case "hidden": "TerminalHiddenTitlebar" case "hidden": "TerminalHiddenTitlebar"
case "transparent": "TerminalTransparentTitlebar" case "transparent": "TerminalTransparentTitlebar"
case "tabs": case "tabs":
#if compiler(>=6.2)
if #available(macOS 26.0, *) { if #available(macOS 26.0, *) {
"TerminalTabsTitlebarTahoe" "TerminalTabsTitlebarTahoe"
} else { } else {
"TerminalTabsTitlebarVentura" "TerminalTabsTitlebarVentura"
} }
#else
"TerminalTabsTitlebarVentura"
#endif
default: defaultValue default: defaultValue
} }
@ -50,6 +54,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
/// The configuration derived from the Ghostty config so we don't need to rely on references. /// The configuration derived from the Ghostty config so we don't need to rely on references.
private(set) var derivedConfig: DerivedConfig private(set) var derivedConfig: DerivedConfig
/// The notification cancellable for focused surface property changes. /// The notification cancellable for focused surface property changes.
private var surfaceAppearanceCancellables: Set<AnyCancellable> = [] private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
@ -100,6 +105,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
selector: #selector(onCloseOtherTabs), selector: #selector(onCloseOtherTabs),
name: .ghosttyCloseOtherTabs, name: .ghosttyCloseOtherTabs,
object: nil) object: nil)
center.addObserver(
self,
selector: #selector(onCloseTabsOnTheRight),
name: .ghosttyCloseTabsOnTheRight,
object: nil)
center.addObserver( center.addObserver(
self, self,
selector: #selector(onResetWindowSize), selector: #selector(onResetWindowSize),
@ -139,7 +149,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) { override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
super.surfaceTreeDidChange(from: from, to: to) super.surfaceTreeDidChange(from: from, to: to)
// Whenever our surface tree changes in any way (new split, close split, etc.) // Whenever our surface tree changes in any way (new split, close split, etc.)
// we want to invalidate our state. // we want to invalidate our state.
invalidateRestorableState() invalidateRestorableState()
@ -186,7 +196,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
$0.window?.isMainWindow ?? false $0.window?.isMainWindow ?? false
} ?? lastMain ?? all.last } ?? lastMain ?? all.last
} }
// The last controller to be main. We use this when paired with "preferredParent" // The last controller to be main. We use this when paired with "preferredParent"
// to find the preferred window to attach new tabs, perform actions, etc. We // to find the preferred window to attach new tabs, perform actions, etc. We
// always prefer the main window but if there isn't any (because we're triggered // always prefer the main window but if there isn't any (because we're triggered
@ -373,9 +383,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
withTarget: controller, withTarget: controller,
expiresAfter: controller.undoExpiration expiresAfter: controller.undoExpiration
) { target in ) { target in
// Close the tab when undoing // Close the tab when undoing. We do this in a DispatchQueue because
undoManager.disableUndoRegistration { // for some people on macOS Tahoe this caused a crash and the queue
target.closeTab(nil) // fixes it.
// https://github.com/ghostty-org/ghostty/pull/9512
DispatchQueue.main.async {
undoManager.disableUndoRegistration {
target.closeTab(nil)
}
} }
// Register redo action // Register redo action
@ -416,15 +431,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
return return
} }
/// Surface-level config will be updated in
// This is a surface-level config update. If we have the surface, we /// ``Ghostty/Ghostty/SurfaceView/derivedConfig`` then
// update our appearance based on it. /// ``TerminalController/focusedSurfaceDidChange(to:)``
guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(surfaceView) else { return }
// We can't use surfaceView.derivedConfig because it may not be updated
// yet since it also responds to notifications.
syncAppearance(.init(config))
} }
/// Update the accessory view of each tab according to the keyboard /// Update the accessory view of each tab according to the keyboard
@ -499,53 +508,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
window.syncAppearance(surfaceConfig) window.syncAppearance(surfaceConfig)
} }
/// Returns the default size of the window. This is contextual based on the focused surface because /// Adjusts the given frame for the configured window position.
/// the focused surface may specify a different default size than others. func adjustForWindowPosition(frame: NSRect, on screen: NSScreen) -> NSRect {
private var defaultSize: NSRect? { guard let x = derivedConfig.windowPositionX else { return frame }
guard let screen = window?.screen ?? NSScreen.main else { return nil } guard let y = derivedConfig.windowPositionY else { return frame }
if derivedConfig.maximize { // Convert top-left coordinates to bottom-left origin using our utility extension
return screen.visibleFrame let origin = screen.origin(
} else if let focusedSurface, fromTopLeftOffsetX: CGFloat(x),
let initialSize = focusedSurface.initialSize { offsetY: CGFloat(y),
// Get the current frame of the window windowSize: frame.size)
guard var frame = window?.frame else { return nil }
// Calculate the chrome size (window size minus view size) // Clamp the origin to ensure the window stays fully visible on screen
let chromeWidth = frame.size.width - focusedSurface.frame.size.width var safeOrigin = origin
let chromeHeight = frame.size.height - focusedSurface.frame.size.height let vf = screen.visibleFrame
safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width)
safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height)
// Calculate the new width and height, clamping to the screen's size // Return our new origin
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width) var result = frame
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height) result.origin = safeOrigin
return result
// 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 frame
}
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 frame
} }
/// This is called anytime a node in the surface tree is being removed. /// This is called anytime a node in the surface tree is being removed.
@ -576,7 +559,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeWindowImmediately() closeWindowImmediately()
return return
} }
// Undo // Undo
if let undoManager, let undoState { if let undoManager, let undoState {
// Register undo action to restore the tab // Register undo action to restore the tab
@ -597,15 +580,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
} }
} }
} }
window.close() window.close()
} }
private func closeOtherTabsImmediately() { private func closeOtherTabsImmediately() {
guard let window = window else { return } guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return } guard let tabGroup = window.tabGroup else { return }
guard tabGroup.windows.count > 1 else { return } guard tabGroup.windows.count > 1 else { return }
// Start an undo grouping // Start an undo grouping
if let undoManager { if let undoManager {
undoManager.beginUndoGrouping() undoManager.beginUndoGrouping()
@ -613,7 +596,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
defer { defer {
undoManager?.endUndoGrouping() undoManager?.endUndoGrouping()
} }
// Iterate through all tabs except the current one. // Iterate through all tabs except the current one.
for window in tabGroup.windows where window != self.window { for window in tabGroup.windows where window != self.window {
// We ignore any non-terminal tabs. They don't currently exist and we can't // We ignore any non-terminal tabs. They don't currently exist and we can't
@ -625,10 +608,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
controller.closeTabImmediately(registerRedo: false) controller.closeTabImmediately(registerRedo: false)
} }
} }
if let undoManager { if let undoManager {
undoManager.setActionName("Close Other Tabs") undoManager.setActionName("Close Other Tabs")
// We need to register an undo that refocuses this window. Otherwise, the // We need to register an undo that refocuses this window. Otherwise, the
// undo operation above for each tab will steal focus. // undo operation above for each tab will steal focus.
undoManager.registerUndo( undoManager.registerUndo(
@ -638,7 +621,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
DispatchQueue.main.async { DispatchQueue.main.async {
target.window?.makeKeyAndOrderFront(nil) target.window?.makeKeyAndOrderFront(nil)
} }
// Register redo action // Register redo action
undoManager.registerUndo( undoManager.registerUndo(
withTarget: target, withTarget: target,
@ -650,6 +633,46 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
} }
} }
private func closeTabsOnTheRightImmediately() {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
guard !tabsToClose.isEmpty else { return }
undoManager?.beginUndoGrouping()
defer {
undoManager?.endUndoGrouping()
}
for (_, candidate) in tabsToClose {
if let controller = candidate.windowController as? TerminalController {
controller.closeTabImmediately(registerRedo: false)
}
}
if let undoManager {
undoManager.setActionName("Close Tabs to the Right")
undoManager.registerUndo(
withTarget: self,
expiresAfter: undoExpiration
) { target in
DispatchQueue.main.async {
target.window?.makeKeyAndOrderFront(nil)
}
undoManager.registerUndo(
withTarget: target,
expiresAfter: target.undoExpiration
) { target in
target.closeTabsOnTheRightImmediately()
}
}
}
}
/// Closes the current window (including any other tabs) immediately and without /// Closes the current window (including any other tabs) immediately and without
/// confirmation. This will setup proper undo state so the action can be undone. /// confirmation. This will setup proper undo state so the action can be undone.
private func closeWindowImmediately() { private func closeWindowImmediately() {
@ -724,7 +747,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
case (nil, nil): return true case (nil, nil): return true
} }
} }
// Find the index of the key window in our sorted states. This is a bit verbose // Find the index of the key window in our sorted states. This is a bit verbose
// but we only need this for this style of undo so we don't want to add it to // but we only need this for this style of undo so we don't want to add it to
// UndoState. // UndoState.
@ -750,12 +773,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
let controllers = undoStates.map { undoState in let controllers = undoStates.map { undoState in
TerminalController(ghostty, with: undoState) TerminalController(ghostty, with: undoState)
} }
// The first controller becomes the parent window for all tabs. // The first controller becomes the parent window for all tabs.
// If we don't have a first controller (shouldn't be possible?) // If we don't have a first controller (shouldn't be possible?)
// then we can't restore tabs. // then we can't restore tabs.
guard let firstController = controllers.first else { return } guard let firstController = controllers.first else { return }
// Add all subsequent controllers as tabs to the first window // Add all subsequent controllers as tabs to the first window
for controller in controllers.dropFirst() { for controller in controllers.dropFirst() {
controller.showWindow(nil) controller.showWindow(nil)
@ -764,7 +787,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
firstWindow.addTabbedWindow(newWindow, ordered: .above) firstWindow.addTabbedWindow(newWindow, ordered: .above)
} }
} }
// Make the appropriate window key. If we had a key window, restore it. // Make the appropriate window key. If we had a key window, restore it.
// Otherwise, make the last window key. // Otherwise, make the last window key.
if let keyWindowIndex, keyWindowIndex < controllers.count { if let keyWindowIndex, keyWindowIndex < controllers.count {
@ -785,32 +808,25 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
/// Close all windows, asking for confirmation if necessary. /// Close all windows, asking for confirmation if necessary.
static func closeAllWindows() { static func closeAllWindows() {
let needsConfirm: Bool = all.contains { // The window we use for confirmations. Try to find the first window that
$0.surfaceTree.contains { $0.needsConfirmQuit } // needs quit confirmation. This lets us attach the confirmation to something
} // that is running.
guard let confirmWindow = all
if (!needsConfirm) { .first(where: { $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })?
.surfaceTree.first(where: { $0.needsConfirmQuit })?
.window
else {
closeAllWindowsImmediately() closeAllWindowsImmediately()
return return
} }
// If we don't have a main window, we just close all windows because
// we have no window to show the modal on top of. I'm sure there's a way
// to do an app-level alert but I don't know how and this case should never
// really happen.
guard let alertWindow = preferredParent?.window else {
closeAllWindowsImmediately()
return
}
// If we need confirmation by any, show one confirmation for all windows
let alert = NSAlert() let alert = NSAlert()
alert.messageText = "Close All Windows?" alert.messageText = "Close All Windows?"
alert.informativeText = "All terminal sessions will be terminated." alert.informativeText = "All terminal sessions will be terminated."
alert.addButton(withTitle: "Close All Windows") alert.addButton(withTitle: "Close All Windows")
alert.addButton(withTitle: "Cancel") alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning alert.alertStyle = .warning
alert.beginSheetModal(for: alertWindow, completionHandler: { response in alert.beginSheetModal(for: confirmWindow, completionHandler: { response in
if (response == .alertFirstButtonReturn) { if (response == .alertFirstButtonReturn) {
// This is important so that we avoid losing focus when Stage // This is important so that we avoid losing focus when Stage
// Manager is used (#8336) // Manager is used (#8336)
@ -837,6 +853,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
let focusedSurface: UUID? let focusedSurface: UUID?
let tabIndex: Int? let tabIndex: Int?
weak var tabGroup: NSWindowTabGroup? weak var tabGroup: NSWindowTabGroup?
let tabColor: TerminalTabColor
} }
convenience init(_ ghostty: Ghostty.App, convenience init(_ ghostty: Ghostty.App,
@ -848,6 +865,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
showWindow(nil) showWindow(nil)
if let window { if let window {
window.setFrame(undoState.frame, display: true) window.setFrame(undoState.frame, display: true)
if let terminalWindow = window as? TerminalWindow {
terminalWindow.tabColor = undoState.tabColor
}
// If we have a tab group and index, restore the tab to its original position // If we have a tab group and index, restore the tab to its original position
if let tabGroup = undoState.tabGroup, if let tabGroup = undoState.tabGroup,
@ -883,7 +903,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
surfaceTree: surfaceTree, surfaceTree: surfaceTree,
focusedSurface: focusedSurface?.id, focusedSurface: focusedSurface?.id,
tabIndex: window.tabGroup?.windows.firstIndex(of: window), tabIndex: window.tabGroup?.windows.firstIndex(of: window),
tabGroup: window.tabGroup) tabGroup: window.tabGroup,
tabColor: (window as? TerminalWindow)?.tabColor ?? .none)
} }
//MARK: - NSWindowController //MARK: - NSWindowController
@ -897,9 +918,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
super.windowDidLoad() super.windowDidLoad()
guard let window else { return } 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 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 // 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 // you shouldn't use it. Its safe in this case because for a new window we should
@ -919,19 +937,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// If this is our first surface then our focused surface will be nil // If this is our first surface then our focused surface will be nil
// so we force the focused surface to the leaf. // so we force the focused surface to the leaf.
focusedSurface = view focusedSurface = view
if let defaultSize {
window.setFrame(defaultSize, display: true)
}
} }
// Initialize our content view to the SwiftUI root // Initialize our content view to the SwiftUI root
window.contentView = NSHostingView(rootView: TerminalView( window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty, ghostty: self.ghostty,
viewModel: self, 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 // 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 // its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it. // it.
@ -1042,7 +1079,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
if let window { if let window {
LastWindowPosition.shared.save(window) LastWindowPosition.shared.save(window)
} }
// Remember our last main // Remember our last main
Self.lastMain = self Self.lastMain = self
} }
@ -1089,27 +1126,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
@IBAction func closeOtherTabs(_ sender: Any?) { @IBAction func closeOtherTabs(_ sender: Any?) {
guard let window = window else { return } guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return } guard let tabGroup = window.tabGroup else { return }
// If we only have one window then we have no other tabs to close // If we only have one window then we have no other tabs to close
guard tabGroup.windows.count > 1 else { return } guard tabGroup.windows.count > 1 else { return }
// Check if we have to confirm close. // Check if we have to confirm close.
guard tabGroup.windows.contains(where: { window in guard tabGroup.windows.contains(where: { window in
// Ignore ourself // Ignore ourself
if window == self.window { return false } if window == self.window { return false }
// Ignore non-terminals // Ignore non-terminals
guard let controller = window.windowController as? TerminalController else { guard let controller = window.windowController as? TerminalController else {
return false return false
} }
// Check if any surfaces require confirmation // Check if any surfaces require confirmation
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
}) else { }) else {
self.closeOtherTabsImmediately() self.closeOtherTabsImmediately()
return return
} }
confirmClose( confirmClose(
messageText: "Close Other Tabs?", messageText: "Close Other Tabs?",
informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed." informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed."
@ -1118,9 +1155,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
} }
} }
@IBAction func closeTabsOnTheRight(_ sender: Any?) {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return }
let tabsToClose = tabGroup.windows.enumerated().filter { $0.offset > currentIndex }
guard !tabsToClose.isEmpty else { return }
let needsConfirm = tabsToClose.contains { (_, candidate) in
guard let controller = candidate.windowController as? TerminalController else {
return false
}
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
}
if !needsConfirm {
self.closeTabsOnTheRightImmediately()
return
}
confirmClose(
messageText: "Close Tabs on the Right?",
informativeText: "At least one tab to the right still has a running process. If you close the tab the process will be killed."
) {
self.closeTabsOnTheRightImmediately()
}
}
@IBAction func returnToDefaultSize(_ sender: Any?) { @IBAction func returnToDefaultSize(_ sender: Any?) {
guard let defaultSize else { return } guard let window, let defaultSize else { return }
window?.setFrame(defaultSize, display: true) defaultSize.apply(to: window)
} }
@IBAction override func closeWindow(_ sender: Any?) { @IBAction override func closeWindow(_ sender: Any?) {
@ -1130,24 +1196,19 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// if we're closing the window. If we don't have a tabgroup for any // if we're closing the window. If we don't have a tabgroup for any
// reason we check ourselves. // reason we check ourselves.
let windows: [NSWindow] = window.tabGroup?.windows ?? [window] let windows: [NSWindow] = window.tabGroup?.windows ?? [window]
guard let confirmController = windows
// Check if any windows require close confirmation. .compactMap({ $0.windowController as? TerminalController })
let needsConfirm = windows.contains { tabWindow in .first(where: { $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })
guard let controller = tabWindow.windowController as? TerminalController else { else {
return false
}
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
}
// If none need confirmation then we can just close all the windows.
if !needsConfirm {
closeWindowImmediately() closeWindowImmediately()
return return
} }
confirmClose( // We call confirmClose on the proper controller so the alert is
// attached to the window that needs confirmation.
confirmController.confirmClose(
messageText: "Close Window?", messageText: "Close Window?",
informativeText: "All terminal sessions in this window will be terminated." informativeText: "All terminal sessions in this window will be terminated.",
) { ) {
self.closeWindowImmediately() self.closeWindowImmediately()
} }
@ -1164,7 +1225,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
} }
//MARK: - TerminalViewDelegate //MARK: - TerminalViewDelegate
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
super.focusedSurfaceDidChange(to: to) super.focusedSurfaceDidChange(to: to)
@ -1228,7 +1289,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// Get our target window // Get our target window
let targetWindow = tabbedWindows[finalIndex] let targetWindow = tabbedWindows[finalIndex]
// Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs. // Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs.
// I believe this is due to messed up constraints for our hacky tab bar. I'd like to // I believe this is due to messed up constraints for our hacky tab bar. I'd like to
// find a better workaround. For now, this improves things dramatically. // find a better workaround. For now, this improves things dramatically.
@ -1241,7 +1302,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
DispatchQueue.main.async { DispatchQueue.main.async {
selectedWindow.makeKey() selectedWindow.makeKey()
} }
return return
} }
} }
@ -1324,6 +1385,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeOtherTabs(self) closeOtherTabs(self)
} }
@objc private func onCloseTabsOnTheRight(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return }
closeTabsOnTheRight(self)
}
@objc private func onCloseWindow(notification: SwiftUI.Notification) { @objc private func onCloseWindow(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return } guard surfaceTree.contains(target) else { return }
@ -1358,12 +1425,16 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
let macosWindowButtons: Ghostty.MacOSWindowButtons let macosWindowButtons: Ghostty.MacOSWindowButtons
let macosTitlebarStyle: String let macosTitlebarStyle: String
let maximize: Bool let maximize: Bool
let windowPositionX: Int16?
let windowPositionY: Int16?
init() { init() {
self.backgroundColor = Color(NSColor.windowBackgroundColor) self.backgroundColor = Color(NSColor.windowBackgroundColor)
self.macosWindowButtons = .visible self.macosWindowButtons = .visible
self.macosTitlebarStyle = "system" self.macosTitlebarStyle = "system"
self.maximize = false self.maximize = false
self.windowPositionX = nil
self.windowPositionY = nil
} }
init(_ config: Ghostty.Config) { init(_ config: Ghostty.Config) {
@ -1371,43 +1442,99 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
self.macosWindowButtons = config.macosWindowButtons self.macosWindowButtons = config.macosWindowButtons
self.macosTitlebarStyle = config.macosTitlebarStyle self.macosTitlebarStyle = config.macosTitlebarStyle
self.maximize = config.maximize self.maximize = config.maximize
self.windowPositionX = config.windowPositionX
self.windowPositionY = config.windowPositionY
} }
} }
} }
// MARK: NSMenuItemValidation // MARK: NSMenuItemValidation
extension TerminalController: NSMenuItemValidation { extension TerminalController {
func validateMenuItem(_ item: NSMenuItem) -> Bool { override func validateMenuItem(_ item: NSMenuItem) -> Bool {
switch item.action { switch item.action {
case #selector(closeTabsOnTheRight):
guard let window, let tabGroup = window.tabGroup else { return false }
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false }
return tabGroup.windows.enumerated().contains { $0.offset > currentIndex }
case #selector(returnToDefaultSize): case #selector(returnToDefaultSize):
guard let window else { return false } guard let window else { return false }
// Native fullscreen windows can't revert to default size. // Native fullscreen windows can't revert to default size.
if window.styleMask.contains(.fullScreen) { if window.styleMask.contains(.fullScreen) {
return false return false
} }
// If we're fullscreen at all then we can't change size // If we're fullscreen at all then we can't change size
if fullscreenStyle?.isFullscreen ?? false { if fullscreenStyle?.isFullscreen ?? false {
return false return false
} }
// If our window is already the default size or we don't have a // If our window is already the default size or we don't have a
// default size, then disable. // default size, then disable.
guard let defaultSize, return defaultSize?.isChanged(for: window) ?? false
window.frame.size != .init(
width: defaultSize.size.width,
height: defaultSize.size.height
)
else {
return false
}
return true
default: 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

@ -4,14 +4,20 @@ import Cocoa
class TerminalRestorableState: Codable { class TerminalRestorableState: Codable {
static let selfKey = "state" static let selfKey = "state"
static let versionKey = "version" static let versionKey = "version"
static let version: Int = 5 static let version: Int = 7
let focusedSurface: String? let focusedSurface: String?
let surfaceTree: SplitTree<Ghostty.SurfaceView> let surfaceTree: SplitTree<Ghostty.SurfaceView>
let effectiveFullscreenMode: FullscreenMode?
let tabColor: TerminalTabColor
let titleOverride: String?
init(from controller: TerminalController) { init(from controller: TerminalController) {
self.focusedSurface = controller.focusedSurface?.id.uuidString self.focusedSurface = controller.focusedSurface?.id.uuidString
self.surfaceTree = controller.surfaceTree self.surfaceTree = controller.surfaceTree
self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode
self.tabColor = (controller.window as? TerminalWindow)?.tabColor ?? .none
self.titleOverride = controller.titleOverride
} }
init?(coder aDecoder: NSCoder) { init?(coder aDecoder: NSCoder) {
@ -28,6 +34,9 @@ class TerminalRestorableState: Codable {
self.surfaceTree = v.value.surfaceTree self.surfaceTree = v.value.surfaceTree
self.focusedSurface = v.value.focusedSurface self.focusedSurface = v.value.focusedSurface
self.effectiveFullscreenMode = v.value.effectiveFullscreenMode
self.tabColor = v.value.tabColor
self.titleOverride = v.value.titleOverride
} }
func encode(with coder: NSCoder) { func encode(with coder: NSCoder) {
@ -91,6 +100,12 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
return return
} }
// Restore our tab color
(window as? TerminalWindow)?.tabColor = state.tabColor
// Restore the tab title override
c.titleOverride = state.titleOverride
// Setup our restored state on the controller // Setup our restored state on the controller
// Find the focused surface in surfaceTree // Find the focused surface in surfaceTree
if let focusedStr = state.focusedSurface { if let focusedStr = state.focusedSurface {
@ -109,6 +124,13 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
} }
completionHandler(window, nil) completionHandler(window, nil)
guard let mode = state.effectiveFullscreenMode, mode != .native else {
// We let AppKit handle native fullscreen
return
}
// Give the window to AppKit first, then adjust its frame and style
// to minimise any visible frame changes.
c.toggleFullscreen(mode: mode)
} }
/// This restores the focus state of the surfaceview within the given window. When restoring, /// This restores the focus state of the surfaceview within the given window. When restoring,

View File

@ -0,0 +1,185 @@
import AppKit
import SwiftUI
enum TerminalTabColor: Int, CaseIterable, Codable {
case none
case blue
case purple
case pink
case red
case orange
case yellow
case green
case teal
case graphite
var localizedName: String {
switch self {
case .none:
return "None"
case .blue:
return "Blue"
case .purple:
return "Purple"
case .pink:
return "Pink"
case .red:
return "Red"
case .orange:
return "Orange"
case .yellow:
return "Yellow"
case .green:
return "Green"
case .teal:
return "Teal"
case .graphite:
return "Graphite"
}
}
var displayColor: NSColor? {
switch self {
case .none:
return nil
case .blue:
return .systemBlue
case .purple:
return .systemPurple
case .pink:
return .systemPink
case .red:
return .systemRed
case .orange:
return .systemOrange
case .yellow:
return .systemYellow
case .green:
return .systemGreen
case .teal:
if #available(macOS 13.0, *) {
return .systemMint
} else {
return .systemTeal
}
case .graphite:
return .systemGray
}
}
func swatchImage(selected: Bool) -> NSImage {
let size = NSSize(width: 18, height: 18)
return NSImage(size: size, flipped: false) { rect in
let circleRect = rect.insetBy(dx: 1, dy: 1)
let circlePath = NSBezierPath(ovalIn: circleRect)
if let fillColor = self.displayColor {
fillColor.setFill()
circlePath.fill()
} else {
NSColor.clear.setFill()
circlePath.fill()
NSColor.quaternaryLabelColor.setStroke()
circlePath.lineWidth = 1
circlePath.stroke()
}
if self == .none {
let slash = NSBezierPath()
slash.move(to: NSPoint(x: circleRect.minX + 2, y: circleRect.minY + 2))
slash.line(to: NSPoint(x: circleRect.maxX - 2, y: circleRect.maxY - 2))
slash.lineWidth = 1.5
NSColor.secondaryLabelColor.setStroke()
slash.stroke()
}
if selected {
let highlight = NSBezierPath(ovalIn: rect.insetBy(dx: 0.5, dy: 0.5))
highlight.lineWidth = 2
NSColor.controlAccentColor.setStroke()
highlight.stroke()
}
return true
}
}
}
// MARK: - Menu View
/// A SwiftUI view displaying a color palette for tab color selection.
/// Used as a custom view inside an NSMenuItem in the tab context menu.
struct TabColorMenuView: View {
@State private var currentSelection: TerminalTabColor
let onSelect: (TerminalTabColor) -> Void
init(selectedColor: TerminalTabColor, onSelect: @escaping (TerminalTabColor) -> Void) {
self._currentSelection = State(initialValue: selectedColor)
self.onSelect = onSelect
}
var body: some View {
VStack(alignment: .leading, spacing: 3) {
Text("Tab Color")
.padding(.bottom, 2)
ForEach(Self.paletteRows, id: \.self) { row in
HStack(spacing: 2) {
ForEach(row, id: \.self) { color in
TabColorSwatch(
color: color,
isSelected: color == currentSelection
) {
currentSelection = color
onSelect(color)
}
}
}
}
}
.padding(.leading, Self.leadingPadding)
.padding(.trailing, 12)
.padding(.top, 4)
.padding(.bottom, 4)
}
static let paletteRows: [[TerminalTabColor]] = [
[.none, .blue, .purple, .pink, .red],
[.orange, .yellow, .green, .teal, .graphite],
]
/// Leading padding to align with the menu's icon gutter.
/// macOS 26 introduced icons in menus, requiring additional padding.
private static var leadingPadding: CGFloat {
if #available(macOS 26.0, *) {
return 40
} else {
return 12
}
}
}
/// A single color swatch button in the tab color palette.
private struct TabColorSwatch: View {
let color: TerminalTabColor
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Group {
if color == .none {
Image(systemName: isSelected ? "circle.slash" : "circle")
.foregroundStyle(.secondary)
} else if let displayColor = color.displayColor {
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle.fill")
.foregroundStyle(Color(nsColor: displayColor))
}
}
.font(.system(size: 16))
.frame(width: 20, height: 20)
}
.buttonStyle(.plain)
.help(color.localizedName)
}
}

View File

@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject {
/// The command palette state. /// The command palette state.
var commandPaletteIsShowing: Bool { get set } var commandPaletteIsShowing: Bool { get set }
/// The update overlay should be visible.
var updateOverlayIsVisible: Bool { get }
} }
/// The main terminal view. This terminal view supports splits. /// The main terminal view. This terminal view supports splits.
@ -42,7 +45,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// An optional delegate to receive information about terminal changes. // An optional delegate to receive information about terminal changes.
weak var delegate: (any TerminalViewDelegate)? = nil weak var delegate: (any TerminalViewDelegate)? = nil
// The most recently focused surface, equal to focusedSurface when // The most recently focused surface, equal to focusedSurface when
// it is non-nil. // it is non-nil.
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init() @State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init()
@ -97,6 +100,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
guard let size = newValue else { return } guard let size = newValue else { return }
self.delegate?.cellSizeDidChange(to: size) 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 // 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 : []) .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])
@ -105,10 +110,34 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
TerminalCommandPaletteView( TerminalCommandPaletteView(
surfaceView: surfaceView, surfaceView: surfaceView,
isPresented: $viewModel.commandPaletteIsShowing, isPresented: $viewModel.commandPaletteIsShowing,
ghosttyConfig: ghostty.config) { action in ghosttyConfig: ghostty.config,
updateViewModel: (NSApp.delegate as? AppDelegate)?.updateViewModel) { action in
self.delegate?.performAction(action, on: surfaceView) self.delegate?.performAction(action, on: surfaceView)
} }
} }
// Show update information above all else.
if viewModel.updateOverlayIsVisible {
UpdateOverlay()
}
}
.frame(maxWidth: .greatestFiniteMagnitude, maxHeight: .greatestFiniteMagnitude)
}
}
}
fileprivate struct UpdateOverlay: View {
var body: some View {
if let appDelegate = NSApp.delegate as? AppDelegate {
VStack {
Spacer()
HStack {
Spacer()
UpdatePill(model: appDelegate.updateViewModel)
.padding(.bottom, 9)
.padding(.trailing, 9)
}
} }
} }
} }

View File

@ -1,6 +1,9 @@
import AppKit import AppKit
class HiddenTitlebarTerminalWindow: TerminalWindow { class HiddenTitlebarTerminalWindow: TerminalWindow {
// No titlebar, we don't support accessories.
override var supportsUpdateAccessory: Bool { false }
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()

View File

@ -5,6 +5,12 @@ import GhosttyKit
/// The base class for all standalone, "normal" terminal windows. This sets the basic /// The base class for all standalone, "normal" terminal windows. This sets the basic
/// style and configuration of the window based on the app configuration. /// style and configuration of the window based on the app configuration.
class TerminalWindow: NSWindow { class TerminalWindow: NSWindow {
/// Posted when a terminal window awakes from nib.
static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake")
/// Posted when a terminal window will close
static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose")
/// This is the key in UserDefaults to use for the default `level` value. This is /// This is the key in UserDefaults to use for the default `level` value. This is
/// used by the manual float on top menu item feature. /// used by the manual float on top menu item feature.
static let defaultLevelKey: String = "TerminalDefaultLevel" static let defaultLevelKey: String = "TerminalDefaultLevel"
@ -15,14 +21,47 @@ class TerminalWindow: NSWindow {
/// Reset split zoom button in titlebar /// Reset split zoom button in titlebar
private let resetZoomAccessory = NSTitlebarAccessoryViewController() private let resetZoomAccessory = NSTitlebarAccessoryViewController()
/// Update notification UI in titlebar
private let updateAccessory = NSTitlebarAccessoryViewController()
/// Visual indicator that mirrors the selected tab color.
private lazy var tabColorIndicator: NSHostingView<TabColorIndicatorView> = {
let view = NSHostingView(rootView: TabColorIndicatorView(tabColor: tabColor))
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
/// The configuration derived from the Ghostty config so we don't need to rely on references. /// The configuration derived from the Ghostty config so we don't need to rely on references.
private(set) var derivedConfig: DerivedConfig = .init() private(set) var derivedConfig: DerivedConfig = .init()
/// Sets up our tab context menu
private var tabMenuObserver: NSObjectProtocol? = nil
/// Whether this window supports the update accessory. If this is false, then views within this
/// window should determine how to show update notifications.
var supportsUpdateAccessory: Bool {
// Native window supports it.
true
}
/// Glass effect view for liquid glass background when transparency is enabled
private var glassEffectView: NSView?
/// Gets the terminal controller from the window controller. /// Gets the terminal controller from the window controller.
var terminalController: TerminalController? { var terminalController: TerminalController? {
windowController as? TerminalController windowController as? TerminalController
} }
/// The color assigned to this window's tab. Setting this updates the tab color indicator
/// and marks the window's restorable state as dirty.
var tabColor: TerminalTabColor = .none {
didSet {
guard tabColor != oldValue else { return }
tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor)
invalidateRestorableState()
}
}
// MARK: NSWindow Overrides // MARK: NSWindow Overrides
override var toolbar: NSToolbar? { override var toolbar: NSToolbar? {
@ -35,6 +74,20 @@ class TerminalWindow: NSWindow {
} }
override func awakeFromNib() { override func awakeFromNib() {
// Notify that this terminal window has loaded
NotificationCenter.default.post(name: Self.terminalDidAwake, object: self)
// This is fragile, but there doesn't seem to be an official API for customizing
// native tab bar menus.
tabMenuObserver = NotificationCenter.default.addObserver(
forName: Notification.Name(rawValue: "NSMenuWillOpenNotification"),
object: nil,
queue: .main
) { [weak self] n in
guard let self, let menu = n.object as? NSMenu else { return }
self.configureTabContextMenuIfNeeded(menu)
}
// This is required so that window restoration properly creates our tabs // This is required so that window restoration properly creates our tabs
// again. I'm not sure why this is required. If you don't do this, then // again. I'm not sure why this is required. If you don't do this, then
// tabs restore as separate windows. // tabs restore as separate windows.
@ -42,14 +95,14 @@ class TerminalWindow: NSWindow {
DispatchQueue.main.async { DispatchQueue.main.async {
self.tabbingMode = .automatic self.tabbingMode = .automatic
} }
// All new windows are based on the app config at the time of creation. // All new windows are based on the app config at the time of creation.
guard let appDelegate = NSApp.delegate as? AppDelegate else { return } guard let appDelegate = NSApp.delegate as? AppDelegate else { return }
let config = appDelegate.ghostty.config let config = appDelegate.ghostty.config
// Setup our initial config // Setup our initial config
derivedConfig = .init(config) derivedConfig = .init(config)
// If there is a hardcoded title in the configuration, we set that // If there is a hardcoded title in the configuration, we set that
// immediately. Future `set_title` apprt actions will override this // immediately. Future `set_title` apprt actions will override this
// if necessary but this ensures our window loads with the proper // if necessary but this ensures our window loads with the proper
@ -65,8 +118,7 @@ class TerminalWindow: NSWindow {
// fallback to original centering behavior // fallback to original centering behavior
setInitialWindowPosition( setInitialWindowPosition(
x: config.windowPositionX, x: config.windowPositionX,
y: config.windowPositionY, y: config.windowPositionY)
windowDecorations: config.windowDecorations)
// If our traffic buttons should be hidden, then hide them // If our traffic buttons should be hidden, then hide them
if config.macosWindowButtons == .hidden { if config.macosWindowButtons == .hidden {
@ -85,14 +137,32 @@ class TerminalWindow: NSWindow {
})) }))
addTitlebarAccessoryViewController(resetZoomAccessory) addTitlebarAccessoryViewController(resetZoomAccessory)
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
// Create update notification accessory
if supportsUpdateAccessory {
updateAccessory.layoutAttribute = .right
updateAccessory.view = NonDraggableHostingView(rootView: UpdateAccessoryView(
viewModel: viewModel,
model: appDelegate.updateViewModel
))
addTitlebarAccessoryViewController(updateAccessory)
updateAccessory.view.translatesAutoresizingMaskIntoConstraints = false
}
} }
// Setup the accessory view for tabs that shows our keyboard shortcuts, // Setup the accessory view for tabs that shows our keyboard shortcuts,
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
// where buttons were not clickable. // where buttons were not clickable.
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor)
let stackView = NSStackView()
stackView.orientation = .horizontal
stackView.setHuggingPriority(.defaultHigh, for: .horizontal) stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
stackView.spacing = 3 stackView.spacing = 4
stackView.alignment = .centerY
stackView.addArrangedSubview(tabColorIndicator)
stackView.addArrangedSubview(keyEquivalentLabel)
stackView.addArrangedSubview(resetZoomTabButton)
tab.accessoryView = stackView tab.accessoryView = stackView
// Get our saved level // Get our saved level
@ -104,6 +174,11 @@ class TerminalWindow: NSWindow {
override var canBecomeKey: Bool { return true } override var canBecomeKey: Bool { return true }
override var canBecomeMain: Bool { return true } override var canBecomeMain: Bool { return true }
override func close() {
NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
super.close()
}
override func becomeKey() { override func becomeKey() {
super.becomeKey() super.becomeKey()
resetZoomTabButton.contentTintColor = .controlAccentColor resetZoomTabButton.contentTintColor = .controlAccentColor
@ -124,6 +199,12 @@ class TerminalWindow: NSWindow {
} else { } else {
tabBarDidDisappear() tabBarDidDisappear()
} }
viewModel.isMainWindow = true
}
override func resignMain() {
super.resignMain()
viewModel.isMainWindow = false
} }
override func mergeAllWindows(_ sender: Any?) { override func mergeAllWindows(_ sender: Any?) {
@ -162,9 +243,35 @@ class TerminalWindow: NSWindow {
/// added. /// added.
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
func findTitlebarView() -> NSView? {
// Find our tab bar. If it doesn't exist we don't do anything.
//
// In normal window, `NSTabBar` typically appears as a subview of `NSTitlebarView` within `NSThemeFrame`.
// In fullscreen, the system creates a dedicated fullscreen window and the view hierarchy changes;
// in that case, the `titlebarView` is only accessible via a reference on `NSThemeFrame`.
// ref: https://github.com/mozilla-firefox/firefox/blob/054e2b072785984455b3b59acad9444ba1eeffb4/widget/cocoa/nsCocoaWindow.mm#L7205
guard let themeFrameView = contentView?.rootView else { return nil }
let titlebarView = if themeFrameView.responds(to: Selector(("titlebarView"))) {
themeFrameView.value(forKey: "titlebarView") as? NSView
} else {
NSView?.none
}
return titlebarView
}
func findTabBar() -> NSView? {
findTitlebarView()?.firstDescendant(withClassName: "NSTabBar")
}
/// Returns true if there is a tab bar visible on this window. /// Returns true if there is a tab bar visible on this window.
var hasTabBar: Bool { var hasTabBar: Bool {
contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil findTabBar() != nil
}
var hasMoreThanOneTabs: Bool {
/// accessing ``tabGroup?.windows`` here
/// will cause other edge cases, be careful
(tabbedWindows?.count ?? 0) > 1
} }
func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool {
@ -198,6 +305,9 @@ class TerminalWindow: NSWindow {
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) { if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
removeTitlebarAccessoryViewController(at: idx) removeTitlebarAccessoryViewController(at: idx)
} }
// We don't need to do this with the update accessory. I don't know why but
// everything works fine.
} }
private func tabBarDidDisappear() { private func tabBarDidDisappear() {
@ -260,7 +370,7 @@ class TerminalWindow: NSWindow {
button.isBordered = false button.isBordered = false
button.allowsExpansionToolTips = true button.allowsExpansionToolTips = true
button.toolTip = "Reset Zoom" button.toolTip = "Reset Zoom"
button.contentTintColor = .controlAccentColor button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor
button.state = .on button.state = .on
button.image = NSImage(named:"ResetZoom") button.image = NSImage(named:"ResetZoom")
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
@ -277,6 +387,12 @@ class TerminalWindow: NSWindow {
// Whenever we change the window title we must also update our // Whenever we change the window title we must also update our
// tab title if we're using custom fonts. // tab title if we're using custom fonts.
tab.attributedTitle = attributedTitle tab.attributedTitle = attributedTitle
/// We also needs to update this here, just in case
/// the value is not what we want
///
/// Check ``titlebarFont`` down below
/// to see why we need to check `hasMoreThanOneTabs` here
titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs
} }
} }
@ -286,6 +402,12 @@ class TerminalWindow: NSWindow {
let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize)
titlebarTextField?.font = font titlebarTextField?.font = font
/// We check `hasMoreThanOneTabs` here because the system
/// may copy this setting to the tabs text field at some point(e.g. entering/exiting fullscreen),
/// which can cause the title to be vertically misaligned (shifted downward).
///
/// This behaviour is the opposite of what happens in the title bars text field, which is quite odd...
titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs
tab.attributedTitle = attributedTitle tab.attributedTitle = attributedTitle
} }
} }
@ -338,6 +460,7 @@ class TerminalWindow: NSWindow {
// have no effect if the window is not visible. Ultimately, we'll have this called // have no effect if the window is not visible. Ultimately, we'll have this called
// at some point when a surface becomes focused. // at some point when a surface becomes focused.
guard isVisible else { return } guard isVisible else { return }
defer { updateColorSchemeForSurfaceTree() }
// Basic properties // Basic properties
appearance = surfaceConfig.windowAppearance appearance = surfaceConfig.windowAppearance
@ -356,7 +479,15 @@ class TerminalWindow: NSWindow {
// Terminal.app more easily. // Terminal.app more easily.
backgroundColor = .white.withAlphaComponent(0.001) backgroundColor = .white.withAlphaComponent(0.001)
if let appDelegate = NSApp.delegate as? AppDelegate { // Add liquid glass behind terminal content
if #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle {
setupGlassLayer()
} else if let appDelegate = NSApp.delegate as? AppDelegate {
// If we had a prior glass layer we should remove it
if #available(macOS 26.0, *) {
removeGlassLayer()
}
ghostty_set_window_background_blur( ghostty_set_window_background_blur(
appDelegate.ghostty.app, appDelegate.ghostty.app,
Unmanaged.passUnretained(self).toOpaque()) Unmanaged.passUnretained(self).toOpaque())
@ -364,6 +495,11 @@ class TerminalWindow: NSWindow {
} else { } else {
isOpaque = true isOpaque = true
// Remove liquid glass when not transparent
if #available(macOS 26.0, *) {
removeGlassLayer()
}
let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor)
self.backgroundColor = backgroundColor.withAlphaComponent(1) self.backgroundColor = backgroundColor.withAlphaComponent(1)
} }
@ -400,9 +536,13 @@ class TerminalWindow: NSWindow {
return derivedConfig.backgroundColor.withAlphaComponent(alpha) return derivedConfig.backgroundColor.withAlphaComponent(alpha)
} }
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { func updateColorSchemeForSurfaceTree() {
terminalController?.updateColorSchemeForSurfaceTree()
}
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
// If we don't have an X/Y then we try to use the previously saved window pos. // If we don't have an X/Y then we try to use the previously saved window pos.
guard let x, let y else { guard x != nil, y != nil else {
if (!LastWindowPosition.shared.restore(self)) { if (!LastWindowPosition.shared.restore(self)) {
center() center()
} }
@ -416,19 +556,14 @@ class TerminalWindow: NSWindow {
return return
} }
// Convert top-left coordinates to bottom-left origin using our utility extension // We have an X/Y, use our controller function to set it up.
let origin = screen.origin( guard let terminalController else {
fromTopLeftOffsetX: CGFloat(x), center()
offsetY: CGFloat(y), return
windowSize: frame.size) }
// Clamp the origin to ensure the window stays fully visible on screen let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen)
var safeOrigin = origin setFrameOrigin(frame.origin)
let vf = screen.visibleFrame
safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width)
safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height)
setFrameOrigin(safeOrigin)
} }
private func hideWindowButtons() { private func hideWindowButtons() {
@ -437,19 +572,75 @@ class TerminalWindow: NSWindow {
standardWindowButton(.zoomButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true
} }
deinit {
if let observer = tabMenuObserver {
NotificationCenter.default.removeObserver(observer)
}
}
#if compiler(>=6.2)
// MARK: Glass
@available(macOS 26.0, *)
private func setupGlassLayer() {
// Remove existing glass effect view
removeGlassLayer()
// Get the window content view (parent of the NSHostingView)
guard let contentView else { return }
guard let windowContentView = contentView.superview else { return }
// Create NSGlassEffectView for native glass effect
let effectView = NSGlassEffectView()
// Map Ghostty config to NSGlassEffectView style
switch derivedConfig.backgroundBlur {
case .macosGlassRegular:
effectView.style = NSGlassEffectView.Style.regular
case .macosGlassClear:
effectView.style = NSGlassEffectView.Style.clear
default:
// Should not reach here since we check for glass style before calling
// setupGlassLayer()
assertionFailure()
}
effectView.cornerRadius = derivedConfig.windowCornerRadius
effectView.tintColor = preferredBackgroundColor
effectView.frame = windowContentView.bounds
effectView.autoresizingMask = [.width, .height]
// Position BELOW the terminal content to act as background
windowContentView.addSubview(effectView, positioned: .below, relativeTo: contentView)
glassEffectView = effectView
}
@available(macOS 26.0, *)
private func removeGlassLayer() {
glassEffectView?.removeFromSuperview()
glassEffectView = nil
}
#endif // compiler(>=6.2)
// MARK: Config // MARK: Config
struct DerivedConfig { struct DerivedConfig {
let title: String? let title: String?
let backgroundBlur: Ghostty.Config.BackgroundBlur
let backgroundColor: NSColor let backgroundColor: NSColor
let backgroundOpacity: Double let backgroundOpacity: Double
let macosWindowButtons: Ghostty.MacOSWindowButtons let macosWindowButtons: Ghostty.MacOSWindowButtons
let macosTitlebarStyle: String
let windowCornerRadius: CGFloat
init() { init() {
self.title = nil self.title = nil
self.backgroundColor = NSColor.windowBackgroundColor self.backgroundColor = NSColor.windowBackgroundColor
self.backgroundOpacity = 1 self.backgroundOpacity = 1
self.macosWindowButtons = .visible self.macosWindowButtons = .visible
self.backgroundBlur = .disabled
self.macosTitlebarStyle = "transparent"
self.windowCornerRadius = 16
} }
init(_ config: Ghostty.Config) { init(_ config: Ghostty.Config) {
@ -457,6 +648,18 @@ class TerminalWindow: NSWindow {
self.backgroundColor = NSColor(config.backgroundColor) self.backgroundColor = NSColor(config.backgroundColor)
self.backgroundOpacity = config.backgroundOpacity self.backgroundOpacity = config.backgroundOpacity
self.macosWindowButtons = config.macosWindowButtons self.macosWindowButtons = config.macosWindowButtons
self.backgroundBlur = config.backgroundBlur
self.macosTitlebarStyle = config.macosTitlebarStyle
// Set corner radius based on macos-titlebar-style
// Native, transparent, and hidden styles use 16pt radius
// Tabs style uses 20pt radius
switch config.macosTitlebarStyle {
case "tabs":
self.windowCornerRadius = 20
default:
self.windowCornerRadius = 16
}
} }
} }
} }
@ -467,28 +670,28 @@ extension TerminalWindow {
class ViewModel: ObservableObject { class ViewModel: ObservableObject {
@Published var isSurfaceZoomed: Bool = false @Published var isSurfaceZoomed: Bool = false
@Published var hasToolbar: Bool = false @Published var hasToolbar: Bool = false
@Published var isMainWindow: Bool = true
/// Calculates the top padding based on toolbar visibility and macOS version
fileprivate var accessoryTopPadding: CGFloat {
if #available(macOS 26.0, *) {
return hasToolbar ? 10 : 5
} else {
return hasToolbar ? 9 : 4
}
}
} }
struct ResetZoomAccessoryView: View { struct ResetZoomAccessoryView: View {
@ObservedObject var viewModel: ViewModel @ObservedObject var viewModel: ViewModel
let action: () -> Void let action: () -> Void
// The padding from the top that the view appears. This was all just manually
// measured based on the OS.
var topPadding: CGFloat {
if #available(macOS 26.0, *) {
return viewModel.hasToolbar ? 10 : 5
} else {
return viewModel.hasToolbar ? 9 : 4
}
}
var body: some View { var body: some View {
if viewModel.isSurfaceZoomed { if viewModel.isSurfaceZoomed {
VStack { VStack {
Button(action: action) { Button(action: action) {
Image("ResetZoom") Image("ResetZoom")
.foregroundColor(.accentColor) .foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.help("Reset Split Zoom") .help("Reset Split Zoom")
@ -497,10 +700,141 @@ extension TerminalWindow {
} }
// With a toolbar, the window title is taller, so we need more padding // With a toolbar, the window title is taller, so we need more padding
// to properly align. // to properly align.
.padding(.top, topPadding) .padding(.top, viewModel.accessoryTopPadding)
// We always need space at the end of the titlebar // We always need space at the end of the titlebar
.padding(.trailing, 10) .padding(.trailing, 10)
} }
} }
} }
/// A pill-shaped button that displays update status and provides access to update actions.
struct UpdateAccessoryView: View {
@ObservedObject var viewModel: ViewModel
@ObservedObject var model: UpdateViewModel
var body: some View {
// We use the same top/trailing padding so that it hugs the same.
UpdatePill(model: model)
.padding(.top, viewModel.accessoryTopPadding)
.padding(.trailing, viewModel.accessoryTopPadding)
}
}
}
/// A small circle indicator displayed in the tab accessory view that shows
/// the user-assigned tab color. When no color is set, the view is hidden.
private struct TabColorIndicatorView: View {
/// The tab color to display.
let tabColor: TerminalTabColor
var body: some View {
if let color = tabColor.displayColor {
Circle()
.fill(Color(color))
.frame(width: 6, height: 6)
} else {
Circle()
.fill(Color.clear)
.frame(width: 6, height: 6)
.hidden()
}
}
}
// MARK: - Tab Context Menu
extension TerminalWindow {
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
private static let changeTitleMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.changeTitleMenuItem")
private static let tabColorSeparatorIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorSeparator")
private static let tabColorPaletteIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorPalette")
func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
guard isTabContextMenu(menu) else { return }
// Get the target from an existing menu item. The native tab context menu items
// target the specific window/controller that was right-clicked, not the focused one.
// We need to use that same target so validation and action use the correct tab.
let targetController = menu.items
.first { $0.action == NSSelectorFromString("performClose:") }
.flatMap { $0.target as? NSWindow }
.flatMap { $0.windowController as? TerminalController }
// Close tabs to the right
let item = NSMenuItem(title: "Close Tabs to the Right", action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "")
item.identifier = Self.closeTabsOnRightMenuItemIdentifier
item.target = targetController
item.setImageIfDesired(systemSymbolName: "xmark")
if menu.insertItem(item, after: NSSelectorFromString("performCloseOtherTabs:")) == nil,
menu.insertItem(item, after: NSSelectorFromString("performClose:")) == nil {
menu.addItem(item)
}
// Other close items should have the xmark to match Safari on macOS 26
for menuItem in menu.items {
if menuItem.action == NSSelectorFromString("performClose:") ||
menuItem.action == NSSelectorFromString("performCloseOtherTabs:") {
menuItem.setImageIfDesired(systemSymbolName: "xmark")
}
}
appendTabModifierSection(to: menu, target: targetController)
}
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
guard NSApp.keyWindow === self else { return false }
// These selectors must all exist for it to be a tab context menu.
let requiredSelectors: Set<String> = [
"performClose:",
"performCloseOtherTabs:",
"moveTabToNewWindow:",
"toggleTabOverview:"
]
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
return requiredSelectors.isSubset(of: selectorNames)
}
private func appendTabModifierSection(to menu: NSMenu, target: TerminalController?) {
menu.removeItems(withIdentifiers: [
Self.tabColorSeparatorIdentifier,
Self.changeTitleMenuItemIdentifier,
Self.tabColorPaletteIdentifier
])
let separator = NSMenuItem.separator()
separator.identifier = Self.tabColorSeparatorIdentifier
menu.addItem(separator)
// Change Title...
let changeTitleItem = NSMenuItem(title: "Change Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "")
changeTitleItem.identifier = Self.changeTitleMenuItemIdentifier
changeTitleItem.target = target
changeTitleItem.setImageIfDesired(systemSymbolName: "pencil.line")
menu.addItem(changeTitleItem)
let paletteItem = NSMenuItem()
paletteItem.identifier = Self.tabColorPaletteIdentifier
paletteItem.view = makeTabColorPaletteView(
selectedColor: (target?.window as? TerminalWindow)?.tabColor ?? .none
) { [weak target] color in
(target?.window as? TerminalWindow)?.tabColor = color
}
menu.addItem(paletteItem)
}
}
private func makeTabColorPaletteView(
selectedColor: TerminalTabColor,
selectionHandler: @escaping (TerminalTabColor) -> Void
) -> NSView {
let hostingView = NSHostingView(rootView: TabColorMenuView(
selectedColor: selectedColor,
onSelect: selectionHandler
))
hostingView.frame.size = hostingView.intrinsicContentSize
return hostingView
} }

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