Merge branch 'ghostty-org:main' into feat-list-themes-write-config
commit
e58bbc1d3e
|
|
@ -9,4 +9,6 @@ pkg/glfw/wayland-headers/** linguist-vendored
|
|||
pkg/libintl/config.h linguist-generated=true
|
||||
pkg/libintl/libintl.h linguist-generated=true
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
name: Milestone Update
|
||||
steps:
|
||||
- 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
|
||||
with:
|
||||
action: bind-pr # `bind-pr` is the default action
|
||||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
|
||||
# Bind milestone to closed issue that has a merged PR fix
|
||||
- name: Set Milestone for Issue
|
||||
uses: hustcer/milestone-action@b57a7e52e9913b6b0cdefb10add762af0398659d # v2.9
|
||||
uses: hustcer/milestone-action@dcd6c3742acc1846929c054251c64cccd555a00d # v3.0
|
||||
if: github.event.issue.state == 'closed'
|
||||
with:
|
||||
action: bind-issue
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
on: [push, pull_request]
|
||||
name: Nix
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name != 'main' && github.ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Nix"
|
||||
|
|
@ -34,15 +39,15 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
|
||||
echo "Version is valid: ${{ github.event.inputs.version }}"
|
||||
|
||||
- name: Exract the Version
|
||||
- name: Extract the Version
|
||||
id: extract_version
|
||||
run: |
|
||||
VERSION=${{ github.event.inputs.version }}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
|
@ -80,16 +80,16 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
path: |-
|
||||
|
|
@ -132,7 +132,7 @@ jobs:
|
|||
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
with:
|
||||
|
|
@ -269,7 +269,7 @@ jobs:
|
|||
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: macos
|
||||
path: |-
|
||||
|
|
@ -286,7 +286,7 @@ jobs:
|
|||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
|
|
@ -306,10 +306,10 @@ jobs:
|
|||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ jobs:
|
|||
mv appcast_new.xml appcast.xml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: sparkle
|
||||
path: |-
|
||||
|
|
@ -357,17 +357,17 @@ jobs:
|
|||
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
- name: Download Sparkle Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: sparkle
|
||||
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ jobs:
|
|||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -30,11 +29,11 @@ jobs:
|
|||
commit: ${{ steps.extract_build_info.outputs.commit }}
|
||||
commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -67,7 +66,7 @@ jobs:
|
|||
needs: [setup, build-macos]
|
||||
if: needs.setup.outputs.should_skip != 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Tip Tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
|
|
@ -82,7 +81,7 @@ jobs:
|
|||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
|
|
@ -105,7 +104,7 @@ jobs:
|
|||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
|
|
@ -128,7 +127,7 @@ jobs:
|
|||
env:
|
||||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install sentry-cli
|
||||
run: |
|
||||
|
|
@ -151,7 +150,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -161,14 +159,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- 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
|
||||
|
||||
- name: Update Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -206,7 +204,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -220,7 +217,7 @@ jobs:
|
|||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
|
@ -359,7 +356,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -373,7 +370,6 @@ jobs:
|
|||
# Create our appcast for Sparkle
|
||||
- name: Generate Appcast
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
env:
|
||||
|
|
@ -408,7 +404,6 @@ jobs:
|
|||
# gets out of sync with the binaries.
|
||||
- name: Prep R2 Storage for Appcast
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
run: |
|
||||
|
|
@ -418,7 +413,6 @@ jobs:
|
|||
|
||||
- name: Upload Appcast to R2
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
|
|
@ -444,7 +438,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -458,7 +451,7 @@ jobs:
|
|||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
|
@ -590,7 +583,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
@ -629,7 +622,6 @@ jobs:
|
|||
(
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.repository_owner == 'ghostty-org' &&
|
||||
github.ref_name == 'main'
|
||||
)
|
||||
|
|
@ -643,7 +635,7 @@ jobs:
|
|||
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
|
|
@ -775,7 +767,7 @@ jobs:
|
|||
|
||||
# Update Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
name: 'Ghostty Tip ("Nightly")'
|
||||
prerelease: true
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
run-id: ${{ inputs.source-run-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
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ on:
|
|||
|
||||
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:
|
||||
required:
|
||||
name: "Required Checks: Test"
|
||||
|
|
@ -19,7 +24,7 @@ jobs:
|
|||
- build-linux-libghostty
|
||||
- build-nix
|
||||
- build-macos
|
||||
- build-macos-matrix
|
||||
- build-macos-freetype
|
||||
- build-snap
|
||||
- build-windows
|
||||
- test
|
||||
|
|
@ -39,7 +44,7 @@ jobs:
|
|||
- test-debian-13
|
||||
- valgrind
|
||||
- zig-fmt
|
||||
- flatpak
|
||||
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
|
|
@ -69,17 +74,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -94,7 +99,16 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
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 }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
@ -103,17 +117,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -136,17 +150,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -170,17 +184,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -204,6 +218,7 @@ jobs:
|
|||
aarch64-linux,
|
||||
x86_64-linux,
|
||||
x86_64-windows,
|
||||
wasm32-freestanding,
|
||||
]
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
|
|
@ -212,17 +227,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -248,17 +263,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -277,17 +292,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -310,17 +325,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -356,17 +371,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -382,7 +397,7 @@ jobs:
|
|||
|
||||
- name: Upload artifact
|
||||
id: upload-artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
path: |-
|
||||
|
|
@ -394,7 +409,7 @@ jobs:
|
|||
needs: [build-dist, build-snap]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Trigger Snap workflow
|
||||
run: |
|
||||
|
|
@ -406,12 +421,30 @@ jobs:
|
|||
env:
|
||||
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:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
|
@ -449,12 +482,12 @@ jobs:
|
|||
cd macos
|
||||
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
|
||||
|
||||
build-macos-matrix:
|
||||
build-macos-freetype:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
|
@ -478,18 +511,10 @@ jobs:
|
|||
- name: Test All
|
||||
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=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
|
||||
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=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:
|
||||
runs-on: windows-2022
|
||||
|
|
@ -499,7 +524,7 @@ jobs:
|
|||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# This could be from a script if we wanted to but inlining here for now
|
||||
# in one place.
|
||||
|
|
@ -508,9 +533,9 @@ jobs:
|
|||
- name: Install zig
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Get the zig version from build.zig so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig" -Raw
|
||||
$pattern = 'buildpkg\.requireZig\("(.*?)"\);'
|
||||
# Get the zig version from build.zig.zon so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig.zon" -Raw
|
||||
$pattern = 'minimum_zig_version\s*=\s*"([^"]+)"'
|
||||
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
||||
$version = "zig-x86_64-windows-$zigVersion"
|
||||
Write-Output $version
|
||||
|
|
@ -563,22 +588,29 @@ jobs:
|
|||
test:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-md
|
||||
outputs:
|
||||
zig_version: ${{ steps.zig.outputs.version }}
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Get required Zig version
|
||||
id: zig
|
||||
run: |
|
||||
echo "version=$(sed -n -E 's/^\s*\.?minimum_zig_version\s*=\s*"([^"]+)".*/\1/p' build.zig.zon)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -610,17 +642,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -658,17 +690,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -693,17 +725,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -720,7 +752,7 @@ jobs:
|
|||
needs: test
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
|
||||
- uses: DeterminateSystems/nix-installer-action@main
|
||||
|
|
@ -757,17 +789,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -787,14 +819,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -815,14 +847,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -842,14 +874,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -869,14 +901,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -896,14 +928,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -923,14 +955,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -957,14 +989,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -984,14 +1016,14 @@ jobs:
|
|||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1018,17 +1050,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1050,10 +1082,10 @@ jobs:
|
|||
uses: namespacelabs/nscloud-setup@d1c625762f7c926a54bd39252efff0705fd11c64 # v0.0.10
|
||||
|
||||
- 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
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
|
||||
|
|
@ -1070,32 +1102,6 @@ jobs:
|
|||
build-args: |
|
||||
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:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-lg
|
||||
|
|
@ -1106,17 +1112,17 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1133,57 +1139,62 @@ jobs:
|
|||
run: |
|
||||
nix develop -c zig build test-valgrind
|
||||
|
||||
build-freebsd:
|
||||
name: Build on FreeBSD
|
||||
needs: test
|
||||
runs-on: namespace-profile-mitchellh-sm-systemd
|
||||
if: false # FIXME: FreeBSD does not yet ship with Zig 0.15
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
- "14.3"
|
||||
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
|
||||
steps:
|
||||
- name: Checkout Ghostty
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Start SSH
|
||||
run: |
|
||||
sudo systemctl start ssh
|
||||
|
||||
- name: Set up FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@487ce35b96fae3e60d45b521735f5aa436ecfade # v1.2.4
|
||||
with:
|
||||
release: ${{ matrix.release }}
|
||||
copyback: false
|
||||
usesh: true
|
||||
prepare: |
|
||||
pkg install -y \
|
||||
devel/blueprint-compiler \
|
||||
devel/gettext \
|
||||
devel/git \
|
||||
devel/pkgconf \
|
||||
graphics/wayland \
|
||||
lang/zig \
|
||||
security/ca_root_nss \
|
||||
textproc/hs-pandoc \
|
||||
x11-fonts/jetbrains-mono \
|
||||
x11-toolkits/libadwaita \
|
||||
x11-toolkits/gtk40 \
|
||||
x11-toolkits/gtk4-layer-shell
|
||||
|
||||
run: |
|
||||
zig env
|
||||
|
||||
- name: Run tests
|
||||
shell: freebsd {0}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
zig build test
|
||||
|
||||
- name: Build GTK app runtime
|
||||
shell: freebsd {0}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
zig build
|
||||
./zig-out/bin/ghostty +version
|
||||
# build-freebsd:
|
||||
# name: Build on FreeBSD
|
||||
# needs: test
|
||||
# runs-on: namespace-profile-mitchellh-sm-systemd
|
||||
# strategy:
|
||||
# matrix:
|
||||
# release:
|
||||
# - "14.3"
|
||||
# - "15.0"
|
||||
# timeout-minutes: 10
|
||||
# steps:
|
||||
# - name: Checkout Ghostty
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
#
|
||||
# - name: Start SSH
|
||||
# run: |
|
||||
# sudo systemctl start ssh
|
||||
#
|
||||
# - name: Set up FreeBSD VM
|
||||
# uses: vmactions/freebsd-vm@487ce35b96fae3e60d45b521735f5aa436ecfade # v1.2.4
|
||||
# with:
|
||||
# release: ${{ matrix.release }}
|
||||
# copyback: false
|
||||
# usesh: true
|
||||
# prepare: |
|
||||
# pkg install -y \
|
||||
# devel/blueprint-compiler \
|
||||
# devel/gettext \
|
||||
# devel/git \
|
||||
# devel/pkgconf \
|
||||
# ftp/curl \
|
||||
# graphics/wayland \
|
||||
# security/ca_root_nss \
|
||||
# textproc/hs-pandoc \
|
||||
# x11-fonts/jetbrains-mono \
|
||||
# x11-toolkits/libadwaita \
|
||||
# x11-toolkits/gtk40 \
|
||||
# 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" && \
|
||||
# mkdir /opt && \
|
||||
# tar -xf /tmp/zig.tar.xz -C /opt && \
|
||||
# rm /tmp/zig.tar.xz && \
|
||||
# ln -s "/opt/zig-x86_64-freebsd-${{ needs.test.outputs.zig_version }}/zig" /usr/local/bin/zig
|
||||
#
|
||||
# run: |
|
||||
# zig env
|
||||
#
|
||||
# - name: Run tests
|
||||
# shell: freebsd {0}
|
||||
# run: |
|
||||
# cd $GITHUB_WORKSPACE
|
||||
# zig build test
|
||||
#
|
||||
# - name: Build GTK app runtime
|
||||
# shell: freebsd {0}
|
||||
# run: |
|
||||
# cd $GITHUB_WORKSPACE
|
||||
# zig build
|
||||
# ./zig-out/bin/ghostty +version
|
||||
|
|
|
|||
|
|
@ -17,19 +17,19 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@7baedde84bbf5063413d621f282834bc2654d0c1 # v1.2.18
|
||||
uses: namespacelabs/nscloud-cache-action@446d8f390563cd54ca27e8de5bdb816f63c0b706 # v1.2.21
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@9280e7aca88deada44c930f1e2c78e21c3ae3edd # v31.7.0
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -37,16 +37,33 @@ jobs:
|
|||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Run zig fetch
|
||||
id: zig_fetch
|
||||
- name: Download colorschemes
|
||||
id: download
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Get the latest release from iTerm2-Color-Schemes
|
||||
RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest)
|
||||
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 "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
|
||||
run: |
|
||||
|
|
@ -62,7 +79,7 @@ jobs:
|
|||
run: nix build .#ghostty
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
|
||||
with:
|
||||
title: Update iTerm2 colorschemes
|
||||
base: main
|
||||
|
|
@ -75,5 +92,5 @@ jobs:
|
|||
build.zig.zon.json
|
||||
flatpak/zig-packages.json
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ zig-cache/
|
|||
.zig-cache/
|
||||
zig-out/
|
||||
/result*
|
||||
/.nixos-test-history
|
||||
example/*.wasm
|
||||
test/ghostty
|
||||
test/cases/**/*.actual.png
|
||||
|
|
|
|||
13
AGENTS.md
13
AGENTS.md
|
|
@ -13,11 +13,22 @@ A file for [guiding coding agents](https://agents.md/).
|
|||
## Directory Structure
|
||||
|
||||
- Shared Zig core: `src/`
|
||||
- C API: `include/ghostty.h`
|
||||
- C API: `include`
|
||||
- macOS app: `macos/`
|
||||
- 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
|
||||
|
||||
- Do not use `xcodebuild`
|
||||
- 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`
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@
|
|||
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR
|
||||
/po/he_IL.UTF-8.po @ghostty-org/he_IL
|
||||
/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/hr_HR.UTF-8.po @ghostty-org/hr_HR
|
||||
|
||||
|
|
|
|||
397
CONTRIBUTING.md
397
CONTRIBUTING.md
|
|
@ -17,15 +17,62 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
|
|||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Ghostty,
|
||||
> it must be disclosed in the pull request.
|
||||
> The Ghostty project allows AI-**assisted** _code contributions_, which
|
||||
> must be properly disclosed in the pull request.
|
||||
|
||||
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
|
||||
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,
|
||||
so long as it is limited to single keywords or short phrases.
|
||||
|
||||
The submitter must have also tested the pull request on all impacted
|
||||
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:
|
||||
|
||||
|
|
@ -36,6 +83,11 @@ Or a more detailed disclosure:
|
|||
> I consulted ChatGPT to understand the codebase but the solution
|
||||
> 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
|
||||
on the other end of the pull request, but it also makes it difficult to
|
||||
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
|
||||
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.
|
||||
|
||||
## Quick Guide
|
||||
|
|
@ -74,22 +121,47 @@ submission.
|
|||
|
||||
### I have a bug! / Something isn't working
|
||||
|
||||
1. Search the issue tracker and discussions for similar issues. Tip: also
|
||||
search for [closed issues] and [discussions] — your issue might have already
|
||||
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
|
||||
maintainers to figure out important details about your setup. Because of
|
||||
this, please make sure that you _only_ use the "Issue Triage" category for
|
||||
reporting bugs — thank you!
|
||||
First, search the issue tracker and discussions for similar issues. Tip: also
|
||||
search for [closed issues] and [discussions] — your issue might have already
|
||||
been fixed!
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> If there is an _open_ issue or discussion that matches your problem,
|
||||
> **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
|
||||
[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
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -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
|
||||
[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
|
||||
`#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
|
||||
[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
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> 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.
|
||||
|
|
|
|||
68
Doxyfile
68
Doxyfile
|
|
@ -2,9 +2,52 @@
|
|||
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = "libghostty"
|
||||
INPUT = include/ghostty/vt.h
|
||||
PROJECT_LOGO = images/gnome/64.png
|
||||
INPUT = include/ghostty
|
||||
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
|
||||
|
|
@ -12,6 +55,26 @@ RECURSIVE = NO
|
|||
|
||||
GENERATE_HTML = YES
|
||||
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
|
||||
|
|
@ -20,6 +83,7 @@ HTML_OUTPUT = zig-out/share/ghostty/doc/libghostty
|
|||
GENERATE_MAN = YES
|
||||
MAN_OUTPUT = zig-out/share/man
|
||||
MAN_EXTENSION = .3
|
||||
MAN_LINKS = YES
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Other Output
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
40
HACKING.md
40
HACKING.md
|
|
@ -50,24 +50,22 @@ macOS users don't require any additional dependencies.
|
|||
## Xcode Version and SDKs
|
||||
|
||||
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
|
||||
installed or selected. Use the `xcode-select` command to
|
||||
ensure that the correct version of Xcode is selected:
|
||||
|
||||
```shell-session
|
||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
||||
sudo xcode-select --switch /Applications/Xcode.app
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Main branch development of Ghostty is preparing for the next major
|
||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
||||
> **Xcode 26 and the macOS 26 SDK**.
|
||||
> Main branch development of Ghostty requires **Xcode 26 and the macOS 26 SDK**.
|
||||
>
|
||||
> 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
|
||||
|
||||
|
|
@ -95,6 +93,36 @@ produced.
|
|||
> 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.
|
||||
|
||||
## 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
|
||||
|
||||
### Prettier
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -22,7 +22,7 @@ vendor/glad/include/glad/glad.h: vendor/glad/include/glad/gl.h
|
|||
|
||||
clean:
|
||||
rm -rf \
|
||||
zig-out zig-cache \
|
||||
zig-out .zig-cache \
|
||||
macos/build \
|
||||
macos/GhosttyKit.xcframework
|
||||
.PHONY: clean
|
||||
|
|
|
|||
21
README.md
21
README.md
|
|
@ -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
|
||||
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
|
||||
within Swift. The Swift app links to `libghostty` and uses the C API to
|
||||
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
|
||||
|
||||
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
|
||||
> 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
|
||||
> depending when the crash occurred.
|
||||
> depending on when the crash occurred.
|
||||
|
|
|
|||
41
build.zig
41
build.zig
|
|
@ -3,19 +3,19 @@ const assert = std.debug.assert;
|
|||
const builtin = @import("builtin");
|
||||
const buildpkg = @import("src/build/main.zig");
|
||||
|
||||
const appVersion = @import("build.zig.zon").version;
|
||||
const minimumZigVersion = @import("build.zig.zon").minimum_zig_version;
|
||||
|
||||
comptime {
|
||||
buildpkg.requireZig("0.15.1");
|
||||
buildpkg.requireZig(minimumZigVersion);
|
||||
}
|
||||
|
||||
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
|
||||
// want to know what options are available, you can run `--help` or
|
||||
// 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 u8,
|
||||
"test-filter",
|
||||
|
|
@ -56,7 +56,7 @@ pub fn build(b: *std.Build) !void {
|
|||
);
|
||||
|
||||
// Ghostty resources like terminfo, shell integration, themes, etc.
|
||||
const resources = try buildpkg.GhosttyResources.init(b, &config);
|
||||
const resources = try buildpkg.GhosttyResources.init(b, &config, &deps);
|
||||
const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
|
||||
|
||||
// Ghostty executable, the actual runnable Ghostty program.
|
||||
|
|
@ -102,10 +102,19 @@ pub fn build(b: *std.Build) !void {
|
|||
);
|
||||
|
||||
// libghostty-vt
|
||||
const libghostty_vt_shared = try buildpkg.GhosttyLibVt.initShared(
|
||||
b,
|
||||
&mod,
|
||||
);
|
||||
const libghostty_vt_shared = shared: {
|
||||
if (config.target.result.cpu.arch.isWasm()) {
|
||||
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(b.getInstallStep());
|
||||
|
||||
|
|
@ -309,13 +318,3 @@ pub fn build(b: *std.Build) !void {
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
.{
|
||||
.name = .ghostty,
|
||||
.version = "1.2.1",
|
||||
.version = "1.3.0-dev",
|
||||
.paths = .{""},
|
||||
.fingerprint = 0x64407a2a0b4147e5,
|
||||
.minimum_zig_version = "0.15.1",
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
|
||||
|
|
@ -15,14 +15,14 @@
|
|||
},
|
||||
.vaxis = .{
|
||||
// rockorager/libvaxis
|
||||
.url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz",
|
||||
.hash = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ",
|
||||
.url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
.lazy = true,
|
||||
},
|
||||
.z2d = .{
|
||||
// vancluever/z2d
|
||||
.url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz",
|
||||
.hash = "z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef",
|
||||
.url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
|
|
@ -38,9 +38,9 @@
|
|||
.lazy = true,
|
||||
},
|
||||
.uucode = .{
|
||||
// TODO: currently the use-llvm branch because its broken on self-hosted
|
||||
.url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
.hash = "uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT",
|
||||
// jacobsandlund/uucode
|
||||
.url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
// codeberg ifreund/zig-wayland
|
||||
|
|
@ -50,15 +50,15 @@
|
|||
},
|
||||
.zf = .{
|
||||
// natecraddock/zf
|
||||
.url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR",
|
||||
.url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
.lazy = true,
|
||||
},
|
||||
.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.
|
||||
.url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
.url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
|
|
@ -116,8 +116,8 @@
|
|||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz",
|
||||
.hash = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
|
||||
.hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -24,10 +24,10 @@
|
|||
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
|
||||
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
|
||||
},
|
||||
"gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV": {
|
||||
"gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": {
|
||||
"name": "gobject",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"hash": "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo="
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
"hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="
|
||||
},
|
||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||
"name": "gtk4_layer_shell",
|
||||
|
|
@ -49,10 +49,10 @@
|
|||
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
||||
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
||||
},
|
||||
"N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv": {
|
||||
"N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz",
|
||||
"hash": "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ="
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
|
||||
"hash": "sha256-5QePBQlSsz9W2r4zTS3QD+cDAeyObhR51E2AkJ3ZIUk="
|
||||
},
|
||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||
"name": "jetbrains_mono",
|
||||
|
|
@ -109,20 +109,20 @@
|
|||
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
|
||||
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
|
||||
},
|
||||
"uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT": {
|
||||
"uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM": {
|
||||
"name": "uucode",
|
||||
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
"hash": "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc="
|
||||
"url": "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
|
||||
"hash": "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8="
|
||||
},
|
||||
"vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ": {
|
||||
"name": "vaxis",
|
||||
"url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz",
|
||||
"hash": "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI="
|
||||
"uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": {
|
||||
"name": "uucode",
|
||||
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="
|
||||
},
|
||||
"vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA": {
|
||||
"vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
|
||||
"name": "vaxis",
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz",
|
||||
"hash": "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM="
|
||||
"url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="
|
||||
},
|
||||
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
|
||||
"name": "wayland",
|
||||
|
|
@ -139,25 +139,15 @@
|
|||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
||||
},
|
||||
"z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef": {
|
||||
"z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": {
|
||||
"name": "z2d",
|
||||
"url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz",
|
||||
"hash": "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg="
|
||||
"url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
"hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="
|
||||
},
|
||||
"zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR": {
|
||||
"zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {
|
||||
"name": "zf",
|
||||
"url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz",
|
||||
"hash": "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ="
|
||||
},
|
||||
"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="
|
||||
"url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="
|
||||
},
|
||||
"zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": {
|
||||
"name": "zig_js",
|
||||
|
|
@ -174,11 +164,6 @@
|
|||
"url": "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||
"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": {
|
||||
"name": "zigimg",
|
||||
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
fetchurl,
|
||||
fetchgit,
|
||||
runCommandLocal,
|
||||
zig_0_14,
|
||||
zig_0_15,
|
||||
name ? "zig-packages",
|
||||
}: let
|
||||
unpackZigArtifact = {
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
}:
|
||||
runCommandLocal name
|
||||
{
|
||||
nativeBuildInputs = [zig_0_14];
|
||||
nativeBuildInputs = [zig_0_15];
|
||||
}
|
||||
''
|
||||
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 {
|
||||
name = "gobject";
|
||||
url = "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst";
|
||||
hash = "sha256-SXiqGm81aUn6yq1wFXgNTAULdKOHS/Rzkp5OgNkkcXo=";
|
||||
url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst";
|
||||
hash = "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -163,11 +163,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv";
|
||||
name = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz";
|
||||
hash = "sha256-mdhUxAAqKxRRXwED2laabUo9ZZqZa/MZAsO0+Y9L7uQ=";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz";
|
||||
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 {
|
||||
name = "uucode";
|
||||
url = "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz";
|
||||
hash = "sha256-VomSYOF8fRJwb/8GtVG/QqR6c95zSkQt4649C/4KXAc=";
|
||||
url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732";
|
||||
hash = "sha256-sHPh+TQSdUGus/QTbj7KSJJkTuNTrK4VNmQDjS30Lf8=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ";
|
||||
name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E";
|
||||
path = fetchZigArtifact {
|
||||
name = "vaxis";
|
||||
url = "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz";
|
||||
hash = "sha256-7H5a0J7uUsrzlO7JNAf/Ussi9WxvmsbyJSmhqvl+rqI=";
|
||||
name = "uucode";
|
||||
url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
|
||||
hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA";
|
||||
name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS";
|
||||
path = fetchZigArtifact {
|
||||
name = "vaxis";
|
||||
url = "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz";
|
||||
hash = "sha256-eq5YC26OY0i2cdQJ0ZXMZ+o2vHQLEFNNGzQt5Zuz4BM=";
|
||||
url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
|
||||
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 {
|
||||
name = "z2d";
|
||||
url = "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz";
|
||||
hash = "sha256-5/qRZAIh1U42v7jql9W0jr2zzQZtu39DxJPLVrSybJg=";
|
||||
url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz";
|
||||
hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR";
|
||||
name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh";
|
||||
path = fetchZigArtifact {
|
||||
name = "zf";
|
||||
url = "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz";
|
||||
hash = "sha256-8BinbanSfZeBA8SBAopVxwJObN36/BTpxVHABKicsMQ=";
|
||||
};
|
||||
}
|
||||
{
|
||||
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=";
|
||||
url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
|
||||
hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -362,14 +346,6 @@ in
|
|||
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";
|
||||
path = fetchZigArtifact {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
git+https://codeberg.org/ivanstepanovftw/zg#4fe689e56ce2ed5a8f59308b471bccd7da89fac9
|
||||
git+https://github.com/ivanstepanovftw/zigimg#aa4c31db872612c39edbb79f753b3cd9a79fe726
|
||||
https://codeberg.org/chaten/zg/archive/749197a3f9d25e211615960c02380a3d659b20f9.tar.gz
|
||||
git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732
|
||||
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/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/gettext-0.24.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/harfbuzz-11.0.0.tar.xz
|
||||
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.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/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.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/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.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/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz
|
||||
https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.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-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.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/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz
|
||||
https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.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_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
|
||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||
https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -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 <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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"> $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 -->
|
||||
|
|
@ -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 = "";
|
||||
}
|
||||
});
|
||||
})();
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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/
|
||||
```
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
.{
|
||||
.name = .zig_formatter,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x578de530797eafe6,
|
||||
.dependencies = .{
|
||||
.ghostty = .{ .path = "../../" },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
|
|
@ -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});
|
||||
}
|
||||
59
flake.lock
59
flake.lock
|
|
@ -3,11 +3,11 @@
|
|||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1747046372,
|
||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -34,36 +34,45 @@
|
|||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 315532800,
|
||||
"narHash": "sha256-YwoXN6fthkakCFD7nXPcUK+rkNr6ZTNTuF8zdGaxZo0=",
|
||||
"rev": "dc704e6102e76aad573f63b74c742cd96f8f1e6c",
|
||||
"lastModified": 1763191728,
|
||||
"narHash": "sha256-gI9PpaoX4/f28HkjcTbFVpFhtOxSDtOEdFaHZrdETe0=",
|
||||
"rev": "1d4c88323ac36805d09657d13a5273aea1b34f0c",
|
||||
"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": {
|
||||
"type": "tarball",
|
||||
"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": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zig": "zig",
|
||||
"zon2nix": "zon2nix"
|
||||
|
|
@ -97,11 +106,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1759192380,
|
||||
"narHash": "sha256-0BWJgt4OSzxCESij5oo8WLWrPZ+1qLp8KUQe32QeV4Q=",
|
||||
"lastModified": 1763295135,
|
||||
"narHash": "sha256-sGv/NHCmEnJivguGwB5w8LRmVqr1P72OjS+NzcJsssE=",
|
||||
"owner": "mitchellh",
|
||||
"repo": "zig-overlay",
|
||||
"rev": "0bcd1401ed43d10f10cbded49624206553e92f57",
|
||||
"rev": "64f8b42cfc615b2cf99144adf2b7728c7847c72a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -112,7 +121,9 @@
|
|||
},
|
||||
"zon2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1758405547,
|
||||
|
|
|
|||
47
flake.nix
47
flake.nix
|
|
@ -6,7 +6,9 @@
|
|||
# glibc versions used by our dependencies from Nix are compatible with the
|
||||
# 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";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
|
|
@ -28,10 +30,14 @@
|
|||
zon2nix = {
|
||||
url = "github:jcollie/zon2nix?rev=bf983aa90ff169372b9fa8c02e57ea75e0b42245";
|
||||
inputs = {
|
||||
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch
|
||||
# we are using for "normal" builds.
|
||||
#
|
||||
# nixpkgs.follows = "nixpkgs";
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager?ref=release-25.05";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -41,6 +47,7 @@
|
|||
nixpkgs,
|
||||
zig,
|
||||
zon2nix,
|
||||
home-manager,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs.lib.recursiveUpdate {} (
|
||||
|
|
@ -48,10 +55,18 @@
|
|||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.15.1";
|
||||
wraptest = pkgs.callPackage ./nix/wraptest.nix {};
|
||||
devShells.${system}.default = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.15.2";
|
||||
wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
|
||||
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
|
||||
|
|
@ -72,6 +87,10 @@
|
|||
|
||||
formatter.${system} = pkgs.alejandra;
|
||||
|
||||
checks.${system} = import ./nix/tests.nix {
|
||||
inherit home-manager nixpkgs self system;
|
||||
};
|
||||
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
|
|
@ -88,6 +107,9 @@
|
|||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
meta = {
|
||||
description = "start a vm from ${toString module}";
|
||||
};
|
||||
}
|
||||
);
|
||||
in {
|
||||
|
|
@ -107,17 +129,12 @@
|
|||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-releasefast;
|
||||
ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-releasefast;
|
||||
};
|
||||
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 = {
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ modules:
|
|||
- chmod a+x /app/zig/zig
|
||||
sources:
|
||||
- type: archive
|
||||
sha256: c61c5da6edeea14ca51ecd5e4520c6f4189ef5250383db33d01848293bfafe05
|
||||
url: https://ziglang.org/download/0.15.1/zig-x86_64-linux-0.15.1.tar.xz
|
||||
sha256: 02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239
|
||||
url: https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz
|
||||
only-arches: [x86_64]
|
||||
- type: archive
|
||||
sha256: bb4a8d2ad735e7fba764c497ddf4243cb129fece4148da3222a7046d3f1f19fe
|
||||
url: https://ziglang.org/download/0.15.1/zig-aarch64-linux-0.15.1.tar.xz
|
||||
sha256: 958ed7d1e00d0ea76590d27666efbf7a932281b3d7ba0c6b01b0ff26498f667f
|
||||
url: https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz
|
||||
only-arches: [aarch64]
|
||||
|
||||
- name: bzip2-redirect
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-09-20-20-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQCqJhDL0KnF_X7M4L7o7JePsJBbrYpEr7UV",
|
||||
"sha256": "4978aa1a6f356949facaad7015780d4c050b74a3874bf473929e4e80d924717a"
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
"sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/iterm2_themes-release-20250922-150534-d28055b.tgz",
|
||||
"dest": "vendor/p/N-V-__8AACEnAwCkyrJ_XqdomYlR8bpuh0l8WDidEz-BAzIv",
|
||||
"sha256": "99d854c4002a2b14515f0103da569a6d4a3d659a996bf31902c3b4f98f4beee4"
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
|
||||
"dest": "vendor/p/N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
|
||||
"sha256": "e5078f050952b33f56dabe334d2dd00fe70301ec8e6e1479d44d80909dd92149"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -132,22 +132,22 @@
|
|||
"sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/uucode-f81f8ef8518b8ec5a7fca30ec5fdbc76cc6197df.tar.gz",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPjQHQADuCy1VMWftjrMl3iWqgMpUugWVQJG6_7xT",
|
||||
"sha256": "56899260e17c7d12706fff06b551bf42a47a73de734a442de3ae3d0bfe0a5c07"
|
||||
"type": "git",
|
||||
"url": "https://github.com/jacobsandlund/uucode",
|
||||
"commit": "5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/vaxis-9fc9015d5f147568e18c5e7ca28f15bf8b293760.tar.gz",
|
||||
"dest": "vendor/p/vaxis-0.5.1-BWNV_O8fCQAeUeVrESVc-2BdXloEXkFqReDJL7Q6XTSZ",
|
||||
"sha256": "ec7e5ad09eee52caf394eec93407ff52cb22f56c6f9ac6f22529a1aaf97eaea2"
|
||||
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
|
||||
"sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/1bf887aa7e3736bad69fd4e277a378946edb0f2a.tar.gz",
|
||||
"dest": "vendor/p/vaxis-0.5.1-BWNV_H0PCQAeMusmtLzh9P9xO2IW242GZ2IRe9iKYhcA",
|
||||
"sha256": "7aae580b6e8e6348b671d409d195cc67ea36bc740b10534d1b342de59bb3e013"
|
||||
"url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
"sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -169,27 +169,15 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/z2d-a1237f6881d99b75abd8a20a934e62e34b44a005.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.8.2-pre-j5P_HlVRFgCsBTQ3EgUoKbYHx5JMnyH1mHsOSPiafnef",
|
||||
"sha256": "e7fa91640221d54e36bfb8ea97d5b48ebdb3cd066dbb7f43c493cb56b4b26c98"
|
||||
"url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
"sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://deps.files.ghostty.org/zf-52ad2e5528ab754f77437edf08a07b5ec843661c.tar.gz",
|
||||
"dest": "vendor/p/zf-0.10.3-OIRy8QGJAACJcu3tCGtfbJnnd3Y4QL7OW_X8PJ8u_ASR",
|
||||
"sha256": "f018a76da9d27d978103c481028a55c7024e6cddfafc14e9c551c004a89cb0c4"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
"url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
"sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -209,12 +197,6 @@
|
|||
"dest": "vendor/p/wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||
"sha256": "4f146b735ed0d527f520e3bf71d3e93f72c3d0fa583ae8edd3a4851f7079124e"
|
||||
},
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/ivanstepanovftw/zigimg",
|
||||
"commit": "aa4c31db872612c39edbb79f753b3cd9a79fe726",
|
||||
"dest": "vendor/p/zigimg-0.1.0-8_eo2mWmEgBoqdr0sH9O5GTqDHthkoEPM5_tipcBRreL"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ typedef enum {
|
|||
GHOSTTY_CLIPBOARD_SELECTION,
|
||||
} ghostty_clipboard_e;
|
||||
|
||||
typedef struct {
|
||||
const char *mime;
|
||||
const char *data;
|
||||
} ghostty_clipboard_content_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_CLIPBOARD_REQUEST_PASTE,
|
||||
GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ,
|
||||
|
|
@ -507,6 +512,12 @@ typedef enum {
|
|||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} 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
|
||||
typedef enum {
|
||||
GHOSTTY_RESIZE_SPLIT_UP,
|
||||
|
|
@ -568,6 +579,12 @@ typedef enum {
|
|||
GHOSTTY_QUIT_TIMER_STOP,
|
||||
} ghostty_action_quit_timer_e;
|
||||
|
||||
// apprt.action.Readonly
|
||||
typedef enum {
|
||||
GHOSTTY_READONLY_OFF,
|
||||
GHOSTTY_READONLY_ON,
|
||||
} ghostty_action_readonly_e;
|
||||
|
||||
// apprt.action.DesktopNotification.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
|
|
@ -579,6 +596,12 @@ typedef struct {
|
|||
const char* title;
|
||||
} 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
|
||||
typedef struct {
|
||||
const char* pwd;
|
||||
|
|
@ -695,6 +718,7 @@ typedef struct {
|
|||
typedef enum {
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_UNKNOWN,
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_TEXT,
|
||||
GHOSTTY_ACTION_OPEN_URL_KIND_HTML,
|
||||
} ghostty_action_open_url_kind_e;
|
||||
|
||||
// apprt.action.OpenUrl.C
|
||||
|
|
@ -708,6 +732,7 @@ typedef struct {
|
|||
typedef enum {
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
|
||||
GHOSTTY_ACTION_CLOSE_TAB_MODE_RIGHT,
|
||||
} ghostty_action_close_tab_mode_e;
|
||||
|
||||
// apprt.surface.Message.ChildExited
|
||||
|
|
@ -741,6 +766,28 @@ typedef struct {
|
|||
uint64_t duration;
|
||||
} ghostty_action_command_finished_s;
|
||||
|
||||
// apprt.action.StartSearch.C
|
||||
typedef struct {
|
||||
const char* needle;
|
||||
} ghostty_action_start_search_s;
|
||||
|
||||
// apprt.action.SearchTotal
|
||||
typedef struct {
|
||||
ssize_t total;
|
||||
} ghostty_action_search_total_s;
|
||||
|
||||
// apprt.action.SearchSelected
|
||||
typedef struct {
|
||||
ssize_t selected;
|
||||
} ghostty_action_search_selected_s;
|
||||
|
||||
// terminal.Scrollbar
|
||||
typedef struct {
|
||||
uint64_t total;
|
||||
uint64_t offset;
|
||||
uint64_t len;
|
||||
} ghostty_action_scrollbar_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
|
|
@ -759,6 +806,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_MOVE_TAB,
|
||||
GHOSTTY_ACTION_GOTO_TAB,
|
||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||
GHOSTTY_ACTION_GOTO_WINDOW,
|
||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||
GHOSTTY_ACTION_EQUALIZE_SPLITS,
|
||||
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
|
||||
|
|
@ -767,6 +815,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_RESET_WINDOW_SIZE,
|
||||
GHOSTTY_ACTION_INITIAL_SIZE,
|
||||
GHOSTTY_ACTION_CELL_SIZE,
|
||||
GHOSTTY_ACTION_SCROLLBAR,
|
||||
GHOSTTY_ACTION_RENDER,
|
||||
GHOSTTY_ACTION_INSPECTOR,
|
||||
GHOSTTY_ACTION_SHOW_GTK_INSPECTOR,
|
||||
|
|
@ -797,7 +846,12 @@ typedef enum {
|
|||
GHOSTTY_ACTION_PROGRESS_REPORT,
|
||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||
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 {
|
||||
ghostty_action_split_direction_e new_split;
|
||||
|
|
@ -805,13 +859,16 @@ typedef union {
|
|||
ghostty_action_move_tab_s move_tab;
|
||||
ghostty_action_goto_tab_e goto_tab;
|
||||
ghostty_action_goto_split_e goto_split;
|
||||
ghostty_action_goto_window_e goto_window;
|
||||
ghostty_action_resize_split_s resize_split;
|
||||
ghostty_action_size_limit_s size_limit;
|
||||
ghostty_action_initial_size_s initial_size;
|
||||
ghostty_action_cell_size_s cell_size;
|
||||
ghostty_action_scrollbar_s scrollbar;
|
||||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_prompt_title_e prompt_title;
|
||||
ghostty_action_pwd_s pwd;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||
|
|
@ -829,6 +886,10 @@ typedef union {
|
|||
ghostty_surface_message_childexited_s child_exited;
|
||||
ghostty_action_progress_report_s progress_report;
|
||||
ghostty_action_command_finished_s command_finished;
|
||||
ghostty_action_start_search_s start_search;
|
||||
ghostty_action_search_total_s search_total;
|
||||
ghostty_action_search_selected_s search_selected;
|
||||
ghostty_action_readonly_e readonly;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -846,8 +907,9 @@ typedef void (*ghostty_runtime_confirm_read_clipboard_cb)(
|
|||
void*,
|
||||
ghostty_clipboard_request_e);
|
||||
typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
|
||||
const char*,
|
||||
ghostty_clipboard_e,
|
||||
const ghostty_clipboard_content_s*,
|
||||
size_t,
|
||||
bool);
|
||||
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
|
||||
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* @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
|
||||
* 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,
|
||||
* extracted from the [Ghostty](https://ghostty.org) terminal emulator.
|
||||
*
|
||||
* libghostty-vt contains the logic for handling the core parts of a terminal
|
||||
* emulator: parsing terminal escape sequences and maintaining terminal state.
|
||||
* It can handle scrollback, line wrapping, reflow on resize, and more.
|
||||
* emulator: parsing terminal escape sequences, maintaining terminal state,
|
||||
* 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.
|
||||
* Breaking changes are expected in future versions. Use with caution in production code.
|
||||
|
|
@ -27,9 +28,41 @@
|
|||
* @section groups_sec API Reference
|
||||
*
|
||||
* 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 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 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
|
||||
|
|
@ -39,414 +72,13 @@
|
|||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Types
|
||||
|
||||
/**
|
||||
* 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
|
||||
#include <ghostty/vt/result.h>
|
||||
#include <ghostty/vt/allocator.h>
|
||||
#include <ghostty/vt/osc.h>
|
||||
#include <ghostty/vt/sgr.h>
|
||||
#include <ghostty/vt/key.h>
|
||||
#include <ghostty/vt/paste.h>
|
||||
#include <ghostty/vt/wasm.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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 */
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
<key>NSMenuItem</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>New Ghostty Tab Here</string>
|
||||
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Tab Here</string>
|
||||
</dict>
|
||||
<key>NSMessage</key>
|
||||
<string>openTab</string>
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
<key>NSMenuItem</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>New Ghostty Window Here</string>
|
||||
<string>New $(INFOPLIST_KEY_CFBundleDisplayName) Window Here</string>
|
||||
</dict>
|
||||
<key>NSMessage</key>
|
||||
<string>openWindow</string>
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
Features/QuickTerminal/QuickTerminalController.swift,
|
||||
Features/QuickTerminal/QuickTerminalPosition.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreen.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreenStateCache.swift,
|
||||
Features/QuickTerminal/QuickTerminalSize.swift,
|
||||
Features/QuickTerminal/QuickTerminalSpaceBehavior.swift,
|
||||
Features/QuickTerminal/QuickTerminalWindow.swift,
|
||||
|
|
@ -114,6 +115,7 @@
|
|||
Features/Terminal/ErrorView.swift,
|
||||
Features/Terminal/TerminalController.swift,
|
||||
Features/Terminal/TerminalRestorable.swift,
|
||||
Features/Terminal/TerminalTabColor.swift,
|
||||
Features/Terminal/TerminalView.swift,
|
||||
"Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift",
|
||||
"Features/Terminal/Window Styles/Terminal.xib",
|
||||
|
|
@ -125,7 +127,14 @@
|
|||
"Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift",
|
||||
"Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift",
|
||||
"Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift",
|
||||
Features/Update/UpdateBadge.swift,
|
||||
Features/Update/UpdateController.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/Ghostty.Command.swift,
|
||||
Ghostty/Ghostty.Error.swift,
|
||||
|
|
@ -134,6 +143,7 @@
|
|||
Ghostty/Ghostty.Surface.swift,
|
||||
Ghostty/InspectorView.swift,
|
||||
"Ghostty/NSEvent+Extension.swift",
|
||||
Ghostty/SurfaceScrollView.swift,
|
||||
Ghostty/SurfaceView_AppKit.swift,
|
||||
Helpers/AppInfo.swift,
|
||||
Helpers/CodableBridge.swift,
|
||||
|
|
@ -147,6 +157,7 @@
|
|||
"Helpers/Extensions/NSAppearance+Extension.swift",
|
||||
"Helpers/Extensions/NSApplication+Extension.swift",
|
||||
"Helpers/Extensions/NSImage+Extension.swift",
|
||||
"Helpers/Extensions/NSMenu+Extension.swift",
|
||||
"Helpers/Extensions/NSMenuItem+Extension.swift",
|
||||
"Helpers/Extensions/NSPasteboard+Extension.swift",
|
||||
"Helpers/Extensions/NSScreen+Extension.swift",
|
||||
|
|
@ -160,6 +171,7 @@
|
|||
Helpers/KeyboardLayout.swift,
|
||||
Helpers/LastWindowPosition.swift,
|
||||
Helpers/MetalView.swift,
|
||||
Helpers/NonDraggableHostingView.swift,
|
||||
Helpers/PermissionRequest.swift,
|
||||
Helpers/Private/CGS.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_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
@ -767,7 +780,7 @@
|
|||
EXECUTABLE_NAME = ghostty;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Ghostty[DEBUG]";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
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.";
|
||||
|
|
@ -785,6 +798,7 @@
|
|||
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_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
@ -839,6 +853,7 @@
|
|||
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_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
INFOPLIST_PREPROCESS = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sparkle-project/Sparkle",
|
||||
"state" : {
|
||||
"revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d",
|
||||
"version" : "2.7.3"
|
||||
"revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
import OSLog
|
||||
import Sparkle
|
||||
|
|
@ -43,6 +44,11 @@ class AppDelegate: NSObject,
|
|||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
@IBOutlet private var menuPasteSelection: NSMenuItem?
|
||||
@IBOutlet private var menuSelectAll: NSMenuItem?
|
||||
@IBOutlet private var menuFindParent: NSMenuItem?
|
||||
@IBOutlet private var menuFind: NSMenuItem?
|
||||
@IBOutlet private var menuFindNext: NSMenuItem?
|
||||
@IBOutlet private var menuFindPrevious: NSMenuItem?
|
||||
@IBOutlet private var menuHideFindBar: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuToggleVisibility: NSMenuItem?
|
||||
@IBOutlet private var menuToggleFullScreen: NSMenuItem?
|
||||
|
|
@ -62,6 +68,8 @@ class AppDelegate: NSObject,
|
|||
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTitle: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTabTitle: NSMenuItem?
|
||||
@IBOutlet private var menuReadonly: NSMenuItem?
|
||||
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||
@IBOutlet private var menuCommandPalette: NSMenuItem?
|
||||
|
|
@ -98,8 +106,10 @@ class AppDelegate: NSObject,
|
|||
)
|
||||
|
||||
/// Manages updates
|
||||
let updaterController: SPUStandardUpdaterController
|
||||
let updaterDelegate: UpdaterDelegate = UpdaterDelegate()
|
||||
let updateController = UpdateController()
|
||||
var updateViewModel: UpdateViewModel {
|
||||
updateController.viewModel
|
||||
}
|
||||
|
||||
/// The elapsed time since the process was started
|
||||
var timeSinceLaunch: TimeInterval {
|
||||
|
|
@ -107,7 +117,7 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
/// 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.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
|
@ -116,25 +126,9 @@ class AppDelegate: NSObject,
|
|||
private var signals: [DispatchSourceSignal] = []
|
||||
|
||||
/// The custom app icon image that is currently in use.
|
||||
@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)
|
||||
}
|
||||
}
|
||||
@Published private(set) var appIcon: NSImage? = nil
|
||||
|
||||
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()
|
||||
|
||||
ghostty.delegate = self
|
||||
|
|
@ -179,7 +173,7 @@ class AppDelegate: NSObject,
|
|||
ghosttyConfigDidChange(config: ghostty.config)
|
||||
|
||||
// Start our update checker.
|
||||
updaterController.startUpdater()
|
||||
updateController.startUpdater()
|
||||
|
||||
// Register our service provider. This must happen after everything is initialized.
|
||||
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) {
|
||||
// If we're back manually then clear the hidden state because macOS handles it.
|
||||
self.hiddenState = nil
|
||||
|
|
@ -323,6 +322,12 @@ class AppDelegate: NSObject,
|
|||
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
|
||||
let windows = NSApplication.shared.windows
|
||||
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
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -533,8 +543,9 @@ class AppDelegate: NSObject,
|
|||
self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller")
|
||||
self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection")
|
||||
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
|
||||
self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
|
||||
self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill")
|
||||
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
|
||||
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
|
||||
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.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line")
|
||||
self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.filled.on.square")
|
||||
self.menuFindParent?.setImageIfDesired(systemSymbolName: "text.page.badge.magnifyingglass")
|
||||
}
|
||||
|
||||
/// Sync all of our menu item keyboard shortcuts with the Ghostty configuration.
|
||||
|
|
@ -578,6 +590,9 @@ class AppDelegate: NSObject,
|
|||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
||||
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||
syncMenuShortcut(config, action: "start_search", menuItem: self.menuFind)
|
||||
syncMenuShortcut(config, action: "search:next", menuItem: self.menuFindNext)
|
||||
syncMenuShortcut(config, action: "search:previous", menuItem: self.menuFindPrevious)
|
||||
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
syncMenuShortcut(config, action: "goto_split:previous", menuItem: self.menuPreviousSplit)
|
||||
|
|
@ -597,6 +612,7 @@ class AppDelegate: NSObject,
|
|||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
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_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
|
||||
|
|
@ -714,6 +730,10 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
@objc private func ghosttyBellDidRing(_ notification: Notification) {
|
||||
if (ghostty.config.bellFeatures.contains(.system)) {
|
||||
NSSound.beep()
|
||||
}
|
||||
|
||||
if (ghostty.config.bellFeatures.contains(.attention)) {
|
||||
// Bounce the dock icon if we're not focused.
|
||||
NSApp.requestUserAttention(.informationalRequest)
|
||||
|
|
@ -806,13 +826,21 @@ class AppDelegate: NSObject,
|
|||
// defined by our "auto-update" configuration (if set) or fall back to Sparkle
|
||||
// user-based defaults.
|
||||
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool == false {
|
||||
updaterController.updater.automaticallyChecksForUpdates = false
|
||||
updaterController.updater.automaticallyDownloadsUpdates = false
|
||||
updateController.updater.automaticallyChecksForUpdates = false
|
||||
updateController.updater.automaticallyDownloadsUpdates = false
|
||||
} else if let autoUpdate = config.autoUpdate {
|
||||
updaterController.updater.automaticallyChecksForUpdates =
|
||||
updateController.updater.automaticallyChecksForUpdates =
|
||||
autoUpdate == .check || autoUpdate == .download
|
||||
updaterController.updater.automaticallyDownloadsUpdates =
|
||||
updateController.updater.automaticallyDownloadsUpdates =
|
||||
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
|
||||
|
|
@ -860,49 +888,64 @@ class AppDelegate: NSObject,
|
|||
} else {
|
||||
GlobalEventTap.shared.disable()
|
||||
}
|
||||
Task {
|
||||
await updateAppIcon(from: config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the appearance of our app with the theme specified in the config.
|
||||
private func syncAppearance(config: Ghostty.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) {
|
||||
case .official:
|
||||
self.appIcon = nil
|
||||
// Discard saved icon name
|
||||
appIconName = nil
|
||||
break
|
||||
|
||||
case .blueprint:
|
||||
self.appIcon = NSImage(named: "BlueprintImage")!
|
||||
appIcon = NSImage(named: "BlueprintImage")!
|
||||
|
||||
case .chalkboard:
|
||||
self.appIcon = NSImage(named: "ChalkboardImage")!
|
||||
appIcon = NSImage(named: "ChalkboardImage")!
|
||||
|
||||
case .glass:
|
||||
self.appIcon = NSImage(named: "GlassImage")!
|
||||
appIcon = NSImage(named: "GlassImage")!
|
||||
|
||||
case .holographic:
|
||||
self.appIcon = NSImage(named: "HolographicImage")!
|
||||
appIcon = NSImage(named: "HolographicImage")!
|
||||
|
||||
case .microchip:
|
||||
self.appIcon = NSImage(named: "MicrochipImage")!
|
||||
appIcon = NSImage(named: "MicrochipImage")!
|
||||
|
||||
case .paper:
|
||||
self.appIcon = NSImage(named: "PaperImage")!
|
||||
appIcon = NSImage(named: "PaperImage")!
|
||||
|
||||
case .retro:
|
||||
self.appIcon = NSImage(named: "RetroImage")!
|
||||
appIcon = NSImage(named: "RetroImage")!
|
||||
|
||||
case .xray:
|
||||
self.appIcon = NSImage(named: "XrayImage")!
|
||||
appIcon = NSImage(named: "XrayImage")!
|
||||
|
||||
case .custom:
|
||||
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
|
||||
self.appIcon = userIcon
|
||||
appIcon = userIcon
|
||||
appIconName = config.macosCustomIcon
|
||||
} 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:
|
||||
// Discard saved icon name
|
||||
// if no valid colours were found
|
||||
appIconName = nil
|
||||
guard let ghostColor = config.macosIconGhostColor else { break }
|
||||
guard let screenColors = config.macosIconScreenColor else { break }
|
||||
guard let icon = ColorizedGhosttyIcon(
|
||||
|
|
@ -910,8 +953,38 @@ class AppDelegate: NSObject,
|
|||
ghostColor: ghostColor,
|
||||
frame: config.macosIconFrame
|
||||
).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
|
||||
|
|
@ -1004,7 +1077,8 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
@IBAction func checkForUpdates(_ sender: Any?) {
|
||||
updaterController.checkForUpdates(sender)
|
||||
updateController.checkForUpdates()
|
||||
//UpdateSimulator.happyPath.simulate(with: updateViewModel)
|
||||
}
|
||||
|
||||
@IBAction func newWindow(_ sender: Any?) {
|
||||
|
|
@ -1012,7 +1086,10 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
|
||||
@IBAction func newTab(_ sender: Any?) {
|
||||
_ = TerminalController.newTab(ghostty)
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: TerminalController.preferredParent?.window
|
||||
)
|
||||
}
|
||||
|
||||
@IBAction func closeAllWindows(_ sender: Any?) {
|
||||
|
|
@ -1046,8 +1123,6 @@ class AppDelegate: NSObject,
|
|||
guard let keyWindow = NSApp.keyWindow,
|
||||
!keyWindow.styleMask.contains(.fullScreen) else { return }
|
||||
|
||||
// Keep track of our hidden state to restore properly
|
||||
self.hiddenState = .init()
|
||||
NSApp.hide(nil)
|
||||
return
|
||||
}
|
||||
|
|
@ -1096,11 +1171,11 @@ class AppDelegate: NSObject,
|
|||
}
|
||||
}
|
||||
|
||||
private struct ToggleVisibilityState {
|
||||
struct ToggleVisibilityState {
|
||||
let hiddenWindows: [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
|
||||
// right window if it was hidden.
|
||||
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.
|
||||
//
|
||||
// We also ignore fullscreen windows because they don't hide anyways.
|
||||
self.hiddenWindows = NSApp.windows.filter {
|
||||
var visibleWindows = [Weak<NSWindow>]()
|
||||
NSApp.windows.filter {
|
||||
$0.isVisible &&
|
||||
!$0.styleMask.contains(.fullScreen)
|
||||
}.map { Weak($0) }
|
||||
}.forEach { window in
|
||||
// We only keep track of selectedWindow if it's in a tabGroup,
|
||||
// so we can keep its selection state when restoring
|
||||
let windowToHide = window.tabGroup?.selectedWindow ?? window
|
||||
if !visibleWindows.contains(where: { $0.value === windowToHide }) {
|
||||
visibleWindows.append(Weak(windowToHide))
|
||||
}
|
||||
}
|
||||
self.hiddenWindows = visibleWindows
|
||||
}
|
||||
|
||||
func restore() {
|
||||
|
|
@ -1188,3 +1272,8 @@ extension AppDelegate: NSMenuItemValidation {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@globalActor
|
||||
fileprivate actor AppIconActor: GlobalActor {
|
||||
static let shared = AppIconActor()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24412" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24412"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
<connections>
|
||||
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
|
||||
<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="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||
<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="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||
<outlet property="menuEqualizeSplits" destination="3gH-VD-vL9" id="SiZ-ce-FOF"/>
|
||||
<outlet property="menuFind" destination="nwE-0w-30h" id="idg-Nc-apE"/>
|
||||
<outlet property="menuFindNext" destination="XqU-X8-q32" id="vNh-AH-6gZ"/>
|
||||
<outlet property="menuFindParent" destination="cE3-Bt-FcH" id="2dc-ok-hgH"/>
|
||||
<outlet property="menuFindPrevious" destination="1hd-2Z-wVm" id="sSo-wO-2MW"/>
|
||||
<outlet property="menuFloatOnTop" destination="uRj-7z-1Nh" id="94n-o9-Jol"/>
|
||||
<outlet property="menuHideFindBar" destination="xzC-AG-HAc" id="HCo-o6-VWv"/>
|
||||
<outlet property="menuIncreaseFontSize" destination="CIH-ey-Z6x" id="hkc-9C-80E"/>
|
||||
<outlet property="menuMoveSplitDividerDown" destination="Zj7-2W-fdF" id="997-LL-nlN"/>
|
||||
<outlet property="menuMoveSplitDividerLeft" destination="wSR-ny-j1a" id="HCZ-CI-2ob"/>
|
||||
|
|
@ -41,6 +47,7 @@
|
|||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
|
||||
<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="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
|
||||
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
|
||||
|
|
@ -245,6 +252,39 @@
|
|||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VYS-RG-uZD"/>
|
||||
<menuItem title="Find" id="cE3-Bt-FcH">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Find" id="vPo-Sd-cTP">
|
||||
<items>
|
||||
<menuItem title="Find..." id="nwE-0w-30h">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="find:" target="-1" id="PeY-3u-IxC"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Next" id="XqU-X8-q32">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="findNext:" target="-1" id="Dka-ng-aSs"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Find Previous" id="1hd-2Z-wVm">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="findPrevious:" target="-1" id="Zvs-bs-ZR4"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="KlV-2C-wYr"/>
|
||||
<menuItem title="Hide Find Bar" id="xzC-AG-HAc">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="findHide:" target="-1" id="hGP-K9-yN9"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Xbz-ms-irt"/>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
|
|
@ -277,12 +317,24 @@
|
|||
<action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<connections>
|
||||
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
|
||||
</connections>
|
||||
</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 title="Quick Terminal" id="1pv-LF-NBJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ struct CloseTerminalIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ struct CommandPaletteIntent: AppIntent {
|
|||
)
|
||||
var command: CommandEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ struct FocusTerminalIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ struct GetTerminalDetailsIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Get \(\.$detail) from \(\.$terminal)")
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ struct InputTextIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -74,8 +76,10 @@ struct KeyEventIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -136,8 +140,10 @@ struct MouseButtonIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -197,8 +203,10 @@ struct MousePosIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
@ -265,8 +273,10 @@ struct MouseScrollIntent: AppIntent {
|
|||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,10 @@ struct KeybindIntent: AppIntent {
|
|||
)
|
||||
var action: String
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,10 @@ struct NewTerminalIntent: AppIntent {
|
|||
|
||||
// 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
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
|
||||
static var openAppWhenRun = false
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ struct QuickTerminalIntent: AppIntent {
|
|||
static var title: LocalizedStringResource = "Open the Quick Terminal"
|
||||
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
|
||||
|
||||
#if compiler(>=6.2)
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
|
||||
|
|
|
|||
|
|
@ -45,19 +45,16 @@ struct ClipboardConfirmationView: View {
|
|||
.font(.system(size: 42))
|
||||
.padding()
|
||||
.frame(alignment: .center)
|
||||
|
||||
|
||||
Text(request.text())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding()
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Text(contents)
|
||||
.textSelection(.enabled)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.padding(.all, 4)
|
||||
}
|
||||
|
||||
|
||||
TextEditor(text: .constant(contents))
|
||||
.focusable(false)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(Action.text(.cancel, request)) { onCancel() }
|
||||
|
|
|
|||
|
|
@ -5,7 +5,28 @@ struct CommandOption: Identifiable, Hashable {
|
|||
let title: String
|
||||
let description: String?
|
||||
let symbols: [String]?
|
||||
let leadingIcon: String?
|
||||
let badge: String?
|
||||
let emphasis: Bool
|
||||
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 {
|
||||
lhs.id == rhs.id
|
||||
|
|
@ -23,6 +44,7 @@ struct CommandPaletteView: View {
|
|||
@State private var query = ""
|
||||
@State private var selectedIndex: UInt?
|
||||
@State private var hoveredOptionID: UUID?
|
||||
@FocusState private var isTextFieldFocused: Bool
|
||||
|
||||
// The options that we should show, taking into account any filtering from
|
||||
// the query.
|
||||
|
|
@ -51,7 +73,7 @@ struct CommandPaletteView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
CommandPaletteQuery(query: $query) { event in
|
||||
CommandPaletteQuery(query: $query, isTextFieldFocused: _isTextFieldFocused) { event in
|
||||
switch (event) {
|
||||
case .exit:
|
||||
isPresented = false
|
||||
|
|
@ -123,6 +145,28 @@ struct CommandPaletteView: View {
|
|||
.shadow(radius: 32, x: 0, y: 12)
|
||||
.padding()
|
||||
.environment(\.colorScheme, scheme)
|
||||
.onChange(of: isPresented) { newValue in
|
||||
// Reset focus when quickly showing and hiding.
|
||||
// macOS will destroy this view after a while,
|
||||
// so task/onAppear will not be called again.
|
||||
// If you toggle it rather quickly, we reset
|
||||
// it here when dismissing.
|
||||
isTextFieldFocused = newValue
|
||||
if !isPresented {
|
||||
// This is optional, since most of the time
|
||||
// there will be a delay before the next use.
|
||||
// To keep behavior the same as before, we reset it.
|
||||
query = ""
|
||||
}
|
||||
}
|
||||
.task {
|
||||
// Grab focus on the first appearance.
|
||||
// This happens right after onAppear,
|
||||
// so we don’t 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
|
||||
@FocusState private var isTextFieldFocused: Bool
|
||||
|
||||
init(query: Binding<String>, isTextFieldFocused: FocusState<Bool>, onEvent: ((KeyboardEvent) -> Void)? = nil) {
|
||||
_query = query
|
||||
self.onEvent = onEvent
|
||||
_isTextFieldFocused = isTextFieldFocused
|
||||
}
|
||||
|
||||
enum KeyboardEvent {
|
||||
case exit
|
||||
case submit
|
||||
|
|
@ -164,14 +214,6 @@ fileprivate struct CommandPaletteQuery: View {
|
|||
.frame(height: 48)
|
||||
.textFieldStyle(.plain)
|
||||
.focused($isTextFieldFocused)
|
||||
.onAppear {
|
||||
// We want to grab focus on appearance. We have to do this after a tick
|
||||
// on macOS Tahoe otherwise this doesn't work. See:
|
||||
// https://github.com/ghostty-org/ghostty/issues/8497
|
||||
DispatchQueue.main.async {
|
||||
isTextFieldFocused = true
|
||||
}
|
||||
}
|
||||
.onChange(of: isTextFieldFocused) { focused in
|
||||
if !focused {
|
||||
onEvent?(.exit)
|
||||
|
|
@ -198,7 +240,7 @@ fileprivate struct CommandTable: View {
|
|||
} else {
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(Array(options.enumerated()), id: \.1.id) { index, option in
|
||||
CommandRow(
|
||||
option: option,
|
||||
|
|
@ -240,15 +282,36 @@ fileprivate struct CommandRow: View {
|
|||
|
||||
var body: some View {
|
||||
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)
|
||||
.fontWeight(option.emphasis ? .medium : .regular)
|
||||
|
||||
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 {
|
||||
ShortcutSymbolsView(symbols: symbols)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
isSelected
|
||||
? Color.accentColor.opacity(0.2)
|
||||
|
|
@ -256,6 +319,10 @@ fileprivate struct CommandRow: View {
|
|||
? Color.secondary.opacity(0.2)
|
||||
: Color.clear)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5)
|
||||
.strokeBorder(Color.accentColor.opacity(option.emphasis && !isSelected ? 0.3 : 0), lineWidth: 1.5)
|
||||
)
|
||||
.cornerRadius(5)
|
||||
}
|
||||
.help(option.description ?? "")
|
||||
|
|
|
|||
|
|
@ -11,15 +11,54 @@ struct TerminalCommandPaletteView: View {
|
|||
|
||||
/// The configuration so we can lookup keyboard shortcuts.
|
||||
@ObservedObject var ghosttyConfig: Ghostty.Config
|
||||
|
||||
/// The update view model for showing update commands.
|
||||
var updateViewModel: UpdateViewModel?
|
||||
|
||||
/// The callback when an action is submitted.
|
||||
var onAction: ((String) -> Void)
|
||||
|
||||
// The commands available to the command palette.
|
||||
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 {
|
||||
return try surface.commands().map { c in
|
||||
let terminalCommands = try surface.commands().map { c in
|
||||
return CommandOption(
|
||||
title: c.title,
|
||||
description: c.description,
|
||||
|
|
@ -28,9 +67,12 @@ struct TerminalCommandPaletteView: View {
|
|||
onAction(c.action)
|
||||
}
|
||||
}
|
||||
options.append(contentsOf: terminalCommands)
|
||||
} catch {
|
||||
return []
|
||||
return options
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -48,19 +90,19 @@ struct TerminalCommandPaletteView: View {
|
|||
backgroundColor: ghosttyConfig.backgroundColor,
|
||||
options: commandOptions
|
||||
)
|
||||
.transition(
|
||||
.move(edge: .top)
|
||||
.combined(with: .opacity)
|
||||
.animation(.spring(response: 0.4, dampingFraction: 0.8))
|
||||
) // Spring animation
|
||||
.zIndex(1) // Ensure it's on top
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .top)
|
||||
}
|
||||
.transition(
|
||||
.move(edge: .top)
|
||||
.combined(with: .opacity)
|
||||
)
|
||||
}
|
||||
}
|
||||
.animation(.spring(response: 0.4, dampingFraction: 0.8), value: isPresented)
|
||||
.onChange(of: isPresented) { newValue in
|
||||
// When the command palette disappears we need to send focus back to the
|
||||
// surface view we were overlaid on top of. There's probably a better way
|
||||
|
|
|
|||
|
|
@ -21,13 +21,8 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// The active space when the quick terminal was last shown.
|
||||
private var previousActiveSpace: CGSSpace? = nil
|
||||
|
||||
/// The saved state when the quick terminal's surface tree becomes empty.
|
||||
///
|
||||
/// 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>
|
||||
/// Cache for per-screen window state.
|
||||
private let screenStateCache = QuickTerminalScreenStateCache()
|
||||
|
||||
/// Non-nil if we have hidden dock state.
|
||||
private var hiddenDock: HiddenDock? = nil
|
||||
|
|
@ -37,7 +32,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
|
||||
/// Tracks if we're currently handling a manual resize to prevent recursion
|
||||
private var isHandlingResize: Bool = false
|
||||
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
|
|
@ -45,10 +40,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
) {
|
||||
self.position = position
|
||||
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
|
||||
// that we don't start a terminal process. This gets started when the
|
||||
|
|
@ -351,7 +342,10 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// animate out.
|
||||
if surfaceTree.isEmpty,
|
||||
let ghostty_app = ghostty.app {
|
||||
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil)
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1"
|
||||
|
||||
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
surfaceTree = SplitTree(view: view)
|
||||
focusedSurface = view
|
||||
}
|
||||
|
|
@ -379,17 +373,15 @@ class QuickTerminalController: BaseTerminalController {
|
|||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Grab our last closed frame to use, and clear our state since we're animating in.
|
||||
// We only use the last closed frame if we're opening on the same screen.
|
||||
let lastClosedFrame: NSRect? = lastClosedFrames.object(forKey: screen)?.frame
|
||||
lastClosedFrames.removeObject(forKey: screen)
|
||||
// Grab our last closed frame to use from the cache.
|
||||
let closedFrame = screenStateCache.frame(for: screen)
|
||||
|
||||
// Move our window off screen to the initial animation position.
|
||||
position.setInitial(
|
||||
in: window,
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
closedFrame: closedFrame)
|
||||
|
||||
// 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
|
||||
|
|
@ -424,7 +416,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
in: window.animator(),
|
||||
on: screen,
|
||||
terminalSize: derivedConfig.quickTerminalSize,
|
||||
closedFrame: lastClosedFrame)
|
||||
closedFrame: closedFrame)
|
||||
}, completionHandler: {
|
||||
// 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.
|
||||
|
|
@ -513,7 +505,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
// would reset the window to its minimum content size.
|
||||
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.
|
||||
|
|
@ -524,6 +516,10 @@ class QuickTerminalController: BaseTerminalController {
|
|||
if !window.isOnActiveSpace {
|
||||
self.previousApp = nil
|
||||
window.orderOut(self)
|
||||
// If our application is hidden previously, we hide it again
|
||||
if (NSApp.delegate as? AppDelegate)?.hiddenState != nil {
|
||||
NSApp.hide(nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -560,12 +556,17 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// This causes the window to be removed from the screen list and macOS
|
||||
// handles what should be focused next.
|
||||
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() {
|
||||
guard let window else { return }
|
||||
|
||||
defer { updateColorSchemeForSurfaceTree() }
|
||||
// Change the collection behavior of the window depending on the configuration.
|
||||
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
||||
|
||||
|
|
@ -598,7 +599,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window)
|
||||
}
|
||||
|
||||
// MARK: First Responder
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
|
|
@ -736,14 +736,6 @@ class QuickTerminalController: BaseTerminalController {
|
|||
hidden = false
|
||||
}
|
||||
}
|
||||
|
||||
private class LastClosedState {
|
||||
let frame: NSRect
|
||||
|
||||
init(frame: NSRect) {
|
||||
self.frame = frame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ struct QuickTerminalSize {
|
|||
case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS:
|
||||
self = .pixels(cStruct.value.pixels)
|
||||
default:
|
||||
assertionFailure()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ struct SettingsView: View {
|
|||
VStack(alignment: .leading) {
|
||||
Text("Coming Soon. 🚧").font(.title)
|
||||
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)
|
||||
.lineLimit(nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ struct SplitView<L: View, R: View>: View {
|
|||
let left: L
|
||||
let right: R
|
||||
|
||||
/// Called when the divider is double-tapped to equalize splits.
|
||||
let onEqualize: () -> Void
|
||||
|
||||
/// The minimum size (in points) of a split
|
||||
let minSize: CGFloat = 10
|
||||
|
||||
|
|
@ -56,6 +59,9 @@ struct SplitView<L: View, R: View>: View {
|
|||
split: $split)
|
||||
.position(splitterPoint)
|
||||
.gesture(dragGesture(geo.size, splitterPoint: splitterPoint))
|
||||
.onTapGesture(count: 2) {
|
||||
onEqualize()
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .contain)
|
||||
.accessibilityLabel(splitViewLabel)
|
||||
|
|
@ -69,7 +75,8 @@ struct SplitView<L: View, R: View>: View {
|
|||
dividerColor: Color,
|
||||
resizeIncrements: NSSize = .init(width: 1, height: 1),
|
||||
@ViewBuilder left: (() -> L),
|
||||
@ViewBuilder right: (() -> R)
|
||||
@ViewBuilder right: (() -> R),
|
||||
onEqualize: @escaping () -> Void
|
||||
) {
|
||||
self.direction = direction
|
||||
self._split = split
|
||||
|
|
@ -77,6 +84,7 @@ struct SplitView<L: View, R: View>: View {
|
|||
self.resizeIncrements = resizeIncrements
|
||||
self.left = left()
|
||||
self.right = right()
|
||||
self.onEqualize = onEqualize
|
||||
}
|
||||
|
||||
private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ struct TerminalSplitSubtreeView: View {
|
|||
},
|
||||
right: {
|
||||
TerminalSplitSubtreeView(node: split.right, onResize: onResize)
|
||||
},
|
||||
onEqualize: {
|
||||
guard let surface = node.leftmostLeaf().surface else { return }
|
||||
ghostty.splitEqualize(surface: surface)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ class BaseTerminalController: NSWindowController,
|
|||
|
||||
/// This can be set to show/hide the command palette.
|
||||
@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.
|
||||
var focusFollowsMouse: Bool {
|
||||
|
|
@ -69,12 +72,24 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The previous frame information from the window
|
||||
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.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
/// The cancellables related to our focused surface.
|
||||
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.
|
||||
var undoExpiration: Duration {
|
||||
ghostty.config.undoTimeout
|
||||
|
|
@ -319,6 +334,37 @@ class BaseTerminalController: NSWindowController,
|
|||
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.
|
||||
func closeSurface(
|
||||
_ view: Ghostty.SurfaceView,
|
||||
|
|
@ -566,23 +612,12 @@ class BaseTerminalController: NSWindowController,
|
|||
// Get the direction from the notification
|
||||
guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] 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
|
||||
guard let targetNode = surfaceTree.root?.node(view: target) else { return }
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
|
@ -723,10 +758,13 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
|
||||
private func titleDidChange(to: String) {
|
||||
lastComputedTitle = to
|
||||
applyTitleToWindow()
|
||||
}
|
||||
|
||||
private func applyTitleToWindow() {
|
||||
guard let window else { return }
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
window.title = titleOverride ?? lastComputedTitle
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -900,6 +949,28 @@ class BaseTerminalController: NSWindowController,
|
|||
fullscreenStyle = NativeFullscreen(window)
|
||||
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
|
||||
|
|
@ -989,6 +1060,10 @@ class BaseTerminalController: NSWindowController,
|
|||
window.performClose(sender)
|
||||
}
|
||||
|
||||
@IBAction func changeTabTitle(_ sender: Any) {
|
||||
promptTabTitle()
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
|
|
@ -1087,6 +1162,22 @@ class BaseTerminalController: NSWindowController,
|
|||
@IBAction func toggleCommandPalette(_ sender: Any?) {
|
||||
commandPaletteIsShowing.toggle()
|
||||
}
|
||||
|
||||
@IBAction func find(_ sender: Any) {
|
||||
focusedSurface?.find(sender)
|
||||
}
|
||||
|
||||
@IBAction func findNext(_ sender: Any) {
|
||||
focusedSurface?.findNext(sender)
|
||||
}
|
||||
|
||||
@IBAction func findPrevious(_ sender: Any) {
|
||||
focusedSurface?.findNext(sender)
|
||||
}
|
||||
|
||||
@IBAction func findHide(_ sender: Any) {
|
||||
focusedSurface?.findHide(sender)
|
||||
}
|
||||
|
||||
@objc func resetTerminal(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
case "hidden": "TerminalHiddenTitlebar"
|
||||
case "transparent": "TerminalTransparentTitlebar"
|
||||
case "tabs":
|
||||
#if compiler(>=6.2)
|
||||
if #available(macOS 26.0, *) {
|
||||
"TerminalTabsTitlebarTahoe"
|
||||
} else {
|
||||
"TerminalTabsTitlebarVentura"
|
||||
}
|
||||
#else
|
||||
"TerminalTabsTitlebarVentura"
|
||||
#endif
|
||||
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.
|
||||
private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
|
||||
/// The notification cancellable for focused surface property changes.
|
||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||
|
||||
|
|
@ -100,6 +105,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
selector: #selector(onCloseOtherTabs),
|
||||
name: .ghosttyCloseOtherTabs,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseTabsOnTheRight),
|
||||
name: .ghosttyCloseTabsOnTheRight,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onResetWindowSize),
|
||||
|
|
@ -139,7 +149,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||
super.surfaceTreeDidChange(from: from, to: to)
|
||||
|
||||
|
||||
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
||||
// we want to invalidate our state.
|
||||
invalidateRestorableState()
|
||||
|
|
@ -186,7 +196,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
$0.window?.isMainWindow ?? false
|
||||
} ?? lastMain ?? all.last
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
// 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,
|
||||
expiresAfter: controller.undoExpiration
|
||||
) { target in
|
||||
// Close the tab when undoing
|
||||
undoManager.disableUndoRegistration {
|
||||
target.closeTab(nil)
|
||||
// Close the tab when undoing. We do this in a DispatchQueue because
|
||||
// for some people on macOS Tahoe this caused a crash and the queue
|
||||
// fixes it.
|
||||
// https://github.com/ghostty-org/ghostty/pull/9512
|
||||
DispatchQueue.main.async {
|
||||
undoManager.disableUndoRegistration {
|
||||
target.closeTab(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Register redo action
|
||||
|
|
@ -416,15 +431,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// This is a surface-level config update. If we have the surface, we
|
||||
// update our appearance based on it.
|
||||
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))
|
||||
/// Surface-level config will be updated in
|
||||
/// ``Ghostty/Ghostty/SurfaceView/derivedConfig`` then
|
||||
/// ``TerminalController/focusedSurfaceDidChange(to:)``
|
||||
}
|
||||
|
||||
/// Update the accessory view of each tab according to the keyboard
|
||||
|
|
@ -499,53 +508,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
window.syncAppearance(surfaceConfig)
|
||||
}
|
||||
|
||||
/// Returns the default size of the window. This is contextual based on the focused surface because
|
||||
/// the focused surface may specify a different default size than others.
|
||||
private var defaultSize: NSRect? {
|
||||
guard let screen = window?.screen ?? NSScreen.main else { return nil }
|
||||
/// Adjusts the given frame for the configured window position.
|
||||
func adjustForWindowPosition(frame: NSRect, on screen: NSScreen) -> NSRect {
|
||||
guard let x = derivedConfig.windowPositionX else { return frame }
|
||||
guard let y = derivedConfig.windowPositionY else { return frame }
|
||||
|
||||
if derivedConfig.maximize {
|
||||
return screen.visibleFrame
|
||||
} else if let focusedSurface,
|
||||
let initialSize = focusedSurface.initialSize {
|
||||
// Get the current frame of the window
|
||||
guard var frame = window?.frame else { return nil }
|
||||
// Convert top-left coordinates to bottom-left origin using our utility extension
|
||||
let origin = screen.origin(
|
||||
fromTopLeftOffsetX: CGFloat(x),
|
||||
offsetY: CGFloat(y),
|
||||
windowSize: frame.size)
|
||||
|
||||
// Calculate the chrome size (window size minus view size)
|
||||
let chromeWidth = frame.size.width - focusedSurface.frame.size.width
|
||||
let chromeHeight = frame.size.height - focusedSurface.frame.size.height
|
||||
// Clamp the origin to ensure the window stays fully visible on screen
|
||||
var safeOrigin = 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)
|
||||
|
||||
// Calculate the new width and height, clamping to the screen's size
|
||||
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
|
||||
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
|
||||
|
||||
// Update the frame size while keeping the window's position intact
|
||||
frame.size.width = newWidth
|
||||
frame.size.height = newHeight
|
||||
|
||||
// Ensure the window doesn't go outside the screen boundaries
|
||||
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
|
||||
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
|
||||
|
||||
return 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
|
||||
// Return our new origin
|
||||
var result = frame
|
||||
result.origin = safeOrigin
|
||||
return result
|
||||
}
|
||||
|
||||
/// This is called anytime a node in the surface tree is being removed.
|
||||
|
|
@ -576,7 +559,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
closeWindowImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Undo
|
||||
if let undoManager, let undoState {
|
||||
// Register undo action to restore the tab
|
||||
|
|
@ -597,15 +580,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.close()
|
||||
}
|
||||
|
||||
|
||||
private func closeOtherTabsImmediately() {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
guard tabGroup.windows.count > 1 else { return }
|
||||
|
||||
|
||||
// Start an undo grouping
|
||||
if let undoManager {
|
||||
undoManager.beginUndoGrouping()
|
||||
|
|
@ -613,7 +596,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
defer {
|
||||
undoManager?.endUndoGrouping()
|
||||
}
|
||||
|
||||
|
||||
// Iterate through all tabs except the current one.
|
||||
for window in tabGroup.windows where window != self.window {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let undoManager {
|
||||
undoManager.setActionName("Close Other Tabs")
|
||||
|
||||
|
||||
// We need to register an undo that refocuses this window. Otherwise, the
|
||||
// undo operation above for each tab will steal focus.
|
||||
undoManager.registerUndo(
|
||||
|
|
@ -638,7 +621,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
DispatchQueue.main.async {
|
||||
target.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
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
|
||||
/// confirmation. This will setup proper undo state so the action can be undone.
|
||||
private func closeWindowImmediately() {
|
||||
|
|
@ -724,7 +747,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
case (nil, nil): return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
// UndoState.
|
||||
|
|
@ -750,12 +773,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
let controllers = undoStates.map { undoState in
|
||||
TerminalController(ghostty, with: undoState)
|
||||
}
|
||||
|
||||
|
||||
// The first controller becomes the parent window for all tabs.
|
||||
// If we don't have a first controller (shouldn't be possible?)
|
||||
// then we can't restore tabs.
|
||||
guard let firstController = controllers.first else { return }
|
||||
|
||||
|
||||
// Add all subsequent controllers as tabs to the first window
|
||||
for controller in controllers.dropFirst() {
|
||||
controller.showWindow(nil)
|
||||
|
|
@ -764,7 +787,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
firstWindow.addTabbedWindow(newWindow, ordered: .above)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make the appropriate window key. If we had a key window, restore it.
|
||||
// Otherwise, make the last window key.
|
||||
if let keyWindowIndex, keyWindowIndex < controllers.count {
|
||||
|
|
@ -785,32 +808,25 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
/// Close all windows, asking for confirmation if necessary.
|
||||
static func closeAllWindows() {
|
||||
let needsConfirm: Bool = all.contains {
|
||||
$0.surfaceTree.contains { $0.needsConfirmQuit }
|
||||
}
|
||||
|
||||
if (!needsConfirm) {
|
||||
// The window we use for confirmations. Try to find the first window that
|
||||
// needs quit confirmation. This lets us attach the confirmation to something
|
||||
// that is running.
|
||||
guard let confirmWindow = all
|
||||
.first(where: { $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })?
|
||||
.surfaceTree.first(where: { $0.needsConfirmQuit })?
|
||||
.window
|
||||
else {
|
||||
closeAllWindowsImmediately()
|
||||
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()
|
||||
alert.messageText = "Close All Windows?"
|
||||
alert.informativeText = "All terminal sessions will be terminated."
|
||||
alert.addButton(withTitle: "Close All Windows")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: alertWindow, completionHandler: { response in
|
||||
alert.beginSheetModal(for: confirmWindow, completionHandler: { response in
|
||||
if (response == .alertFirstButtonReturn) {
|
||||
// This is important so that we avoid losing focus when Stage
|
||||
// Manager is used (#8336)
|
||||
|
|
@ -837,6 +853,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
let focusedSurface: UUID?
|
||||
let tabIndex: Int?
|
||||
weak var tabGroup: NSWindowTabGroup?
|
||||
let tabColor: TerminalTabColor
|
||||
}
|
||||
|
||||
convenience init(_ ghostty: Ghostty.App,
|
||||
|
|
@ -848,6 +865,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
showWindow(nil)
|
||||
if let window {
|
||||
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 let tabGroup = undoState.tabGroup,
|
||||
|
|
@ -883,7 +903,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
surfaceTree: surfaceTree,
|
||||
focusedSurface: focusedSurface?.id,
|
||||
tabIndex: window.tabGroup?.windows.firstIndex(of: window),
|
||||
tabGroup: window.tabGroup)
|
||||
tabGroup: window.tabGroup,
|
||||
tabColor: (window as? TerminalWindow)?.tabColor ?? .none)
|
||||
}
|
||||
|
||||
//MARK: - NSWindowController
|
||||
|
|
@ -897,9 +918,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
super.windowDidLoad()
|
||||
guard let window else { return }
|
||||
|
||||
// Store our initial frame so we can know our default later.
|
||||
initialFrame = window.frame
|
||||
|
||||
// I copy this because we may change the source in the future but also because
|
||||
// I regularly audit our codebase for "ghostty.config" access because generally
|
||||
// you shouldn't use it. Its safe in this case because for a new window we should
|
||||
|
|
@ -919,19 +937,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
// If this is our first surface then our focused surface will be nil
|
||||
// so we force the focused surface to the leaf.
|
||||
focusedSurface = view
|
||||
|
||||
if let defaultSize {
|
||||
window.setFrame(defaultSize, display: true)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize our content view to the SwiftUI root
|
||||
window.contentView = NSHostingView(rootView: TerminalView(
|
||||
ghostty: self.ghostty,
|
||||
viewModel: self,
|
||||
delegate: self
|
||||
delegate: self,
|
||||
))
|
||||
|
||||
// If we have a default size, we want to apply it.
|
||||
if let defaultSize {
|
||||
switch (defaultSize) {
|
||||
case .frame:
|
||||
// Frames can be applied immediately
|
||||
defaultSize.apply(to: window)
|
||||
|
||||
case .contentIntrinsicSize:
|
||||
// Content intrinsic size requires a short delay so that AppKit
|
||||
// can layout our SwiftUI views.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .microseconds(10_000)) { [weak window] in
|
||||
guard let window else { return }
|
||||
defaultSize.apply(to: window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store our initial frame so we can know our default later. This MUST
|
||||
// be after the defaultSize call above so that we don't re-apply our frame.
|
||||
// Note: we probably want to set this on the first frame change or something
|
||||
// so it respects cascade.
|
||||
initialFrame = window.frame
|
||||
|
||||
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
||||
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
|
||||
// it.
|
||||
|
|
@ -1042,7 +1079,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
if let window {
|
||||
LastWindowPosition.shared.save(window)
|
||||
}
|
||||
|
||||
|
||||
// Remember our last main
|
||||
Self.lastMain = self
|
||||
}
|
||||
|
|
@ -1089,27 +1126,27 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
@IBAction func closeOtherTabs(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else { return }
|
||||
|
||||
|
||||
// If we only have one window then we have no other tabs to close
|
||||
guard tabGroup.windows.count > 1 else { return }
|
||||
|
||||
|
||||
// Check if we have to confirm close.
|
||||
guard tabGroup.windows.contains(where: { window in
|
||||
// Ignore ourself
|
||||
if window == self.window { return false }
|
||||
|
||||
|
||||
// Ignore non-terminals
|
||||
guard let controller = window.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Check if any surfaces require confirmation
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}) else {
|
||||
self.closeOtherTabsImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
confirmClose(
|
||||
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."
|
||||
|
|
@ -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?) {
|
||||
guard let defaultSize else { return }
|
||||
window?.setFrame(defaultSize, display: true)
|
||||
guard let window, let defaultSize else { return }
|
||||
defaultSize.apply(to: window)
|
||||
}
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any?) {
|
||||
|
|
@ -1130,24 +1196,19 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
// if we're closing the window. If we don't have a tabgroup for any
|
||||
// reason we check ourselves.
|
||||
let windows: [NSWindow] = window.tabGroup?.windows ?? [window]
|
||||
|
||||
// Check if any windows require close confirmation.
|
||||
let needsConfirm = windows.contains { tabWindow in
|
||||
guard let controller = tabWindow.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
|
||||
}
|
||||
|
||||
// If none need confirmation then we can just close all the windows.
|
||||
if !needsConfirm {
|
||||
guard let confirmController = windows
|
||||
.compactMap({ $0.windowController as? TerminalController })
|
||||
.first(where: { $0.surfaceTree.contains(where: { $0.needsConfirmQuit }) })
|
||||
else {
|
||||
closeWindowImmediately()
|
||||
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?",
|
||||
informativeText: "All terminal sessions in this window will be terminated."
|
||||
informativeText: "All terminal sessions in this window will be terminated.",
|
||||
) {
|
||||
self.closeWindowImmediately()
|
||||
}
|
||||
|
|
@ -1164,7 +1225,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
|
||||
//MARK: - TerminalViewDelegate
|
||||
|
||||
|
||||
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||
super.focusedSurfaceDidChange(to: to)
|
||||
|
||||
|
|
@ -1228,7 +1289,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
|
||||
// Get our target window
|
||||
let targetWindow = tabbedWindows[finalIndex]
|
||||
|
||||
|
||||
// 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
|
||||
// find a better workaround. For now, this improves things dramatically.
|
||||
|
|
@ -1241,7 +1302,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
DispatchQueue.main.async {
|
||||
selectedWindow.makeKey()
|
||||
}
|
||||
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -1324,6 +1385,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
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) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree.contains(target) else { return }
|
||||
|
|
@ -1358,12 +1425,16 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
let macosWindowButtons: Ghostty.MacOSWindowButtons
|
||||
let macosTitlebarStyle: String
|
||||
let maximize: Bool
|
||||
let windowPositionX: Int16?
|
||||
let windowPositionY: Int16?
|
||||
|
||||
init() {
|
||||
self.backgroundColor = Color(NSColor.windowBackgroundColor)
|
||||
self.macosWindowButtons = .visible
|
||||
self.macosTitlebarStyle = "system"
|
||||
self.maximize = false
|
||||
self.windowPositionX = nil
|
||||
self.windowPositionY = nil
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
|
|
@ -1371,43 +1442,99 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
self.macosWindowButtons = config.macosWindowButtons
|
||||
self.macosTitlebarStyle = config.macosTitlebarStyle
|
||||
self.maximize = config.maximize
|
||||
self.windowPositionX = config.windowPositionX
|
||||
self.windowPositionY = config.windowPositionY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSMenuItemValidation
|
||||
|
||||
extension TerminalController: NSMenuItemValidation {
|
||||
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
extension TerminalController {
|
||||
override func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
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):
|
||||
guard let window else { return false }
|
||||
|
||||
|
||||
// Native fullscreen windows can't revert to default size.
|
||||
if window.styleMask.contains(.fullScreen) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// If we're fullscreen at all then we can't change size
|
||||
if fullscreenStyle?.isFullscreen ?? false {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// If our window is already the default size or we don't have a
|
||||
// default size, then disable.
|
||||
guard let defaultSize,
|
||||
window.frame.size != .init(
|
||||
width: defaultSize.size.width,
|
||||
height: defaultSize.size.height
|
||||
)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
return defaultSize?.isChanged(for: window) ?? false
|
||||
|
||||
default:
|
||||
return true
|
||||
return super.validateMenuItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Default Size
|
||||
|
||||
extension TerminalController {
|
||||
/// The possible default sizes for a terminal. The size can't purely be known as a
|
||||
/// window frame because if we set `window-width/height` then it is based
|
||||
/// on content size.
|
||||
enum DefaultSize {
|
||||
/// A frame, set with `window.setFrame`
|
||||
case frame(NSRect)
|
||||
|
||||
/// A content size, set with `window.setContentSize`
|
||||
case contentIntrinsicSize
|
||||
|
||||
func isChanged(for window: NSWindow) -> Bool {
|
||||
switch self {
|
||||
case .frame(let rect):
|
||||
return window.frame != rect
|
||||
case .contentIntrinsicSize:
|
||||
guard let view = window.contentView else {
|
||||
return false
|
||||
}
|
||||
|
||||
return view.frame.size != view.intrinsicContentSize
|
||||
}
|
||||
}
|
||||
|
||||
func apply(to window: NSWindow) {
|
||||
switch self {
|
||||
case .frame(let rect):
|
||||
window.setFrame(rect, display: true)
|
||||
case .contentIntrinsicSize:
|
||||
guard let size = window.contentView?.intrinsicContentSize else {
|
||||
return
|
||||
}
|
||||
|
||||
window.setContentSize(size)
|
||||
window.constrainToScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var defaultSize: DefaultSize? {
|
||||
if derivedConfig.maximize, let screen = window?.screen ?? NSScreen.main {
|
||||
// Maximize takes priority, we take up the full screen we're on.
|
||||
return .frame(screen.visibleFrame)
|
||||
} else if focusedSurface?.initialSize != nil {
|
||||
// Initial size as requested by the configuration (e.g. `window-width`)
|
||||
// takes next priority.
|
||||
return .contentIntrinsicSize
|
||||
} else if let initialFrame {
|
||||
// The initial frame we had when we started otherwise.
|
||||
return .frame(initialFrame)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,20 @@ import Cocoa
|
|||
class TerminalRestorableState: Codable {
|
||||
static let selfKey = "state"
|
||||
static let versionKey = "version"
|
||||
static let version: Int = 5
|
||||
static let version: Int = 7
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
let effectiveFullscreenMode: FullscreenMode?
|
||||
let tabColor: TerminalTabColor
|
||||
let titleOverride: String?
|
||||
|
||||
init(from controller: TerminalController) {
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
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) {
|
||||
|
|
@ -28,6 +34,9 @@ class TerminalRestorableState: Codable {
|
|||
|
||||
self.surfaceTree = v.value.surfaceTree
|
||||
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) {
|
||||
|
|
@ -91,6 +100,12 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|||
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
|
||||
// Find the focused surface in surfaceTree
|
||||
if let focusedStr = state.focusedSurface {
|
||||
|
|
@ -109,6 +124,13 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|||
}
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,9 @@ protocol TerminalViewModel: ObservableObject {
|
|||
|
||||
/// The command palette state.
|
||||
var commandPaletteIsShowing: Bool { get set }
|
||||
|
||||
/// The update overlay should be visible.
|
||||
var updateOverlayIsVisible: Bool { get }
|
||||
}
|
||||
|
||||
/// 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.
|
||||
weak var delegate: (any TerminalViewDelegate)? = nil
|
||||
|
||||
|
||||
// The most recently focused surface, equal to focusedSurface when
|
||||
// it is non-nil.
|
||||
@State private var lastFocusedSurface: Weak<Ghostty.SurfaceView> = .init()
|
||||
|
|
@ -97,6 +100,8 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||
guard let size = newValue else { return }
|
||||
self.delegate?.cellSizeDidChange(to: size)
|
||||
}
|
||||
.frame(idealWidth: lastFocusedSurface.value?.initialSize?.width,
|
||||
idealHeight: lastFocusedSurface.value?.initialSize?.height)
|
||||
}
|
||||
// Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style
|
||||
.ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : [])
|
||||
|
|
@ -105,10 +110,34 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
|||
TerminalCommandPaletteView(
|
||||
surfaceView: surfaceView,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import AppKit
|
||||
|
||||
class HiddenTitlebarTerminalWindow: TerminalWindow {
|
||||
// No titlebar, we don't support accessories.
|
||||
override var supportsUpdateAccessory: Bool { false }
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ import GhosttyKit
|
|||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||
/// style and configuration of the window based on the app configuration.
|
||||
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
|
||||
/// used by the manual float on top menu item feature.
|
||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||
|
|
@ -15,14 +21,47 @@ class TerminalWindow: NSWindow {
|
|||
/// Reset split zoom button in titlebar
|
||||
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.
|
||||
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.
|
||||
var terminalController: 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
|
||||
|
||||
override var toolbar: NSToolbar? {
|
||||
|
|
@ -35,6 +74,20 @@ class TerminalWindow: NSWindow {
|
|||
}
|
||||
|
||||
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
|
||||
// again. I'm not sure why this is required. If you don't do this, then
|
||||
// tabs restore as separate windows.
|
||||
|
|
@ -42,14 +95,14 @@ class TerminalWindow: NSWindow {
|
|||
DispatchQueue.main.async {
|
||||
self.tabbingMode = .automatic
|
||||
}
|
||||
|
||||
|
||||
// All new windows are based on the app config at the time of creation.
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return }
|
||||
let config = appDelegate.ghostty.config
|
||||
|
||||
// Setup our initial config
|
||||
derivedConfig = .init(config)
|
||||
|
||||
|
||||
// If there is a hardcoded title in the configuration, we set that
|
||||
// immediately. Future `set_title` apprt actions will override this
|
||||
// if necessary but this ensures our window loads with the proper
|
||||
|
|
@ -65,8 +118,7 @@ class TerminalWindow: NSWindow {
|
|||
// fallback to original centering behavior
|
||||
setInitialWindowPosition(
|
||||
x: config.windowPositionX,
|
||||
y: config.windowPositionY,
|
||||
windowDecorations: config.windowDecorations)
|
||||
y: config.windowPositionY)
|
||||
|
||||
// If our traffic buttons should be hidden, then hide them
|
||||
if config.macosWindowButtons == .hidden {
|
||||
|
|
@ -85,14 +137,32 @@ class TerminalWindow: NSWindow {
|
|||
}))
|
||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||
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,
|
||||
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
||||
// 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.spacing = 3
|
||||
stackView.spacing = 4
|
||||
stackView.alignment = .centerY
|
||||
stackView.addArrangedSubview(tabColorIndicator)
|
||||
stackView.addArrangedSubview(keyEquivalentLabel)
|
||||
stackView.addArrangedSubview(resetZoomTabButton)
|
||||
tab.accessoryView = stackView
|
||||
|
||||
// Get our saved level
|
||||
|
|
@ -104,6 +174,11 @@ class TerminalWindow: NSWindow {
|
|||
override var canBecomeKey: 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() {
|
||||
super.becomeKey()
|
||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
||||
|
|
@ -124,6 +199,12 @@ class TerminalWindow: NSWindow {
|
|||
} else {
|
||||
tabBarDidDisappear()
|
||||
}
|
||||
viewModel.isMainWindow = true
|
||||
}
|
||||
|
||||
override func resignMain() {
|
||||
super.resignMain()
|
||||
viewModel.isMainWindow = false
|
||||
}
|
||||
|
||||
override func mergeAllWindows(_ sender: Any?) {
|
||||
|
|
@ -162,9 +243,35 @@ class TerminalWindow: NSWindow {
|
|||
/// added.
|
||||
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.
|
||||
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 {
|
||||
|
|
@ -198,6 +305,9 @@ class TerminalWindow: NSWindow {
|
|||
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
||||
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() {
|
||||
|
|
@ -260,7 +370,7 @@ class TerminalWindow: NSWindow {
|
|||
button.isBordered = false
|
||||
button.allowsExpansionToolTips = true
|
||||
button.toolTip = "Reset Zoom"
|
||||
button.contentTintColor = .controlAccentColor
|
||||
button.contentTintColor = isMainWindow ? .controlAccentColor : .secondaryLabelColor
|
||||
button.state = .on
|
||||
button.image = NSImage(named:"ResetZoom")
|
||||
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
|
||||
// tab title if we're using custom fonts.
|
||||
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)
|
||||
|
||||
titlebarTextField?.font = font
|
||||
/// We check `hasMoreThanOneTabs` here because the system
|
||||
/// may copy this setting to the tab’s 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 bar’s text field, which is quite odd...
|
||||
titlebarTextField?.usesSingleLineMode = !hasMoreThanOneTabs
|
||||
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
|
||||
// at some point when a surface becomes focused.
|
||||
guard isVisible else { return }
|
||||
defer { updateColorSchemeForSurfaceTree() }
|
||||
|
||||
// Basic properties
|
||||
appearance = surfaceConfig.windowAppearance
|
||||
|
|
@ -356,7 +479,15 @@ class TerminalWindow: NSWindow {
|
|||
// Terminal.app more easily.
|
||||
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(
|
||||
appDelegate.ghostty.app,
|
||||
Unmanaged.passUnretained(self).toOpaque())
|
||||
|
|
@ -364,6 +495,11 @@ class TerminalWindow: NSWindow {
|
|||
} else {
|
||||
isOpaque = true
|
||||
|
||||
// Remove liquid glass when not transparent
|
||||
if #available(macOS 26.0, *) {
|
||||
removeGlassLayer()
|
||||
}
|
||||
|
||||
let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor)
|
||||
self.backgroundColor = backgroundColor.withAlphaComponent(1)
|
||||
}
|
||||
|
|
@ -400,9 +536,13 @@ class TerminalWindow: NSWindow {
|
|||
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.
|
||||
guard let x, let y else {
|
||||
guard x != nil, y != nil else {
|
||||
if (!LastWindowPosition.shared.restore(self)) {
|
||||
center()
|
||||
}
|
||||
|
|
@ -416,19 +556,14 @@ class TerminalWindow: NSWindow {
|
|||
return
|
||||
}
|
||||
|
||||
// Convert top-left coordinates to bottom-left origin using our utility extension
|
||||
let origin = screen.origin(
|
||||
fromTopLeftOffsetX: CGFloat(x),
|
||||
offsetY: CGFloat(y),
|
||||
windowSize: frame.size)
|
||||
|
||||
// Clamp the origin to ensure the window stays fully visible on screen
|
||||
var safeOrigin = 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)
|
||||
// We have an X/Y, use our controller function to set it up.
|
||||
guard let terminalController else {
|
||||
center()
|
||||
return
|
||||
}
|
||||
|
||||
let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen)
|
||||
setFrameOrigin(frame.origin)
|
||||
}
|
||||
|
||||
private func hideWindowButtons() {
|
||||
|
|
@ -437,19 +572,75 @@ class TerminalWindow: NSWindow {
|
|||
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
|
||||
|
||||
struct DerivedConfig {
|
||||
let title: String?
|
||||
let backgroundBlur: Ghostty.Config.BackgroundBlur
|
||||
let backgroundColor: NSColor
|
||||
let backgroundOpacity: Double
|
||||
let macosWindowButtons: Ghostty.MacOSWindowButtons
|
||||
let macosTitlebarStyle: String
|
||||
let windowCornerRadius: CGFloat
|
||||
|
||||
init() {
|
||||
self.title = nil
|
||||
self.backgroundColor = NSColor.windowBackgroundColor
|
||||
self.backgroundOpacity = 1
|
||||
self.macosWindowButtons = .visible
|
||||
self.backgroundBlur = .disabled
|
||||
self.macosTitlebarStyle = "transparent"
|
||||
self.windowCornerRadius = 16
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
|
|
@ -457,6 +648,18 @@ class TerminalWindow: NSWindow {
|
|||
self.backgroundColor = NSColor(config.backgroundColor)
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
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 {
|
||||
@Published var isSurfaceZoomed: 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 {
|
||||
@ObservedObject var viewModel: ViewModel
|
||||
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 {
|
||||
if viewModel.isSurfaceZoomed {
|
||||
VStack {
|
||||
Button(action: action) {
|
||||
Image("ResetZoom")
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundColor(viewModel.isMainWindow ? .accentColor : .secondary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.help("Reset Split Zoom")
|
||||
|
|
@ -497,10 +700,141 @@ extension TerminalWindow {
|
|||
}
|
||||
// With a toolbar, the window title is taller, so we need more padding
|
||||
// to properly align.
|
||||
.padding(.top, topPadding)
|
||||
.padding(.top, viewModel.accessoryTopPadding)
|
||||
// We always need space at the end of the titlebar
|
||||
.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
Loading…
Reference in New Issue