mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-19 18:26:13 +03:00
Merge branch 'main' into fix-copy-url
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash}]
|
||||
[*.{sh,bash,elv}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@ -36,13 +36,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
4
.github/workflows/release-pr.yml
vendored
4
.github/workflows/release-pr.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -211,7 +211,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
6
.github/workflows/release-tag.yml
vendored
6
.github/workflows/release-tag.yml
vendored
@ -83,13 +83,13 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
@ -130,7 +130,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
19
.github/workflows/release-tip.yml
vendored
19
.github/workflows/release-tip.yml
vendored
@ -107,12 +107,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -164,7 +164,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -175,6 +175,9 @@ jobs:
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: Xcode Version
|
||||
run: xcodebuild -version
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
@ -381,7 +384,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -392,6 +395,9 @@ jobs:
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: Xcode Version
|
||||
run: xcodebuild -version
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
@ -558,7 +564,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -569,6 +575,9 @@ jobs:
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: Xcode Version
|
||||
run: xcodebuild -version
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
env:
|
||||
|
129
.github/workflows/test.yml
vendored
129
.github/workflows/test.yml
vendored
@ -31,6 +31,7 @@ jobs:
|
||||
- prettier
|
||||
- alejandra
|
||||
- typos
|
||||
- shellcheck
|
||||
- translations
|
||||
- blueprint-compiler
|
||||
- test-pkg-linux
|
||||
@ -68,14 +69,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -99,14 +100,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -135,14 +136,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -164,14 +165,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -197,14 +198,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -241,14 +242,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -277,7 +278,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -288,6 +289,9 @@ jobs:
|
||||
- name: Xcode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: Xcode Version
|
||||
run: xcodebuild -version
|
||||
|
||||
- name: get the Zig deps
|
||||
id: deps
|
||||
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
|
||||
@ -357,7 +361,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -409,7 +413,7 @@ jobs:
|
||||
mkdir dist
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@ -504,14 +508,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -546,14 +550,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -561,7 +565,16 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test GTK Build
|
||||
- name: Test
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }} \
|
||||
test
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
@ -585,14 +598,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -612,7 +625,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -640,12 +653,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -668,12 +681,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -695,12 +708,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -722,12 +735,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -749,12 +762,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -766,6 +779,40 @@ jobs:
|
||||
- name: typos check
|
||||
run: nix develop -c typos
|
||||
|
||||
shellcheck:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-xsm
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
skipPush: true
|
||||
useDaemon: false # sometimes fails on short jobs
|
||||
- name: shellcheck
|
||||
run: |
|
||||
nix develop -c shellcheck \
|
||||
--check-sourced \
|
||||
--color=always \
|
||||
--severity=warning \
|
||||
--shell=bash \
|
||||
--external-sources \
|
||||
$(find . \( -name "*.sh" -o -name "*.bash" \) -type f ! -path "./zig-out/*" ! -path "./macos/build/*" ! -path "./.git/*" | sort)
|
||||
|
||||
translations:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-xsm
|
||||
@ -776,12 +823,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -803,12 +850,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -838,14 +885,14 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
- uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@ -896,13 +943,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
4
.github/workflows/update-colorschemes.yml
vendored
4
.github/workflows/update-colorschemes.yml
vendored
@ -22,14 +22,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@449c929cd5138e6607e7e78458e88cc476e76f89 # v1.2.8
|
||||
uses: namespacelabs/nscloud-cache-action@0ac1550c04676e19d39872be6216ccbf9c6bab43 # v1.2.9
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31.4.1
|
||||
uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31.5.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
33
build.zig
33
build.zig
@ -8,7 +8,22 @@ comptime {
|
||||
}
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
// This defines all the available build options (e.g. `-D`).
|
||||
const config = try buildpkg.Config.init(b);
|
||||
const test_filter = b.option(
|
||||
[]const u8,
|
||||
"test-filter",
|
||||
"Filter for test. Only applies to Zig tests.",
|
||||
);
|
||||
|
||||
// All our steps which we'll hook up later. The steps are shown
|
||||
// up here just so that they are more self-documenting.
|
||||
const run_step = b.step("run", "Run the app");
|
||||
const test_step = b.step("test", "Run all tests");
|
||||
const translations_step = b.step(
|
||||
"update-translations",
|
||||
"Update translation files",
|
||||
);
|
||||
|
||||
// Ghostty resources like terminfo, shell integration, themes, etc.
|
||||
const resources = try buildpkg.GhosttyResources.init(b, &config);
|
||||
@ -131,7 +146,6 @@ pub fn build(b: *std.Build) !void {
|
||||
b.getInstallPath(.prefix, "share/ghostty"),
|
||||
);
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
break :run;
|
||||
}
|
||||
@ -157,16 +171,18 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
);
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
// Run uses the native macOS app
|
||||
run_step.dependOn(&macos_app_native_only.open.step);
|
||||
|
||||
// If we have no test filters, install the tests too
|
||||
if (test_filter == null) {
|
||||
macos_app_native_only.addTestStepDependencies(test_step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
{
|
||||
const test_step = b.step("test", "Run all tests");
|
||||
const test_filter = b.option([]const u8, "test-filter", "Filter for test");
|
||||
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "ghostty-test",
|
||||
.filters = if (test_filter) |v| &.{v} else &.{},
|
||||
@ -180,18 +196,13 @@ pub fn build(b: *std.Build) !void {
|
||||
}),
|
||||
});
|
||||
|
||||
{
|
||||
if (config.emit_test_exe) b.installArtifact(test_exe);
|
||||
_ = try deps.add(test_exe);
|
||||
const test_run = b.addRunArtifact(test_exe);
|
||||
test_step.dependOn(&test_run.step);
|
||||
}
|
||||
}
|
||||
|
||||
// update-translations does what it sounds like and updates the "pot"
|
||||
// files. These should be committed to the repo.
|
||||
{
|
||||
const step = b.step("update-translations", "Update translation files");
|
||||
step.dependOn(i18n.update_step);
|
||||
}
|
||||
translations_step.dependOn(i18n.update_step);
|
||||
}
|
||||
|
5
dist/linux/systemd.service.in
vendored
5
dist/linux/systemd.service.in
vendored
@ -1,9 +1,12 @@
|
||||
[Unit]
|
||||
Description=@NAME@
|
||||
After=graphical-session.target
|
||||
After=dbus.socket
|
||||
Requires=dbus.socket
|
||||
|
||||
[Service]
|
||||
Type=dbus
|
||||
Type=notify-reload
|
||||
ReloadSignal=SIGUSR2
|
||||
BusName=@APPID@
|
||||
ExecStart=@GHOSTTY@ --launched-from=systemd
|
||||
|
||||
|
@ -932,6 +932,9 @@ bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
|
||||
// Don't use these unless you know what you're doing.
|
||||
void ghostty_set_window_background_blur(ghostty_app_t, void*);
|
||||
|
||||
// Benchmark API, if available.
|
||||
bool ghostty_benchmark_cli(const char*, const char*);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 56;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@ -152,6 +152,16 @@
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = A5B30529299BEAAA0047F10C /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = A5B30530299BEAAA0047F10C;
|
||||
remoteInfo = Ghostty;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */ = {isa = PBXFileReference; lastKnownFileType = folder; name = bat; path = "../zig-out/share/bat"; sourceTree = "<group>"; };
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
@ -199,6 +209,7 @@
|
||||
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = "<group>"; };
|
||||
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = "<group>"; };
|
||||
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
|
||||
A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A553F4122E06EB1600257779 /* Ghostty.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; name = Ghostty.icon; path = ../images/Ghostty.icon; sourceTree = SOURCE_ROOT; };
|
||||
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
@ -291,7 +302,18 @@
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
A54F45F42E1F047A0046BD5C /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
A54F45F02E1F047A0046BD5C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A5B3052E299BEAAA0047F10C /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -590,6 +612,7 @@
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */,
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
|
||||
A54CD6ED299BEB14008C95BB /* Sources */,
|
||||
A54F45F42E1F047A0046BD5C /* Tests */,
|
||||
A5D495A3299BECBA00DD1313 /* Frameworks */,
|
||||
A5A1F8862A489D7400D1E8BC /* Resources */,
|
||||
A5B30532299BEAAA0047F10C /* Products */,
|
||||
@ -601,6 +624,7 @@
|
||||
children = (
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */,
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */,
|
||||
A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -674,6 +698,29 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */;
|
||||
buildPhases = (
|
||||
A54F45EF2E1F047A0046BD5C /* Sources */,
|
||||
A54F45F02E1F047A0046BD5C /* Frameworks */,
|
||||
A54F45F12E1F047A0046BD5C /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
A54F45F42E1F047A0046BD5C /* Tests */,
|
||||
);
|
||||
name = GhosttyTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = GhosttyTests;
|
||||
productReference = A54F45F32E1F047A0046BD5C /* GhosttyTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
A5B30530299BEAAA0047F10C /* Ghostty */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = A5B30540299BEAAB0047F10C /* Build configuration list for PBXNativeTarget "Ghostty" */;
|
||||
@ -718,9 +765,13 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1520;
|
||||
LastSwiftUpdateCheck = 2600;
|
||||
LastUpgradeCheck = 1610;
|
||||
TargetAttributes = {
|
||||
A54F45F22E1F047A0046BD5C = {
|
||||
CreatedOnToolsVersion = 26.0;
|
||||
TestTargetID = A5B30530299BEAAA0047F10C;
|
||||
};
|
||||
A5B30530299BEAAA0047F10C = {
|
||||
CreatedOnToolsVersion = 14.2;
|
||||
LastSwiftMigration = 1510;
|
||||
@ -748,11 +799,19 @@
|
||||
targets = (
|
||||
A5B30530299BEAAA0047F10C /* Ghostty */,
|
||||
A5D4499C2B53AE7B000F5B83 /* Ghostty-iOS */,
|
||||
A54F45F22E1F047A0046BD5C /* GhosttyTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
A54F45F12E1F047A0046BD5C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A5B3052F299BEAAA0047F10C /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -794,6 +853,13 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
A54F45EF2E1F047A0046BD5C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
A5B3052D299BEAAA0047F10C /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -925,6 +991,14 @@
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
A54F45F82E1F047A0046BD5C /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = A5B30530299BEAAA0047F10C /* Ghostty */;
|
||||
targetProxy = A54F45F72E1F047A0046BD5C /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3B39CAA22B33946300DABEB8 /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@ -1034,6 +1108,76 @@
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
A54F45F92E1F047A0046BD5C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
A54F45FA2E1F047A0046BD5C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
A54F45FB2E1F047A0046BD5C /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.5;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.mitchellh.GhosttyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Ghostty.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ghostty";
|
||||
};
|
||||
name = ReleaseLocal;
|
||||
};
|
||||
A5B3053E299BEAAB0047F10C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@ -1378,6 +1522,16 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
A54F45FC2E1F047A0046BD5C /* Build configuration list for PBXNativeTarget "GhosttyTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
A54F45F92E1F047A0046BD5C /* Debug */,
|
||||
A54F45FA2E1F047A0046BD5C /* Release */,
|
||||
A54F45FB2E1F047A0046BD5C /* ReleaseLocal */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = ReleaseLocal;
|
||||
};
|
||||
A5B3052C299BEAAA0047F10C /* Build configuration list for PBXProject "Ghostty" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -28,6 +28,19 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "A54F45F22E1F047A0046BD5C"
|
||||
BuildableName = "GhosttyTests.xctest"
|
||||
BlueprintName = "GhosttyTests"
|
||||
ReferencedContainer = "container:Ghostty.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
32
macos/Tests/BenchmarkTests.swift
Normal file
32
macos/Tests/BenchmarkTests.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// GhosttyTests.swift
|
||||
// GhosttyTests
|
||||
//
|
||||
// Created by Mitchell Hashimoto on 7/9/25.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import GhosttyKit
|
||||
|
||||
extension Tag {
|
||||
@Tag static var benchmark: Self
|
||||
}
|
||||
|
||||
/// The whole idea behind these benchmarks is that they're run by right-clicking
|
||||
/// in Xcode and using "Profile" to open them in instruments. They aren't meant to
|
||||
/// be run in general.
|
||||
///
|
||||
/// When running them, set the `if:` to `true`. There's probably a better
|
||||
/// programmatic way to do this but I don't know it yet!
|
||||
@Suite(
|
||||
"Benchmarks",
|
||||
.enabled(if: false),
|
||||
.tags(.benchmark)
|
||||
)
|
||||
struct BenchmarkTests {
|
||||
@Test func example() async throws {
|
||||
ghostty_benchmark_cli(
|
||||
"terminal-stream",
|
||||
"--data=/Users/mitchellh/Documents/ghostty/bug.osc.txt")
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@
|
||||
|
||||
set -e # Exit immediately if a command exits with a non-zero status
|
||||
|
||||
SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||
SCRIPT_PATH="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)"
|
||||
INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock"
|
||||
OUTPUT_DIR="blob"
|
||||
|
||||
|
@ -61,6 +61,7 @@
|
||||
pinact,
|
||||
hyperfine,
|
||||
typos,
|
||||
shellcheck,
|
||||
uv,
|
||||
wayland,
|
||||
wayland-scanner,
|
||||
@ -101,6 +102,7 @@ in
|
||||
alejandra
|
||||
pinact
|
||||
typos
|
||||
shellcheck
|
||||
|
||||
# Testing
|
||||
parallel
|
||||
|
@ -15,6 +15,7 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
const macos = b.dependency("macos", .{ .target = target, .optimize = optimize });
|
||||
|
||||
const module = harfbuzz: {
|
||||
const module = b.addModule("harfbuzz", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
@ -25,6 +26,13 @@ pub fn build(b: *std.Build) !void {
|
||||
},
|
||||
});
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(bool, "coretext", coretext_enabled);
|
||||
options.addOption(bool, "freetype", freetype_enabled);
|
||||
module.addOptions("build_options", options);
|
||||
break :harfbuzz module;
|
||||
};
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
|
@ -1,7 +1,8 @@
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("hb.h");
|
||||
@cInclude("hb-ft.h");
|
||||
if (builtin.os.tag == .macos) @cInclude("hb-coretext.h");
|
||||
if (build_options.freetype) @cInclude("hb-ft.h");
|
||||
if (build_options.coretext) @cInclude("hb-coretext.h");
|
||||
});
|
||||
|
@ -18,15 +18,12 @@ pub fn build(b: *std.Build) !void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
lib.addCSourceFile(.{
|
||||
.file = b.path("os/zig_log.c"),
|
||||
.flags = flags.items,
|
||||
.file = b.path("os/zig_macos.c"),
|
||||
.flags = &.{"-std=c99"},
|
||||
});
|
||||
lib.addCSourceFile(.{
|
||||
.file = b.path("text/ext.c"),
|
||||
.flags = flags.items,
|
||||
});
|
||||
lib.linkFramework("CoreFoundation");
|
||||
lib.linkFramework("CoreGraphics");
|
||||
|
@ -23,6 +23,7 @@ pub const c = @cImport({
|
||||
@cInclude("IOSurface/IOSurfaceRef.h");
|
||||
@cInclude("dispatch/dispatch.h");
|
||||
@cInclude("os/log.h");
|
||||
@cInclude("os/signpost.h");
|
||||
|
||||
if (builtin.os.tag == .macos) {
|
||||
@cInclude("Carbon/Carbon.h");
|
||||
|
@ -1,6 +1,7 @@
|
||||
const log = @import("os/log.zig");
|
||||
|
||||
pub const c = @import("os/c.zig");
|
||||
pub const signpost = @import("os/signpost.zig");
|
||||
pub const Log = log.Log;
|
||||
pub const LogType = log.LogType;
|
||||
|
||||
|
@ -8,10 +8,10 @@ pub const Log = opaque {
|
||||
subsystem: [:0]const u8,
|
||||
category: [:0]const u8,
|
||||
) *Log {
|
||||
return @as(?*Log, @ptrFromInt(@intFromPtr(c.os_log_create(
|
||||
return @ptrCast(c.os_log_create(
|
||||
subsystem.ptr,
|
||||
category.ptr,
|
||||
)))).?;
|
||||
).?);
|
||||
}
|
||||
|
||||
pub fn release(self: *Log) void {
|
||||
@ -32,7 +32,11 @@ pub const Log = opaque {
|
||||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
const str = nosuspend std.fmt.allocPrintZ(alloc, format, args) catch return;
|
||||
const str = nosuspend std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
format,
|
||||
args,
|
||||
) catch return;
|
||||
defer alloc.free(str);
|
||||
zig_os_log_with_type(self, typ, str.ptr);
|
||||
}
|
||||
|
214
pkg/macos/os/signpost.zig
Normal file
214
pkg/macos/os/signpost.zig
Normal file
@ -0,0 +1,214 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const logpkg = @import("log.zig");
|
||||
const Log = logpkg.Log;
|
||||
|
||||
/// This should be called once at the start of the program to intialize
|
||||
/// some required state for signpost logging.
|
||||
///
|
||||
/// This is all to workaround a Zig bug:
|
||||
/// https://github.com/ziglang/zig/issues/24370
|
||||
pub fn init() void {
|
||||
if (__dso_handle != null) return;
|
||||
|
||||
const sym = comptime sym: {
|
||||
const root = @import("root");
|
||||
|
||||
// If we have a main function, use that as the symbol.
|
||||
if (@hasDecl(root, "main")) break :sym root.main;
|
||||
|
||||
// Otherwise, we're in a library, so we just use the first
|
||||
// function in our root module. I actually don't know if this is
|
||||
// all required or if we can just use the real `__dso_handle` symbol,
|
||||
// but this seems to work for now.
|
||||
for (@typeInfo(root).@"struct".decls) |decl_info| {
|
||||
const decl = @field(root, decl_info.name);
|
||||
if (@typeInfo(@TypeOf(decl)) == .@"fn") break :sym decl;
|
||||
}
|
||||
|
||||
@compileError("no functions found in root module");
|
||||
};
|
||||
|
||||
// Since __dso_handle is not automatically populated by the linker,
|
||||
// we populate it by looking up the main function's module address
|
||||
// which should be a mach-o header.
|
||||
var info: DlInfo = undefined;
|
||||
const result = dladdr(sym, &info);
|
||||
assert(result != 0);
|
||||
__dso_handle = @ptrCast(@alignCast(info.dli_fbase));
|
||||
}
|
||||
|
||||
/// This should REALLY be an extern var that is populated by the linker,
|
||||
/// but there is a Zig bug: https://github.com/ziglang/zig/issues/24370
|
||||
var __dso_handle: ?*c.mach_header = null;
|
||||
|
||||
// Import the necessary C functions and types
|
||||
extern "c" fn dladdr(addr: ?*const anyopaque, info: *DlInfo) c_int;
|
||||
|
||||
// Define the Dl_info structure
|
||||
const DlInfo = extern struct {
|
||||
dli_fname: [*:0]const u8, // Pathname of shared object
|
||||
dli_fbase: ?*anyopaque, // Base address of shared object
|
||||
dli_sname: [*:0]const u8, // Name of nearest symbol
|
||||
dli_saddr: ?*anyopaque, // Address of nearest symbol
|
||||
};
|
||||
|
||||
/// Checks whether signpost logging is enabled for the given log handle.
|
||||
/// Returns true if signposts will be recorded for this log, false otherwise.
|
||||
/// This can be used to avoid expensive operations when signpost logging is disabled.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_enabled?language=objc
|
||||
pub fn enabled(log: *Log) bool {
|
||||
return c.os_signpost_enabled(@ptrCast(log));
|
||||
}
|
||||
|
||||
/// Emits a signpost event - a single point in time marker.
|
||||
/// Events are useful for marking when specific actions occur, such as
|
||||
/// user interactions, state changes, or other discrete occurrences.
|
||||
/// The event will appear as a vertical line in Instruments.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_event_emit?language=objc
|
||||
pub fn emitEvent(
|
||||
log: *Log,
|
||||
id: Id,
|
||||
comptime name: [:0]const u8,
|
||||
) void {
|
||||
emitWithName(log, id, .event, name);
|
||||
}
|
||||
|
||||
/// Marks the beginning of a time interval.
|
||||
/// Use this with intervalEnd to measure the duration of operations.
|
||||
/// The same ID must be used for both the begin and end calls.
|
||||
/// Intervals appear as horizontal bars in Instruments timeline.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_interval_begin?language=objc
|
||||
pub fn intervalBegin(log: *Log, id: Id, comptime name: [:0]const u8) void {
|
||||
emitWithName(log, id, .interval_begin, name);
|
||||
}
|
||||
|
||||
/// Marks the end of a time interval.
|
||||
/// Must be paired with a prior intervalBegin call using the same ID.
|
||||
/// The name should match the name used in intervalBegin.
|
||||
/// Instruments will calculate and display the duration between begin and end.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_interval_end?language=objc
|
||||
pub fn intervalEnd(log: *Log, id: Id, comptime name: [:0]const u8) void {
|
||||
emitWithName(log, id, .interval_end, name);
|
||||
}
|
||||
|
||||
/// The internal function to emit a signpost with a specific name.
|
||||
fn emitWithName(
|
||||
log: *Log,
|
||||
id: Id,
|
||||
typ: Type,
|
||||
comptime name: [:0]const u8,
|
||||
) void {
|
||||
// Init must be called by this point.
|
||||
assert(__dso_handle != null);
|
||||
|
||||
var buf: [2]u8 = @splat(0);
|
||||
c._os_signpost_emit_with_name_impl(
|
||||
__dso_handle,
|
||||
@ptrCast(log),
|
||||
@intFromEnum(typ),
|
||||
@intFromEnum(id),
|
||||
name.ptr,
|
||||
"".ptr,
|
||||
&buf,
|
||||
buf.len,
|
||||
);
|
||||
}
|
||||
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_id_t?language=objc
|
||||
pub const Id = enum(u64) {
|
||||
null = 0, // OS_SIGNPOST_ID_NULL
|
||||
invalid = 0xFFFFFFFFFFFFFFFF, // OS_SIGNPOST_ID_INVALID
|
||||
exclusive = 0xEEEEB0B5B2B2EEEE, // OS_SIGNPOST_ID_EXCLUSIVE
|
||||
_,
|
||||
|
||||
/// Generates a new signpost ID for use with signpost operations.
|
||||
/// The ID is unique for the given log handle and can be used to track
|
||||
/// asynchronous operations or mark specific points of interest in the code.
|
||||
/// Returns a unique signpost ID that can be used with os_signpost functions.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_id_generate?language=objc
|
||||
pub fn generate(log: *Log) Id {
|
||||
return @enumFromInt(c.os_signpost_id_generate(@ptrCast(log)));
|
||||
}
|
||||
|
||||
/// Creates a signpost ID based on a pointer value.
|
||||
/// This is useful for tracking operations associated with a specific object
|
||||
/// or memory location. The same pointer will always generate the same ID
|
||||
/// for a given log handle, allowing correlation of signpost events.
|
||||
/// Pass null to get the null signpost ID.
|
||||
///
|
||||
/// https://developer.apple.com/documentation/os/os_signpost_id_for_pointer?language=objc
|
||||
pub fn forPointer(log: *Log, ptr: ?*anyopaque) Id {
|
||||
return @enumFromInt(c.os_signpost_id_make_with_pointer(
|
||||
@ptrCast(log),
|
||||
@ptrCast(ptr),
|
||||
));
|
||||
}
|
||||
|
||||
test "generate ID" {
|
||||
// We can't really test the return value because it may return null
|
||||
// if signposts are disabled.
|
||||
const id: Id = .generate(Log.create("com.mitchellh.ghostty", "test"));
|
||||
try std.testing.expect(id != .invalid);
|
||||
}
|
||||
|
||||
test "generate ID for pointer" {
|
||||
var foo: usize = 0x1234;
|
||||
const id: Id = .forPointer(Log.create("com.mitchellh.ghostty", "test"), &foo);
|
||||
try std.testing.expect(id != .null);
|
||||
}
|
||||
};
|
||||
|
||||
/// https://developer.apple.com/documentation/os/ossignposttype?language=objc
|
||||
pub const Type = enum(u8) {
|
||||
event = 0, // OS_SIGNPOST_EVENT
|
||||
interval_begin = 1, // OS_SIGNPOST_INTERVAL_BEGIN
|
||||
interval_end = 2, // OS_SIGNPOST_INTERVAL_END
|
||||
|
||||
pub const mask: u8 = 0x03; // OS_SIGNPOST_TYPE_MASK
|
||||
};
|
||||
|
||||
/// Special os_log category values that surface in Instruments and other
|
||||
/// tooling.
|
||||
pub const Category = struct {
|
||||
/// Points of Interest appear as a dedicated track in Instruments.
|
||||
/// Use this for high-level application events that help understand
|
||||
/// the flow of your application.
|
||||
pub const points_of_interest: [:0]const u8 = "PointsOfInterest";
|
||||
|
||||
/// Dynamic Tracing category enables runtime-configurable logging.
|
||||
/// Signposts in this category can be enabled/disabled dynamically
|
||||
/// without recompiling.
|
||||
pub const dynamic_tracing: [:0]const u8 = "DynamicTracing";
|
||||
|
||||
/// Dynamic Stack Tracing category captures call stacks at signpost
|
||||
/// events. This provides deeper debugging information but has higher
|
||||
/// performance overhead.
|
||||
pub const dynamic_stack_tracing: [:0]const u8 = "DynamicStackTracing";
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Id;
|
||||
}
|
||||
|
||||
test enabled {
|
||||
_ = enabled(Log.create("com.mitchellh.ghostty", "test"));
|
||||
}
|
||||
|
||||
test "intervals" {
|
||||
init();
|
||||
|
||||
const log = Log.create("com.mitchellh.ghostty", "test");
|
||||
defer log.release();
|
||||
|
||||
// Test that we can begin and end an interval
|
||||
const id = Id.generate(log);
|
||||
intervalBegin(log, id, "Test Interval");
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#include <os/log.h>
|
||||
#include <os/signpost.h>
|
||||
|
||||
// A wrapper so we can use the os_log_with_type macro.
|
||||
void zig_os_log_with_type(
|
@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-07-08 15:13-0500\n"
|
||||
"PO-Revision-Date: 2025-03-31 03:08+0200\n"
|
||||
"Last-Translator: Ruben Engelbrecht <hey@rme.gg>\n"
|
||||
"PO-Revision-Date: 2025-07-09 16:11-0400\n"
|
||||
"Last-Translator: Hojin You <dev.hojin@gmail.com>\n"
|
||||
"Language-Team: Korean <translation-team-ko@googlegroups.com>\n"
|
||||
"Language: ko\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@ -87,7 +87,7 @@ msgstr "오른쪽으로 창 나누기"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
|
||||
msgid "Execute a command…"
|
||||
msgstr ""
|
||||
msgstr "명령을 실행하세요…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
|
||||
@ -160,7 +160,7 @@ msgstr "설정 열기"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
|
||||
msgid "Command Palette"
|
||||
msgstr ""
|
||||
msgstr "명령 팔레트"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
|
||||
msgid "Terminal Inspector"
|
||||
@ -208,12 +208,12 @@ msgstr "허용"
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
msgstr "이 분할에 대한 선택 기억하기"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
msgstr "이 창을 다시 보려면 설정을 다시 불러오세요"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
@ -285,9 +285,8 @@ msgid "View Open Tabs"
|
||||
msgstr "열린 탭 보기"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:266
|
||||
#, fuzzy
|
||||
msgid "New Split"
|
||||
msgstr "나누기"
|
||||
msgstr "새 분할"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:329
|
||||
msgid ""
|
||||
|
@ -29,6 +29,7 @@ const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const systemd = @import("../../os/systemd.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
@ -1035,6 +1036,12 @@ pub fn reloadConfig(
|
||||
target: apprt.action.Target,
|
||||
opts: apprt.action.ReloadConfig,
|
||||
) !void {
|
||||
// Tell systemd that reloading has started.
|
||||
systemd.notify.reloading();
|
||||
|
||||
// When we exit this function tell systemd that reloading has finished.
|
||||
defer systemd.notify.ready();
|
||||
|
||||
if (opts.soft) {
|
||||
switch (target) {
|
||||
.app => try self.core_app.updateConfig(self, &self.config),
|
||||
@ -1367,6 +1374,9 @@ pub fn run(self: *App) !void {
|
||||
log.warn("error handling configuration changes err={}", .{err});
|
||||
};
|
||||
|
||||
// Tell systemd that we are ready.
|
||||
systemd.notify.ready();
|
||||
|
||||
while (self.running) {
|
||||
_ = glib.MainContext.iteration(self.ctx, 1);
|
||||
|
||||
|
@ -2333,6 +2333,7 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
|
||||
env.remove("DBUS_STARTER_BUS_TYPE");
|
||||
env.remove("INVOCATION_ID");
|
||||
env.remove("JOURNAL_STREAM");
|
||||
env.remove("NOTIFY_SOCKET");
|
||||
|
||||
// Unset environment varies set by snaps if we're running in a snap.
|
||||
// This allows Ghostty to further launch additional snaps.
|
||||
|
@ -103,7 +103,7 @@ pub inline fn runtimeUntil(
|
||||
test "atLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast, runtimeUntil };
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
@ -118,3 +118,23 @@ test "atLeast" {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
test "runtimeUntil" {
|
||||
const testing = std.testing;
|
||||
|
||||
// This is an array in case we add a comptime variant.
|
||||
const funs = &.{runtimeUntil};
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This is a trivial helper script to help run the codepoint-width benchmark.
|
||||
# You probably want to tweak this script depending on what you're
|
||||
# trying to measure.
|
||||
|
||||
# Options:
|
||||
# - "ascii", uniform random ASCII bytes
|
||||
# - "utf8", uniform random unicode characters, encoded as utf8
|
||||
# - "rand", pure random data, will contain many invalid code sequences.
|
||||
DATA="utf8"
|
||||
SIZE="25000000"
|
||||
|
||||
# Add additional arguments
|
||||
ARGS=""
|
||||
|
||||
# Generate the benchmark input ahead of time so it's not included in the time.
|
||||
./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data
|
||||
#cat ~/Downloads/JAPANESEBIBLE.txt > /tmp/ghostty_bench_data
|
||||
|
||||
# Uncomment to instead use the contents of `stream.txt` as input.
|
||||
# yes $(cat ./stream.txt) | head -c $SIZE > /tmp/ghostty_bench_data
|
||||
|
||||
hyperfine \
|
||||
--warmup 10 \
|
||||
-n noop \
|
||||
"./zig-out/bin/bench-codepoint-width --mode=noop${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n wcwidth \
|
||||
"./zig-out/bin/bench-codepoint-width --mode=wcwidth${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n table \
|
||||
"./zig-out/bin/bench-codepoint-width --mode=table${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n simd \
|
||||
"./zig-out/bin/bench-codepoint-width --mode=simd${ARGS} </tmp/ghostty_bench_data"
|
||||
|
@ -1,204 +0,0 @@
|
||||
//! This benchmark tests the throughput of codepoint width calculation.
|
||||
//! This is a common operation in terminal character printing and the
|
||||
//! motivating factor to write this benchmark was discovering that our
|
||||
//! codepoint width function was 30% of the runtime of every character
|
||||
//! print.
|
||||
//!
|
||||
//! This will consume all of the available stdin, so you should run it
|
||||
//! with `head` in a pipe to restrict. For example, to test ASCII input:
|
||||
//!
|
||||
//! bench-stream --mode=gen-ascii | head -c 50M | bench-codepoint-width --mode=ziglyph
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const cli = @import("../cli.zig");
|
||||
const simd = @import("../simd/main.zig");
|
||||
const table = @import("../unicode/main.zig").table;
|
||||
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .noop,
|
||||
|
||||
/// The size for read buffers. Doesn't usually need to be changed. The
|
||||
/// main point is to make this runtime known so we can avoid compiler
|
||||
/// optimizations.
|
||||
@"buffer-size": usize = 4096,
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub fn deinit(self: *Args) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const Mode = enum {
|
||||
/// The baseline mode copies the data from the fd into a buffer. This
|
||||
/// is used to show the minimal overhead of reading the fd into memory
|
||||
/// and establishes a baseline for the other modes.
|
||||
noop,
|
||||
|
||||
/// libc wcwidth
|
||||
wcwidth,
|
||||
|
||||
/// Use ziglyph library to calculate the display width of each codepoint.
|
||||
ziglyph,
|
||||
|
||||
/// Our SIMD implementation.
|
||||
simd,
|
||||
|
||||
/// Test our lookup table implementation.
|
||||
table,
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
// We want to use the c allocator because it is much faster than GPA.
|
||||
const alloc = std.heap.c_allocator;
|
||||
|
||||
// Parse our args
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
const reader = std.io.getStdIn().reader();
|
||||
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
||||
|
||||
// Handle the modes that do not depend on terminal state first.
|
||||
switch (args.mode) {
|
||||
.noop => try benchNoop(reader, buf),
|
||||
.wcwidth => try benchWcwidth(reader, buf),
|
||||
.ziglyph => try benchZiglyph(reader, buf),
|
||||
.simd => try benchSimd(reader, buf),
|
||||
.table => try benchTable(reader, buf),
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchNoop(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
_ = d.next(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "c" fn wcwidth(c: u32) c_int;
|
||||
|
||||
noinline fn benchWcwidth(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
const width = wcwidth(cp);
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchTable(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
// This is the same trick we do in terminal.zig so we
|
||||
// keep it here.
|
||||
const width = if (cp <= 0xFF) 1 else table.get(@intCast(cp)).width;
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchZiglyph(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
const width = ziglyph.display_width.codePointWidth(cp, .half);
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchSimd(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
const width = simd.codepointWidth(cp);
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This is a trivial helper script to help run the grapheme-break benchmark.
|
||||
# You probably want to tweak this script depending on what you're
|
||||
# trying to measure.
|
||||
|
||||
# Options:
|
||||
# - "ascii", uniform random ASCII bytes
|
||||
# - "utf8", uniform random unicode characters, encoded as utf8
|
||||
# - "rand", pure random data, will contain many invalid code sequences.
|
||||
DATA="utf8"
|
||||
SIZE="25000000"
|
||||
|
||||
# Add additional arguments
|
||||
ARGS=""
|
||||
|
||||
# Generate the benchmark input ahead of time so it's not included in the time.
|
||||
./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data
|
||||
#cat ~/Downloads/JAPANESEBIBLE.txt > /tmp/ghostty_bench_data
|
||||
|
||||
# Uncomment to instead use the contents of `stream.txt` as input.
|
||||
# yes $(cat ./stream.txt) | head -c $SIZE > /tmp/ghostty_bench_data
|
||||
|
||||
hyperfine \
|
||||
--warmup 10 \
|
||||
-n noop \
|
||||
"./zig-out/bin/bench-grapheme-break --mode=noop${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n ziglyph \
|
||||
"./zig-out/bin/bench-grapheme-break --mode=ziglyph${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n table \
|
||||
"./zig-out/bin/bench-grapheme-break --mode=table${ARGS} </tmp/ghostty_bench_data"
|
||||
|
||||
|
@ -1,144 +0,0 @@
|
||||
//! This benchmark tests the throughput of grapheme break calculation.
|
||||
//! This is a common operation in terminal character printing for terminals
|
||||
//! that support grapheme clustering.
|
||||
//!
|
||||
//! This will consume all of the available stdin, so you should run it
|
||||
//! with `head` in a pipe to restrict. For example, to test ASCII input:
|
||||
//!
|
||||
//! bench-stream --mode=gen-ascii | head -c 50M | bench-grapheme-break --mode=ziglyph
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const cli = @import("../cli.zig");
|
||||
const simd = @import("../simd/main.zig");
|
||||
const unicode = @import("../unicode/main.zig");
|
||||
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .noop,
|
||||
|
||||
/// The size for read buffers. Doesn't usually need to be changed. The
|
||||
/// main point is to make this runtime known so we can avoid compiler
|
||||
/// optimizations.
|
||||
@"buffer-size": usize = 4096,
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub fn deinit(self: *Args) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const Mode = enum {
|
||||
/// The baseline mode copies the data from the fd into a buffer. This
|
||||
/// is used to show the minimal overhead of reading the fd into memory
|
||||
/// and establishes a baseline for the other modes.
|
||||
noop,
|
||||
|
||||
/// Use ziglyph library to calculate the display width of each codepoint.
|
||||
ziglyph,
|
||||
|
||||
/// Ghostty's table-based approach.
|
||||
table,
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
// We want to use the c allocator because it is much faster than GPA.
|
||||
const alloc = std.heap.c_allocator;
|
||||
|
||||
// Parse our args
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
const reader = std.io.getStdIn().reader();
|
||||
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
||||
|
||||
// Handle the modes that do not depend on terminal state first.
|
||||
switch (args.mode) {
|
||||
.noop => try benchNoop(reader, buf),
|
||||
.ziglyph => try benchZiglyph(reader, buf),
|
||||
.table => try benchTable(reader, buf),
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchNoop(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
_ = d.next(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchTable(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
var state: unicode.GraphemeBreakState = .{};
|
||||
var cp1: u21 = 0;
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp2| {
|
||||
const v = unicode.graphemeBreak(cp1, @intCast(cp2), &state);
|
||||
buf[0] = @intCast(@intFromBool(v));
|
||||
cp1 = cp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchZiglyph(
|
||||
reader: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
var d: UTF8Decoder = .{};
|
||||
var state: u3 = 0;
|
||||
var cp1: u21 = 0;
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp2| {
|
||||
const v = ziglyph.graphemeBreak(cp1, @intCast(cp2), &state);
|
||||
buf[0] = @intCast(@intFromBool(v));
|
||||
cp1 = cp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This is a trivial helper script to help run the page init benchmark.
|
||||
# You probably want to tweak this script depending on what you're
|
||||
# trying to measure.
|
||||
|
||||
# Uncomment to test with an active terminal state.
|
||||
# ARGS=" --terminal"
|
||||
|
||||
hyperfine \
|
||||
--warmup 10 \
|
||||
-n alloc \
|
||||
"./zig-out/bin/bench-page-init --mode=alloc${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n pool \
|
||||
"./zig-out/bin/bench-page-init --mode=pool${ARGS} </tmp/ghostty_bench_data"
|
||||
|
@ -1,78 +0,0 @@
|
||||
//! This benchmark tests the speed to create a terminal "page". This is
|
||||
//! the internal data structure backing a terminal screen. The creation speed
|
||||
//! is important because it is one of the primary bottlenecks for processing
|
||||
//! large amounts of plaintext data.
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const cli = @import("../cli.zig");
|
||||
const terminal_new = @import("../terminal/main.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .alloc,
|
||||
|
||||
/// The number of pages to create sequentially.
|
||||
count: usize = 10_000,
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub fn deinit(self: *Args) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const Mode = enum {
|
||||
/// The default allocation strategy of the structure.
|
||||
alloc,
|
||||
|
||||
/// Use a memory pool to allocate pages from a backing buffer.
|
||||
pool,
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
// We want to use the c allocator because it is much faster than GPA.
|
||||
const alloc = std.heap.c_allocator;
|
||||
|
||||
// Parse our args
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
// Handle the modes that do not depend on terminal state first.
|
||||
switch (args.mode) {
|
||||
.alloc => try benchAlloc(args.count),
|
||||
.pool => try benchPool(alloc, args.count),
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchAlloc(count: usize) !void {
|
||||
for (0..count) |_| {
|
||||
_ = try terminal_new.Page.init(terminal_new.page.std_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchPool(alloc: Allocator, count: usize) !void {
|
||||
var list = try terminal_new.PageList.init(
|
||||
alloc,
|
||||
terminal_new.page.std_capacity.cols,
|
||||
terminal_new.page.std_capacity.rows,
|
||||
0,
|
||||
);
|
||||
defer list.deinit();
|
||||
|
||||
for (0..count) |_| {
|
||||
_ = try list.grow();
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
//! This benchmark tests the throughput of the terminal escape code parser.
|
||||
//!
|
||||
//! To benchmark, this takes an input stream (which is expected to come in
|
||||
//! as fast as possible), runs it through the parser, and does nothing
|
||||
//! with the parse result. This bottlenecks and tests the throughput of the
|
||||
//! parser.
|
||||
//!
|
||||
//! Usage:
|
||||
//!
|
||||
//! "--f=<path>" - A file to read to parse. If path is "-" then stdin
|
||||
//! is read. Required.
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const cli = @import("../cli.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
|
||||
pub fn main() !void {
|
||||
// Just use a GPA
|
||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||
var gpa = GPA{};
|
||||
defer _ = gpa.deinit();
|
||||
const alloc = gpa.allocator();
|
||||
|
||||
// Parse our args
|
||||
var args: Args = args: {
|
||||
var args: Args = .{};
|
||||
errdefer args.deinit();
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
break :args args;
|
||||
};
|
||||
defer args.deinit();
|
||||
|
||||
// Read the file for our input
|
||||
const file = file: {
|
||||
if (std.mem.eql(u8, args.f, "-"))
|
||||
break :file std.io.getStdIn();
|
||||
|
||||
@panic("file reading not implemented yet");
|
||||
};
|
||||
|
||||
// Read all into memory (TODO: support buffers one day)
|
||||
const input = try file.reader().readAllAlloc(
|
||||
alloc,
|
||||
1024 * 1024 * 1024 * 1024 * 16, // 16 GB
|
||||
);
|
||||
defer alloc.free(input);
|
||||
|
||||
// Run our parser
|
||||
var p: terminal.Parser = .{};
|
||||
for (input) |c| {
|
||||
const actions = p.next(c);
|
||||
//std.log.warn("actions={any}", .{actions});
|
||||
_ = actions;
|
||||
}
|
||||
}
|
||||
|
||||
const Args = struct {
|
||||
f: []const u8 = "-",
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub fn deinit(self: *Args) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This is a trivial helper script to help run the stream benchmark.
|
||||
# You probably want to tweak this script depending on what you're
|
||||
# trying to measure.
|
||||
|
||||
# Options:
|
||||
# - "ascii", uniform random ASCII bytes
|
||||
# - "utf8", uniform random unicode characters, encoded as utf8
|
||||
# - "rand", pure random data, will contain many invalid code sequences.
|
||||
DATA="ascii"
|
||||
SIZE="25000000"
|
||||
|
||||
# Uncomment to test with an active terminal state.
|
||||
# ARGS=" --terminal"
|
||||
|
||||
# Generate the benchmark input ahead of time so it's not included in the time.
|
||||
./zig-out/bin/bench-stream --mode=gen-$DATA | head -c $SIZE > /tmp/ghostty_bench_data
|
||||
|
||||
# Uncomment to instead use the contents of `stream.txt` as input. (Ignores SIZE)
|
||||
# echo $(cat ./stream.txt) > /tmp/ghostty_bench_data
|
||||
|
||||
hyperfine \
|
||||
--warmup 10 \
|
||||
-n memcpy \
|
||||
"./zig-out/bin/bench-stream --mode=noop${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n scalar \
|
||||
"./zig-out/bin/bench-stream --mode=scalar${ARGS} </tmp/ghostty_bench_data" \
|
||||
-n simd \
|
||||
"./zig-out/bin/bench-stream --mode=simd${ARGS} </tmp/ghostty_bench_data"
|
@ -1,253 +0,0 @@
|
||||
//! This benchmark tests the throughput of the VT stream. It has a few
|
||||
//! modes in order to test different methods of stream processing. It
|
||||
//! provides a "noop" mode to give us the `memcpy` speed.
|
||||
//!
|
||||
//! This will consume all of the available stdin, so you should run it
|
||||
//! with `head` in a pipe to restrict. For example, to test ASCII input:
|
||||
//!
|
||||
//! bench-stream --mode=gen-ascii | head -c 50M | bench-stream --mode=simd
|
||||
//!
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const cli = @import("../cli.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const synthetic = @import("../synthetic/main.zig");
|
||||
|
||||
const Args = struct {
|
||||
mode: Mode = .noop,
|
||||
|
||||
/// The PRNG seed used by the input generators.
|
||||
/// -1 uses a random seed (default)
|
||||
seed: i64 = -1,
|
||||
|
||||
/// Process input with a real terminal. This will be MUCH slower than
|
||||
/// the other modes because it has to maintain terminal state but will
|
||||
/// help get more realistic numbers.
|
||||
terminal: Terminal = .none,
|
||||
@"terminal-rows": usize = 80,
|
||||
@"terminal-cols": usize = 120,
|
||||
|
||||
/// The size for read buffers. Doesn't usually need to be changed. The
|
||||
/// main point is to make this runtime known so we can avoid compiler
|
||||
/// optimizations.
|
||||
@"buffer-size": usize = 4096,
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub fn deinit(self: *Args) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
const Terminal = enum { none, new };
|
||||
};
|
||||
|
||||
const Mode = enum {
|
||||
// Do nothing, just read from stdin into a stack-allocated buffer.
|
||||
// This is used to benchmark our base-case: it gives us our maximum
|
||||
// throughput on a basic read.
|
||||
noop,
|
||||
|
||||
// These benchmark the throughput of the terminal stream parsing
|
||||
// with and without SIMD. The "simd" option will use whatever is best
|
||||
// for the running platform.
|
||||
//
|
||||
// Note that these run through the full VT parser but do not apply
|
||||
// the operations to terminal state, so there is no terminal state
|
||||
// overhead.
|
||||
scalar,
|
||||
simd,
|
||||
|
||||
// Generate an infinite stream of random printable ASCII characters.
|
||||
@"gen-ascii",
|
||||
|
||||
// Generate an infinite stream of random printable unicode characters.
|
||||
@"gen-utf8",
|
||||
|
||||
// Generate an infinite stream of arbitrary random bytes.
|
||||
@"gen-rand",
|
||||
|
||||
// Generate an infinite stream of OSC requests. These will be mixed
|
||||
// with valid and invalid OSC requests by default, but the
|
||||
// `-valid` and `-invalid`-suffixed variants can be used to get only
|
||||
// a specific type of OSC request.
|
||||
@"gen-osc",
|
||||
@"gen-osc-valid",
|
||||
@"gen-osc-invalid",
|
||||
};
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_level = .debug,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
// We want to use the c allocator because it is much faster than GPA.
|
||||
const alloc = std.heap.c_allocator;
|
||||
|
||||
// Parse our args
|
||||
var args: Args = .{};
|
||||
defer args.deinit();
|
||||
{
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Args, alloc, &args, &iter);
|
||||
}
|
||||
|
||||
const reader = std.io.getStdIn().reader();
|
||||
const writer = std.io.getStdOut().writer();
|
||||
const buf = try alloc.alloc(u8, args.@"buffer-size");
|
||||
|
||||
// Build our RNG
|
||||
const seed: u64 = if (args.seed >= 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())));
|
||||
var prng = std.Random.DefaultPrng.init(seed);
|
||||
const rand = prng.random();
|
||||
|
||||
// Handle the modes that do not depend on terminal state first.
|
||||
switch (args.mode) {
|
||||
.@"gen-ascii" => {
|
||||
var gen: synthetic.Bytes = .{
|
||||
.rand = rand,
|
||||
.alphabet = synthetic.Bytes.Alphabet.ascii,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-utf8" => {
|
||||
var gen: synthetic.Utf8 = .{
|
||||
.rand = rand,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-rand" => {
|
||||
var gen: synthetic.Bytes = .{ .rand = rand };
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 0.5,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc-valid" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 1.0,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.@"gen-osc-invalid" => {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = 0.0,
|
||||
};
|
||||
try generate(writer, gen.generator());
|
||||
},
|
||||
|
||||
.noop => try benchNoop(reader, buf),
|
||||
|
||||
// Handle the ones that depend on terminal state next
|
||||
inline .scalar,
|
||||
.simd,
|
||||
=> |tag| switch (args.terminal) {
|
||||
.new => {
|
||||
const TerminalStream = terminal.Stream(*TerminalHandler);
|
||||
var t = try terminal.Terminal.init(alloc, .{
|
||||
.cols = @intCast(args.@"terminal-cols"),
|
||||
.rows = @intCast(args.@"terminal-rows"),
|
||||
});
|
||||
var handler: TerminalHandler = .{ .t = &t };
|
||||
var stream: TerminalStream = .{ .handler = &handler };
|
||||
switch (tag) {
|
||||
.scalar => try benchScalar(reader, &stream, buf),
|
||||
.simd => try benchSimd(reader, &stream, buf),
|
||||
else => @compileError("missing case"),
|
||||
}
|
||||
},
|
||||
|
||||
.none => {
|
||||
var stream: terminal.Stream(NoopHandler) = .{ .handler = .{} };
|
||||
switch (tag) {
|
||||
.scalar => try benchScalar(reader, &stream, buf),
|
||||
.simd => try benchSimd(reader, &stream, buf),
|
||||
else => @compileError("missing case"),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(
|
||||
writer: anytype,
|
||||
gen: synthetic.Generator,
|
||||
) !void {
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
const data = try gen.next(&buf);
|
||||
writer.writeAll(data) catch |err| switch (err) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchNoop(reader: anytype, buf: []u8) !void {
|
||||
var total: usize = 0;
|
||||
while (true) {
|
||||
const n = try reader.readAll(buf);
|
||||
if (n == 0) break;
|
||||
total += n;
|
||||
}
|
||||
|
||||
std.log.info("total bytes len={}", .{total});
|
||||
}
|
||||
|
||||
noinline fn benchScalar(
|
||||
reader: anytype,
|
||||
stream: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
|
||||
// Using stream.next directly with a for loop applies a naive
|
||||
// scalar approach.
|
||||
for (buf[0..n]) |c| try stream.next(c);
|
||||
}
|
||||
}
|
||||
|
||||
noinline fn benchSimd(
|
||||
reader: anytype,
|
||||
stream: anytype,
|
||||
buf: []u8,
|
||||
) !void {
|
||||
while (true) {
|
||||
const n = try reader.read(buf);
|
||||
if (n == 0) break;
|
||||
try stream.nextSlice(buf[0..n]);
|
||||
}
|
||||
}
|
||||
|
||||
const NoopHandler = struct {
|
||||
pub fn print(self: NoopHandler, cp: u21) !void {
|
||||
_ = self;
|
||||
_ = cp;
|
||||
}
|
||||
};
|
||||
|
||||
const TerminalHandler = struct {
|
||||
t: *terminal.Terminal,
|
||||
|
||||
pub fn print(self: *TerminalHandler, cp: u21) !void {
|
||||
try self.t.print(cp);
|
||||
}
|
||||
};
|
166
src/benchmark/Benchmark.zig
Normal file
166
src/benchmark/Benchmark.zig
Normal file
@ -0,0 +1,166 @@
|
||||
//! A single benchmark case.
|
||||
const Benchmark = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const macos = @import("macos");
|
||||
const build_config = @import("../build_config.zig");
|
||||
|
||||
ptr: *anyopaque,
|
||||
vtable: VTable,
|
||||
|
||||
/// Create a new benchmark from a pointer and a vtable.
|
||||
///
|
||||
/// This usually is only called by benchmark implementations, not
|
||||
/// benchmark users.
|
||||
pub fn init(
|
||||
pointer: anytype,
|
||||
vtable: VTable,
|
||||
) Benchmark {
|
||||
const Ptr = @TypeOf(pointer);
|
||||
assert(@typeInfo(Ptr) == .pointer); // Must be a pointer
|
||||
assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer
|
||||
assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct
|
||||
return .{ .ptr = pointer, .vtable = vtable };
|
||||
}
|
||||
|
||||
/// Run the benchmark.
|
||||
pub fn run(
|
||||
self: Benchmark,
|
||||
mode: RunMode,
|
||||
) Error!RunResult {
|
||||
// Run our setup function if it exists. We do this first because
|
||||
// we don't want this part of our benchmark and we want to fail fast.
|
||||
if (self.vtable.setupFn) |func| try func(self.ptr);
|
||||
defer if (self.vtable.teardownFn) |func| func(self.ptr);
|
||||
|
||||
// Our result accumulator. This will be returned at the end of the run.
|
||||
var result: RunResult = .{};
|
||||
|
||||
// If we're on macOS, we setup signposts so its easier to find
|
||||
// the results in Instruments. There's a lot of nasty comptime stuff
|
||||
// here but its just to ensure this does nothing on other platforms.
|
||||
const signpost_name = "ghostty";
|
||||
const signpost: if (builtin.target.os.tag.isDarwin()) struct {
|
||||
log: *macos.os.Log,
|
||||
id: macos.os.signpost.Id,
|
||||
} else void = if (builtin.target.os.tag.isDarwin()) darwin: {
|
||||
macos.os.signpost.init();
|
||||
const log = macos.os.Log.create(
|
||||
build_config.bundle_id,
|
||||
macos.os.signpost.Category.points_of_interest,
|
||||
);
|
||||
const id = macos.os.signpost.Id.forPointer(log, self.ptr);
|
||||
macos.os.signpost.intervalBegin(log, id, signpost_name);
|
||||
break :darwin .{ .log = log, .id = id };
|
||||
} else {};
|
||||
defer if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
macos.os.signpost.intervalEnd(
|
||||
signpost.log,
|
||||
signpost.id,
|
||||
signpost_name,
|
||||
);
|
||||
signpost.log.release();
|
||||
};
|
||||
|
||||
const start = std.time.Instant.now() catch return error.BenchmarkFailed;
|
||||
while (true) {
|
||||
// Run our step function. If it fails, we return the error.
|
||||
try self.vtable.stepFn(self.ptr);
|
||||
result.iterations += 1;
|
||||
|
||||
// Get our current monotonic time and check our exit conditions.
|
||||
const now = std.time.Instant.now() catch return error.BenchmarkFailed;
|
||||
const exit = switch (mode) {
|
||||
.once => true,
|
||||
.duration => |ns| now.since(start) >= ns,
|
||||
};
|
||||
|
||||
if (exit) {
|
||||
result.duration = now.since(start);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// We exit within the loop body.
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// The type of benchmark run. This is used to determine how the benchmark
|
||||
/// is executed.
|
||||
pub const RunMode = union(enum) {
|
||||
/// Run the benchmark exactly once.
|
||||
once,
|
||||
|
||||
/// Run the benchmark for a fixed duration in nanoseconds. This
|
||||
/// will not interrupt a running step so if the granularity of the
|
||||
/// duration is too low, benchmark results may be inaccurate.
|
||||
duration: u64,
|
||||
};
|
||||
|
||||
/// The result of a benchmark run.
|
||||
pub const RunResult = struct {
|
||||
/// The total iterations that step was executed. For "once" run
|
||||
/// modes this will always be 1.
|
||||
iterations: u32 = 0,
|
||||
|
||||
/// The total time taken for the run. For "duration" run modes
|
||||
/// this will be relatively close to the requested duration.
|
||||
/// The units are nanoseconds.
|
||||
duration: u64 = 0,
|
||||
};
|
||||
|
||||
/// The possible errors that can occur during various stages of the
|
||||
/// benchmark. Right now its just "failure" which ends the benchmark.
|
||||
pub const Error = error{BenchmarkFailed};
|
||||
|
||||
/// The vtable that must be provided to invoke the real implementation.
|
||||
pub const VTable = struct {
|
||||
/// A single step to execute the benchmark. This should do the work
|
||||
/// that is under test. This may be called multiple times if we're
|
||||
/// testing throughput.
|
||||
stepFn: *const fn (ptr: *anyopaque) Error!void,
|
||||
|
||||
/// Setup and teardown functions. These are called once before
|
||||
/// the first step and once after the last step. They are not part
|
||||
/// of the benchmark results (unless you're benchmarking the full
|
||||
/// binary).
|
||||
setupFn: ?*const fn (ptr: *anyopaque) Error!void = null,
|
||||
teardownFn: ?*const fn (ptr: *anyopaque) void = null,
|
||||
};
|
||||
|
||||
test Benchmark {
|
||||
const testing = std.testing;
|
||||
const Simple = struct {
|
||||
const Self = @This();
|
||||
|
||||
setup_i: usize = 0,
|
||||
step_i: usize = 0,
|
||||
|
||||
pub fn benchmark(self: *Self) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = step,
|
||||
.setupFn = setup,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Error!void {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
self.setup_i += 1;
|
||||
}
|
||||
|
||||
fn step(ptr: *anyopaque) Error!void {
|
||||
const self: *Self = @ptrCast(@alignCast(ptr));
|
||||
self.step_i += 1;
|
||||
}
|
||||
};
|
||||
|
||||
var s: Simple = .{};
|
||||
const b = s.benchmark();
|
||||
const result = try b.run(.once);
|
||||
try testing.expectEqual(1, s.setup_i);
|
||||
try testing.expectEqual(1, s.step_i);
|
||||
try testing.expectEqual(1, result.iterations);
|
||||
try testing.expect(result.duration > 0);
|
||||
}
|
34
src/benchmark/CApi.zig
Normal file
34
src/benchmark/CApi.zig
Normal file
@ -0,0 +1,34 @@
|
||||
const std = @import("std");
|
||||
const cli = @import("cli.zig");
|
||||
const state = &@import("../global.zig").state;
|
||||
|
||||
const log = std.log.scoped(.benchmark);
|
||||
|
||||
/// Run the Ghostty benchmark CLI with the given action and arguments.
|
||||
export fn ghostty_benchmark_cli(
|
||||
action_name_: [*:0]const u8,
|
||||
args: [*:0]const u8,
|
||||
) bool {
|
||||
const action_name = std.mem.sliceTo(action_name_, 0);
|
||||
const action: cli.Action = std.meta.stringToEnum(
|
||||
cli.Action,
|
||||
action_name,
|
||||
) orelse {
|
||||
log.warn("unknown action={s}", .{action_name});
|
||||
return false;
|
||||
};
|
||||
|
||||
cli.mainAction(
|
||||
state.alloc,
|
||||
action,
|
||||
.{ .string = std.mem.sliceTo(args, 0) },
|
||||
) catch |err| {
|
||||
log.warn("failed to run action={s} err={}", .{
|
||||
@tagName(action),
|
||||
err,
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
204
src/benchmark/CodepointWidth.zig
Normal file
204
src/benchmark/CodepointWidth.zig
Normal file
@ -0,0 +1,204 @@
|
||||
//! This benchmark tests the throughput of codepoint width calculation.
|
||||
//! This is a common operation in terminal character printing and the
|
||||
//! motivating factor to write this benchmark was discovering that our
|
||||
//! codepoint width function was 30% of the runtime of every character
|
||||
//! print.
|
||||
const CodepointWidth = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
|
||||
const simd = @import("../simd/main.zig");
|
||||
const table = @import("../unicode/main.zig").table;
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The type of codepoint width calculation to use.
|
||||
mode: Mode = .noop,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Mode = enum {
|
||||
/// The baseline mode copies the data from the fd into a buffer. This
|
||||
/// is used to show the minimal overhead of reading the fd into memory
|
||||
/// and establishes a baseline for the other modes.
|
||||
noop,
|
||||
|
||||
/// libc wcwidth
|
||||
wcwidth,
|
||||
|
||||
/// Our SIMD implementation.
|
||||
simd,
|
||||
|
||||
/// Test our lookup table implementation.
|
||||
table,
|
||||
};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*CodepointWidth {
|
||||
const ptr = try alloc.create(CodepointWidth);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{ .opts = opts };
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *CodepointWidth, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *CodepointWidth) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = switch (self.opts.mode) {
|
||||
.noop => stepNoop,
|
||||
.wcwidth => stepWcwidth,
|
||||
.table => stepTable,
|
||||
.simd => stepSimd,
|
||||
},
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
_ = ptr;
|
||||
}
|
||||
|
||||
extern "c" fn wcwidth(c: u32) c_int;
|
||||
|
||||
fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
const width = wcwidth(cp);
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled
|
||||
// away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
// This is the same trick we do in terminal.zig so we
|
||||
// keep it here.
|
||||
const width = if (cp <= 0xFF)
|
||||
1
|
||||
else
|
||||
table.get(@intCast(cp)).width;
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled
|
||||
// away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stepSimd(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *CodepointWidth = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp| {
|
||||
const width = simd.codepointWidth(cp);
|
||||
|
||||
// Write the width to the buffer to avoid it being compiled
|
||||
// away
|
||||
buf[0] = @intCast(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test CodepointWidth {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *CodepointWidth = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
146
src/benchmark/GraphemeBreak.zig
Normal file
146
src/benchmark/GraphemeBreak.zig
Normal file
@ -0,0 +1,146 @@
|
||||
//! This benchmark tests the throughput of grapheme break calculation.
|
||||
//! This is a common operation in terminal character printing for terminals
|
||||
//! that support grapheme clustering.
|
||||
const GraphemeBreak = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const UTF8Decoder = @import("../terminal/UTF8Decoder.zig");
|
||||
const unicode = @import("../unicode/main.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The type of codepoint width calculation to use.
|
||||
mode: Mode = .table,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const Mode = enum {
|
||||
/// The baseline mode copies the data from the fd into a buffer. This
|
||||
/// is used to show the minimal overhead of reading the fd into memory
|
||||
/// and establishes a baseline for the other modes.
|
||||
noop,
|
||||
|
||||
/// Ghostty's table-based approach.
|
||||
table,
|
||||
};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*GraphemeBreak {
|
||||
const ptr = try alloc.create(GraphemeBreak);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{ .opts = opts };
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *GraphemeBreak, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *GraphemeBreak) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = switch (self.opts.mode) {
|
||||
.noop => stepNoop,
|
||||
.table => stepTable,
|
||||
},
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn stepNoop(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
var d: UTF8Decoder = .{};
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
for (buf[0..n]) |c| {
|
||||
_ = d.next(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stepTable(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *GraphemeBreak = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
var d: UTF8Decoder = .{};
|
||||
var state: unicode.GraphemeBreakState = .{};
|
||||
var cp1: u21 = 0;
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
|
||||
for (buf[0..n]) |c| {
|
||||
const cp_, const consumed = d.next(c);
|
||||
assert(consumed);
|
||||
if (cp_) |cp2| {
|
||||
const v = unicode.graphemeBreak(cp1, @intCast(cp2), &state);
|
||||
buf[0] = @intCast(@intFromBool(v));
|
||||
cp1 = cp2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test GraphemeBreak {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *GraphemeBreak = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
106
src/benchmark/TerminalParser.zig
Normal file
106
src/benchmark/TerminalParser.zig
Normal file
@ -0,0 +1,106 @@
|
||||
//! This benchmark tests the throughput of the terminal escape code parser.
|
||||
const TerminalParser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const terminalpkg = @import("../terminal/main.zig");
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*TerminalParser {
|
||||
const ptr = try alloc.create(TerminalParser);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{ .opts = opts };
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *TerminalParser, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *TerminalParser) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = step,
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *TerminalParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *TerminalParser = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *TerminalParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Get our buffered reader so we're not predominantly
|
||||
// waiting on file IO. It'd be better to move this fully into
|
||||
// memory. If we're IO bound though that should show up on
|
||||
// the benchmark results and... I know writing this that we
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
|
||||
var p: terminalpkg.Parser = .{};
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
for (buf[0..n]) |c| {
|
||||
const actions = p.next(c);
|
||||
//std.log.warn("actions={any}", .{actions});
|
||||
_ = actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test TerminalParser {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *TerminalParser = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
153
src/benchmark/TerminalStream.zig
Normal file
153
src/benchmark/TerminalStream.zig
Normal file
@ -0,0 +1,153 @@
|
||||
//! This benchmark tests the performance of the terminal stream
|
||||
//! handler from input to terminal state update. This is useful to
|
||||
//! test general throughput of VT parsing and handling.
|
||||
//!
|
||||
//! Note that the handler used for this benchmark isn't the full
|
||||
//! terminal handler, since that requires a significant amount of
|
||||
//! state. This is a simplified version that only handles specific
|
||||
//! terminal operations like printing characters. We should expand
|
||||
//! this to include more operations to improve the accuracy of the
|
||||
//! benchmark.
|
||||
//!
|
||||
//! It is a fairly broad benchmark that can be used to determine
|
||||
//! if we need to optimize something more specific (e.g. the parser).
|
||||
const TerminalStream = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const terminalpkg = @import("../terminal/main.zig");
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const Terminal = terminalpkg.Terminal;
|
||||
const Stream = terminalpkg.Stream(*Handler);
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
opts: Options,
|
||||
terminal: Terminal,
|
||||
handler: Handler,
|
||||
stream: Stream,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The size of the terminal. This affects benchmarking when
|
||||
/// dealing with soft line wrapping and the memory impact
|
||||
/// of page sizes.
|
||||
@"terminal-rows": u16 = 80,
|
||||
@"terminal-cols": u16 = 120,
|
||||
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*TerminalStream {
|
||||
const ptr = try alloc.create(TerminalStream);
|
||||
errdefer alloc.destroy(ptr);
|
||||
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.terminal = try .init(alloc, .{
|
||||
.rows = opts.@"terminal-rows",
|
||||
.cols = opts.@"terminal-cols",
|
||||
}),
|
||||
.handler = .{ .t = &ptr.terminal },
|
||||
.stream = .{ .handler = &ptr.handler },
|
||||
};
|
||||
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *TerminalStream, alloc: Allocator) void {
|
||||
self.terminal.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *TerminalStream) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = step,
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *TerminalStream = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Always reset our terminal state
|
||||
self.terminal.fullReset();
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *TerminalStream = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *TerminalStream = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Get our buffered reader so we're not predominantly
|
||||
// waiting on file IO. It'd be better to move this fully into
|
||||
// memory. If we're IO bound though that should show up on
|
||||
// the benchmark results and... I know writing this that we
|
||||
// aren't currently IO bound.
|
||||
const f = self.data_f orelse return;
|
||||
var r = std.io.bufferedReader(f.reader());
|
||||
|
||||
var buf: [4096]u8 = undefined;
|
||||
while (true) {
|
||||
const n = r.read(&buf) catch |err| {
|
||||
log.warn("error reading data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
if (n == 0) break; // EOF reached
|
||||
const chunk = buf[0..n];
|
||||
self.stream.nextSlice(chunk) catch |err| {
|
||||
log.warn("error processing data file chunk err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements the handler interface for the terminal.Stream.
|
||||
/// We should expand this to include more operations to make
|
||||
/// our benchmark more realistic.
|
||||
const Handler = struct {
|
||||
t: *Terminal,
|
||||
|
||||
pub fn print(self: *Handler, cp: u21) !void {
|
||||
try self.t.print(cp);
|
||||
}
|
||||
};
|
||||
|
||||
test TerminalStream {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *TerminalStream = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
94
src/benchmark/cli.zig
Normal file
94
src/benchmark/cli.zig
Normal file
@ -0,0 +1,94 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
/// The available actions for the CLI. This is the list of available
|
||||
/// benchmarks. View docs for each individual one in the predictably
|
||||
/// named files.
|
||||
pub const Action = enum {
|
||||
@"codepoint-width",
|
||||
@"grapheme-break",
|
||||
@"terminal-parser",
|
||||
@"terminal-stream",
|
||||
|
||||
/// Returns the struct associated with the action. The struct
|
||||
/// should have a few decls:
|
||||
///
|
||||
/// - `const Options`: The CLI options for the action.
|
||||
/// - `fn create`: Create a new instance of the action from options.
|
||||
/// - `fn benchmark`: Returns a `Benchmark` instance for the action.
|
||||
///
|
||||
/// See TerminalStream for an example.
|
||||
pub fn Struct(comptime action: Action) type {
|
||||
return switch (action) {
|
||||
.@"terminal-stream" => @import("TerminalStream.zig"),
|
||||
.@"codepoint-width" => @import("CodepointWidth.zig"),
|
||||
.@"grapheme-break" => @import("GraphemeBreak.zig"),
|
||||
.@"terminal-parser" => @import("TerminalParser.zig"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// An entrypoint for the benchmark CLI.
|
||||
pub fn main() !void {
|
||||
const alloc = std.heap.c_allocator;
|
||||
const action_ = try cli.action.detectArgs(Action, alloc);
|
||||
const action = action_ orelse return error.NoAction;
|
||||
try mainAction(alloc, action, .cli);
|
||||
}
|
||||
|
||||
/// Arguments that can be passed to the benchmark.
|
||||
pub const Args = union(enum) {
|
||||
/// The arguments passed to the CLI via argc/argv.
|
||||
cli,
|
||||
|
||||
/// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
pub fn mainAction(
|
||||
alloc: Allocator,
|
||||
action: Action,
|
||||
args: Args,
|
||||
) !void {
|
||||
switch (action) {
|
||||
inline else => |comptime_action| {
|
||||
const BenchmarkImpl = Action.Struct(comptime_action);
|
||||
try mainActionImpl(BenchmarkImpl, alloc, args);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mainActionImpl(
|
||||
comptime BenchmarkImpl: type,
|
||||
alloc: Allocator,
|
||||
args: Args,
|
||||
) !void {
|
||||
// First, parse our CLI options.
|
||||
const Options = BenchmarkImpl.Options;
|
||||
var opts: Options = .{};
|
||||
defer if (@hasDecl(Options, "deinit")) opts.deinit();
|
||||
switch (args) {
|
||||
.cli => {
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Options, alloc, &opts, &iter);
|
||||
},
|
||||
.string => |str| {
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
str,
|
||||
);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Options, alloc, &opts, &iter);
|
||||
},
|
||||
}
|
||||
|
||||
// Create our implementation
|
||||
const impl = try BenchmarkImpl.create(alloc, opts);
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
// Initialize our benchmark
|
||||
const b = impl.benchmark();
|
||||
_ = try b.run(.once);
|
||||
}
|
11
src/benchmark/main.zig
Normal file
11
src/benchmark/main.zig
Normal file
@ -0,0 +1,11 @@
|
||||
pub const cli = @import("cli.zig");
|
||||
pub const Benchmark = @import("Benchmark.zig");
|
||||
pub const CApi = @import("CApi.zig");
|
||||
pub const TerminalStream = @import("TerminalStream.zig");
|
||||
pub const CodepointWidth = @import("CodepointWidth.zig");
|
||||
pub const GraphemeBreak = @import("GraphemeBreak.zig");
|
||||
pub const TerminalParser = @import("TerminalParser.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
20
src/benchmark/options.zig
Normal file
20
src/benchmark/options.zig
Normal file
@ -0,0 +1,20 @@
|
||||
//! This file contains helpers for CLI options.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Returns the data file for the given path in a way that is consistent
|
||||
/// across our CLI. If the path is not set then no file is returned.
|
||||
/// If the path is "-", then we will return stdin. If the path is
|
||||
/// a file then we will open and return the handle.
|
||||
pub fn dataFile(path_: ?[]const u8) !?std.fs.File {
|
||||
const path = path_ orelse return null;
|
||||
|
||||
// Stdin
|
||||
if (std.mem.eql(u8, path, "-")) return std.io.getStdIn();
|
||||
|
||||
// Normal file
|
||||
const file = try std.fs.cwd().openFile(path, .{});
|
||||
errdefer file.close();
|
||||
|
||||
return file;
|
||||
}
|
@ -528,11 +528,6 @@ pub const ExeEntrypoint = enum {
|
||||
webgen_config,
|
||||
webgen_actions,
|
||||
webgen_commands,
|
||||
bench_parser,
|
||||
bench_stream,
|
||||
bench_codepoint_width,
|
||||
bench_grapheme_break,
|
||||
bench_page_init,
|
||||
};
|
||||
|
||||
/// The release channel for the build.
|
||||
|
@ -14,52 +14,37 @@ pub fn init(
|
||||
var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
|
||||
errdefer steps.deinit();
|
||||
|
||||
// Open the directory ./src/bench
|
||||
const c_dir_path = b.pathFromRoot("src/bench");
|
||||
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });
|
||||
defer c_dir.close();
|
||||
|
||||
// Go through and add each as a step
|
||||
var c_dir_it = c_dir.iterate();
|
||||
while (try c_dir_it.next()) |entry| {
|
||||
// Get the index of the last '.' so we can strip the extension.
|
||||
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
|
||||
if (index == 0) continue;
|
||||
|
||||
// If it doesn't end in 'zig' then ignore
|
||||
if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue;
|
||||
|
||||
// Name of the conformance app and full path to the entrypoint.
|
||||
const name = entry.name[0..index];
|
||||
|
||||
// Executable builder.
|
||||
const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name});
|
||||
const c_exe = b.addExecutable(.{
|
||||
.name = bin_name,
|
||||
// Our synthetic data generator
|
||||
{
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ghostty-gen",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.root_source_file = b.path("src/main_gen.zig"),
|
||||
.target = deps.config.target,
|
||||
// We always want our datagen to be fast because it
|
||||
// takes awhile to run.
|
||||
.optimize = .ReleaseFast,
|
||||
}),
|
||||
});
|
||||
exe.linkLibC();
|
||||
_ = try deps.add(exe);
|
||||
try steps.append(exe);
|
||||
}
|
||||
|
||||
// Our benchmarking application.
|
||||
{
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "ghostty-bench",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_bench.zig"),
|
||||
.target = deps.config.target,
|
||||
// We always want our benchmarks to be in release mode.
|
||||
.optimize = .ReleaseFast,
|
||||
}),
|
||||
});
|
||||
c_exe.linkLibC();
|
||||
|
||||
// Update our entrypoint
|
||||
var enum_name: [64]u8 = undefined;
|
||||
@memcpy(enum_name[0..name.len], name);
|
||||
std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_');
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
const new_deps = try deps.changeEntrypoint(b, std.meta.stringToEnum(
|
||||
Config.ExeEntrypoint,
|
||||
try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}),
|
||||
).?);
|
||||
|
||||
_ = try new_deps.add(c_exe);
|
||||
|
||||
try steps.append(c_exe);
|
||||
exe.linkLibC();
|
||||
_ = try deps.add(exe);
|
||||
try steps.append(exe);
|
||||
}
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
|
@ -115,7 +115,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
|
||||
// Capture stderr so it doesn't spew into the parent build.
|
||||
// On the flip side, if the test fails we won't know why so
|
||||
// that sucks but we should have already ran tests at this point.
|
||||
_ = step.captureStdErr();
|
||||
// NOTE(mitchellh): temporarily disabled to diagnose heisenbug
|
||||
//_ = step.captureStdErr();
|
||||
|
||||
break :step step;
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
const GhosttyLib = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const RunStep = std.Build.Step.Run;
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
const LibtoolStep = @import("LibtoolStep.zig");
|
||||
@ -11,6 +12,7 @@ step: *std.Build.Step,
|
||||
|
||||
/// The final static library file
|
||||
output: std.Build.LazyPath,
|
||||
dsym: ?std.Build.LazyPath,
|
||||
|
||||
pub fn initStatic(
|
||||
b: *std.Build,
|
||||
@ -18,9 +20,14 @@ pub fn initStatic(
|
||||
) !GhosttyLib {
|
||||
const lib = b.addStaticLibrary(.{
|
||||
.name = "ghostty",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_c.zig"),
|
||||
.target = deps.config.target,
|
||||
.optimize = deps.config.optimize,
|
||||
.strip = deps.config.strip,
|
||||
.omit_frame_pointer = deps.config.strip,
|
||||
.unwind_tables = if (deps.config.strip) .none else .sync,
|
||||
}),
|
||||
});
|
||||
lib.linkLibC();
|
||||
|
||||
@ -37,6 +44,7 @@ pub fn initStatic(
|
||||
if (!deps.config.target.result.os.tag.isDarwin()) return .{
|
||||
.step = &lib.step,
|
||||
.output = lib.getEmittedBin(),
|
||||
.dsym = null,
|
||||
};
|
||||
|
||||
// Create a static lib that contains all our dependencies.
|
||||
@ -50,6 +58,9 @@ pub fn initStatic(
|
||||
return .{
|
||||
.step = libtool.step,
|
||||
.output = libtool.output,
|
||||
|
||||
// Static libraries cannot have dSYMs because they aren't linked.
|
||||
.dsym = null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -59,16 +70,35 @@ pub fn initShared(
|
||||
) !GhosttyLib {
|
||||
const lib = b.addSharedLibrary(.{
|
||||
.name = "ghostty",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main_c.zig"),
|
||||
.target = deps.config.target,
|
||||
.optimize = deps.config.optimize,
|
||||
.strip = deps.config.strip,
|
||||
.omit_frame_pointer = deps.config.strip,
|
||||
.unwind_tables = if (deps.config.strip) .none else .sync,
|
||||
}),
|
||||
});
|
||||
_ = try deps.add(lib);
|
||||
|
||||
// Get our debug symbols
|
||||
const dsymutil: ?std.Build.LazyPath = dsymutil: {
|
||||
if (!deps.config.target.result.os.tag.isDarwin()) {
|
||||
break :dsymutil null;
|
||||
}
|
||||
|
||||
const dsymutil = RunStep.create(b, "dsymutil");
|
||||
dsymutil.addArgs(&.{"dsymutil"});
|
||||
dsymutil.addFileArg(lib.getEmittedBin());
|
||||
dsymutil.addArgs(&.{"-o"});
|
||||
const output = dsymutil.addOutputFileArg("libghostty.dSYM");
|
||||
break :dsymutil output;
|
||||
};
|
||||
|
||||
return .{
|
||||
.step = &lib.step,
|
||||
.output = lib.getEmittedBin(),
|
||||
.dsym = dsymutil,
|
||||
};
|
||||
}
|
||||
|
||||
@ -95,6 +125,10 @@ pub fn initMacOSUniversal(
|
||||
return .{
|
||||
.step = universal.step,
|
||||
.output = universal.output,
|
||||
|
||||
// You can't run dsymutil on a universal binary, you have to
|
||||
// do it on the individual binaries.
|
||||
.dsym = null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -64,20 +64,24 @@ pub fn init(
|
||||
.{
|
||||
.library = macos_universal.output,
|
||||
.headers = b.path("include"),
|
||||
.dsym = macos_universal.dsym,
|
||||
},
|
||||
.{
|
||||
.library = ios.output,
|
||||
.headers = b.path("include"),
|
||||
.dsym = ios.dsym,
|
||||
},
|
||||
.{
|
||||
.library = ios_sim.output,
|
||||
.headers = b.path("include"),
|
||||
.dsym = ios_sim.dsym,
|
||||
},
|
||||
},
|
||||
|
||||
.native => &.{.{
|
||||
.library = macos_native.output,
|
||||
.headers = b.path("include"),
|
||||
.dsym = macos_native.dsym,
|
||||
}},
|
||||
},
|
||||
});
|
||||
|
@ -12,6 +12,7 @@ const XCFramework = @import("GhosttyXCFramework.zig");
|
||||
build: *std.Build.Step.Run,
|
||||
open: *std.Build.Step.Run,
|
||||
copy: *std.Build.Step.Run,
|
||||
xctest: *std.Build.Step.Run,
|
||||
|
||||
pub const Deps = struct {
|
||||
xcframework: *const XCFramework,
|
||||
@ -33,6 +34,21 @@ pub fn init(
|
||||
=> "Release",
|
||||
};
|
||||
|
||||
const xc_arch: ?[]const u8 = switch (deps.xcframework.target) {
|
||||
// Universal is our default target, so we don't have to
|
||||
// add anything.
|
||||
.universal => null,
|
||||
|
||||
// Native we need to override the architecture in the Xcode
|
||||
// project with the -arch flag.
|
||||
.native => switch (builtin.cpu.arch) {
|
||||
.aarch64 => "arm64",
|
||||
.x86_64 => "x86_64",
|
||||
else => @panic("unsupported macOS arch"),
|
||||
},
|
||||
};
|
||||
|
||||
const env = try std.process.getEnvMap(b.allocator);
|
||||
const app_path = b.fmt("macos/build/{s}/Ghostty.app", .{xc_config});
|
||||
|
||||
// Our step to build the Ghostty macOS app.
|
||||
@ -41,12 +57,13 @@ pub fn init(
|
||||
// we create a new empty environment.
|
||||
const env_map = try b.allocator.create(std.process.EnvMap);
|
||||
env_map.* = .init(b.allocator);
|
||||
if (env.get("PATH")) |v| try env_map.put("PATH", v);
|
||||
|
||||
const build = RunStep.create(b, "xcodebuild");
|
||||
build.has_side_effects = true;
|
||||
build.cwd = b.path("macos");
|
||||
build.env_map = env_map;
|
||||
build.addArgs(&.{
|
||||
const step = RunStep.create(b, "xcodebuild");
|
||||
step.has_side_effects = true;
|
||||
step.cwd = b.path("macos");
|
||||
step.env_map = env_map;
|
||||
step.addArgs(&.{
|
||||
"xcodebuild",
|
||||
"-target",
|
||||
"Ghostty",
|
||||
@ -54,36 +71,55 @@ pub fn init(
|
||||
xc_config,
|
||||
});
|
||||
|
||||
switch (deps.xcframework.target) {
|
||||
// Universal is our default target, so we don't have to
|
||||
// add anything.
|
||||
.universal => {},
|
||||
|
||||
// Native we need to override the architecture in the Xcode
|
||||
// project with the -arch flag.
|
||||
.native => build.addArgs(&.{
|
||||
"-arch",
|
||||
switch (builtin.cpu.arch) {
|
||||
.aarch64 => "arm64",
|
||||
.x86_64 => "x86_64",
|
||||
else => @panic("unsupported macOS arch"),
|
||||
},
|
||||
}),
|
||||
}
|
||||
// If we have a specific architecture, we need to pass it
|
||||
// to xcodebuild.
|
||||
if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch });
|
||||
|
||||
// We need the xcframework
|
||||
deps.xcframework.addStepDependencies(&build.step);
|
||||
deps.xcframework.addStepDependencies(&step.step);
|
||||
|
||||
// We also need all these resources because the xcode project
|
||||
// references them via symlinks.
|
||||
deps.resources.addStepDependencies(&build.step);
|
||||
deps.i18n.addStepDependencies(&build.step);
|
||||
deps.docs.installDummy(&build.step);
|
||||
deps.resources.addStepDependencies(&step.step);
|
||||
deps.i18n.addStepDependencies(&step.step);
|
||||
deps.docs.installDummy(&step.step);
|
||||
|
||||
// Expect success
|
||||
build.expectExitCode(0);
|
||||
step.expectExitCode(0);
|
||||
|
||||
break :build build;
|
||||
break :build step;
|
||||
};
|
||||
|
||||
const xctest = xctest: {
|
||||
const env_map = try b.allocator.create(std.process.EnvMap);
|
||||
env_map.* = .init(b.allocator);
|
||||
if (env.get("PATH")) |v| try env_map.put("PATH", v);
|
||||
|
||||
const step = RunStep.create(b, "xcodebuild test");
|
||||
step.has_side_effects = true;
|
||||
step.cwd = b.path("macos");
|
||||
step.env_map = env_map;
|
||||
step.addArgs(&.{
|
||||
"xcodebuild",
|
||||
"test",
|
||||
"-scheme",
|
||||
"Ghostty",
|
||||
});
|
||||
if (xc_arch) |arch| step.addArgs(&.{ "-arch", arch });
|
||||
|
||||
// We need the xcframework
|
||||
deps.xcframework.addStepDependencies(&step.step);
|
||||
|
||||
// We also need all these resources because the xcode project
|
||||
// references them via symlinks.
|
||||
deps.resources.addStepDependencies(&step.step);
|
||||
deps.i18n.addStepDependencies(&step.step);
|
||||
deps.docs.installDummy(&step.step);
|
||||
|
||||
// Expect success
|
||||
step.expectExitCode(0);
|
||||
|
||||
break :xctest step;
|
||||
};
|
||||
|
||||
// Our step to open the resulting Ghostty app.
|
||||
@ -143,6 +179,7 @@ pub fn init(
|
||||
.build = build,
|
||||
.open = open,
|
||||
.copy = copy,
|
||||
.xctest = xctest,
|
||||
};
|
||||
}
|
||||
|
||||
@ -155,3 +192,10 @@ pub fn installXcframework(self: *const Ghostty) void {
|
||||
const b = self.build.step.owner;
|
||||
b.getInstallStep().dependOn(&self.build.step);
|
||||
}
|
||||
|
||||
pub fn addTestStepDependencies(
|
||||
self: *const Ghostty,
|
||||
other_step: *std.Build.Step,
|
||||
) void {
|
||||
other_step.dependOn(&self.xctest.step);
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ pub fn add(
|
||||
if (b.lazyDependency("harfbuzz", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-freetype" = true,
|
||||
.@"enable-freetype" = self.config.font_backend.hasFreetype(),
|
||||
.@"enable-coretext" = self.config.font_backend.hasCoretext(),
|
||||
})) |harfbuzz_dep| {
|
||||
step.root_module.addImport(
|
||||
|
@ -26,6 +26,9 @@ pub const Library = struct {
|
||||
|
||||
/// Path to a directory with the headers.
|
||||
headers: LazyPath,
|
||||
|
||||
/// Path to a debug symbols file (.dSYM) if available.
|
||||
dsym: ?LazyPath,
|
||||
};
|
||||
|
||||
step: *Step,
|
||||
@ -52,6 +55,10 @@ pub fn create(b: *std.Build, opts: Options) *XCFrameworkStep {
|
||||
run.addFileArg(lib.library);
|
||||
run.addArg("-headers");
|
||||
run.addFileArg(lib.headers);
|
||||
if (lib.dsym) |dsym| {
|
||||
run.addArg("-debug-symbols");
|
||||
run.addFileArg(dsym);
|
||||
}
|
||||
}
|
||||
run.addArg("-output");
|
||||
run.addArg(opts.out_path);
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Config = @import("../config/Config.zig");
|
||||
const Action = @import("../cli/action.zig").Action;
|
||||
const Action = @import("../cli.zig").ghostty.Action;
|
||||
|
||||
/// A bash completions configuration that contains all the available commands
|
||||
/// and options.
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Config = @import("../config/Config.zig");
|
||||
const Action = @import("../cli/action.zig").Action;
|
||||
const Action = @import("../cli.zig").ghostty.Action;
|
||||
|
||||
/// A fish completions configuration that contains all the available commands
|
||||
/// and options.
|
||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const help_strings = @import("help_strings");
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const Config = @import("../../config/Config.zig");
|
||||
const Action = @import("../../cli/action.zig").Action;
|
||||
const Action = @import("../../cli/ghostty.zig").Action;
|
||||
const KeybindAction = @import("../../input/Binding.zig").Action;
|
||||
|
||||
pub fn substitute(alloc: std.mem.Allocator, input: []const u8, writer: anytype) !void {
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Config = @import("../config/Config.zig");
|
||||
const Action = @import("../cli/action.zig").Action;
|
||||
const Action = @import("../cli.zig").ghostty.Action;
|
||||
|
||||
/// A zsh completions configuration that contains all the available commands
|
||||
/// and options.
|
||||
|
@ -1,7 +1,8 @@
|
||||
const diags = @import("cli/diagnostics.zig");
|
||||
|
||||
pub const args = @import("cli/args.zig");
|
||||
pub const Action = @import("cli/action.zig").Action;
|
||||
pub const action = @import("cli/action.zig");
|
||||
pub const ghostty = @import("cli/ghostty.zig");
|
||||
pub const CompatibilityHandler = args.CompatibilityHandler;
|
||||
pub const compatibilityRenamed = args.compatibilityRenamed;
|
||||
pub const DiagnosticList = diags.DiagnosticList;
|
||||
|
@ -1,314 +1,277 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const help_strings = @import("help_strings");
|
||||
|
||||
const list_fonts = @import("list_fonts.zig");
|
||||
const help = @import("help.zig");
|
||||
const version = @import("version.zig");
|
||||
const list_keybinds = @import("list_keybinds.zig");
|
||||
const list_themes = @import("list_themes.zig");
|
||||
const list_colors = @import("list_colors.zig");
|
||||
const list_actions = @import("list_actions.zig");
|
||||
const edit_config = @import("edit_config.zig");
|
||||
const show_config = @import("show_config.zig");
|
||||
const validate_config = @import("validate_config.zig");
|
||||
const crash_report = @import("crash_report.zig");
|
||||
const show_face = @import("show_face.zig");
|
||||
const boo = @import("boo.zig");
|
||||
|
||||
/// Special commands that can be invoked via CLI flags. These are all
|
||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
||||
/// "version" which can be invoked additionally with `--version`.
|
||||
pub const Action = enum {
|
||||
/// Output the version and exit
|
||||
version,
|
||||
|
||||
/// Output help information for the CLI or configuration
|
||||
help,
|
||||
|
||||
/// List available fonts
|
||||
@"list-fonts",
|
||||
|
||||
/// List available keybinds
|
||||
@"list-keybinds",
|
||||
|
||||
/// List available themes
|
||||
@"list-themes",
|
||||
|
||||
/// List named RGB colors
|
||||
@"list-colors",
|
||||
|
||||
/// List keybind actions
|
||||
@"list-actions",
|
||||
|
||||
/// Edit the config file in the configured terminal editor.
|
||||
@"edit-config",
|
||||
|
||||
/// Dump the config to stdout
|
||||
@"show-config",
|
||||
|
||||
// Validate passed config file
|
||||
@"validate-config",
|
||||
|
||||
// Show which font face Ghostty loads a codepoint from.
|
||||
@"show-face",
|
||||
|
||||
// List, (eventually) view, and (eventually) send crash reports.
|
||||
@"crash-report",
|
||||
|
||||
// Boo!
|
||||
boo,
|
||||
|
||||
pub const Error = error{
|
||||
pub const DetectError = error{
|
||||
/// Multiple actions were detected. You can specify at most one
|
||||
/// action on the CLI otherwise the behavior desired is ambiguous.
|
||||
MultipleActions,
|
||||
|
||||
/// An unknown action was specified.
|
||||
InvalidAction,
|
||||
};
|
||||
};
|
||||
|
||||
/// This should be returned by actions that want to print the help text.
|
||||
pub const help_error = error.ActionHelpRequested;
|
||||
|
||||
/// Detect the action from CLI args.
|
||||
pub fn detectCLI(alloc: Allocator) !?Action {
|
||||
/// Detect the action from CLI args.
|
||||
pub fn detectArgs(comptime E: type, alloc: Allocator) !?E {
|
||||
var iter = try std.process.argsWithAllocator(alloc);
|
||||
defer iter.deinit();
|
||||
return try detectIter(&iter);
|
||||
}
|
||||
return try detectIter(E, &iter);
|
||||
}
|
||||
|
||||
/// Detect the action from any iterator, used primarily for tests.
|
||||
pub fn detectIter(iter: anytype) Error!?Action {
|
||||
var pending_help: bool = false;
|
||||
var pending: ?Action = null;
|
||||
/// Detect the action from any iterator. Each iterator value should yield
|
||||
/// a CLI argument such as "--foo".
|
||||
///
|
||||
/// The comptime type E must be an enum with the available actions.
|
||||
/// If the type E has a decl `detectSpecialCase`, then it will be called
|
||||
/// for each argument to allow handling of special cases. The function
|
||||
/// signature for `detectSpecialCase` should be:
|
||||
///
|
||||
/// fn detectSpecialCase(arg: []const u8) ?SpecialCase(E)
|
||||
///
|
||||
pub fn detectIter(
|
||||
comptime E: type,
|
||||
iter: anytype,
|
||||
) DetectError!?E {
|
||||
var fallback: ?E = null;
|
||||
var pending: ?E = null;
|
||||
while (iter.next()) |arg| {
|
||||
// If we see a "-e" and we haven't seen a command yet, then
|
||||
// we are done looking for commands. This special case enables
|
||||
// `ghostty -e ghostty +command`. If we've seen a command we
|
||||
// still want to keep looking because
|
||||
// `ghostty +command -e +command` is invalid.
|
||||
if (std.mem.eql(u8, arg, "-e") and pending == null) return null;
|
||||
|
||||
// Special case, --version always outputs the version no
|
||||
// matter what, no matter what other args exist.
|
||||
if (std.mem.eql(u8, arg, "--version")) return .version;
|
||||
|
||||
// --help matches "help" but if a subcommand is specified
|
||||
// then we match the subcommand.
|
||||
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||
pending_help = true;
|
||||
continue;
|
||||
// Allow handling of special cases.
|
||||
if (@hasDecl(E, "detectSpecialCase")) special: {
|
||||
const special = E.detectSpecialCase(arg) orelse break :special;
|
||||
switch (special) {
|
||||
.action => |a| return a,
|
||||
.fallback => |a| fallback = a,
|
||||
.abort_if_no_action => if (pending == null) return null,
|
||||
}
|
||||
}
|
||||
|
||||
// Commands must start with "+"
|
||||
if (arg.len == 0 or arg[0] != '+') continue;
|
||||
if (pending != null) return Error.MultipleActions;
|
||||
pending = std.meta.stringToEnum(Action, arg[1..]) orelse return Error.InvalidAction;
|
||||
if (pending != null) return DetectError.MultipleActions;
|
||||
pending = std.meta.stringToEnum(E, arg[1..]) orelse
|
||||
return DetectError.InvalidAction;
|
||||
}
|
||||
|
||||
// If we have an action, we always return that action, even if we've
|
||||
// seen "--help" or "-h" because the action may have its own help text.
|
||||
if (pending != null) return pending;
|
||||
|
||||
// If we've seen "--help" or "-h" then we return the help action.
|
||||
if (pending_help) return .help;
|
||||
// If we have no action but we have a fallback, then we return that.
|
||||
if (fallback) |a| return a;
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
/// Run the action. This returns the exit code to exit with.
|
||||
pub fn run(self: Action, alloc: Allocator) !u8 {
|
||||
return self.runMain(alloc) catch |err| switch (err) {
|
||||
// If help is requested, then we use some comptime trickery
|
||||
// to find this action in the help strings and output that.
|
||||
help_error => err: {
|
||||
inline for (@typeInfo(Action).@"enum".fields) |field| {
|
||||
// Future note: for now we just output the help text directly
|
||||
// to stdout. In the future we can style this much prettier
|
||||
// for all commands by just changing this one place.
|
||||
|
||||
if (std.mem.eql(u8, field.name, @tagName(self))) {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const text = @field(help_strings.Action, field.name) ++ "\n";
|
||||
stdout.writeAll(text) catch |write_err| {
|
||||
std.log.warn("failed to write help text: {}\n", .{write_err});
|
||||
break :err 1;
|
||||
};
|
||||
|
||||
break :err 0;
|
||||
}
|
||||
}
|
||||
|
||||
break :err err;
|
||||
},
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
fn runMain(self: Action, alloc: Allocator) !u8 {
|
||||
return switch (self) {
|
||||
.version => try version.run(alloc),
|
||||
.help => try help.run(alloc),
|
||||
.@"list-fonts" => try list_fonts.run(alloc),
|
||||
.@"list-keybinds" => try list_keybinds.run(alloc),
|
||||
.@"list-themes" => try list_themes.run(alloc),
|
||||
.@"list-colors" => try list_colors.run(alloc),
|
||||
.@"list-actions" => try list_actions.run(alloc),
|
||||
.@"edit-config" => try edit_config.run(alloc),
|
||||
.@"show-config" => try show_config.run(alloc),
|
||||
.@"validate-config" => try validate_config.run(alloc),
|
||||
.@"crash-report" => try crash_report.run(alloc),
|
||||
.@"show-face" => try show_face.run(alloc),
|
||||
.boo => try boo.run(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the filename associated with an action. This is a relative
|
||||
/// path from the root src/ directory.
|
||||
pub fn file(comptime self: Action) []const u8 {
|
||||
comptime {
|
||||
const filename = filename: {
|
||||
const tag = @tagName(self);
|
||||
var filename: [tag.len]u8 = undefined;
|
||||
_ = std.mem.replace(u8, tag, "-", "_", &filename);
|
||||
break :filename &filename;
|
||||
};
|
||||
|
||||
return "cli/" ++ filename ++ ".zig";
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options of action. Supports generating shell completions
|
||||
/// without duplicating the mapping from Action to relevant Option
|
||||
/// @import(..) declaration.
|
||||
pub fn options(comptime self: Action) type {
|
||||
comptime {
|
||||
return switch (self) {
|
||||
.version => version.Options,
|
||||
.help => help.Options,
|
||||
.@"list-fonts" => list_fonts.Options,
|
||||
.@"list-keybinds" => list_keybinds.Options,
|
||||
.@"list-themes" => list_themes.Options,
|
||||
.@"list-colors" => list_colors.Options,
|
||||
.@"list-actions" => list_actions.Options,
|
||||
.@"edit-config" => edit_config.Options,
|
||||
.@"show-config" => show_config.Options,
|
||||
.@"validate-config" => validate_config.Options,
|
||||
.@"crash-report" => crash_report.Options,
|
||||
.@"show-face" => show_face.Options,
|
||||
.boo => boo.Options,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "parse action none" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action == null);
|
||||
return null;
|
||||
}
|
||||
|
||||
test "parse action version" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
/// The action enum E can implement the decl `detectSpecialCase` to
|
||||
/// return this enum in order to perform various special case actions.
|
||||
pub fn SpecialCase(comptime E: type) type {
|
||||
return union(enum) {
|
||||
/// Immediately return this action.
|
||||
action: E,
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false --version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
/// Return this action if no other action is found.
|
||||
fallback: E,
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--c=84 --d --version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
/// If there is no pending action (we haven't seen an action yet)
|
||||
/// then we should return no action. This is kind of weird but is
|
||||
/// a special case to allow "-e" in Ghostty.
|
||||
abort_if_no_action,
|
||||
};
|
||||
}
|
||||
|
||||
test "parse action plus" {
|
||||
test "detect direct match" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum { foo, bar, baz };
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false +version",
|
||||
"+foo",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--c=84 --d +version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.foo, result.?);
|
||||
}
|
||||
|
||||
test "parse action plus ignores -e" {
|
||||
test "detect invalid match" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum { foo, bar, baz };
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 -e +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try Action.detectIter(&iter);
|
||||
try testing.expect(action == null);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+list-fonts --a=42 -e +version",
|
||||
"+invalid",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try testing.expectError(
|
||||
Action.Error.MultipleActions,
|
||||
Action.detectIter(&iter),
|
||||
DetectError.InvalidAction,
|
||||
detectIter(Enum, &iter),
|
||||
);
|
||||
}
|
||||
|
||||
test "detect multiple actions" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum { foo, bar, baz };
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+foo +bar",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try testing.expectError(
|
||||
DetectError.MultipleActions,
|
||||
detectIter(Enum, &iter),
|
||||
);
|
||||
}
|
||||
|
||||
test "detect no match" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum { foo, bar, baz };
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--some-flag",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expect(result == null);
|
||||
}
|
||||
|
||||
test "detect special case action" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum {
|
||||
foo,
|
||||
bar,
|
||||
|
||||
fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) {
|
||||
return if (std.mem.eql(u8, arg, "--special"))
|
||||
.{ .action = .foo }
|
||||
else
|
||||
null;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--special +bar",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.foo, result.?);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+bar --special",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.foo, result.?);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+bar",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.bar, result.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "detect special case fallback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum {
|
||||
foo,
|
||||
bar,
|
||||
|
||||
fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) {
|
||||
return if (std.mem.eql(u8, arg, "--special"))
|
||||
.{ .fallback = .foo }
|
||||
else
|
||||
null;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--special",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.foo, result.?);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+bar --special",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.bar, result.?);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--special +bar",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.bar, result.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "detect special case abort_if_no_action" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const Enum = enum {
|
||||
foo,
|
||||
bar,
|
||||
|
||||
fn detectSpecialCase(arg: []const u8) ?SpecialCase(@This()) {
|
||||
return if (std.mem.eql(u8, arg, "-e"))
|
||||
.abort_if_no_action
|
||||
else
|
||||
null;
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"-e",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expect(result == null);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+foo -e",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expectEqual(Enum.foo, result.?);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"-e +bar",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const result = try detectIter(Enum, &iter);
|
||||
try testing.expect(result == null);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const help_strings = @import("help_strings");
|
||||
const vaxis = @import("vaxis");
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config.zig").Config;
|
||||
const crash = @import("../crash/main.zig");
|
||||
|
||||
|
@ -3,7 +3,7 @@ const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const configpkg = @import("../config.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
290
src/cli/ghostty.zig
Normal file
290
src/cli/ghostty.zig
Normal file
@ -0,0 +1,290 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const help_strings = @import("help_strings");
|
||||
const actionpkg = @import("action.zig");
|
||||
const SpecialCase = actionpkg.SpecialCase;
|
||||
|
||||
const list_fonts = @import("list_fonts.zig");
|
||||
const help = @import("help.zig");
|
||||
const version = @import("version.zig");
|
||||
const list_keybinds = @import("list_keybinds.zig");
|
||||
const list_themes = @import("list_themes.zig");
|
||||
const list_colors = @import("list_colors.zig");
|
||||
const list_actions = @import("list_actions.zig");
|
||||
const ssh_cache = @import("ssh_cache.zig");
|
||||
const edit_config = @import("edit_config.zig");
|
||||
const show_config = @import("show_config.zig");
|
||||
const validate_config = @import("validate_config.zig");
|
||||
const crash_report = @import("crash_report.zig");
|
||||
const show_face = @import("show_face.zig");
|
||||
const boo = @import("boo.zig");
|
||||
|
||||
/// Special commands that can be invoked via CLI flags. These are all
|
||||
/// invoked by using `+<action>` as a CLI flag. The only exception is
|
||||
/// "version" which can be invoked additionally with `--version`.
|
||||
pub const Action = enum {
|
||||
/// Output the version and exit
|
||||
version,
|
||||
|
||||
/// Output help information for the CLI or configuration
|
||||
help,
|
||||
|
||||
/// List available fonts
|
||||
@"list-fonts",
|
||||
|
||||
/// List available keybinds
|
||||
@"list-keybinds",
|
||||
|
||||
/// List available themes
|
||||
@"list-themes",
|
||||
|
||||
/// List named RGB colors
|
||||
@"list-colors",
|
||||
|
||||
/// List keybind actions
|
||||
@"list-actions",
|
||||
|
||||
/// Manage SSH terminfo cache for automatic remote host setup
|
||||
@"ssh-cache",
|
||||
|
||||
/// Edit the config file in the configured terminal editor.
|
||||
@"edit-config",
|
||||
|
||||
/// Dump the config to stdout
|
||||
@"show-config",
|
||||
|
||||
// Validate passed config file
|
||||
@"validate-config",
|
||||
|
||||
// Show which font face Ghostty loads a codepoint from.
|
||||
@"show-face",
|
||||
|
||||
// List, (eventually) view, and (eventually) send crash reports.
|
||||
@"crash-report",
|
||||
|
||||
// Boo!
|
||||
boo,
|
||||
|
||||
pub fn detectSpecialCase(arg: []const u8) ?SpecialCase(Action) {
|
||||
// If we see a "-e" and we haven't seen a command yet, then
|
||||
// we are done looking for commands. This special case enables
|
||||
// `ghostty -e ghostty +command`. If we've seen a command we
|
||||
// still want to keep looking because
|
||||
// `ghostty +command -e +command` is invalid.
|
||||
if (std.mem.eql(u8, arg, "-e")) return .abort_if_no_action;
|
||||
|
||||
// Special case, --version always outputs the version no
|
||||
// matter what, no matter what other args exist.
|
||||
if (std.mem.eql(u8, arg, "--version")) {
|
||||
return .{ .action = .version };
|
||||
}
|
||||
|
||||
// --help matches "help" but if a subcommand is specified
|
||||
// then we match the subcommand.
|
||||
if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
|
||||
return .{ .fallback = .help };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// This should be returned by actions that want to print the help text.
|
||||
pub const help_error = error.ActionHelpRequested;
|
||||
|
||||
/// Run the action. This returns the exit code to exit with.
|
||||
pub fn run(self: Action, alloc: Allocator) !u8 {
|
||||
return self.runMain(alloc) catch |err| switch (err) {
|
||||
// If help is requested, then we use some comptime trickery
|
||||
// to find this action in the help strings and output that.
|
||||
help_error => err: {
|
||||
inline for (@typeInfo(Action).@"enum".fields) |field| {
|
||||
// Future note: for now we just output the help text directly
|
||||
// to stdout. In the future we can style this much prettier
|
||||
// for all commands by just changing this one place.
|
||||
|
||||
if (std.mem.eql(u8, field.name, @tagName(self))) {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const text = @field(help_strings.Action, field.name) ++ "\n";
|
||||
stdout.writeAll(text) catch |write_err| {
|
||||
std.log.warn("failed to write help text: {}\n", .{write_err});
|
||||
break :err 1;
|
||||
};
|
||||
|
||||
break :err 0;
|
||||
}
|
||||
}
|
||||
|
||||
break :err err;
|
||||
},
|
||||
else => err,
|
||||
};
|
||||
}
|
||||
|
||||
fn runMain(self: Action, alloc: Allocator) !u8 {
|
||||
return switch (self) {
|
||||
.version => try version.run(alloc),
|
||||
.help => try help.run(alloc),
|
||||
.@"list-fonts" => try list_fonts.run(alloc),
|
||||
.@"list-keybinds" => try list_keybinds.run(alloc),
|
||||
.@"list-themes" => try list_themes.run(alloc),
|
||||
.@"list-colors" => try list_colors.run(alloc),
|
||||
.@"list-actions" => try list_actions.run(alloc),
|
||||
.@"ssh-cache" => try ssh_cache.run(alloc),
|
||||
.@"edit-config" => try edit_config.run(alloc),
|
||||
.@"show-config" => try show_config.run(alloc),
|
||||
.@"validate-config" => try validate_config.run(alloc),
|
||||
.@"crash-report" => try crash_report.run(alloc),
|
||||
.@"show-face" => try show_face.run(alloc),
|
||||
.boo => try boo.run(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the filename associated with an action. This is a relative
|
||||
/// path from the root src/ directory.
|
||||
pub fn file(comptime self: Action) []const u8 {
|
||||
comptime {
|
||||
const filename = filename: {
|
||||
const tag = @tagName(self);
|
||||
var filename: [tag.len]u8 = undefined;
|
||||
_ = std.mem.replace(u8, tag, "-", "_", &filename);
|
||||
break :filename &filename;
|
||||
};
|
||||
|
||||
return "cli/" ++ filename ++ ".zig";
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options of action. Supports generating shell completions
|
||||
/// without duplicating the mapping from Action to relevant Option
|
||||
/// @import(..) declaration.
|
||||
pub fn options(comptime self: Action) type {
|
||||
comptime {
|
||||
return switch (self) {
|
||||
.version => version.Options,
|
||||
.help => help.Options,
|
||||
.@"list-fonts" => list_fonts.Options,
|
||||
.@"list-keybinds" => list_keybinds.Options,
|
||||
.@"list-themes" => list_themes.Options,
|
||||
.@"list-colors" => list_colors.Options,
|
||||
.@"list-actions" => list_actions.Options,
|
||||
.@"ssh-cache" => ssh_cache.Options,
|
||||
.@"edit-config" => edit_config.Options,
|
||||
.@"show-config" => show_config.Options,
|
||||
.@"validate-config" => validate_config.Options,
|
||||
.@"crash-report" => crash_report.Options,
|
||||
.@"show-face" => show_face.Options,
|
||||
.boo => boo.Options,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "parse action none" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action == null);
|
||||
}
|
||||
|
||||
test "parse action version" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false --version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--c=84 --d --version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse action plus" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 --b --b-f=false +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--c=84 --d +version --a=42 --b --b-f=false",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action.? == .version);
|
||||
}
|
||||
}
|
||||
|
||||
test "parse action plus ignores -e" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"--a=42 -e +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
const action = try actionpkg.detectIter(Action, &iter);
|
||||
try testing.expect(action == null);
|
||||
}
|
||||
|
||||
{
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
"+list-fonts --a=42 -e +version",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try testing.expectError(
|
||||
actionpkg.DetectError.MultipleActions,
|
||||
actionpkg.detectIter(Action, &iter),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
|
||||
// Note that this options struct doesn't implement the `help` decl like other
|
||||
// actions. That is because the help command is special and wants to handle its
|
||||
|
@ -1,6 +1,6 @@
|
||||
const std = @import("std");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const helpgen_actions = @import("../input/helpgen_actions.zig");
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
const std = @import("std");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const args = @import("args.zig");
|
||||
const x11_color = @import("../terminal/main.zig").x11_color;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const args = @import("args.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Arena = std.heap.ArenaAllocator;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const configpkg = @import("../config.zig");
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const inputpkg = @import("../input.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config/Config.zig");
|
||||
const themepkg = @import("../config/theme.zig");
|
||||
const tui = @import("tui.zig");
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const args = @import("args.zig");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const configpkg = @import("../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const args = @import("args.zig");
|
||||
const diagnostics = @import("diagnostics.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
|
549
src/cli/ssh-cache/DiskCache.zig
Normal file
549
src/cli/ssh-cache/DiskCache.zig
Normal file
@ -0,0 +1,549 @@
|
||||
/// An SSH terminfo entry cache that stores its cache data on
|
||||
/// disk. The cache only stores metadata (hostname, terminfo value,
|
||||
/// etc.) and does not store any sensitive data.
|
||||
const DiskCache = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../../os/main.zig").xdg;
|
||||
const TempDir = @import("../../os/main.zig").TempDir;
|
||||
const Entry = @import("Entry.zig");
|
||||
|
||||
// 512KB - sufficient for approximately 10k entries
|
||||
const MAX_CACHE_SIZE = 512 * 1024;
|
||||
|
||||
/// Path to a file where the cache is stored.
|
||||
path: []const u8,
|
||||
|
||||
pub const DefaultPathError = Allocator.Error || error{
|
||||
/// The general error that is returned for any filesystem error
|
||||
/// that may have resulted in the XDG lookup failing.
|
||||
XdgLookupFailed,
|
||||
};
|
||||
|
||||
pub const Error = error{ CacheIsLocked, HostnameIsInvalid };
|
||||
|
||||
/// Returns the default path for the cache for a given program.
|
||||
///
|
||||
/// On all platforms, this is `${XDG_STATE_HOME}/ghostty/ssh_cache`.
|
||||
///
|
||||
/// The returned value is allocated and must be freed by the caller.
|
||||
pub fn defaultPath(
|
||||
alloc: Allocator,
|
||||
program: []const u8,
|
||||
) DefaultPathError![]const u8 {
|
||||
const state_dir: []const u8 = xdg.state(
|
||||
alloc,
|
||||
.{ .subdir = program },
|
||||
) catch |err| return switch (err) {
|
||||
error.OutOfMemory => error.OutOfMemory,
|
||||
else => error.XdgLookupFailed,
|
||||
};
|
||||
defer alloc.free(state_dir);
|
||||
return try std.fs.path.join(alloc, &.{ state_dir, "ssh_cache" });
|
||||
}
|
||||
|
||||
/// Clear all cache data stored in the disk cache.
|
||||
/// This removes the cache file from disk, effectively clearing all cached
|
||||
/// SSH terminfo entries.
|
||||
pub fn clear(self: DiskCache) !void {
|
||||
std.fs.cwd().deleteFile(self.path) catch |err| switch (err) {
|
||||
error.FileNotFound => {},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
pub const AddResult = enum { added, updated };
|
||||
|
||||
/// Add or update a hostname entry in the cache.
|
||||
/// Returns AddResult.added for new entries or AddResult.updated for existing ones.
|
||||
/// The cache file is created if it doesn't exist with secure permissions (0600).
|
||||
pub fn add(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
hostname: []const u8,
|
||||
) !AddResult {
|
||||
if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid;
|
||||
|
||||
// Create cache directory if needed
|
||||
if (std.fs.path.dirname(self.path)) |dir| {
|
||||
std.fs.makeDirAbsolute(dir) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => {},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
// Open or create cache file with secure permissions
|
||||
const file = std.fs.createFileAbsolute(self.path, .{
|
||||
.read = true,
|
||||
.truncate = false,
|
||||
.mode = 0o600,
|
||||
}) catch |err| switch (err) {
|
||||
error.PathAlreadyExists => blk: {
|
||||
const existing_file = try std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{ .mode = .read_write },
|
||||
);
|
||||
errdefer existing_file.close();
|
||||
try fixupPermissions(existing_file);
|
||||
break :blk existing_file;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
// Lock
|
||||
_ = file.tryLock(.exclusive) catch return error.CacheIsLocked;
|
||||
defer file.unlock();
|
||||
|
||||
var entries = try readEntries(alloc, file);
|
||||
defer deinitEntries(alloc, &entries);
|
||||
|
||||
// Add or update entry
|
||||
const gop = try entries.getOrPut(hostname);
|
||||
const result: AddResult = if (!gop.found_existing) add: {
|
||||
const hostname_copy = try alloc.dupe(u8, hostname);
|
||||
errdefer alloc.free(hostname_copy);
|
||||
const terminfo_copy = try alloc.dupe(u8, "xterm-ghostty");
|
||||
errdefer alloc.free(terminfo_copy);
|
||||
|
||||
gop.key_ptr.* = hostname_copy;
|
||||
gop.value_ptr.* = .{
|
||||
.hostname = gop.key_ptr.*,
|
||||
.timestamp = std.time.timestamp(),
|
||||
.terminfo_version = terminfo_copy,
|
||||
};
|
||||
break :add .added;
|
||||
} else update: {
|
||||
// Update timestamp for existing entry
|
||||
gop.value_ptr.timestamp = std.time.timestamp();
|
||||
break :update .updated;
|
||||
};
|
||||
|
||||
try self.writeCacheFile(alloc, entries, null);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Remove a hostname entry from the cache.
|
||||
/// No error is returned if the hostname doesn't exist or the cache file is missing.
|
||||
pub fn remove(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
hostname: []const u8,
|
||||
) !void {
|
||||
if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid;
|
||||
|
||||
// Open our file
|
||||
const file = std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{ .mode = .read_write },
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
try fixupPermissions(file);
|
||||
|
||||
// Acquire exclusive lock
|
||||
_ = file.tryLock(.exclusive) catch return error.CacheIsLocked;
|
||||
defer file.unlock();
|
||||
|
||||
// Read existing entries
|
||||
var entries = try readEntries(alloc, file);
|
||||
defer deinitEntries(alloc, &entries);
|
||||
|
||||
// Remove the entry if it exists and ensure we free the memory
|
||||
if (entries.fetchRemove(hostname)) |kv| {
|
||||
assert(kv.key.ptr == kv.value.hostname.ptr);
|
||||
alloc.free(kv.value.hostname);
|
||||
alloc.free(kv.value.terminfo_version);
|
||||
}
|
||||
|
||||
try self.writeCacheFile(alloc, entries, null);
|
||||
}
|
||||
|
||||
/// Check if a hostname exists in the cache.
|
||||
/// Returns false if the cache file doesn't exist.
|
||||
pub fn contains(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
hostname: []const u8,
|
||||
) !bool {
|
||||
if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid;
|
||||
|
||||
// Open our file
|
||||
const file = std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{ .mode = .read_write },
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return false,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
try fixupPermissions(file);
|
||||
|
||||
// Read existing entries
|
||||
var entries = try readEntries(alloc, file);
|
||||
defer deinitEntries(alloc, &entries);
|
||||
|
||||
return entries.contains(hostname);
|
||||
}
|
||||
|
||||
fn fixupPermissions(file: std.fs.File) !void {
|
||||
// Ensure file has correct permissions (readable/writable by
|
||||
// owner only)
|
||||
const stat = try file.stat();
|
||||
if (stat.mode & 0o777 != 0o600) {
|
||||
try file.chmod(0o600);
|
||||
}
|
||||
}
|
||||
|
||||
fn writeCacheFile(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
entries: std.StringHashMap(Entry),
|
||||
expire_days: ?u32,
|
||||
) !void {
|
||||
var td: TempDir = try .init();
|
||||
defer td.deinit();
|
||||
|
||||
const tmp_file = try td.dir.createFile("ssh-cache", .{ .mode = 0o600 });
|
||||
defer tmp_file.close();
|
||||
const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache");
|
||||
defer alloc.free(tmp_path);
|
||||
|
||||
const writer = tmp_file.writer();
|
||||
var iter = entries.iterator();
|
||||
while (iter.next()) |kv| {
|
||||
// Only write non-expired entries
|
||||
if (kv.value_ptr.isExpired(expire_days)) continue;
|
||||
try kv.value_ptr.format(writer);
|
||||
}
|
||||
|
||||
// Atomic replace
|
||||
try std.fs.renameAbsolute(tmp_path, self.path);
|
||||
}
|
||||
|
||||
/// List all entries in the cache.
|
||||
/// The returned HashMap must be freed using `deinitEntries`.
|
||||
/// Returns an empty map if the cache file doesn't exist.
|
||||
pub fn list(
|
||||
self: DiskCache,
|
||||
alloc: Allocator,
|
||||
) !std.StringHashMap(Entry) {
|
||||
// Open our file
|
||||
const file = std.fs.openFileAbsolute(
|
||||
self.path,
|
||||
.{},
|
||||
) catch |err| switch (err) {
|
||||
error.FileNotFound => return .init(alloc),
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
return readEntries(alloc, file);
|
||||
}
|
||||
|
||||
/// Free memory allocated by the `list` function.
|
||||
/// This must be called to properly deallocate all entry data.
|
||||
pub fn deinitEntries(
|
||||
alloc: Allocator,
|
||||
entries: *std.StringHashMap(Entry),
|
||||
) void {
|
||||
// All our entries we dupe the memory owned by the hostname and the
|
||||
// terminfo, and we always match the hostname key and value.
|
||||
var it = entries.iterator();
|
||||
while (it.next()) |entry| {
|
||||
assert(entry.key_ptr.*.ptr == entry.value_ptr.hostname.ptr);
|
||||
alloc.free(entry.value_ptr.hostname);
|
||||
alloc.free(entry.value_ptr.terminfo_version);
|
||||
}
|
||||
entries.deinit();
|
||||
}
|
||||
|
||||
fn readEntries(
|
||||
alloc: Allocator,
|
||||
file: std.fs.File,
|
||||
) !std.StringHashMap(Entry) {
|
||||
const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE);
|
||||
defer alloc.free(content);
|
||||
|
||||
var entries = std.StringHashMap(Entry).init(alloc);
|
||||
var lines = std.mem.tokenizeScalar(u8, content, '\n');
|
||||
while (lines.next()) |line| {
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r");
|
||||
const entry = Entry.parse(trimmed) orelse continue;
|
||||
|
||||
// Always allocate hostname first to avoid key pointer confusion
|
||||
const hostname = try alloc.dupe(u8, entry.hostname);
|
||||
errdefer alloc.free(hostname);
|
||||
|
||||
const gop = try entries.getOrPut(hostname);
|
||||
if (!gop.found_existing) {
|
||||
const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version);
|
||||
gop.value_ptr.* = .{
|
||||
.hostname = hostname,
|
||||
.timestamp = entry.timestamp,
|
||||
.terminfo_version = terminfo_copy,
|
||||
};
|
||||
} else {
|
||||
// Don't need the copy since entry already exists
|
||||
alloc.free(hostname);
|
||||
|
||||
// Handle duplicate entries - keep newer timestamp
|
||||
if (entry.timestamp > gop.value_ptr.timestamp) {
|
||||
gop.value_ptr.timestamp = entry.timestamp;
|
||||
if (!std.mem.eql(
|
||||
u8,
|
||||
gop.value_ptr.terminfo_version,
|
||||
entry.terminfo_version,
|
||||
)) {
|
||||
alloc.free(gop.value_ptr.terminfo_version);
|
||||
const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version);
|
||||
gop.value_ptr.terminfo_version = terminfo_copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Supports both standalone hostnames and user@hostname format
|
||||
fn isValidCacheKey(key: []const u8) bool {
|
||||
// 253 + 1 + 64 for user@hostname
|
||||
if (key.len == 0 or key.len > 320) return false;
|
||||
|
||||
// Check for user@hostname format
|
||||
if (std.mem.indexOf(u8, key, "@")) |at_pos| {
|
||||
const user = key[0..at_pos];
|
||||
const hostname = key[at_pos + 1 ..];
|
||||
return isValidUser(user) and isValidHostname(hostname);
|
||||
}
|
||||
|
||||
return isValidHostname(key);
|
||||
}
|
||||
|
||||
// Basic hostname validation - accepts domains and IPs
|
||||
// (including IPv6 in brackets)
|
||||
fn isValidHostname(host: []const u8) bool {
|
||||
if (host.len == 0 or host.len > 253) return false;
|
||||
|
||||
// Handle IPv6 addresses in brackets
|
||||
if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') {
|
||||
const ipv6_part = host[1 .. host.len - 1];
|
||||
if (ipv6_part.len == 0) return false;
|
||||
var has_colon = false;
|
||||
for (ipv6_part) |c| {
|
||||
switch (c) {
|
||||
'a'...'f', 'A'...'F', '0'...'9' => {},
|
||||
':' => has_colon = true,
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
return has_colon;
|
||||
}
|
||||
|
||||
// Standard hostname/domain validation
|
||||
for (host) |c| {
|
||||
switch (c) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
|
||||
// No leading/trailing dots or hyphens, no consecutive dots
|
||||
if (host[0] == '.' or host[0] == '-' or
|
||||
host[host.len - 1] == '.' or host[host.len - 1] == '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return std.mem.indexOf(u8, host, "..") == null;
|
||||
}
|
||||
|
||||
fn isValidUser(user: []const u8) bool {
|
||||
if (user.len == 0 or user.len > 64) return false;
|
||||
for (user) |c| {
|
||||
switch (c) {
|
||||
'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {},
|
||||
else => return false,
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
test "disk cache default path" {
|
||||
const testing = std.testing;
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
const path = try DiskCache.defaultPath(alloc, "ghostty");
|
||||
defer alloc.free(path);
|
||||
try testing.expect(path.len > 0);
|
||||
}
|
||||
|
||||
test "disk cache clear" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Create our path
|
||||
var td: TempDir = try .init();
|
||||
defer td.deinit();
|
||||
{
|
||||
var file = try td.dir.createFile("cache", .{});
|
||||
defer file.close();
|
||||
try file.writer().writeAll("HELLO!");
|
||||
}
|
||||
const path = try td.dir.realpathAlloc(alloc, "cache");
|
||||
defer alloc.free(path);
|
||||
|
||||
// Setup our cache
|
||||
const cache: DiskCache = .{ .path = path };
|
||||
try cache.clear();
|
||||
|
||||
// Verify the file is gone
|
||||
try testing.expectError(
|
||||
error.FileNotFound,
|
||||
td.dir.openFile("cache", .{}),
|
||||
);
|
||||
}
|
||||
|
||||
test "disk cache operations" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
// Create our path
|
||||
var td: TempDir = try .init();
|
||||
defer td.deinit();
|
||||
{
|
||||
var file = try td.dir.createFile("cache", .{});
|
||||
defer file.close();
|
||||
try file.writer().writeAll("HELLO!");
|
||||
}
|
||||
const path = try td.dir.realpathAlloc(alloc, "cache");
|
||||
defer alloc.free(path);
|
||||
|
||||
// Setup our cache
|
||||
const cache: DiskCache = .{ .path = path };
|
||||
try testing.expectEqual(
|
||||
AddResult.added,
|
||||
try cache.add(alloc, "example.com"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
AddResult.updated,
|
||||
try cache.add(alloc, "example.com"),
|
||||
);
|
||||
try testing.expect(
|
||||
try cache.contains(alloc, "example.com"),
|
||||
);
|
||||
|
||||
// List
|
||||
var entries = try cache.list(alloc);
|
||||
deinitEntries(alloc, &entries);
|
||||
|
||||
// Remove
|
||||
try cache.remove(alloc, "example.com");
|
||||
try testing.expect(
|
||||
!(try cache.contains(alloc, "example.com")),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
AddResult.added,
|
||||
try cache.add(alloc, "example.com"),
|
||||
);
|
||||
}
|
||||
|
||||
// Tests
|
||||
test "hostname validation - valid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("example.com"));
|
||||
try testing.expect(isValidHostname("sub.example.com"));
|
||||
try testing.expect(isValidHostname("host-name.domain.org"));
|
||||
try testing.expect(isValidHostname("192.168.1.1"));
|
||||
try testing.expect(isValidHostname("a"));
|
||||
try testing.expect(isValidHostname("1"));
|
||||
}
|
||||
|
||||
test "hostname validation - IPv6 addresses" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidHostname("[::1]"));
|
||||
try testing.expect(isValidHostname("[2001:db8::1]"));
|
||||
try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported
|
||||
try testing.expect(!isValidHostname("[]")); // Empty IPv6
|
||||
try testing.expect(!isValidHostname("[invalid]")); // No colons
|
||||
}
|
||||
|
||||
test "hostname validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(!isValidHostname(""));
|
||||
try testing.expect(!isValidHostname("host\nname"));
|
||||
try testing.expect(!isValidHostname(".example.com"));
|
||||
try testing.expect(!isValidHostname("example.com."));
|
||||
try testing.expect(!isValidHostname("host..domain"));
|
||||
try testing.expect(!isValidHostname("-hostname"));
|
||||
try testing.expect(!isValidHostname("hostname-"));
|
||||
try testing.expect(!isValidHostname("host name"));
|
||||
try testing.expect(!isValidHostname("host_name"));
|
||||
try testing.expect(!isValidHostname("host@domain"));
|
||||
try testing.expect(!isValidHostname("host:port"));
|
||||
|
||||
// Too long
|
||||
const long_host = "a" ** 254;
|
||||
try testing.expect(!isValidHostname(long_host));
|
||||
}
|
||||
|
||||
test "user validation - valid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidUser("user"));
|
||||
try testing.expect(isValidUser("deploy"));
|
||||
try testing.expect(isValidUser("test-user"));
|
||||
try testing.expect(isValidUser("user_name"));
|
||||
try testing.expect(isValidUser("user.name"));
|
||||
try testing.expect(isValidUser("user123"));
|
||||
try testing.expect(isValidUser("a"));
|
||||
}
|
||||
|
||||
test "user validation - complex realistic cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidUser("git"));
|
||||
try testing.expect(isValidUser("ubuntu"));
|
||||
try testing.expect(isValidUser("root"));
|
||||
try testing.expect(isValidUser("service.account"));
|
||||
try testing.expect(isValidUser("user-with-dashes"));
|
||||
}
|
||||
|
||||
test "user validation - invalid cases" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(!isValidUser(""));
|
||||
try testing.expect(!isValidUser("user name"));
|
||||
try testing.expect(!isValidUser("user@domain"));
|
||||
try testing.expect(!isValidUser("user:group"));
|
||||
try testing.expect(!isValidUser("user\nname"));
|
||||
|
||||
// Too long
|
||||
const long_user = "a" ** 65;
|
||||
try testing.expect(!isValidUser(long_user));
|
||||
}
|
||||
|
||||
test "cache key validation - hostname format" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidCacheKey("example.com"));
|
||||
try testing.expect(isValidCacheKey("sub.example.com"));
|
||||
try testing.expect(isValidCacheKey("192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("[::1]"));
|
||||
try testing.expect(!isValidCacheKey(""));
|
||||
try testing.expect(!isValidCacheKey(".invalid.com"));
|
||||
}
|
||||
|
||||
test "cache key validation - user@hostname format" {
|
||||
const testing = std.testing;
|
||||
try testing.expect(isValidCacheKey("user@example.com"));
|
||||
try testing.expect(isValidCacheKey("deploy@prod.server.com"));
|
||||
try testing.expect(isValidCacheKey("test-user@192.168.1.1"));
|
||||
try testing.expect(isValidCacheKey("user_name@host.domain.org"));
|
||||
try testing.expect(isValidCacheKey("git@github.com"));
|
||||
try testing.expect(isValidCacheKey("ubuntu@[::1]"));
|
||||
try testing.expect(!isValidCacheKey("@example.com"));
|
||||
try testing.expect(!isValidCacheKey("user@"));
|
||||
try testing.expect(!isValidCacheKey("user@@host"));
|
||||
try testing.expect(!isValidCacheKey("user@.invalid.com"));
|
||||
}
|
154
src/cli/ssh-cache/Entry.zig
Normal file
154
src/cli/ssh-cache/Entry.zig
Normal file
@ -0,0 +1,154 @@
|
||||
/// A single entry within our SSH entry cache. Our SSH entry cache
|
||||
/// stores which hosts we've sent our terminfo to so that we don't have
|
||||
/// to send it again. It doesn't store any sensitive information.
|
||||
const Entry = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
hostname: []const u8,
|
||||
timestamp: i64,
|
||||
terminfo_version: []const u8,
|
||||
|
||||
pub fn parse(line: []const u8) ?Entry {
|
||||
const trimmed = std.mem.trim(u8, line, " \t\r\n");
|
||||
if (trimmed.len == 0) return null;
|
||||
|
||||
// Parse format: hostname|timestamp|terminfo_version
|
||||
var iter = std.mem.tokenizeScalar(u8, trimmed, '|');
|
||||
const hostname = iter.next() orelse return null;
|
||||
const timestamp_str = iter.next() orelse return null;
|
||||
const terminfo_version = iter.next() orelse "xterm-ghostty";
|
||||
const timestamp = std.fmt.parseInt(i64, timestamp_str, 10) catch |err| {
|
||||
std.log.warn(
|
||||
"Invalid timestamp in cache entry: {s} err={}",
|
||||
.{ timestamp_str, err },
|
||||
);
|
||||
return null;
|
||||
};
|
||||
|
||||
return .{
|
||||
.hostname = hostname,
|
||||
.timestamp = timestamp,
|
||||
.terminfo_version = terminfo_version,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn format(self: Entry, writer: anytype) !void {
|
||||
try writer.print(
|
||||
"{s}|{d}|{s}\n",
|
||||
.{ self.hostname, self.timestamp, self.terminfo_version },
|
||||
);
|
||||
}
|
||||
|
||||
pub fn isExpired(self: Entry, expire_days_: ?u32) bool {
|
||||
const expire_days = expire_days_ orelse return false;
|
||||
const now = std.time.timestamp();
|
||||
const age_days = @divTrunc(now -| self.timestamp, std.time.s_per_day);
|
||||
return age_days > expire_days;
|
||||
}
|
||||
|
||||
test "cache entry expiration" {
|
||||
const testing = std.testing;
|
||||
const now = std.time.timestamp();
|
||||
|
||||
const fresh_entry: Entry = .{
|
||||
.hostname = "test.com",
|
||||
.timestamp = now - std.time.s_per_day, // 1 day old
|
||||
.terminfo_version = "xterm-ghostty",
|
||||
};
|
||||
try testing.expect(!fresh_entry.isExpired(90));
|
||||
|
||||
const old_entry: Entry = .{
|
||||
.hostname = "old.com",
|
||||
.timestamp = now - (std.time.s_per_day * 100), // 100 days old
|
||||
.terminfo_version = "xterm-ghostty",
|
||||
};
|
||||
try testing.expect(old_entry.isExpired(90));
|
||||
|
||||
// Test never-expire case
|
||||
try testing.expect(!old_entry.isExpired(null));
|
||||
}
|
||||
|
||||
test "cache entry expiration exact boundary" {
|
||||
const testing = std.testing;
|
||||
const now = std.time.timestamp();
|
||||
|
||||
// Exactly at expiration boundary
|
||||
const boundary_entry: Entry = .{
|
||||
.hostname = "example.com",
|
||||
.timestamp = now - (std.time.s_per_day * 30),
|
||||
.terminfo_version = "xterm-ghostty",
|
||||
};
|
||||
try testing.expect(!boundary_entry.isExpired(30));
|
||||
try testing.expect(boundary_entry.isExpired(29));
|
||||
}
|
||||
|
||||
test "cache entry expiration large timestamp" {
|
||||
const testing = std.testing;
|
||||
const now = std.time.timestamp();
|
||||
|
||||
const boundary_entry: Entry = .{
|
||||
.hostname = "example.com",
|
||||
.timestamp = now + (std.time.s_per_day * 30),
|
||||
.terminfo_version = "xterm-ghostty",
|
||||
};
|
||||
try testing.expect(!boundary_entry.isExpired(30));
|
||||
}
|
||||
|
||||
test "cache entry parsing valid formats" {
|
||||
const testing = std.testing;
|
||||
|
||||
const entry = Entry.parse("example.com|1640995200|xterm-ghostty").?;
|
||||
try testing.expectEqualStrings("example.com", entry.hostname);
|
||||
try testing.expectEqual(@as(i64, 1640995200), entry.timestamp);
|
||||
try testing.expectEqualStrings("xterm-ghostty", entry.terminfo_version);
|
||||
|
||||
// Test default terminfo version
|
||||
const entry_no_version = Entry.parse("test.com|1640995200").?;
|
||||
try testing.expectEqualStrings(
|
||||
"xterm-ghostty",
|
||||
entry_no_version.terminfo_version,
|
||||
);
|
||||
|
||||
// Test complex hostnames
|
||||
const complex_entry = Entry.parse("user@server.example.com|1640995200|xterm-ghostty").?;
|
||||
try testing.expectEqualStrings(
|
||||
"user@server.example.com",
|
||||
complex_entry.hostname,
|
||||
);
|
||||
}
|
||||
|
||||
test "cache entry parsing invalid formats" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expect(Entry.parse("") == null);
|
||||
|
||||
// Invalid format (no pipe)
|
||||
try testing.expect(Entry.parse("v1") == null);
|
||||
|
||||
// Missing timestamp
|
||||
try testing.expect(Entry.parse("example.com") == null);
|
||||
|
||||
// Invalid timestamp
|
||||
try testing.expect(Entry.parse("example.com|invalid") == null);
|
||||
|
||||
// Empty terminfo should default
|
||||
try testing.expect(Entry.parse("example.com|1640995200|") != null);
|
||||
}
|
||||
|
||||
test "cache entry parsing malformed data resilience" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Extra pipes should not break parsing
|
||||
try testing.expect(Entry.parse("host|123|term|extra") != null);
|
||||
|
||||
// Whitespace handling
|
||||
try testing.expect(Entry.parse(" host|123|term ") != null);
|
||||
try testing.expect(Entry.parse("\n") == null);
|
||||
try testing.expect(Entry.parse(" \t \n") == null);
|
||||
|
||||
// Extremely large timestamp
|
||||
try testing.expect(
|
||||
Entry.parse("host|999999999999999999999999999999999999999999999999|xterm-ghostty") == null,
|
||||
);
|
||||
}
|
208
src/cli/ssh_cache.zig
Normal file
208
src/cli/ssh_cache.zig
Normal file
@ -0,0 +1,208 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const xdg = @import("../os/xdg.zig");
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
pub const Entry = @import("ssh-cache/Entry.zig");
|
||||
pub const DiskCache = @import("ssh-cache/DiskCache.zig");
|
||||
|
||||
pub const Options = struct {
|
||||
clear: bool = false,
|
||||
add: ?[]const u8 = null,
|
||||
remove: ?[]const u8 = null,
|
||||
host: ?[]const u8 = null,
|
||||
@"expire-days": ?u32 = null,
|
||||
|
||||
pub fn deinit(self: *Options) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
pub fn help(self: Options) !void {
|
||||
_ = self;
|
||||
return Action.help_error;
|
||||
}
|
||||
};
|
||||
|
||||
/// Manage the SSH terminfo cache for automatic remote host setup.
|
||||
///
|
||||
/// When SSH integration is enabled with `shell-integration-features = ssh-terminfo`,
|
||||
/// Ghostty automatically installs its terminfo on remote hosts. This command
|
||||
/// manages the cache of successful installations to avoid redundant uploads.
|
||||
///
|
||||
/// The cache stores hostnames (or user@hostname combinations) along with timestamps.
|
||||
/// Entries older than the expiration period are automatically removed during cache
|
||||
/// operations. By default, entries never expire.
|
||||
///
|
||||
/// Only one of `--clear`, `--add`, `--remove`, or `--host` can be specified.
|
||||
/// If multiple are specified, one of the actions will be executed but
|
||||
/// it isn't guaranteed which one. This is entirely unsafe so you should split
|
||||
/// multiple actions into separate commands.
|
||||
///
|
||||
/// Examples:
|
||||
/// ghostty +ssh-cache # List all cached hosts
|
||||
/// ghostty +ssh-cache --host=example.com # Check if host is cached
|
||||
/// ghostty +ssh-cache --add=example.com # Manually add host to cache
|
||||
/// ghostty +ssh-cache --add=user@example.com # Add user@host combination
|
||||
/// ghostty +ssh-cache --remove=example.com # Remove host from cache
|
||||
/// ghostty +ssh-cache --clear # Clear entire cache
|
||||
/// ghostty +ssh-cache --expire-days=30 # Set custom expiration period
|
||||
pub fn run(alloc_gpa: Allocator) !u8 {
|
||||
var arena = std.heap.ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var opts: Options = .{};
|
||||
defer opts.deinit();
|
||||
|
||||
{
|
||||
var iter = try args.argsIterator(alloc_gpa);
|
||||
defer iter.deinit();
|
||||
try args.parse(Options, alloc_gpa, &opts, &iter);
|
||||
}
|
||||
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
|
||||
// Setup our disk cache to the standard location
|
||||
const cache_path = try DiskCache.defaultPath(alloc, "ghostty");
|
||||
const cache: DiskCache = .{ .path = cache_path };
|
||||
|
||||
if (opts.clear) {
|
||||
try cache.clear();
|
||||
try stdout.print("Cache cleared.\n", .{});
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (opts.add) |host| {
|
||||
const result = cache.add(alloc, host) catch |err| switch (err) {
|
||||
DiskCache.Error.HostnameIsInvalid => {
|
||||
try stderr.print("Error: Invalid hostname format '{s}'\n", .{host});
|
||||
try stderr.print("Expected format: hostname or user@hostname\n", .{});
|
||||
return 1;
|
||||
},
|
||||
DiskCache.Error.CacheIsLocked => {
|
||||
try stderr.print("Error: Cache is busy, try again\n", .{});
|
||||
return 1;
|
||||
},
|
||||
else => {
|
||||
try stderr.print(
|
||||
"Error: Unable to add '{s}' to cache. Error: {}\n",
|
||||
.{ host, err },
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
|
||||
switch (result) {
|
||||
.added => try stdout.print("Added '{s}' to cache.\n", .{host}),
|
||||
.updated => try stdout.print("Updated '{s}' cache entry.\n", .{host}),
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (opts.remove) |host| {
|
||||
cache.remove(alloc, host) catch |err| switch (err) {
|
||||
DiskCache.Error.HostnameIsInvalid => {
|
||||
try stderr.print("Error: Invalid hostname format '{s}'\n", .{host});
|
||||
try stderr.print("Expected format: hostname or user@hostname\n", .{});
|
||||
return 1;
|
||||
},
|
||||
DiskCache.Error.CacheIsLocked => {
|
||||
try stderr.print("Error: Cache is busy, try again\n", .{});
|
||||
return 1;
|
||||
},
|
||||
else => {
|
||||
try stderr.print(
|
||||
"Error: Unable to remove '{s}' from cache. Error: {}\n",
|
||||
.{ host, err },
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
try stdout.print("Removed '{s}' from cache.\n", .{host});
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (opts.host) |host| {
|
||||
const cached = cache.contains(alloc, host) catch |err| switch (err) {
|
||||
error.HostnameIsInvalid => {
|
||||
try stderr.print("Error: Invalid hostname format '{s}'\n", .{host});
|
||||
try stderr.print("Expected format: hostname or user@hostname\n", .{});
|
||||
return 1;
|
||||
},
|
||||
else => {
|
||||
try stderr.print(
|
||||
"Error: Unable to check host '{s}' in cache. Error: {}\n",
|
||||
.{ host, err },
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
};
|
||||
|
||||
if (cached) {
|
||||
try stdout.print(
|
||||
"'{s}' has Ghostty terminfo installed.\n",
|
||||
.{host},
|
||||
);
|
||||
return 0;
|
||||
} else {
|
||||
try stdout.print(
|
||||
"'{s}' does not have Ghostty terminfo installed.\n",
|
||||
.{host},
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Default action: list all hosts
|
||||
var entries = try cache.list(alloc);
|
||||
defer DiskCache.deinitEntries(alloc, &entries);
|
||||
try listEntries(alloc, &entries, stdout);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn listEntries(
|
||||
alloc: Allocator,
|
||||
entries: *const std.StringHashMap(Entry),
|
||||
writer: anytype,
|
||||
) !void {
|
||||
if (entries.count() == 0) {
|
||||
try writer.print("No hosts in cache.\n", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort entries by hostname for consistent output
|
||||
var items = std.ArrayList(Entry).init(alloc);
|
||||
defer items.deinit();
|
||||
|
||||
var iter = entries.iterator();
|
||||
while (iter.next()) |kv| {
|
||||
try items.append(kv.value_ptr.*);
|
||||
}
|
||||
|
||||
std.mem.sort(Entry, items.items, {}, struct {
|
||||
fn lessThan(_: void, a: Entry, b: Entry) bool {
|
||||
return std.mem.lessThan(u8, a.hostname, b.hostname);
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
try writer.print("Cached hosts ({d}):\n", .{items.items.len});
|
||||
const now = std.time.timestamp();
|
||||
|
||||
for (items.items) |entry| {
|
||||
const age_days = @divTrunc(now - entry.timestamp, std.time.s_per_day);
|
||||
if (age_days == 0) {
|
||||
try writer.print(" {s} (today)\n", .{entry.hostname});
|
||||
} else if (age_days == 1) {
|
||||
try writer.print(" {s} (yesterday)\n", .{entry.hostname});
|
||||
} else {
|
||||
try writer.print(" {s} ({d} days ago)\n", .{ entry.hostname, age_days });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
_ = DiskCache;
|
||||
_ = Entry;
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const args = @import("args.zig");
|
||||
const Action = @import("action.zig").Action;
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config.zig").Config;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
|
@ -41,7 +41,7 @@ pub const BackgroundImageFit = Config.BackgroundImageFit;
|
||||
pub const LinkPreviews = Config.LinkPreviews;
|
||||
|
||||
// Alternate APIs
|
||||
pub const CAPI = @import("config/CAPI.zig");
|
||||
pub const CApi = @import("config/CApi.zig");
|
||||
pub const Wasm = if (!builtin.target.cpu.arch.isWasm()) struct {} else @import("config/Wasm.zig");
|
||||
|
||||
test {
|
||||
|
@ -689,10 +689,10 @@ palette: Palette = .{},
|
||||
/// other colors at runtime:
|
||||
///
|
||||
/// * `cell-foreground` - Match the cell foreground color.
|
||||
/// (Available since version 1.2.0)
|
||||
/// (Available since: 1.2.0)
|
||||
///
|
||||
/// * `cell-background` - Match the cell background color.
|
||||
/// (Available since version 1.2.0)
|
||||
/// (Available since: 1.2.0)
|
||||
@"cursor-color": ?TerminalColor = null,
|
||||
|
||||
/// The opacity level (opposite of transparency) of the cursor. A value of 1
|
||||
@ -814,6 +814,22 @@ palette: Palette = .{},
|
||||
/// On macOS, changing this configuration requires restarting Ghostty completely.
|
||||
@"background-opacity": f64 = 1.0,
|
||||
|
||||
/// Applies background opacity to cells with an explicit background color
|
||||
/// set.
|
||||
///
|
||||
/// Normally, `background-opacity` is only applied to the window background.
|
||||
/// If a cell has an explicit background color set, such as red, then that
|
||||
/// background color will be fully opaque. An effect of this is that some
|
||||
/// terminal applications that repaint the background color of the terminal
|
||||
/// such as a Neovim and Tmux may not respect the `background-opacity`
|
||||
/// (by design).
|
||||
///
|
||||
/// Setting this to `true` will apply the `background-opacity` to all cells
|
||||
/// regardless of whether they have an explicit background color set or not.
|
||||
///
|
||||
/// Available since: 1.2.0
|
||||
@"background-opacity-cells": bool = false,
|
||||
|
||||
/// Whether to blur the background when `background-opacity` is less than 1.
|
||||
///
|
||||
/// Valid values are:
|
||||
@ -2202,6 +2218,8 @@ keybind: Keybinds = .{},
|
||||
/// its default value is used, so you must explicitly disable features you don't
|
||||
/// want. You can also use `true` or `false` to turn all features on or off.
|
||||
///
|
||||
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
|
||||
///
|
||||
/// Available features:
|
||||
///
|
||||
/// * `cursor` - Set the cursor to a blinking bar at the prompt.
|
||||
@ -2210,7 +2228,26 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// * `title` - Set the window title via shell integration.
|
||||
///
|
||||
/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title`
|
||||
/// * `ssh-env` - Enable SSH environment variable compatibility. Automatically
|
||||
/// converts TERM from `xterm-ghostty` to `xterm-256color` when connecting to
|
||||
/// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION.
|
||||
/// Whether or not these variables will be accepted by the remote host(s) will
|
||||
/// depend on whether or not the variables are allowed in their sshd_config.
|
||||
/// (Available since: 1.2.0)
|
||||
///
|
||||
/// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts.
|
||||
/// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when
|
||||
/// connecting to hosts that lack it. Requires `infocmp` to be available locally
|
||||
/// and `tic` to be available on remote hosts. Once terminfo is installed on a
|
||||
/// remote host, it will be automatically "cached" to avoid repeat installations.
|
||||
/// If desired, the `+ssh-cache` CLI action can be used to manage the installation
|
||||
/// cache manually using various arguments.
|
||||
/// (Available since: 1.2.0)
|
||||
///
|
||||
/// SSH features work independently and can be combined for optimal experience:
|
||||
/// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its
|
||||
/// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to
|
||||
/// `xterm-256color` with environment variables if terminfo installation fails.
|
||||
@"shell-integration-features": ShellIntegrationFeatures = .{},
|
||||
|
||||
/// Custom entries into the command palette.
|
||||
@ -6636,6 +6673,8 @@ pub const ShellIntegrationFeatures = packed struct {
|
||||
cursor: bool = true,
|
||||
sudo: bool = false,
|
||||
title: bool = true,
|
||||
@"ssh-env": bool = false,
|
||||
@"ssh-terminfo": bool = false,
|
||||
};
|
||||
|
||||
pub const RepeatableCommand = struct {
|
||||
|
@ -343,7 +343,14 @@ pub const Face = struct {
|
||||
const cell_width: f64 = @floatFromInt(metrics.cell_width);
|
||||
// const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
||||
|
||||
const glyph_size = opts.constraint.constrain(
|
||||
// We eliminate any negative vertical padding since these overlap
|
||||
// values aren't needed under CoreText with how precisely we apply
|
||||
// constraints, and they can lead to extra height that looks bad
|
||||
// for things like powerline glyphs.
|
||||
var constraint = opts.constraint;
|
||||
constraint.pad_top = @max(0.0, constraint.pad_top);
|
||||
constraint.pad_bottom = @max(0.0, constraint.pad_bottom);
|
||||
const glyph_size = constraint.constrain(
|
||||
.{
|
||||
.width = rect.size.width,
|
||||
.height = rect.size.height,
|
||||
@ -354,48 +361,75 @@ pub const Face = struct {
|
||||
opts.constraint_width,
|
||||
);
|
||||
|
||||
// We manually quantize the position and size of the glyph to whole
|
||||
// pixel boundaries. Since macOS doesn't do font hinting this helps
|
||||
// a lot for legibility at small sizes on low dpi displays.
|
||||
// These calculations are an attempt to mostly imitate the effect of
|
||||
// `shouldSubpixelQuantizeFonts`[^1], which helps maximize legibility
|
||||
// at small pixel sizes (low DPI). We do this math ourselves instead
|
||||
// of letting CoreText do it because it's not entirely clear how the
|
||||
// math in CoreText works and we've run in to edge cases where glyphs
|
||||
// have their bottom or left row cut off due to bad rounding.
|
||||
//
|
||||
// Well, okay, so, it seems like macOS does have a rudimentary auto-
|
||||
// hinter of sorts, except they call it "subpixel quantization"[^1].
|
||||
// This math seems to have a mostly comparable result to whatever it
|
||||
// is that CoreText does, and is even (in my opinion) better in some
|
||||
// cases.
|
||||
//
|
||||
// Why not just use that? Because it's unpredictable and would force
|
||||
// us to have an extra pixel of padding in the atlas for most glyphs
|
||||
// that don't need it, since it's hard to know whether a given glyph
|
||||
// will have its bottom or left edge snapped out an extra pixel.
|
||||
// I'm not entirely certain but I suspect that when you enable the
|
||||
// CoreText option it also does some sort of rudimentary hinting,
|
||||
// but it doesn't seem to make that big of a difference in terms
|
||||
// of legibility in the end.
|
||||
//
|
||||
// Also, this empirically just looks a whole lot better than theirs.
|
||||
// Admittedly this is a very specific use case, we're rendering for
|
||||
// a monospace grid and don't really have to worry about sub-pixel
|
||||
// positioning; I'm sure Apple's technique is better for cases with
|
||||
// proportional text.
|
||||
//
|
||||
// An effort was made to more or less match Apple's quantization in
|
||||
// terms of resulting whole-pixel glyph sizes. Oddly it looks like
|
||||
// Apple is still horizontally quantizing to thirds of a pixel, as
|
||||
// if they're doing subpixel rendering for a horizontally striped
|
||||
// LCD, even though they haven't done subpixel rendering for years.
|
||||
// We don't match them on that, it tends to just make it blurrier.
|
||||
//
|
||||
// [^1]: Well I'm 80% sure it's hinting since it seems to account for
|
||||
// features inside of the glyph like crossbars, not just the bounding
|
||||
// box like we do. The documentation is... sparse. Ref:
|
||||
// https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc
|
||||
// [^1]: https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc
|
||||
|
||||
// We only want to apply quantization if we don't have any
|
||||
// constraints and this isn't a bitmap glyph, since CoreText
|
||||
// doesn't seem to apply its quantization to bitmap glyphs.
|
||||
//
|
||||
// TODO: Maybe gate this so it only applies at small font sizes,
|
||||
// or else offer a user config option that can disable it.
|
||||
const x = @round(glyph_size.x);
|
||||
const y = @round(glyph_size.y);
|
||||
// We subtract a third here so that we behave (somewhat) like the weird
|
||||
// one third pixel quantization that Apple does. This is basically just
|
||||
// a fudge factor though.
|
||||
const width = @max(1.0, @ceil(glyph_size.width + glyph_size.x - x - 1.0 / 3.0));
|
||||
const height = @max(1.0, @ceil(glyph_size.height + glyph_size.y - y));
|
||||
const should_quantize = !sbix and std.meta.eql(opts.constraint, .none);
|
||||
|
||||
const px_width: u32 = @intFromFloat(@ceil(width));
|
||||
const px_height: u32 = @intFromFloat(@ceil(height));
|
||||
// We offset our glyph by its bearings when we draw it, using `@floor`
|
||||
// here rounds it *up* since we negate it right outside. Moving it by
|
||||
// whole pixels ensures that we don't disturb the pixel alignment of
|
||||
// the glyph, fractional pixels will still be drawn on all sides as
|
||||
// necessary.
|
||||
const draw_x = -@floor(rect.origin.x);
|
||||
const draw_y = -@floor(rect.origin.y);
|
||||
|
||||
// We use `x` and `y` for our full pixel bearings post-raster.
|
||||
// We need to subtract the fractional pixel of difference from
|
||||
// the edge of the draw area to the edge of the actual glyph.
|
||||
const frac_x = rect.origin.x + draw_x;
|
||||
const frac_y = rect.origin.y + draw_y;
|
||||
const x = glyph_size.x - frac_x;
|
||||
const y = glyph_size.y - frac_y;
|
||||
|
||||
// We never modify the width.
|
||||
//
|
||||
// When using the CoreText option the widths do seem to be
|
||||
// modified extremely subtly, but even at very small font
|
||||
// sizes it's hardly a noticeable difference.
|
||||
const width = glyph_size.width;
|
||||
|
||||
// If the top of the glyph (taking in to account the y position)
|
||||
// is within half a pixel of an exact pixel edge, we round up the
|
||||
// height, otherwise leave it alone.
|
||||
//
|
||||
// This seems to match what CoreText does.
|
||||
const frac_top = (glyph_size.height + frac_y) - @floor(glyph_size.height + frac_y);
|
||||
const height =
|
||||
if (should_quantize)
|
||||
if (frac_top >= 0.5)
|
||||
glyph_size.height + 1 - frac_top
|
||||
else
|
||||
glyph_size.height
|
||||
else
|
||||
glyph_size.height;
|
||||
|
||||
// Add the fractional pixel to the width and height and take
|
||||
// the ceiling to get a canvas size that will definitely fit
|
||||
// our drawn glyph.
|
||||
const px_width: u32 = @intFromFloat(@ceil(width + frac_x));
|
||||
const px_height: u32 = @intFromFloat(@ceil(height + frac_y));
|
||||
|
||||
// Settings that are specific to if we are rendering text or emoji.
|
||||
const color: struct {
|
||||
@ -505,13 +539,8 @@ pub const Face = struct {
|
||||
height / rect.size.height,
|
||||
);
|
||||
|
||||
// We want to render the glyphs at (0,0), but the glyphs themselves
|
||||
// are offset by bearings, so we have to undo those bearings in order
|
||||
// to get them to 0,0.
|
||||
self.font.drawGlyphs(&glyphs, &.{.{
|
||||
.x = -rect.origin.x,
|
||||
.y = -rect.origin.y,
|
||||
}}, ctx);
|
||||
// Draw our glyph.
|
||||
self.font.drawGlyphs(&glyphs, &.{.{ .x = draw_x, .y = draw_y }}, ctx);
|
||||
|
||||
// Write our rasterized glyph to the atlas.
|
||||
const region = try atlas.reserve(alloc, px_width, px_height);
|
||||
@ -519,7 +548,7 @@ pub const Face = struct {
|
||||
|
||||
// This should be the distance from the bottom of
|
||||
// the cell to the top of the glyph's bounding box.
|
||||
const offset_y: i32 = @as(i32, @intFromFloat(@ceil(y + height)));
|
||||
const offset_y: i32 = @as(i32, @intFromFloat(@round(y))) + @as(i32, @intCast(px_height));
|
||||
|
||||
// This should be the distance from the left of
|
||||
// the cell to the left of the glyph's bounding box.
|
||||
@ -538,9 +567,7 @@ pub const Face = struct {
|
||||
// since in that case the position was already calculated with the
|
||||
// new cell width in mind.
|
||||
if (opts.constraint.align_horizontal == .none) {
|
||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
|
||||
const advance = advances[0].width;
|
||||
const advance = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, null);
|
||||
const new_advance =
|
||||
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
|
||||
// If the original advance is greater than the cell width then
|
||||
@ -552,13 +579,13 @@ pub const Face = struct {
|
||||
// We also don't want to do anything if the advance is zero or
|
||||
// less, since this is used for stuff like combining characters.
|
||||
if (advance > new_advance or advance <= 0.0) {
|
||||
break :offset_x @intFromFloat(@ceil(x));
|
||||
break :offset_x @intFromFloat(@round(x));
|
||||
}
|
||||
break :offset_x @intFromFloat(
|
||||
@round(x + (new_advance - advance) / 2),
|
||||
);
|
||||
} else {
|
||||
break :offset_x @intFromFloat(@ceil(x));
|
||||
break :offset_x @intFromFloat(@round(x));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -30,7 +30,7 @@ pub const GlobalState = struct {
|
||||
|
||||
gpa: ?GPA,
|
||||
alloc: std.mem.Allocator,
|
||||
action: ?cli.Action,
|
||||
action: ?cli.ghostty.Action,
|
||||
logging: Logging,
|
||||
rlimits: ResourceLimits = .{},
|
||||
|
||||
@ -92,7 +92,10 @@ pub const GlobalState = struct {
|
||||
unreachable;
|
||||
|
||||
// We first try to parse any action that we may be executing.
|
||||
self.action = try cli.Action.detectCLI(self.alloc);
|
||||
self.action = try cli.action.detectArgs(
|
||||
cli.ghostty.Action,
|
||||
self.alloc,
|
||||
);
|
||||
|
||||
// If we have an action executing, we disable logging by default
|
||||
// since we write to stderr we don't want logs messing up our
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("config/Config.zig");
|
||||
const Action = @import("cli/action.zig").Action;
|
||||
const Action = @import("cli.zig").ghostty.Action;
|
||||
const KeybindAction = @import("input/Binding.zig").Action;
|
||||
|
||||
pub fn main() !void {
|
||||
|
@ -10,11 +10,6 @@ const entrypoint = switch (build_config.exe_entrypoint) {
|
||||
.webgen_config => @import("build/webgen/main_config.zig"),
|
||||
.webgen_actions => @import("build/webgen/main_actions.zig"),
|
||||
.webgen_commands => @import("build/webgen/main_commands.zig"),
|
||||
.bench_parser => @import("bench/parser.zig"),
|
||||
.bench_stream => @import("bench/stream.zig"),
|
||||
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
|
||||
.bench_grapheme_break => @import("bench/grapheme-break.zig"),
|
||||
.bench_page_init => @import("bench/page-init.zig"),
|
||||
};
|
||||
|
||||
/// The main entrypoint for the program.
|
||||
|
5
src/main_bench.zig
Normal file
5
src/main_bench.zig
Normal file
@ -0,0 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const benchmark = @import("benchmark/main.zig");
|
||||
|
||||
pub const main = benchmark.cli.main;
|
@ -33,10 +33,16 @@ pub const std_options = main.std_options;
|
||||
comptime {
|
||||
// These structs need to be referenced so the `export` functions
|
||||
// are truly exported by the C API lib.
|
||||
_ = @import("config.zig").CAPI;
|
||||
if (@hasDecl(apprt.runtime, "CAPI")) {
|
||||
_ = apprt.runtime.CAPI;
|
||||
}
|
||||
|
||||
// Our config API
|
||||
_ = @import("config.zig").CApi;
|
||||
|
||||
// Any apprt-specific C API, mainly libghostty for apprt.embedded.
|
||||
if (@hasDecl(apprt.runtime, "CAPI")) _ = apprt.runtime.CAPI;
|
||||
|
||||
// Our benchmark API. We probably want to gate this on a build
|
||||
// config in the future but for now we always just export it.
|
||||
_ = @import("benchmark/main.zig").CApi;
|
||||
}
|
||||
|
||||
/// ghostty_info_s
|
||||
@ -72,7 +78,7 @@ pub const String = extern struct {
|
||||
};
|
||||
|
||||
/// Initialize ghostty global state.
|
||||
export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
|
||||
pub export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
|
||||
assert(builtin.link_libc);
|
||||
|
||||
std.os.argv = argv[0..argc];
|
||||
@ -86,7 +92,7 @@ export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
|
||||
|
||||
/// Runs an action if it is specified. If there is no action this returns
|
||||
/// false. If there is an action then this doesn't return.
|
||||
export fn ghostty_cli_try_action() void {
|
||||
pub export fn ghostty_cli_try_action() void {
|
||||
const action = state.action orelse return;
|
||||
std.log.info("executing CLI action={}", .{action});
|
||||
posix.exit(action.run(state.alloc) catch |err| {
|
||||
@ -98,7 +104,7 @@ export fn ghostty_cli_try_action() void {
|
||||
}
|
||||
|
||||
/// Return metadata about Ghostty, such as version, build mode, etc.
|
||||
export fn ghostty_info() Info {
|
||||
pub export fn ghostty_info() Info {
|
||||
return .{
|
||||
.mode = switch (builtin.mode) {
|
||||
.Debug => .debug,
|
||||
@ -117,11 +123,11 @@ export fn ghostty_info() Info {
|
||||
/// the function call.
|
||||
///
|
||||
/// This should only be used for singular strings maintained by Ghostty.
|
||||
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
|
||||
pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
|
||||
return internal_os.i18n._(msgid);
|
||||
}
|
||||
|
||||
/// Free a string allocated by Ghostty.
|
||||
export fn ghostty_string_free(str: String) void {
|
||||
pub export fn ghostty_string_free(str: String) void {
|
||||
state.alloc.free(str.ptr.?[0..str.len]);
|
||||
}
|
||||
|
5
src/main_gen.zig
Normal file
5
src/main_gen.zig
Normal file
@ -0,0 +1,5 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const synthetic = @import("synthetic/main.zig");
|
||||
|
||||
pub const main = synthetic.cli.main;
|
@ -182,6 +182,7 @@ test {
|
||||
_ = @import("surface_mouse.zig");
|
||||
|
||||
// Libraries
|
||||
_ = @import("benchmark/main.zig");
|
||||
_ = @import("crash/main.zig");
|
||||
_ = @import("datastruct/main.zig");
|
||||
_ = @import("inspector/main.zig");
|
||||
|
@ -17,7 +17,7 @@ test "read /proc/sys/kernel/osrelease" {
|
||||
if (comptime builtin.os.tag != .linux) return null;
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
const kernel_info = try getKernelInfo(allocator);
|
||||
const kernel_info = getKernelInfo(allocator).?;
|
||||
defer allocator.free(kernel_info);
|
||||
|
||||
// Since we can't hardcode the info in tests, just check
|
||||
|
@ -2,6 +2,8 @@
|
||||
//! system. These aren't restricted to syscalls or low-level operations, but
|
||||
//! also OS-specific features and conventions.
|
||||
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const dbus = @import("dbus.zig");
|
||||
const desktop = @import("desktop.zig");
|
||||
const env = @import("env.zig");
|
||||
@ -14,7 +16,7 @@ const openpkg = @import("open.zig");
|
||||
const pipepkg = @import("pipe.zig");
|
||||
const resourcesdir = @import("resourcesdir.zig");
|
||||
const systemd = @import("systemd.zig");
|
||||
const kernelInfo = @import("kernel_info.zig");
|
||||
const kernel_info = @import("kernel_info.zig");
|
||||
|
||||
// Namespaces
|
||||
pub const args = @import("args.zig");
|
||||
@ -59,8 +61,12 @@ pub const pipe = pipepkg.pipe;
|
||||
pub const resourcesDir = resourcesdir.resourcesDir;
|
||||
pub const ResourcesDir = resourcesdir.ResourcesDir;
|
||||
pub const ShellEscapeWriter = shell.ShellEscapeWriter;
|
||||
pub const getKernelInfo = kernelInfo.getKernelInfo;
|
||||
pub const getKernelInfo = kernel_info.getKernelInfo;
|
||||
|
||||
test {
|
||||
_ = i18n;
|
||||
|
||||
if (comptime builtin.os.tag == .linux) {
|
||||
_ = kernel_info;
|
||||
}
|
||||
}
|
||||
|
@ -63,3 +63,136 @@ pub fn launchedBySystemd() bool {
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// systemd notifications. Used by Ghostty to inform systemd of the state of the
|
||||
/// process. Currently only used to notify systemd that we are ready and that
|
||||
/// configuration reloading has started.
|
||||
///
|
||||
/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html
|
||||
///
|
||||
/// These functions were re-implemented in Zig instead of using the `libsystemd`
|
||||
/// library to avoid the complexity of another external dependency, as well as
|
||||
/// to take advantage of Zig features like `comptime` to ensure minimal impact
|
||||
/// on non-Linux systems (like FreeBSD) that will never support `systemd`.
|
||||
///
|
||||
/// Linux systems that do not use `systemd` should not be impacted as they
|
||||
/// should never start Ghostty with the `NOTIFY_SOCKET` environment variable set
|
||||
/// and these functions essentially become a no-op.
|
||||
///
|
||||
/// See `systemd`'s [Interface Portability and Stability Promise](https://systemd.io/PORTABILITY_AND_STABILITY/)
|
||||
/// for assurances that the interfaces used here will be supported and stable for
|
||||
/// the long term.
|
||||
pub const notify = struct {
|
||||
/// Send the given message to the UNIX socket specified in the NOTIFY_SOCKET
|
||||
/// environment variable. If there NOTIFY_SOCKET environment variable does
|
||||
/// not exist then no message is sent.
|
||||
fn send(message: []const u8) void {
|
||||
// systemd is Linux-only so this is a no-op anywhere else
|
||||
if (comptime builtin.os.tag != .linux) return;
|
||||
|
||||
// Get the socket address that should receive notifications.
|
||||
const socket_path = std.posix.getenv("NOTIFY_SOCKET") orelse return;
|
||||
|
||||
// If the socket address is an empty string return.
|
||||
if (socket_path.len == 0) return;
|
||||
|
||||
// The socket address must be a path or an abstract socket.
|
||||
if (socket_path[0] != '/' and socket_path[0] != '@') {
|
||||
log.warn("only AF_UNIX sockets with path or abstract namespace addresses are supported!", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
var socket_address: std.os.linux.sockaddr.un = undefined;
|
||||
|
||||
// Error out if the supplied socket path is too long.
|
||||
if (socket_address.path.len < socket_path.len) {
|
||||
log.warn("NOTIFY_SOCKET path is too long!", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
socket_address.family = std.os.linux.AF.UNIX;
|
||||
|
||||
@memcpy(socket_address.path[0..socket_path.len], socket_path);
|
||||
socket_address.path[socket_path.len] = 0;
|
||||
|
||||
const socket: std.os.linux.socket_t = socket: {
|
||||
const rc = std.os.linux.socket(
|
||||
std.os.linux.AF.UNIX,
|
||||
std.os.linux.SOCK.DGRAM | std.os.linux.SOCK.CLOEXEC,
|
||||
0,
|
||||
);
|
||||
switch (std.os.linux.E.init(rc)) {
|
||||
.SUCCESS => break :socket @intCast(rc),
|
||||
else => |e| {
|
||||
log.warn("creating socket failed: {s}", .{@tagName(e)});
|
||||
return;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
defer _ = std.os.linux.close(socket);
|
||||
|
||||
connect: {
|
||||
const rc = std.os.linux.connect(
|
||||
socket,
|
||||
&socket_address,
|
||||
@offsetOf(std.os.linux.sockaddr.un, "path") + socket_address.path.len,
|
||||
);
|
||||
switch (std.os.linux.E.init(rc)) {
|
||||
.SUCCESS => break :connect,
|
||||
else => |e| {
|
||||
log.warn("unable to connect to notify socket: {s}", .{@tagName(e)});
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
write: {
|
||||
const rc = std.os.linux.write(socket, message.ptr, message.len);
|
||||
switch (std.os.linux.E.init(rc)) {
|
||||
.SUCCESS => {
|
||||
const written = rc;
|
||||
if (written < message.len) {
|
||||
log.warn("short write to notify socket: {d} < {d}", .{ rc, message.len });
|
||||
return;
|
||||
}
|
||||
break :write;
|
||||
},
|
||||
else => |e| {
|
||||
log.warn("unable to write to notify socket: {s}", .{@tagName(e)});
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell systemd that we are ready or that we are finished reloading.
|
||||
/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#READY=1
|
||||
pub fn ready() void {
|
||||
if (comptime builtin.os.tag != .linux) return;
|
||||
|
||||
send("READY=1");
|
||||
}
|
||||
|
||||
/// Tell systemd that we have started reloading our configuration.
|
||||
/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#RELOADING=1
|
||||
/// and: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#MONOTONIC_USEC=%E2%80%A6
|
||||
pub fn reloading() void {
|
||||
if (comptime builtin.os.tag != .linux) return;
|
||||
|
||||
const ts = std.posix.clock_gettime(.MONOTONIC) catch |err| {
|
||||
log.err("unable to get MONOTONIC clock: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
const now = ts.sec * std.time.us_per_s + @divFloor(ts.nsec, std.time.ns_per_us);
|
||||
|
||||
var buffer: [64]u8 = undefined;
|
||||
const message = std.fmt.bufPrint(&buffer, "RELOADING=1\nMONOTONIC_USEC={d}", .{now}) catch |err| {
|
||||
log.err("unable to format reloading message: {}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
send(message);
|
||||
}
|
||||
};
|
||||
|
@ -156,6 +156,17 @@ pub const Contents = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current cursor glyph if present, checking both cursor lists.
|
||||
pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText {
|
||||
if (self.fg_rows.lists[0].items.len > 0) {
|
||||
return self.fg_rows.lists[0].items[0];
|
||||
}
|
||||
if (self.fg_rows.lists[self.size.rows + 1].items.len > 0) {
|
||||
return self.fg_rows.lists[self.size.rows + 1].items[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Access a background cell. Prefer this function over direct indexing
|
||||
/// of `bg_cells` in order to avoid integer size bugs causing overflows.
|
||||
pub inline fn bgCell(
|
||||
@ -350,14 +361,17 @@ test Contents {
|
||||
};
|
||||
c.setCursor(cursor_cell, .block);
|
||||
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
|
||||
try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
|
||||
|
||||
// And remove it.
|
||||
c.setCursor(null, null);
|
||||
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
|
||||
try testing.expect(c.getCursorGlyph() == null);
|
||||
|
||||
// Add a hollow cursor.
|
||||
c.setCursor(cursor_cell, .block_hollow);
|
||||
try testing.expectEqual(cursor_cell, c.fg_rows.lists[rows + 1].items[0]);
|
||||
try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
|
||||
}
|
||||
|
||||
test "Contents clear retains other content" {
|
||||
|
@ -516,6 +516,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
cursor_text: ?configpkg.Config.TerminalColor,
|
||||
background: terminal.color.RGB,
|
||||
background_opacity: f64,
|
||||
background_opacity_cells: bool,
|
||||
foreground: terminal.color.RGB,
|
||||
selection_background: ?configpkg.Config.TerminalColor,
|
||||
selection_foreground: ?configpkg.Config.TerminalColor,
|
||||
@ -568,6 +569,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
|
||||
return .{
|
||||
.background_opacity = @max(0, @min(1, config.@"background-opacity")),
|
||||
.background_opacity_cells = config.@"background-opacity-cells",
|
||||
.font_thicken = config.@"font-thicken",
|
||||
.font_thicken_strength = config.@"font-thicken-strength",
|
||||
.font_features = font_features.list,
|
||||
@ -2218,10 +2220,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
};
|
||||
|
||||
// Update custom cursor uniforms, if we have a cursor.
|
||||
if (self.cells.fg_rows.lists[0].items.len > 0) {
|
||||
const cursor: shaderpkg.CellText =
|
||||
self.cells.fg_rows.lists[0].items[0];
|
||||
|
||||
if (self.cells.getCursorGlyph()) |cursor| {
|
||||
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
|
||||
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
|
||||
|
||||
@ -2631,6 +2630,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
||||
// Cells that are reversed should be fully opaque.
|
||||
if (style.flags.inverse) break :bg_alpha default;
|
||||
|
||||
// If the user requested to have opacity on all cells, apply it.
|
||||
if (self.config.background_opacity_cells and bg_style != null) {
|
||||
var opacity: f64 = @floatFromInt(default);
|
||||
opacity *= self.config.background_opacity;
|
||||
break :bg_alpha @intFromFloat(opacity);
|
||||
}
|
||||
|
||||
// Cells that have an explicit bg color should be fully opaque.
|
||||
if (bg_style != null) break :bg_alpha default;
|
||||
|
||||
|
@ -26,6 +26,12 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
builtin declare __ghostty_bash_flags="$GHOSTTY_BASH_INJECT"
|
||||
builtin unset ENV GHOSTTY_BASH_INJECT
|
||||
|
||||
# Restore an existing ENV that was replaced by the shell integration code.
|
||||
if [[ -n "$GHOSTTY_BASH_ENV" ]]; then
|
||||
builtin export ENV=$GHOSTTY_BASH_ENV
|
||||
builtin unset GHOSTTY_BASH_ENV
|
||||
fi
|
||||
|
||||
# Restore bash's default 'posix' behavior. Also reset 'inherit_errexit',
|
||||
# which doesn't happen as part of the 'posix' reset.
|
||||
builtin set +o posix
|
||||
@ -95,6 +101,76 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
ssh() {
|
||||
builtin local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SetEnv COLORTERM=truecolor")
|
||||
ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
builtin local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(builtin command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
builtin local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif builtin command -v infocmp >/dev/null 2>&1; then
|
||||
builtin local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
builtin echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if builtin echo "$ssh_terminfo" | builtin command ssh -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
builtin echo "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
builtin echo "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# Import bash-preexec, safe to do multiple times
|
||||
builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh"
|
||||
|
||||
|
@ -38,6 +38,9 @@
|
||||
{
|
||||
use str
|
||||
|
||||
# List of enabled shell integration features
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
|
||||
# helper used by `mark-*` functions
|
||||
fn set-prompt-state {|new| set-env __ghostty_prompt_state $new }
|
||||
|
||||
@ -98,6 +101,83 @@
|
||||
(external sudo) $@args
|
||||
}
|
||||
|
||||
fn ssh-integration {|@args|
|
||||
var ssh-term = "xterm-256color"
|
||||
var ssh-opts = []
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if (has-value $features ssh-env) {
|
||||
set ssh-opts = (conj $ssh-opts ^
|
||||
-o "SetEnv COLORTERM=truecolor" ^
|
||||
-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
}
|
||||
|
||||
if (has-value $features ssh-terminfo) {
|
||||
var ssh-user = ""
|
||||
var ssh-hostname = ""
|
||||
|
||||
# Parse ssh config
|
||||
for line [((external ssh) -G $@args)] {
|
||||
var parts = [(str:fields $line)]
|
||||
if (> (count $parts) 1) {
|
||||
var ssh-key = $parts[0]
|
||||
var ssh-value = $parts[1]
|
||||
if (eq $ssh-key user) {
|
||||
set ssh-user = $ssh-value
|
||||
} elif (eq $ssh-key hostname) {
|
||||
set ssh-hostname = $ssh-value
|
||||
}
|
||||
if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (not-eq $ssh-hostname "") {
|
||||
var ghostty = $E:GHOSTTY_BIN_DIR/"ghostty"
|
||||
var ssh-target = $ssh-user"@"$ssh-hostname
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if (bool ?($ghostty +ssh-cache --host=$ssh-target)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
} elif (has-external infocmp) {
|
||||
var ssh-terminfo = ((external infocmp) -0 -x xterm-ghostty 2>/dev/null | slurp)
|
||||
|
||||
if (not-eq $ssh-terminfo "") {
|
||||
echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2
|
||||
|
||||
use os
|
||||
var ssh-cpath-dir = (os:temp-dir "ghostty-ssh-"$ssh-user".*")
|
||||
var ssh-cpath = $ssh-cpath-dir"/socket"
|
||||
|
||||
if (bool ?(echo $ssh-terminfo | (external ssh) $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null)) {
|
||||
set ssh-term = "xterm-ghostty"
|
||||
set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath)
|
||||
|
||||
# Cache successful installation
|
||||
$ghostty +ssh-cache --add=$ssh-target >/dev/null
|
||||
} else {
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
}
|
||||
} else {
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with [E:TERM = $ssh-term] {
|
||||
(external ssh) $@ssh-opts $@args
|
||||
}
|
||||
}
|
||||
|
||||
defer {
|
||||
mark-prompt-start
|
||||
report-pwd
|
||||
@ -107,8 +187,6 @@
|
||||
set edit:after-readline = (conj $edit:after-readline $mark-output-start~)
|
||||
set edit:after-command = (conj $edit:after-command $mark-output-end~)
|
||||
|
||||
var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)]
|
||||
|
||||
if (has-value $features title) {
|
||||
set after-chdir = (conj $after-chdir {|_| report-pwd })
|
||||
}
|
||||
@ -121,4 +199,7 @@
|
||||
if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
|
||||
edit:add-var sudo~ $sudo-with-terminfo~
|
||||
}
|
||||
if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) (has-external ssh)) {
|
||||
edit:add-var ssh~ $ssh-integration~
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +86,89 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
||||
end
|
||||
end
|
||||
|
||||
# SSH Integration
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
if contains ssh-env $features; or contains ssh-terminfo $features
|
||||
function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration"
|
||||
set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES")
|
||||
set -l ssh_term "xterm-256color"
|
||||
set -l ssh_opts
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if contains ssh-env $features
|
||||
set -a ssh_opts -o "SetEnv COLORTERM=truecolor"
|
||||
set -a ssh_opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"
|
||||
end
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if contains ssh-terminfo $features
|
||||
set -l ssh_user
|
||||
set -l ssh_hostname
|
||||
|
||||
for line in (command ssh -G $argv 2>/dev/null)
|
||||
set -l parts (string split ' ' -- $line)
|
||||
if test (count $parts) -ge 2
|
||||
switch $parts[1]
|
||||
case user
|
||||
set ssh_user $parts[2]
|
||||
case hostname
|
||||
set ssh_hostname $parts[2]
|
||||
end
|
||||
if test -n "$ssh_user"; and test -n "$ssh_hostname"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if test -n "$ssh_hostname"
|
||||
set -l ssh_target "$ssh_user@$ssh_hostname"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1
|
||||
set ssh_term "xterm-ghostty"
|
||||
else if command -q infocmp
|
||||
set -l ssh_terminfo
|
||||
set -l ssh_cpath_dir
|
||||
set -l ssh_cpath
|
||||
|
||||
set ssh_terminfo (infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if test -n "$ssh_terminfo"
|
||||
echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random))
|
||||
set ssh_cpath "$ssh_cpath_dir/socket"
|
||||
|
||||
if echo "$ssh_terminfo" | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null
|
||||
set ssh_term "xterm-ghostty"
|
||||
set -a ssh_opts -o "ControlPath=$ssh_cpath"
|
||||
|
||||
# Cache successful installation
|
||||
if test -x "$GHOSTTY_BIN_DIR/ghostty"
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true
|
||||
end
|
||||
else
|
||||
echo "Warning: Failed to install terminfo." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: Could not generate terminfo data." >&2
|
||||
end
|
||||
else
|
||||
echo "Warning: ghostty command not available for cache management." >&2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
env TERM="$ssh_term" command ssh $ssh_opts $argv
|
||||
end
|
||||
end
|
||||
|
||||
# Setup prompt marking
|
||||
function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror
|
||||
# If we never got the output end event, then we need to send it now.
|
||||
|
@ -244,6 +244,79 @@ _ghostty_deferred_init() {
|
||||
}
|
||||
fi
|
||||
|
||||
# SSH Integration
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then
|
||||
ssh() {
|
||||
emulate -L zsh
|
||||
setopt local_options no_glob_subst
|
||||
|
||||
local ssh_term ssh_opts
|
||||
ssh_term="xterm-256color"
|
||||
ssh_opts=()
|
||||
|
||||
# Configure environment variables for remote session
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then
|
||||
ssh_opts+=(-o "SetEnv COLORTERM=truecolor")
|
||||
ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION")
|
||||
fi
|
||||
|
||||
# Install terminfo on remote host if needed
|
||||
if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then
|
||||
local ssh_user ssh_hostname
|
||||
|
||||
while IFS=' ' read -r ssh_key ssh_value; do
|
||||
case "$ssh_key" in
|
||||
user) ssh_user="$ssh_value" ;;
|
||||
hostname) ssh_hostname="$ssh_value" ;;
|
||||
esac
|
||||
[[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break
|
||||
done < <(command ssh -G "$@" 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_hostname" ]]; then
|
||||
local ssh_target="${ssh_user}@${ssh_hostname}"
|
||||
|
||||
# Check if terminfo is already cached
|
||||
if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then
|
||||
ssh_term="xterm-ghostty"
|
||||
elif (( $+commands[infocmp] )); then
|
||||
local ssh_terminfo ssh_cpath_dir ssh_cpath
|
||||
|
||||
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
|
||||
|
||||
if [[ -n "$ssh_terminfo" ]]; then
|
||||
print "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2
|
||||
|
||||
ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$"
|
||||
ssh_cpath="$ssh_cpath_dir/socket"
|
||||
|
||||
if print "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" '
|
||||
infocmp xterm-ghostty >/dev/null 2>&1 && exit 0
|
||||
command -v tic >/dev/null 2>&1 || exit 1
|
||||
mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0
|
||||
exit 1
|
||||
' 2>/dev/null; then
|
||||
ssh_term="xterm-ghostty"
|
||||
ssh_opts+=(-o "ControlPath=$ssh_cpath")
|
||||
|
||||
# Cache successful installation
|
||||
"$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true
|
||||
else
|
||||
print "Warning: Failed to install terminfo." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: Could not generate terminfo data." >&2
|
||||
fi
|
||||
else
|
||||
print "Warning: ghostty command not available for cache management." >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Execute SSH with TERM environment variable
|
||||
TERM="$ssh_term" command ssh "${ssh_opts[@]}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
# Some zsh users manually run `source ~/.zshrc` in order to apply rc file
|
||||
# changes to the current shell. This is a terrible practice that breaks many
|
||||
# things, including our shell integration. For example, Oh My Zsh and Prezto
|
||||
|
108
src/synthetic/cli.zig
Normal file
108
src/synthetic/cli.zig
Normal file
@ -0,0 +1,108 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const cli = @import("../cli.zig");
|
||||
|
||||
/// The available actions for the CLI. This is the list of available
|
||||
/// synthetic generators. View docs for each individual one in the
|
||||
/// predictably named files under `cli/`.
|
||||
pub const Action = enum {
|
||||
ascii,
|
||||
osc,
|
||||
utf8,
|
||||
|
||||
/// Returns the struct associated with the action. The struct
|
||||
/// should have a few decls:
|
||||
///
|
||||
/// - `const Options`: The CLI options for the action.
|
||||
/// - `fn create`: Create a new instance of the action from options.
|
||||
/// - `fn destroy`: Destroy the instance of the action.
|
||||
///
|
||||
/// See TerminalStream for an example.
|
||||
pub fn Struct(comptime action: Action) type {
|
||||
return switch (action) {
|
||||
.ascii => @import("cli/Ascii.zig"),
|
||||
.osc => @import("cli/Osc.zig"),
|
||||
.utf8 => @import("cli/Utf8.zig"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// An entrypoint for the synthetic generator CLI.
|
||||
pub fn main() !void {
|
||||
const alloc = std.heap.c_allocator;
|
||||
const action_ = try cli.action.detectArgs(Action, alloc);
|
||||
const action = action_ orelse return error.NoAction;
|
||||
try mainAction(alloc, action, .cli);
|
||||
}
|
||||
|
||||
pub const Args = union(enum) {
|
||||
/// The arguments passed to the CLI via argc/argv.
|
||||
cli,
|
||||
|
||||
/// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
|
||||
string: []const u8,
|
||||
};
|
||||
|
||||
pub fn mainAction(
|
||||
alloc: Allocator,
|
||||
action: Action,
|
||||
args: Args,
|
||||
) !void {
|
||||
switch (action) {
|
||||
inline else => |comptime_action| {
|
||||
const Impl = Action.Struct(comptime_action);
|
||||
try mainActionImpl(Impl, alloc, args);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn mainActionImpl(
|
||||
comptime Impl: type,
|
||||
alloc: Allocator,
|
||||
args: Args,
|
||||
) !void {
|
||||
// First, parse our CLI options.
|
||||
const Options = Impl.Options;
|
||||
var opts: Options = .{};
|
||||
defer if (@hasDecl(Options, "deinit")) opts.deinit();
|
||||
switch (args) {
|
||||
.cli => {
|
||||
var iter = try cli.args.argsIterator(alloc);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Options, alloc, &opts, &iter);
|
||||
},
|
||||
.string => |str| {
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
alloc,
|
||||
str,
|
||||
);
|
||||
defer iter.deinit();
|
||||
try cli.args.parse(Options, alloc, &opts, &iter);
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Make this a command line option.
|
||||
const seed: u64 = @truncate(@as(
|
||||
u128,
|
||||
@bitCast(std.time.nanoTimestamp()),
|
||||
));
|
||||
var prng = std.Random.DefaultPrng.init(seed);
|
||||
const rand = prng.random();
|
||||
|
||||
// Our output always goes to stdout.
|
||||
const writer = std.io.getStdOut().writer();
|
||||
|
||||
// Create our implementation
|
||||
const impl = try Impl.create(alloc, opts);
|
||||
defer impl.destroy(alloc);
|
||||
try impl.run(writer, rand);
|
||||
}
|
||||
|
||||
test {
|
||||
// Make sure we ref all our actions
|
||||
inline for (@typeInfo(Action).@"enum".fields) |field| {
|
||||
const action = @field(Action, field.name);
|
||||
const Impl = Action.Struct(action);
|
||||
_ = Impl;
|
||||
}
|
||||
}
|
63
src/synthetic/cli/Ascii.zig
Normal file
63
src/synthetic/cli/Ascii.zig
Normal file
@ -0,0 +1,63 @@
|
||||
const Ascii = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const synthetic = @import("../main.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
_: Options,
|
||||
) !*Ascii {
|
||||
const ptr = try alloc.create(Ascii);
|
||||
errdefer alloc.destroy(ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Ascii, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn run(self: *Ascii, writer: anytype, rand: std.Random) !void {
|
||||
_ = self;
|
||||
|
||||
var gen: synthetic.Bytes = .{
|
||||
.rand = rand,
|
||||
.alphabet = synthetic.Bytes.Alphabet.ascii,
|
||||
};
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
const data = try gen.next(&buf);
|
||||
writer.writeAll(data) catch |err| {
|
||||
const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err);
|
||||
switch (@as(Error, err)) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
error.NoSpaceLeft => return, // fixed buffer full
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test Ascii {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *Ascii = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(1);
|
||||
const rand = prng.random();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try impl.run(writer, rand);
|
||||
}
|
67
src/synthetic/cli/Osc.zig
Normal file
67
src/synthetic/cli/Osc.zig
Normal file
@ -0,0 +1,67 @@
|
||||
const Osc = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const synthetic = @import("../main.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
pub const Options = struct {
|
||||
/// Probability of generating a valid value.
|
||||
@"p-valid": f64 = 0.5,
|
||||
};
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*Osc {
|
||||
const ptr = try alloc.create(Osc);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{ .opts = opts };
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Osc, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn run(self: *Osc, writer: anytype, rand: std.Random) !void {
|
||||
var gen: synthetic.Osc = .{
|
||||
.rand = rand,
|
||||
.p_valid = self.opts.@"p-valid",
|
||||
};
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
const data = try gen.next(&buf);
|
||||
writer.writeAll(data) catch |err| {
|
||||
const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err);
|
||||
switch (@as(Error, err)) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
error.NoSpaceLeft => return, // fixed buffer full
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test Osc {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *Osc = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(1);
|
||||
const rand = prng.random();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try impl.run(writer, rand);
|
||||
}
|
62
src/synthetic/cli/Utf8.zig
Normal file
62
src/synthetic/cli/Utf8.zig
Normal file
@ -0,0 +1,62 @@
|
||||
const Utf8 = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const synthetic = @import("../main.zig");
|
||||
|
||||
const log = std.log.scoped(.@"terminal-stream-bench");
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
_: Options,
|
||||
) !*Utf8 {
|
||||
const ptr = try alloc.create(Utf8);
|
||||
errdefer alloc.destroy(ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Utf8, alloc: Allocator) void {
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn run(self: *Utf8, writer: anytype, rand: std.Random) !void {
|
||||
_ = self;
|
||||
|
||||
var gen: synthetic.Utf8 = .{
|
||||
.rand = rand,
|
||||
};
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
while (true) {
|
||||
const data = try gen.next(&buf);
|
||||
writer.writeAll(data) catch |err| {
|
||||
const Error = error{ NoSpaceLeft, BrokenPipe } || @TypeOf(err);
|
||||
switch (@as(Error, err)) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
error.NoSpaceLeft => return, // fixed buffer full
|
||||
else => return err,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
test Utf8 {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *Utf8 = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
var prng = std.Random.DefaultPrng.init(1);
|
||||
const rand = prng.random();
|
||||
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
|
||||
try impl.run(writer, rand);
|
||||
}
|
@ -13,6 +13,8 @@
|
||||
//! is not limited to that and we may want to extract this to a
|
||||
//! standalone package one day.
|
||||
|
||||
pub const cli = @import("cli.zig");
|
||||
|
||||
pub const Generator = @import("Generator.zig");
|
||||
pub const Bytes = @import("Bytes.zig");
|
||||
pub const Utf8 = @import("Utf8.zig");
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user