Merge branch 'ghostty-org:main' into alt-keybindings-copy-and-paste

This commit is contained in:
deftdawg
2024-12-22 02:06:54 -05:00
committed by GitHub
144 changed files with 2689 additions and 11843 deletions

383
.github/workflows/release-tag.yml vendored Normal file
View File

@ -0,0 +1,383 @@
on:
workflow_dispatch:
inputs:
version:
description: "Version to deploy (format: vX.Y.Z)"
required: true
upload:
description: "Upload final artifacts to R2"
default: false
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
name: Release Tag
# We must only run one release workflow at a time to prevent corrupting
# our release artifacts.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
setup:
runs-on: namespace-profile-ghostty-sm
outputs:
version: ${{ steps.extract_version.outputs.version }}
build: ${{ steps.extract_build_info.outputs.build }}
commit: ${{ steps.extract_build_info.outputs.commit }}
commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
steps:
- name: Validate Version Input
if: github.event_name == 'workflow_dispatch'
run: |
if [[ ! "${{ github.event.inputs.version }}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Version must follow the format vX.Y.Z (e.g., v1.0.0)."
exit 1
fi
echo "Version is valid: ${{ github.event.inputs.version }}"
- name: Exract the Version
id: extract_version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
# Remove the leading 'v' from the tag
VERSION=${GITHUB_REF#refs/tags/v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
VERSION=${{ github.event.inputs.version }}
VERSION=${VERSION#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "Error: Unsupported event type."
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
with:
# Important so that build number generation works
fetch-depth: 0
- name: Extract build info
id: extract_build_info
run: |
GHOSTTY_BUILD=$(git rev-list --count HEAD)
GHOSTTY_COMMIT=$(git rev-parse --short HEAD)
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
echo "build=$GHOSTTY_BUILD" >> $GITHUB_OUTPUT
echo "commit=$GHOSTTY_COMMIT" >> $GITHUB_OUTPUT
echo "commit_long=$GHOSTTY_COMMIT_LONG" >> $GITHUB_OUTPUT
cat $GITHUB_OUTPUT
source-tarball:
runs-on: namespace-profile-ghostty-md
needs: [setup]
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Create Tarball
run: git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
- name: Sign Tarball
run: |
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: source-tarball
path: |-
ghostty-source.tar.gz
ghostty-source.tar.gz.minisig
build-macos:
needs: [setup]
runs-on: namespace-profile-ghostty-macos
timeout-minutes: 90
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle
curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit
run: |
nix develop -c \
zig build \
-Doptimize=ReleaseFast \
-Dversion-string=${GHOSTTY_VERSION}
# The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: |
cd macos
xcodebuild -target Ghostty -configuration Release
# Add all our metadata to Info.plist so we can reference it later.
- name: Update Info.plist
env:
SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }}
run: |
# Version Info
/usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_VERSION" "macos/build/Release/Ghostty.app/Contents/Info.plist"
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
# Turn our base64-encoded certificate back to a regular .p12 file
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
# Codesign Sparkle. Some notes here:
# - The XPC services aren't used since we don't sandbox Ghostty,
# but since they're part of the build, they still need to be
# codesigned.
# - The binaries in the "Versions" folders need to NOT be symlinks.
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
# Codesign the app bundle
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
- name: Create DMG
env:
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
run: |
npm install --global create-dmg
create-dmg \
--identity="$MACOS_CERTIFICATE_NAME" \
./macos/build/Release/Ghostty.app \
./
mv ./Ghostty*.dmg ./Ghostty.dmg
- name: "Notarize DMG"
env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize dmg"
xcrun notarytool submit "Ghostty.dmg" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available. We do this to
# both the app and the dmg
echo "Attach staple"
xcrun stapler staple "Ghostty.dmg"
xcrun stapler staple "macos/build/Release/Ghostty.app"
# Zip up the app and symbols
- name: Zip App
run: |
cd macos/build/Release
zip -9 -r --symlinks ../../../ghostty-macos-universal.zip Ghostty.app
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: macos
path: |-
Ghostty.dmg
ghostty-macos-universal.zip
ghostty-macos-universal-dsym.zip
sentry-dsym:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- name: Install sentry-cli
run: |
curl -sL https://sentry.io/get-cli/ | bash
- name: Download macOS Artifacts
uses: actions/download-artifact@v4
with:
name: macos
- name: Upload dSYM to Sentry
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
run: |
sentry-cli dif upload --project ghostty --wait ghostty-macos-universal-dsym.zip
appcast:
needs: [setup, build-macos]
runs-on: namespace-profile-ghostty-macos
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download macOS Artifacts
uses: actions/download-artifact@v4
with:
name: macos
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
run: |
mkdir -p .action/sparkle
cd .action/sparkle
curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
- name: Generate Appcast
env:
SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }}
run: |
echo "GHOSTTY_VERSION=$GHOSTTY_VERSION"
echo "GHOSTTY_BUILD=$GHOSTTY_BUILD"
echo "GHOSTTY_COMMIT=$GHOSTTY_COMMIT"
echo "GHOSTTY_COMMIT_LONG=$GHOSTTY_COMMIT_LONG"
echo $SPARKLE_KEY > signing.key
sign_update -f signing.key Ghostty.dmg > sign_update.txt
curl -L https://release.files.ghostty.org/appcast.xml > appcast.xml
python3 ./dist/macos/update_appcast_tag.py
test -f appcast_new.xml
mv appcast_new.xml appcast.xml
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sparkle
path: |-
appcast.xml
upload:
if: |-
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.upload == 'true') ||
github.event_name == 'push'
needs: [setup, source-tarball, build-macos, appcast]
runs-on: namespace-profile-ghostty-sm
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Download macOS Artifacts
uses: actions/download-artifact@v4
with:
name: macos
- name: Download Sparkle Artifacts
uses: actions/download-artifact@v4
with:
name: sparkle
- name: Download Source Tarball Artifacts
uses: actions/download-artifact@v4
with:
name: source-tarball
# Upload all of our files EXCEPT the appcast. The appcast triggers
# updates in clients and we don't want to do that until we're
# sure these are uploaded.
- name: Prep Files
run: |
mkdir blob
mkdir -p blob/${GHOSTTY_VERSION}
mv ghostty-source.tar.gz blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz
mv ghostty-source.tar.gz.minisig blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz.minisig
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip
mv ghostty-macos-universal-dsym.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal-dsym.zip
mv Ghostty.dmg blob/${GHOSTTY_VERSION}/Ghostty.dmg
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
r2-bucket: ghostty-release
source-dir: blob
destination-dir: ./
- name: Prep Appcast
run: |
rm -rf blob
mkdir blob
mv appcast.xml blob/appcast.xml
- name: Upload Appcast to R2
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.CF_R2_RELEASE_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_RELEASE_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_RELEASE_SECRET_KEY }}
r2-bucket: ghostty-release
source-dir: blob
destination-dir: ./

View File

@ -111,7 +111,7 @@ jobs:
name: ghostty name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Create Tarball - name: Create Tarball
run: git archive --format=tgz -o ghostty-source.tar.gz HEAD run: git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
- name: Sign Tarball - name: Sign Tarball
run: | run: |
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
@ -239,7 +239,18 @@ jobs:
# Codesign the app bundle # Codesign the app bundle
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app /usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
- name: "Notarize app bundle" - name: Create DMG
env:
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
run: |
npm install --global create-dmg
create-dmg \
--identity="$MACOS_CERTIFICATE_NAME" \
./macos/build/Release/Ghostty.app \
./
mv ./Ghostty*.dmg ./Ghostty.dmg
- name: "Notarize DMG"
env: env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
@ -250,22 +261,18 @@ jobs:
echo "Create keychain profile" echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result. # Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App # This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if # characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious # you're curious
echo "Notarize app" echo "Notarize dmg"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait xcrun notarytool submit "Ghostty.dmg" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be # Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available. # validated by macOS even when an internet connection is not available. We do this to
# both the app and the dmg
echo "Attach staple" echo "Attach staple"
xcrun stapler staple "Ghostty.dmg"
xcrun stapler staple "macos/build/Release/Ghostty.app" xcrun stapler staple "macos/build/Release/Ghostty.app"
# Zip up the app and symbols # Zip up the app and symbols
@ -283,7 +290,9 @@ jobs:
prerelease: true prerelease: true
tag_name: tip tag_name: tip
target_commitish: ${{ github.sha }} target_commitish: ${{ github.sha }}
files: ghostty-macos-universal.zip files: |
ghostty-macos-universal.zip
Ghostty.dmg
token: ${{ secrets.GH_RELEASE_TOKEN }} token: ${{ secrets.GH_RELEASE_TOKEN }}
# Create our appcast for Sparkle # Create our appcast for Sparkle
@ -292,8 +301,8 @@ jobs:
SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }} SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }}
run: | run: |
echo $SPARKLE_KEY > signing.key echo $SPARKLE_KEY > signing.key
sign_update -f signing.key ghostty-macos-universal.zip > sign_update.txt sign_update -f signing.key Ghostty.dmg > sign_update.txt
curl -L https://tip.files.ghostty.dev/appcast.xml > appcast.xml curl -L https://tip.files.ghostty.org/appcast.xml > appcast.xml
python3 ./dist/macos/update_appcast_tip.py python3 ./dist/macos/update_appcast_tip.py
test -f appcast_new.xml test -f appcast_new.xml
@ -304,6 +313,7 @@ jobs:
mkdir -p blob/${GHOSTTY_COMMIT_LONG} mkdir -p blob/${GHOSTTY_COMMIT_LONG}
cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
cp Ghostty.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
- name: Upload to R2 - name: Upload to R2
uses: ryand56/r2-upload-action@latest uses: ryand56/r2-upload-action@latest

View File

@ -14,17 +14,29 @@ package Ghostty for distribution.
## Source Tarballs ## Source Tarballs
Source tarballs with stable checksums are available on the Source tarballs with stable checksums are available for tagged releases
[GitHub releases page](https://github.com/ghostty-org/ghostty/releases). at `release.files.ghostty.org` in the following URL format where
Use the `ghostty-source.tar.gz` asset and _not the GitHub auto-generated `VERSION` is the version number with no prefix such as `1.0.0`:
source tarball_.
Signature files are signed with [minisign](https://jedisct1.github.io/minisign/) using the following public key: ```
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz.minisig
```
Signature files are signed with
[minisign](https://jedisct1.github.io/minisign/)
using the following public key:
``` ```
RWQlAjJC23149WL2sEpT/l0QKy7hMIFhYdQOFy0Z7z7PbneUgvlsnYcV RWQlAjJC23149WL2sEpT/l0QKy7hMIFhYdQOFy0Z7z7PbneUgvlsnYcV
``` ```
**Tip source tarballs** are available on the
[GitHub releases page](https://github.com/ghostty-org/ghostty/releases/tag/tip).
Use the `ghostty-source.tar.gz` asset and _not the GitHub auto-generated
source tarball_. These tarballs are generated for every commit to
the `main` branch and are not associated with a specific version.
## Zig Version ## Zig Version
[Zig](https://ziglang.org) is required to build Ghostty. Prior to Zig 1.0, [Zig](https://ziglang.org) is required to build Ghostty. Prior to Zig 1.0,

628
README.md
View File

@ -9,29 +9,29 @@
<br /> <br />
<a href="#about">About</a> <a href="#about">About</a>
· ·
<a href="#download">Download</a> <a href="https://ghostty.org/download">Download</a>
· ·
<a href="#roadmap-and-status">Roadmap</a> <a href="https://ghostty.org/docs">Documentation</a>
· ·
<a href="#developing-ghostty">Developing</a> <a href="#developing-ghostty">Developing</a>
</p> </p>
<p align="center">
<a href="https://github.com/ghostty-org/ghostty/blob/main/README_TESTERS.md"><b>Testers! Read This Too!</b></a>
</p>
</p> </p>
## About ## About
Ghostty is a cross-platform, GPU-accelerated terminal emulator that aims to Ghostty is a terminal emulator that differentiates itself by being
push the boundaries of what is possible with a terminal emulator by exposing fast, feature-rich, and native. While there are many excellent terminal
modern, opt-in features that enable CLI tool developers to build more feature emulators available, they all force you to choose between speed,
rich, interactive applications. features, or native UIs. Ghostty provides all three.
There are a number of excellent terminal emulator options that exist In all categories, I am not trying to claim that Ghostty is the
today. The unique goal of Ghostty is to have a platform for experimenting best (i.e. the fastest, most feature-rich, or most native). But
with modern, optional, non-standards-compliant features to enhance the Ghostty is competitive in all three categories and Ghostty
capabilities of CLI applications. We aim to be the best in this category, doesn't make you choose between them.
and competitive in the rest.
Ghostty also intends to push the boundaries of what is possible with a
terminal emulator by exposing modern, opt-in features that enable CLI tool
developers to build more feature rich, interactive applications.
While aiming for this ambitious goal, our first step is to make Ghostty While aiming for this ambitious goal, our first step is to make Ghostty
one of the best fully standards compliant terminal emulator, remaining one of the best fully standards compliant terminal emulator, remaining
@ -39,332 +39,15 @@ compatible with all existing shells and software while supporting all of
the latest terminal innovations in the ecosystem. You can use Ghostty the latest terminal innovations in the ecosystem. You can use Ghostty
as a drop-in replacement for your existing terminal emulator. as a drop-in replacement for your existing terminal emulator.
**Project Status:** Ghostty is still in beta but implements most of the For more details, see [About Ghostty](https://ghostty.org/docs/about).
features you'd expect for a daily driver. We currently have hundreds of active
beta users using Ghostty as their primary terminal. See more in
[Roadmap and Status](#roadmap-and-status).
## Download ## Download
| Platform / Package | Links | Notes | See the [download page](https://ghostty.org/download) on the Ghostty website.
| ------------------ | -------------------------------------------------------------------------- | -------------------------- |
| macOS | [Tip ("Nightly")](https://github.com/ghostty-org/ghostty/releases/tag/tip) | MacOS 13+ Universal Binary |
| Linux | [Build from Source](#developing-ghostty) | |
| Linux (NixOS/Nix) | [Use the Flake](#nix-package) | |
| Linux (Arch) | [Use the AUR package](https://aur.archlinux.org/packages/ghostty-git) | |
| Windows | [Build from Source](#developing-ghostty) | [Notes](#windows-notes) |
### Configuration ## Documentation
To configure Ghostty, you must use a configuration file. GUI-based configuration See the [documentation](https://ghostty.org/docs) on the Ghostty website.
is on the roadmap but not yet supported. The configuration file must be
placed at `$XDG_CONFIG_HOME/ghostty/config`, which defaults to
`~/.config/ghostty/config` if the [XDG environment is not set](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html).
The file format is documented below as an example:
```ini
# The syntax is "key = value". The whitespace around the equals doesn't matter.
background = 282c34
foreground= ffffff
# Comments start with a `#` and are only valid on their own line.
# Blank lines are ignored!
keybind = ctrl+z=close_surface
keybind = ctrl+d=new_split:right
# Empty values reset the configuration to the default value
font-family =
# Colors can be changed by setting the 16 colors of `palette`, which each color
# being defined as regular and bold.
#
# black
palette = 0=#1d2021
palette = 8=#7c6f64
# red
palette = 1=#cc241d
palette = 9=#fb4934
# green
palette = 2=#98971a
palette = 10=#b8bb26
# yellow
palette = 3=#d79921
palette = 11=#fabd2f
# blue
palette = 4=#458588
palette = 12=#83a598
# purple
palette = 5=#b16286
palette = 13=#d3869b
# aqua
palette = 6=#689d6a
palette = 14=#8ec07c
# white
palette = 7=#a89984
palette = 15=#fbf1c7
```
#### Configuration Documentation
There are multiple places to find documentation on the configuration options.
All locations are identical (they're all generated from the same source):
1. There are HTML and Markdown formatted docs in the
`$prefix/share/ghostty/docs` directory. This directory is created
when you build or install Ghostty. The `$prefix` is `zig-out` if you're
building from source (or the specified `--prefix` flag). On macOS,
`$prefix` is the `Contents/Resources` subdirectory of the `.app` bundle.
2. There are man pages in the `$prefix/share/man` directory. This directory
is created when you build or install Ghostty.
3. In the CLI, you can run `ghostty +show-config --default --docs`.
Note that this will output the full default configuration with docs to
stdout, so you may want to pipe that through a pager, an editor, etc.
4. In the source code, you can find the configuration structure in the
[Config structure](https://github.com/ghostty-org/ghostty/blob/main/src/config/Config.zig).
The available keys are the keys verbatim, and their possible values are typically
documented in the comments.
5. Not documentation per se, but you can search for the
[public config files](https://github.com/search?q=path%3Aghostty%2Fconfig&type=code)
of many Ghostty users for examples and inspiration.
> [!NOTE]
>
> You may see strange looking blank configurations like `font-family =`. This
> is a valid syntax to specify the default behavior (no value). The
> `+show-config` outputs it so it's clear that key is defaulting and also
> to have something to attach the doc comment to.
> [!NOTE]
>
> Configuration can be reloaded on the fly with the `reload_config`
> command. Not all configuration options can change without restarting Ghostty.
> Any options that require a restart should be documented.
#### Configuration Errors
If your configuration file has any errors, Ghostty does its best to ignore
them and move on. Configuration errors currently show up in the log. The
log is written directly to stderr, so it is up to you to figure out how to
access that for your system (for now). On macOS, you can also use the
system `log` CLI utility. See the [Mac App](#mac-app) section for more
information.
#### Debugging Configuration
You can verify that configuration is being properly loaded by looking at
the debug output of Ghostty. Documentation for how to view the debug output
is in the "building Ghostty" section at the end of the README.
In the debug output, you should see in the first 20 lines or so messages
about loading (or not loading) a configuration file, as well as any errors
it may have encountered. Configuration errors are also shown in a dedicated
window on both macOS and Linux (GTK). Ghostty does not treat configuration
errors as fatal and will fall back to default values for erroneous keys.
You can also view the full configuration Ghostty is loading using
`ghostty +show-config` from the command-line. Use the `--help` flag to
additional options for that command.
### Themes
Ghostty ships with 300+ built-in themes (from
[iTerm2 Color Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes)).
You can configure Ghostty to use any of these themes using the `theme`
configuration. Example:
```
theme = Solarized Dark - Patched
```
You can find a list of built-in themes using the `+list-themes` action:
```
ghostty +list-themes
...
```
On macOS, the themes are built-in to the `Ghostty.app` bundle. On Linux,
theme support requires a valid Ghostty resources dir ("share" directory).
More details about how to validate the resources directory on Linux
is covered in the [shell integration section](#shell-integration-installation-and-verification).
Any custom color configuration (`palette`, `background`, `foreground`, etc.)
in your configuration files will override the theme settings. This can be
used to load a theme and fine-tune specific colors to your liking.
**Interested in contributing a new theme or updating an existing theme?**
Please send theme changes upstream to the
[iTerm2 Color Schemes](https://github.com/mbadolato/iTerm2-Color-Schemes))
repository. Ghostty periodically updates the themes from this source.
_Do not send theme changes to the Ghostty project directly_.
### Shell Integration
Ghostty supports some features that require shell integration. I am aiming
to support many of the features that
[Kitty supports for shell integration](https://sw.kovidgoyal.net/kitty/shell-integration/).
The currently supported shell integration features in Ghostty:
- We do not confirm close for windows where the cursor is at a prompt.
- New terminals start in the working directory of the previously focused terminal.
- Complex prompts resize correctly by allowing the shell to redraw the prompt line.
- Triple-click while holding control (Linux) or command (macOS) to select the output of a command.
- The cursor at the prompt is turned into a bar.
- The `jump_to_prompt` keybinding can be used to scroll the terminal window
forward and back through prompts.
- Alt+click (option+click on macOS) to move the cursor at the prompt.
- `sudo` is wrapped to preserve Ghostty terminfo (disabled by default)
#### Shell Integration Installation and Verification
Ghostty will automatically inject the shell integration code for `bash`, `zsh`
and `fish`. Other shells do not have shell integration code written but will
function fine within Ghostty with the above mentioned shell integration features
inoperative. **If you want to disable automatic shell integration,** set
`shell-integration = none` in your configuration file.
Automatic `bash` shell integration requires Bash version 4 or later and must be
explicitly enabled by setting `shell-integration = bash`.
**For the automatic shell integration to work,** Ghostty must either be run
from the macOS app bundle or be installed in a location where the contents of
`zig-out/share` are available somewhere above the directory where Ghostty
is running from. On Linux, this should automatically work if you run from
the `zig-out` directory tree structure (a standard FHS-style tree).
You may also manually set the `GHOSTTY_RESOURCES_DIR` to point to the
`zig-out/share/ghostty` contents. To validate this directory the file
`$GHOSTTY_RESOURCES_DIR/../terminfo/ghostty.terminfo` should exist.
To verify shell integration is working, look for the following log lines:
```
info(io_exec): using Ghostty resources dir from env var: /Applications/Ghostty.app/Contents/Resources
info(io_exec): shell integration automatically injected shell=termio.shell_integration.Shell.fish
```
If you see any of the following, something is not working correctly.
The main culprit is usually that `GHOSTTY_RESOURCES_DIR` is not pointing
to the right place.
```
ghostty terminfo not found, using xterm-256color
or
shell could not be detected, no automatic shell integration will be injected
```
#### Switching Shells with Shell Integration
Automatic shell integration as described in the previous section only works
for the _initially launched shell_ when Ghostty is started. If you switch
shells within Ghostty, i.e. you manually run `bash` or you use a command
like `nix-shell`, the shell integration _will be lost_ in that shell
(it will keep working in the original shell process).
To make shell integration work in these cases, you must manually source
the Ghostty shell-specific code at the top of your shell configuration
files. Ghostty will automatically set the `GHOSTTY_RESOURCES_DIR` environment
variable when it starts, so you can use this to (1) detect your shell
is launched within Ghostty and (2) to find the shell-integration.
For example, for bash, you'd put this _at the top_ of your `~/.bashrc`:
```bash
# Ghostty shell integration for Bash. This must be at the top of your bashrc!
if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
builtin source "${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash"
fi
```
For details see <a href="https://github.com/ghostty-org/ghostty/blob/main/src/shell-integration/README.md">shell-integration/README.md</a>.
Each shell integration's installation instructions are documented inline:
| Shell | Integration |
| -------- | ---------------------------------------------------------------------------------------------- |
| `bash` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash` |
| `fish` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish` |
| `zsh` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/zsh/ghostty-integration` |
| `elvish` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/elvish/lib/ghostty-integration.elv` |
### Terminfo
Ghostty ships with its own [terminfo](https://en.wikipedia.org/wiki/Terminfo)
entry to tell software about its capabilities. When that entry is detected,
Ghostty sets the `TERM` environment variable to `xterm-ghostty`.
If the Ghostty resources dir ("share" directory) is detected, Ghostty will
set a `TERMINFO` environment variable so `xterm-ghostty` properly advertises
the available capabilities of Ghostty. On macOS, this always happens because
the terminfo is embedded in the app bundle. On Linux, this depends on
appropriate installation (see the installation instructions).
If you use `sudo`, sudo may reset your environment variables and you may see
an error about `missing or unsuitable terminal: xterm-ghostty` when running
some programs. To resolve this, you must either configure sudo to preserve
the `TERMINFO` environment variable, or you can use shell-integration with
the `sudo` feature enabled and Ghostty will alias sudo to automatically do
this for you. To enable the shell-integration feature specify
`shell-integration-features = sudo` in your configuration.
If you use SSH to connect to other machines that do not have Ghostty's terminfo
entry, you will see error messages like `missing or unsuitable terminal:
xterm-ghostty`.
Hopefully someday Ghostty will have terminfo entries pre-distributed
everywhere, but in the meantime there are two ways to resolve the situation:
1. Copy Ghostty's terminfo entry to the remote machine.
2. Configure SSH to fall back to a known terminfo entry.
#### Copy Ghostty's terminfo to a remote machine
The following one-liner will export the terminfo entry from your host and
import it on the remote machine:
```shell-session
infocmp -x | ssh YOUR-SERVER -- tic -x -
```
> [!NOTE]
>
> **macOS versions before Sonoma cannot use the system-bundled `infocmp`.**
> The bundled version of `ncurses` is too old to emit a terminfo entry that can be
> read by more recent versions of `tic`, and the command will fail with a bunch
> of `Illegal character` messages. You can fix this by using Homebrew to install
> a recent version of `ncurses` and replacing `infocmp` above with the full path
> `/opt/homebrew/opt/ncurses/bin/infocmp`.
#### Configure SSH to fall back to a known terminfo entry
If copying around terminfo entries is untenable, you can override `TERM` to a
fallback value using SSH config.
```ssh-config
# .ssh/config
Host example.com
SetEnv TERM=xterm-256color
```
**Requires OpenSSH 8.7 or newer.** [The 8.7 release added
support](https://www.openssh.com/txt/release-8.7) for setting `TERM` via
`SetEnv`.
> [!WARNING]
>
> **Fallback does not support advanced terminal features.** Because
> `xterm-256color` does not include all of Ghostty's capabilities, terminal
> features beyond xterm's like colored and styled underlines will not work.
## Roadmap and Status ## Roadmap and Status
@ -425,8 +108,6 @@ feature rich.
> [!NOTE] > [!NOTE]
> Despite being _very fast_, there is a lot of room for improvement here. > Despite being _very fast_, there is a lot of room for improvement here.
> We still consider some aspects of our performance a "bug" and plan on
> taking a dedicated pass to improve performance before public release.
#### Richer Windowing Features #### Richer Windowing Features
@ -506,40 +187,9 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
## Developing Ghostty ## Developing Ghostty
To build Ghostty, you need [Zig 0.13](https://ziglang.org/) installed. See the documentation on the Ghostty website for
[building Ghostty from source](http://ghostty.org/docs/install/build).
On Linux, you may need to install additional dependencies. See For development, omit the `-Doptimize` flag to build a debug build.
[Linux Installation Tips](#linux-installation-tips). On macOS, you
need Xcode installed with the macOS and iOS SDKs enabled. See
[Mac `.app`](#mac-app).
The official development environment is defined by Nix. You do not need
to use Nix to develop Ghostty, but the Nix environment is the environment
which runs CI tests and builds release artifacts. Any development work on
Ghostty must pass within these Nix environments.
> [!NOTE]
>
> **Zig 0.13 is required.** Ghostty only guarantees that it can build
> against 0.13. Zig is still a fast-moving project so it is likely newer
> versions will not be able to build Ghostty yet. You can find binary
> releases of Zig release builds on the
> [Zig downloads page](https://ziglang.org/download/).
With Zig and necessary dependencies installed, a binary can be built using
`zig build`:
```shell-session
zig build
...
zig-out/bin/ghostty
```
This will build a binary for the currently running system (if supported).
**Note: macOS does not result in a runnable binary with this command.**
macOS builds produce a library (`libghostty.a`) that is used by the Xcode
project in the `macos` directory to produce the final `Ghostty.app`.
On Linux or macOS, you can use `zig build -Dapp-runtime=glfw run` for a quick On Linux or macOS, you can use `zig build -Dapp-runtime=glfw run` for a quick
GLFW-based app for a faster development cycle while developing core GLFW-based app for a faster development cycle while developing core
@ -556,189 +206,6 @@ Other useful commands:
in the current running terminal emulator so if you want to check the in the current running terminal emulator so if you want to check the
behavior of this project, you must run this command in Ghostty. behavior of this project, you must run this command in Ghostty.
### Compiling a Release Build
The normal build will be a _debug build_ which includes a number of
safety features as well as debugging features that dramatically slow down
normal operation of the terminal (by as much as 100x). If you are building
a terminal for day to day usage, build a release version:
```shell-session
zig build -Doptimize=ReleaseFast
...
```
You can verify you have a release version by checking the filesize of the
built binary (`zig-out/bin/ghostty`). The release version should be significantly
smaller than debug builds. On Linux, the release build is around 31MB while the
debug build is around 145MB.
When using the GTK runtime (`-Dapp-runtime=gtk`) a release build will
use a [single-instance application](https://developer.gnome.org/documentation/tutorials/application.html).
If you're developing Ghostty from _inside_ a release build and build & launch a
new one that will not reflect the changes you made, but instead launch a new
window for the existing instance. You can disable this behaviour with the
`--gtk-single-instance=false` flag or by adding `gtk-single-instance = false` to
the configuration file.
### Linux Installation Tips
On Linux, you'll need to install header packages for Ghostty's dependencies
before building it. Typically, these are only gtk4 and libadwaita, since
Ghostty will build everything else static by default. On Ubuntu and Debian, use
```
sudo apt install libgtk-4-dev libadwaita-1-dev git
```
> [!NOTE]
>
> **A recent GTK is required for Ghostty to work with Nvidia (GL) drivers
> under x11.** Ubuntu 22.04 LTS has GTK 4.6 which is not new enough. Ubuntu 23.10
> has GTK 4.12 and works. From [this discussion](https://discourse.gnome.org/t/opengl-context-version-not-respected-on-gtk4-rs/12162?u=cdehais)
> the problem was fixed in GTK by Dec 2022. Also, if you are a BTRFS user, make
> sure to manually upgrade your Kernel (6.6.6 will work). The stock kernel in
> Ubuntu 23.10 is 6.5.0 which has a bug which
> [causes zig to fail its hash check for packages](https://github.com/ziglang/zig/issues/17282).
> [!WARNING]
>
> GTK 4.14 on Wayland has a bug which may cause an immediate crash.
> There is an [open issue](https://gitlab.gnome.org/GNOME/gtk/-/issues/6589/note_2072039)
> to track this GTK bug. You can workaround this issue by running ghostty with
> `GDK_DEBUG=gl-disable-gles ghostty`
>
> However, that fix may not work for you if the GTK version Ghostty is compiled
> against is too old, which mainly currently happens with development builds on NixOS.
>
> If your build of Ghostty immediately crashes after launch, try looking
> through the debug output. If running `./zig-out/bin/ghostty 2>&1 | grep "Unrecognized value"`
> result in the line `Unrecognized value "gl-disable-gles". Try GDK_DEBUG=help`,
> then the GTK version used is too old.
>
> To fix this, you might need to manually tie the `nixpkgs-stable` inputs to your
> system's `nixpkgs` in `flake.nix`:
>
> ```nix
> {
> inputs = {
> # nixpkgs-stable.url = "github:nixos/nixpkgs/release-23.05";
>
> # Assumes your system nixpkgs is called "nixpkgs"
> nixpkgs-stable.url = "nixpkgs";
> }
> }
> ```
On Arch Linux, use
```
sudo pacman -S gtk4 libadwaita
```
On Fedora variants, use
```
sudo dnf install gtk4-devel zig libadwaita-devel
```
On Fedora Atomic variants, use
```
rpm-ostree install gtk4-devel zig libadwaita-devel
```
If you're planning to use a build from source as your daily driver,
I recommend using the `-p` (prefix) flag for `zig build` to install
Ghostty into `~/.local`. This will setup the proper FHS directory structure
that ensures features such as shell integration, icons, GTK shortcuts, etc.
all work.
```
zig build -p $HOME/.local -Doptimize=ReleaseFast
...
```
With a typical Freedesktop-compatible desktop environment (i.e. Gnome,
KDE), this will make Ghostty available as an app in your app launcher.
Note, if you don't see it immediately you may have to log out and log back
in or maybe even restart. For my Gnome environment, it showed up within a
few seconds. For any other desktop environment, you can launch Ghostty
directly using `~/.local/bin/ghostty`.
If Ghostty fails to launch using an app icon in your app launcher,
ensure that `~/.local/bin` is on your _system_ `PATH`. The desktop environment
itself must have that path in the `PATH`. Google for your specific desktop
environment and distribution to learn how to do that.
This _isn't required_, but `~/.local` is a directory that happens to be
on the search path for a lot of software (such as Gnome and KDE) and
installing into a prefix with `-p` sets up a directory structure to ensure
all features of Ghostty work.
### Mac `.app`
To build the official, fully featured macOS application, you must
build on a macOS machine with Xcode installed, and the active developer
directory pointing to it. If you're not sure that's the case, check the
output of `xcode-select --print-path`:
```shell-session
xcode-select --print-path
/Library/Developer/CommandLineTools # <-- BAD
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
xcode-select --print-path
/Applications/Xcode.app/Contents/Developer # <-- GOOD
```
The above can happen if you install the Xcode Command Line Tools _after_ Xcode
is installed. With that out of the way, make sure you have both the macOS and
iOS SDKs installed (from inside Xcode → Settings → Platforms), and let's move
on to building Ghostty:
```shell-session
zig build -Doptimize=ReleaseFast
cd macos && xcodebuild
```
> [!NOTE]
> If you're using the Nix environment on macOS, `xcodebuild` will
> fail due to the linker environment variables Nix sets. You must
> run the `xcodebuild` command specifically outside of the Nix
> environment.
This will output the app to `macos/build/ReleaseLocal/Ghostty.app`.
This app will be not be signed or notarized.
[Official continuous builds are available](https://github.com/ghostty-org/ghostty/releases/tag/tip)
that are both signed and notarized.
The "ReleaseLocal" build configuration is specifically for local release
builds and disables some security features (such as "Library Validation")
to make it easier to run without having to have a code signing identity
and so on. These builds aren't meant for distribution. If you want a release
build with all security features, I highly recommend you use
[the official continuous builds](https://github.com/ghostty-org/ghostty/releases/tag/tip).
When running the app, logs are available via macOS unified logging such
as `Console.app`. The easiest way I've found to view these is to just use the CLI:
```sh
sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
...
```
### Windows Notes
Windows support is still a [work-in-progress](https://github.com/ghostty-org/ghostty/issues/437).
The current status is that a bare bones glfw-based build _works_! The experience
with this build is super minimal: there are no native experiences, only a
single window is supported, no tabs, etc. Therefore, the current status is
simply that the core terminal experience works.
If you want to help with Windows development, please see the
[tracking issue](https://github.com/ghostty-org/ghostty/issues/437). We plan
on vastly improving this experience over time.
### Linting ### Linting
#### Prettier #### Prettier
@ -780,59 +247,6 @@ alejandra .
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
### Nix Package
There is Nix package that can be used in the flake (`packages.ghostty` or `packages.default`).
It can be used in NixOS configurations and otherwise built off of.
Below is an example:
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# NOTE: This will require your git SSH access to the repo.
#
# WARNING:
# Do NOT pin the `nixpkgs` input, as that will
# declare the cache useless. If you do, you will have
# to compile LLVM, Zig and Ghostty itself on your machine,
# which will take a very very long time.
#
# Additionally, if you use NixOS, be sure to **NOT**
# run `nixos-rebuild` as root! Root has a different Git config
# that will ignore any SSH keys configured for the current user,
# denying access to the repository.
#
# Instead, either run `nix flake update` or `nixos-rebuild build`
# as the current user, and then run `sudo nixos-rebuild switch`.
ghostty = {
url = "git+ssh://git@github.com/ghostty-org/ghostty";
# NOTE: The below 2 lines are only required on nixos-unstable,
# if you're on stable, they may break your build
inputs.nixpkgs-stable.follows = "nixpkgs";
inputs.nixpkgs-unstable.follows = "nixpkgs";
};
};
outputs = { nixpkgs, ghostty, ... }: {
nixosConfigurations.mysystem = nixpkgs.lib.nixosSystem {
modules = [
{
environment.systemPackages = [
ghostty.packages.x86_64-linux.default
];
}
];
};
};
}
```
You can also test the build of the nix package at any time by running `nix build .`.
#### Updating the Zig Cache Fixed-Output Derivation Hash #### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output The Nix package depends on a [fixed-output

View File

@ -1,105 +0,0 @@
# Hello Ghostty Testers! 👋👻
Thank you for being an early Ghostty user! I'm super excited to have you
here. **Please, please read the [README](https://github.com/ghostty-org/ghostty#readme)!**
There is a lot of information in the README that I will not be repeating here,
especially about how to get Ghostty and configure it.
## Let's Build An Excellent Terminal
The Ghostty development process so far has been a cycle of inviting people,
getting Ghostty to work great for them, then inviting new people once it
feels stable.
So, if you're part of a new group, _expect there will be bugs_!
Ghostty may work really great for previous testers, but every new group of
users has their own OS quirks, programs, expected features, etc. that tend
to surface new issues. **That's why you're here and I appreciate you so much!**
**I will not invite new groups of testers until Ghostty is _very stable and
excellent_ for the previous group of testers.** So let's work together on getting
Ghostty to a place that works well for you.
## Talking About Ghostty Publicly
Feel free to talk about Ghostty, share screenshots, etc. in the public!
Please don't share source access yet. And obviously, if Ghostty is buggy
or you want to say something mean, I'd rather you talk to me first so
I can try to fix it, this is an early beta after all... I hope no testers
I invite would be mean, though!
## Reporting Issues and Contributing
Please report any issues you have, including feature requests! Because we're
in a closed beta period, there aren't really many rules -- just open 'em up
and we'll have a discussion.
That said, **feel free to contribute!** I would _love_ that. If you want
any help, ask in Discord and I'll do my best to point you in the right direction
or even pair (time permitting) if you're interested.
### Bug Priority
This is the priority of bugs:
1. Crashes. 💥 These are just unacceptable and I'll drop everything to
fix a crash.
2. Escape sequence logic or rendering issues. These are almost as bad as
crashes because they usually make your workflow unusable. This includes
unsupported escape sequences that impact your workflow.
3. Anything else...
## Let's Talk!
You likely landed in the Discord community first, if for some reason you're not
in there, join [here](https://discord.gg/ghostty). Discord is a great place to
share feedback, discuss issues, ask questions and talk to other testers.
## Other FAQ
### Can I Invite a Friend?
Yes, if you have any friends you'd like to add to the beta, you can use
the `/vouch` command in the Ghostty Discord server and mods will be notified
of your vouch request.
If the username of your friend doesn't show up for the `/vouch` command,
it means they either haven't joined the Discord yet or they haven't accepted
the server rules. Please ask them to join the Discord and accept the rules
and then try again.
Vouches are handled on a as-available basis, so please be patient. They're
usually processed quickly, but sometimes it may take a day or two. In very
rare cases, we pause vouches to ensure the stability of the beta. But that's
very rare and has only happened a handful of times.
Anyone you vouch is your responsibility, so please make sure they're a good
fit for the beta and will follow the rules. There is no limit on the number
of people you can vouch in total, but we do rate limit the number of vouches
you can do. If any mods feel you're vouching for too many people, they may
reject your requests.
### I want to help, what can I work on?
I'd really love that, I want to foster a healthy contributing community
with Ghostty over time, and I really appreciate the help.
Take a look at the issues list. Feel free to suggest new things. If you
have a favorite feature from some other terminal emulator, let's build it!
My only ask is that for big features, please ask the Discord first to gauge
interest/acceptance for it before opening up some huge PR.
There are also non-core help we can use: docs, website work, Discord bots,
etc. etc. For example, a web UI to generate a configuration file would be
cool. Or a web UI to preview your color settings.
### Is Ghostty Open Source?
Right now technically not (no license file). But yes, it will be full
open source (by the OSI definition). I'm not sure what license to choose
yet, leaning towards going with MIT for this project but open to ideas.
We will add a license prior to opening up the repository. During the private
beta period, I'll continue with a no license project.

View File

@ -13,6 +13,7 @@ const config_vim = @import("src/config/vim.zig");
const config_sublime_syntax = @import("src/config/sublime_syntax.zig"); const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
const fish_completions = @import("src/build/fish_completions.zig"); const fish_completions = @import("src/build/fish_completions.zig");
const zsh_completions = @import("src/build/zsh_completions.zig"); const zsh_completions = @import("src/build/zsh_completions.zig");
const bash_completions = @import("src/build/bash_completions.zig");
const build_config = @import("src/build_config.zig"); const build_config = @import("src/build_config.zig");
const BuildConfig = build_config.BuildConfig; const BuildConfig = build_config.BuildConfig;
const WasmTarget = @import("src/os/wasm/target.zig").Target; const WasmTarget = @import("src/os/wasm/target.zig").Target;
@ -152,6 +153,12 @@ pub fn build(b: *std.Build) !void {
break :emit_docs path != null; break :emit_docs path != null;
}; };
const emit_webdata = b.option(
bool,
"emit-webdata",
"Build the website data for the website.",
) orelse false;
const emit_xcframework = b.option( const emit_xcframework = b.option(
bool, bool,
"emit-xcframework", "emit-xcframework",
@ -517,6 +524,18 @@ pub fn build(b: *std.Build) !void {
}); });
} }
// bash shell completions
{
const wf = b.addWriteFiles();
_ = wf.add("ghostty.bash", bash_completions.bash_completions);
b.installDirectory(.{
.source_dir = wf.getDirectory(),
.install_dir = .prefix,
.install_subdir = "share/bash-completion/completions",
});
}
// Vim plugin // Vim plugin
{ {
const wf = b.addWriteFiles(); const wf = b.addWriteFiles();
@ -575,6 +594,11 @@ pub fn build(b: *std.Build) !void {
b.getInstallStep().dependOn(&b.addInstallFile(placeholder, path).step); b.getInstallStep().dependOn(&b.addInstallFile(placeholder, path).step);
} }
// Web data
if (emit_webdata) {
try buildWebData(b, config);
}
// App (Linux) // App (Linux)
if (target.result.os.tag == .linux and config.app_runtime != .none) { if (target.result.os.tag == .linux and config.app_runtime != .none) {
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
@ -1565,6 +1589,72 @@ fn buildDocumentation(
} }
} }
/// Generate the website reference data that we merge into the
/// official Ghostty website. This isn't meant to be part of any
/// actual build.
fn buildWebData(
b: *std.Build,
config: BuildConfig,
) !void {
{
const webgen_config = b.addExecutable(.{
.name = "webgen_config",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
try addHelp(b, webgen_config, config);
{
const buildconfig = config: {
var copy = config;
copy.exe_entrypoint = .webgen_config;
break :config copy;
};
const options = b.addOptions();
try buildconfig.addOptions(options);
webgen_config.root_module.addOptions("build_options", options);
}
const webgen_config_step = b.addRunArtifact(webgen_config);
const webgen_config_out = webgen_config_step.captureStdOut();
b.getInstallStep().dependOn(&b.addInstallFile(
webgen_config_out,
"share/ghostty/webdata/config.mdx",
).step);
}
{
const webgen_actions = b.addExecutable(.{
.name = "webgen_actions",
.root_source_file = b.path("src/main.zig"),
.target = b.host,
});
try addHelp(b, webgen_actions, config);
{
const buildconfig = config: {
var copy = config;
copy.exe_entrypoint = .webgen_actions;
break :config copy;
};
const options = b.addOptions();
try buildconfig.addOptions(options);
webgen_actions.root_module.addOptions("build_options", options);
}
const webgen_actions_step = b.addRunArtifact(webgen_actions);
const webgen_actions_out = webgen_actions_step.captureStdOut();
b.getInstallStep().dependOn(&b.addInstallFile(
webgen_actions_out,
"share/ghostty/webdata/actions.mdx",
).step);
}
}
fn benchSteps( fn benchSteps(
b: *std.Build, b: *std.Build,
target: std.Build.ResolvedTarget, target: std.Build.ResolvedTarget,

View File

@ -22,7 +22,7 @@
.hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc", .hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc",
}, },
.ziglyph = .{ .ziglyph = .{
.url = "https://deps.files.ghostty.dev/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", .url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25", .hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
}, },
@ -49,8 +49,8 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5fd82e34a349e36a5b3422d8225c4e044c8b3b4b.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/d6c42066b3045292e0b1154ad84ff22d6863ebf7.tar.gz",
.hash = "122083713c189f1ceab516efd494123386f3a29132a68a6896b651319a8c57d747e4", .hash = "12204358b2848ffd993d3425055bff0a5ba9b1b60bead763a6dea0517965d7290a6c",
}, },
.vaxis = .{ .vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b", .url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",

105
dist/macos/update_appcast_tag.py vendored Normal file
View File

@ -0,0 +1,105 @@
"""
This script is used to update the appcast.xml file for tagged
Ghostty releases.
This expects the following files in the current directory:
- sign_update.txt - contains the output from "sign_update" in the Sparkle
framework for the current build.
- appcast.xml - the existing appcast file.
And the following environment variables to be set:
- GHOSTTY_VERSION - the version number (X.Y.Z format)
- GHOSTTY_BUILD - the build number
- GHOSTTY_COMMIT - the commit hash
The script will output a new appcast file called appcast_new.xml.
"""
import os
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
version = os.environ["GHOSTTY_VERSION"]
build = os.environ["GHOSTTY_BUILD"]
commit = os.environ["GHOSTTY_COMMIT"]
commit_long = os.environ["GHOSTTY_COMMIT_LONG"]
repo = "https://github.com/ghostty-org/ghostty"
# Read our sign_update output
with open("sign_update.txt", "r") as f:
# format is a=b b=c etc. create a map of this. values may contain equal
# signs, so we can't just split on equal signs.
attrs = {}
for pair in f.read().split(" "):
key, value = pair.split("=", 1)
value = value.strip()
if value[0] == '"':
value = value[1:-1]
attrs[key] = value
# We need to register our namespaces before reading or writing any files.
namespaces = { "sparkle": "http://www.andymatuschak.org/xml-namespaces/sparkle" }
for prefix, uri in namespaces.items():
ET.register_namespace(prefix, uri)
# Open our existing appcast and find the channel element. This is where
# we'll add our new item.
et = ET.parse('appcast.xml')
channel = et.find("channel")
# Remove any items with the same version. If we have multiple items with
# the same version, Sparkle will report invalid signatures if it picks
# the wrong one when updating.
for item in channel.findall("item"):
sparkle_version = item.find("sparkle:version", namespaces)
if sparkle_version is not None and sparkle_version.text == build:
channel.remove(item)
# We also remove any item that doesn't have a pubDate. This should
# never happen but it prevents us from having to deal with it later.
if item.find("pubDate") is None:
channel.remove(item)
# Prune the oldest items if we have more than a limit.
prune_amount = 15
pubdate_format = "%a, %d %b %Y %H:%M:%S %z"
items = channel.findall("item")
items.sort(key=lambda item: datetime.strptime(item.find("pubDate").text, pubdate_format))
if len(items) > prune_amount:
for item in items[:-prune_amount]:
channel.remove(item)
# Create the item using some absolutely terrible XML manipulation.
item = ET.SubElement(channel, "item")
elem = ET.SubElement(item, "title")
elem.text = f"Build {build}"
elem = ET.SubElement(item, "pubDate")
elem.text = now.strftime(pubdate_format)
elem = ET.SubElement(item, "sparkle:version")
elem.text = build
elem = ET.SubElement(item, "sparkle:shortVersionString")
elem.text = f"{version}"
elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "13.0.0"
elem = ET.SubElement(item, "description")
elem.text = f"""
<h1>Ghostty v{version}</h1>
<p>
This release was built from commit <code><a href="{repo}/commits/{commit_long}">{commit}</a></code>
on {now.strftime('%Y-%m-%d')}.
</p>
<p>
We don't currently generate release notes for auto-updates.
You can view the complete changelog and release notes on
the <a href="https://ghostty.org">Ghostty website</a>.
</p>
"""
elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://release.files.ghostty.org/{version}/Ghostty.dmg")
elem.set("type", "application/octet-stream")
for key, value in attrs.items():
elem.set(key, value)
# Output the new appcast.
et.write("appcast_new.xml", xml_declaration=True, encoding="utf-8")

View File

@ -80,7 +80,7 @@ elem.text = build
elem = ET.SubElement(item, "sparkle:shortVersionString") elem = ET.SubElement(item, "sparkle:shortVersionString")
elem.text = f"{commit} ({now.strftime('%Y-%m-%d')})" elem.text = f"{commit} ({now.strftime('%Y-%m-%d')})"
elem = ET.SubElement(item, "sparkle:minimumSystemVersion") elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
elem.text = "12.0.0" elem.text = "13.0.0"
elem = ET.SubElement(item, "description") elem = ET.SubElement(item, "description")
elem.text = f""" elem.text = f"""
<p> <p>
@ -94,7 +94,7 @@ commit history <a href="{repo}">on GitHub</a> for all changes.
</p> </p>
""" """
elem = ET.SubElement(item, "enclosure") elem = ET.SubElement(item, "enclosure")
elem.set("url", f"https://tip.files.ghostty.dev/{commit_long}/ghostty-macos-universal.zip") elem.set("url", f"https://tip.files.ghostty.org/{commit_long}/Ghostty.dmg")
elem.set("type", "application/octet-stream") elem.set("type", "application/octet-stream")
for key, value in attrs.items(): for key, value in attrs.items():
elem.set(key, value) elem.set(key, value)

View File

@ -333,6 +333,21 @@ typedef struct {
uint32_t cell_height_px; uint32_t cell_height_px;
} ghostty_surface_size_s; } ghostty_surface_size_s;
// Config types
// config.Color
typedef struct {
uint8_t r;
uint8_t g;
uint8_t b;
} ghostty_config_color_s;
// config.ColorList
typedef struct {
const ghostty_config_color_s* colors;
size_t len;
} ghostty_config_color_list_s;
// apprt.Target.Key // apprt.Target.Key
typedef enum { typedef enum {
GHOSTTY_TARGET_APP, GHOSTTY_TARGET_APP,

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "base.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "beige.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "chrome.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "plastic.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "crt-effect.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "ghosty.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "gloss.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "screen-dark.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "screen-mask.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -42,6 +42,8 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>GhosttyBuild</key>
<string></string>
<key>GhosttyCommit</key> <key>GhosttyCommit</key>
<string></string> <string></string>
<key>LSEnvironment</key> <key>LSEnvironment</key>

View File

@ -39,6 +39,10 @@
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */; };
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */; };
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; };
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; };
@ -94,6 +98,8 @@
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; }; C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; }; C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -122,6 +128,10 @@
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; }; A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = "<group>"; };
A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = "<group>"; };
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = "<group>"; };
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconView.swift; sourceTree = "<group>"; };
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extension.swift"; sourceTree = "<group>"; };
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>"; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
@ -180,6 +190,8 @@
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = "<group>"; }; C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = "<group>"; };
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; }; C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; };
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; }; C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; };
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = "<group>"; };
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -233,6 +245,7 @@
A57D79252C9C8782001D522E /* Secure Input */, A57D79252C9C8782001D522E /* Secure Input */,
A534263E2A7DCC5800EBB7A2 /* Settings */, A534263E2A7DCC5800EBB7A2 /* Settings */,
A51BFC1C2B2FB5AB00E92F16 /* About */, A51BFC1C2B2FB5AB00E92F16 /* About */,
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */,
A51BFC292B30F69F00E92F16 /* Update */, A51BFC292B30F69F00E92F16 /* Update */,
); );
path = Features; path = Features;
@ -252,6 +265,7 @@
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
C1F26EA62B738B9900404083 /* NSView+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
@ -303,6 +317,16 @@
path = macOS; path = macOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */ = {
isa = PBXGroup;
children = (
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */,
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */,
A54B0CE82D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift */,
);
path = "Colorized Ghostty Icon";
sourceTree = "<group>";
};
A54CD6ED299BEB14008C95BB /* Sources */ = { A54CD6ED299BEB14008C95BB /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -371,12 +395,14 @@
A5A1F8862A489D7400D1E8BC /* Resources */ = { A5A1F8862A489D7400D1E8BC /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */,
29C15B1C2CDC3B2000520DD4 /* bat */, 29C15B1C2CDC3B2000520DD4 /* bat */,
55154BDF2B33911F001622DC /* ghostty */, 55154BDF2B33911F001622DC /* ghostty */,
552964E52B34A9B400030505 /* vim */, 552964E52B34A9B400030505 /* vim */,
A586167B2B7703CC009BDB1D /* fish */, A586167B2B7703CC009BDB1D /* fish */,
A5985CE52C33060F00C57AD3 /* man */, A5985CE52C33060F00C57AD3 /* man */,
A5A1F8842A489D6800D1E8BC /* terminfo */, A5A1F8842A489D6800D1E8BC /* terminfo */,
FC5218F92D10FFC7004C93E0 /* zsh */,
); );
name = Resources; name = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
@ -539,6 +565,7 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */, A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */,
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */, A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
@ -547,6 +574,7 @@
29C15B1D2CDC3B2900520DD4 /* bat in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
A586167C2B7703CC009BDB1D /* fish in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */,
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */,
55154BE02B33911F001622DC /* ghostty in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */,
A5985CE62C33060F00C57AD3 /* man in Resources */, A5985CE62C33060F00C57AD3 /* man in Resources */,
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */, A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
@ -572,8 +600,10 @@
files = ( files = (
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
@ -612,6 +642,8 @@
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
A55685E029A03A9F004303CE /* AppError.swift in Sources */, A55685E029A03A9F004303CE /* AppError.swift in Sources */,
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,

View File

@ -98,6 +98,13 @@ class AppDelegate: NSObject,
/// The observer for the app appearance. /// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
/// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage? = nil {
didSet {
NSApplication.shared.applicationIconImage = appIcon
}
}
override init() { override init() {
terminalManager = TerminalManager(ghostty) terminalManager = TerminalManager(ghostty)
updaterController = SPUStandardUpdaterController( updaterController = SPUStandardUpdaterController(
@ -519,6 +526,22 @@ class AppDelegate: NSObject,
} else { } else {
GlobalEventTap.shared.disable() GlobalEventTap.shared.disable()
} }
switch (config.macosIcon) {
case .official:
self.appIcon = nil
break
case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
guard let icon = ColorizedGhosttyIcon(
screenColors: screenColors,
ghostColor: ghostColor,
frame: config.macosIconFrame
).makeImage() else { break }
self.appIcon = icon
}
} }
/// Sync the appearance of our app with the theme specified in the config. /// Sync the appearance of our app with the theme specified in the config.

View File

@ -4,6 +4,7 @@ struct AboutView: View {
@Environment(\.openURL) var openURL @Environment(\.openURL) var openURL
private let githubURL = URL(string: "https://github.com/ghostty-org/ghostty") private let githubURL = URL(string: "https://github.com/ghostty-org/ghostty")
private let docsURL = URL(string: "https://ghostty.org/docs")
/// Read the commit from the bundle. /// Read the commit from the bundle.
private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String } private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
@ -43,7 +44,7 @@ struct AboutView: View {
var body: some View { var body: some View {
VStack(alignment: .center) { VStack(alignment: .center) {
Image("AppIconImage") ghosttyIconImage()
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(height: 128) .frame(height: 128)
@ -77,12 +78,16 @@ struct AboutView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
HStack(spacing: 8) { HStack(spacing: 8) {
if let url = docsURL {
Button("Docs") {
openURL(url)
}
}
if let url = githubURL { if let url = githubURL {
Button("GitHub") { Button("GitHub") {
openURL(url) openURL(url)
} }
} }
} }
if let copy = self.copyright { if let copy = self.copyright {

View File

@ -0,0 +1,55 @@
import Cocoa
struct ColorizedGhosttyIcon {
/// The colors that make up the gradient of the screen.
let screenColors: [NSColor]
/// The color of the ghost.
let ghostColor: NSColor
/// The frame type to use
let frame: Ghostty.MacOSIconFrame
/// Make a custom colorized ghostty icon.
func makeImage() -> NSImage? {
// All of our layers (not in order)
guard let screen = NSImage(named: "CustomIconScreen") else { return nil }
guard let screenMask = NSImage(named: "CustomIconScreenMask") else { return nil }
guard let ghost = NSImage(named: "CustomIconGhost") else { return nil }
guard let crt = NSImage(named: "CustomIconCRT") else { return nil }
guard let gloss = NSImage(named: "CustomIconGloss") else { return nil }
let baseName = switch (frame) {
case .aluminum: "CustomIconBaseAluminum"
case .beige: "CustomIconBaseBeige"
case .chrome: "CustomIconBaseChrome"
case .plastic: "CustomIconBasePlastic"
}
guard let base = NSImage(named: baseName) else { return nil }
// Apply our color in various ways to our layers.
// NOTE: These functions are not built-in, they're implemented as an extension
// to NSImage in NSImage+Extension.swift.
guard let screenGradient = screenMask.gradient(colors: screenColors) else { return nil }
guard let tintedGhost = ghost.tint(color: ghostColor) else { return nil }
// Combine our layers using the proper blending modes
return.combine(images: [
base,
screen,
screenGradient,
ghost,
tintedGhost,
crt,
gloss,
], blendingModes: [
.normal,
.normal,
.color,
.normal,
.color,
.overlay,
.normal,
])
}
}

View File

@ -0,0 +1,15 @@
import SwiftUI
extension View {
/// Returns the ghostty icon to use for views.
func ghosttyIconImage() -> Image {
#if os(macOS)
if let delegate = NSApplication.shared.delegate as? AppDelegate,
let nsImage = delegate.appIcon {
return Image(nsImage: nsImage)
}
#endif
return Image("AppIconImage")
}
}

View File

@ -0,0 +1,13 @@
import SwiftUI
import Cocoa
// For testing.
struct ColorizedGhosttyIconView: View {
var body: some View {
Image(nsImage: ColorizedGhosttyIcon(
screenColors: [.purple, .blue],
ghostColor: .yellow,
frame: .aluminum
).makeImage()!)
}
}

View File

@ -3,11 +3,17 @@ import Cocoa
class UpdaterDelegate: NSObject, SPUUpdaterDelegate { class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
func feedURLString(for updater: SPUUpdater) -> String? { func feedURLString(for updater: SPUUpdater) -> String? {
// Eventually w want to support multiple channels. Sparkle itself supports guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
// channels but we probably don't want some appcasts in the same file (i.e. return nil
// tip) so this would be the place to change that. For now, we hardcode the }
// tip appcast URL since it is all we support.
return "https://tip.files.ghostty.dev/appcast.xml" // Sparkle supports a native concept of "channels" but it requires that
// you share a single appcast file. We don't want to do that so we
// do this instead.
switch (appDelegate.ghostty.config.autoUpdateChannel) {
case .tip: return "https://tip.files.ghostty.org/appcast.xml"
case .stable: return "https://release.files.ghostty.org/appcast.xml"
}
} }
func updaterWillRelaunchApplication(_ updater: SPUUpdater) { func updaterWillRelaunchApplication(_ updater: SPUUpdater) {

View File

@ -252,6 +252,46 @@ extension Ghostty {
return v return v
} }
var macosIcon: MacOSIcon {
let defaultValue = MacOSIcon.official
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSIcon(rawValue: str) ?? defaultValue
}
var macosIconFrame: MacOSIconFrame {
let defaultValue = MacOSIconFrame.aluminum
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-icon-frame"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacOSIconFrame(rawValue: str) ?? defaultValue
}
var macosIconGhostColor: OSColor? {
guard let config = self.config else { return nil }
var v: ghostty_config_color_s = .init()
let key = "macos-icon-ghost-color"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
return .init(ghostty: v)
}
var macosIconScreenColor: [OSColor]? {
guard let config = self.config else { return nil }
var v: ghostty_config_color_list_s = .init()
let key = "macos-icon-screen-color"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return nil }
guard v.len > 0 else { return nil }
let buffer = UnsafeBufferPointer(start: v.colors, count: v.len)
return buffer.map { .init(ghostty: $0) }
}
var focusFollowsMouse : Bool { var focusFollowsMouse : Bool {
guard let config = self.config else { return false } guard let config = self.config else { return false }
var v = false; var v = false;
@ -261,9 +301,9 @@ extension Ghostty {
} }
var backgroundColor: Color { var backgroundColor: Color {
var rgb: UInt32 = 0 var color: ghostty_config_color_s = .init();
let bg_key = "background" let bg_key = "background"
if (!ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count))) { if (!ghostty_config_get(config, &color, bg_key, UInt(bg_key.count))) {
#if os(macOS) #if os(macOS)
return Color(NSColor.windowBackgroundColor) return Color(NSColor.windowBackgroundColor)
#elseif os(iOS) #elseif os(iOS)
@ -273,14 +313,10 @@ extension Ghostty {
#endif #endif
} }
let red = Double(rgb & 0xff) return .init(
let green = Double((rgb >> 8) & 0xff) red: Double(color.r) / 255,
let blue = Double((rgb >> 16) & 0xff) green: Double(color.g) / 255,
blue: Double(color.b) / 255
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
) )
} }
@ -311,21 +347,17 @@ extension Ghostty {
var unfocusedSplitFill: Color { var unfocusedSplitFill: Color {
guard let config = self.config else { return .white } guard let config = self.config else { return .white }
var rgb: UInt32 = 16777215 // white default var color: ghostty_config_color_s = .init();
let key = "unfocused-split-fill" let key = "unfocused-split-fill"
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) { if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
let bg_key = "background" let bg_key = "background"
_ = ghostty_config_get(config, &rgb, bg_key, UInt(bg_key.count)); _ = ghostty_config_get(config, &color, bg_key, UInt(bg_key.count));
} }
let red = Double(rgb & 0xff) return .init(
let green = Double((rgb >> 8) & 0xff) red: Double(color.r),
let blue = Double((rgb >> 16) & 0xff) green: Double(color.g) / 255,
blue: Double(color.b) / 255
return Color(
red: red / 255,
green: green / 255,
blue: blue / 255
) )
} }
@ -408,6 +440,17 @@ extension Ghostty {
return AutoUpdate(rawValue: str) ?? defaultValue return AutoUpdate(rawValue: str) ?? defaultValue
} }
var autoUpdateChannel: AutoUpdateChannel {
let defaultValue = AutoUpdateChannel.stable
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "auto-update-channel"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return AutoUpdateChannel(rawValue: str) ?? defaultValue
}
var autoSecureInput: Bool { var autoSecureInput: Bool {
guard let config = self.config else { return true } guard let config = self.config else { return true }
var v = false; var v = false;

View File

@ -194,13 +194,32 @@ extension Ghostty {
} }
} }
} }
/// macos-icon
enum MacOSIcon: String {
case official
case customStyle = "custom-style"
}
/// macos-icon-frame
enum MacOSIconFrame: String {
case aluminum
case beige
case plastic
case chrome
}
/// Enum for the macos-titlebar-proxy-icon config option /// Enum for the macos-titlebar-proxy-icon config option
enum MacOSTitlebarProxyIcon: String { enum MacOSTitlebarProxyIcon: String {
case visible case visible
case hidden case hidden
} }
/// Enum for auto-update-channel config option
enum AutoUpdateChannel: String {
case tip
case stable
}
} }
// MARK: Surface Notification // MARK: Surface Notification

View File

@ -0,0 +1,90 @@
import Cocoa
extension NSImage {
/// Combine multiple images with the given blend modes. This is useful given a set
/// of layers to create a final rasterized image.
static func combine(images: [NSImage], blendingModes: [CGBlendMode]) -> NSImage? {
guard images.count == blendingModes.count else { return nil }
guard images.count > 0 else { return nil }
// The final size will be the same size as our first image.
let size = images.first!.size
// Create a bitmap context manually
guard let bitmapContext = CGContext(
data: nil,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return nil }
// Clear the context
bitmapContext.setFillColor(.clear)
bitmapContext.fill(.init(origin: .zero, size: size))
// Draw each image with its corresponding blend mode
for (index, image) in images.enumerated() {
guard let cgImage = image.cgImage(
forProposedRect: nil,
context: nil,
hints: nil
) else { return nil }
let blendMode = blendingModes[index]
bitmapContext.setBlendMode(blendMode)
bitmapContext.draw(cgImage, in: CGRect(origin: .zero, size: size))
}
// Create a CGImage from the context
guard let combinedCGImage = bitmapContext.makeImage() else { return nil }
// Wrap the CGImage in an NSImage
return NSImage(cgImage: combinedCGImage, size: size)
}
/// Apply a gradient onto this image, using this image as a mask.
func gradient(colors: [NSColor]) -> NSImage? {
let resultImage = NSImage(size: size)
resultImage.lockFocus()
defer { resultImage.unlockFocus() }
// Draw the gradient
guard let gradient = NSGradient(colors: colors) else { return nil }
gradient.draw(in: .init(origin: .zero, size: size), angle: 90)
// Apply the mask
draw(at: .zero, from: .zero, operation: .destinationIn, fraction: 1.0)
return resultImage
}
// Tint an NSImage with the given color by applying a basic fill on top of it.
func tint(color: NSColor) -> NSImage? {
// Create a new image with the same size as the base image
let newImage = NSImage(size: size)
// Draw into the new image
newImage.lockFocus()
defer { newImage.unlockFocus() }
// Set up the drawing context
guard let context = NSGraphicsContext.current?.cgContext else { return nil }
defer { context.restoreGState() }
// Draw the base image
guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil }
context.draw(cgImage, in: .init(origin: .zero, size: size))
// Set the tint color and blend mode
context.setFillColor(color.cgColor)
context.setBlendMode(.sourceAtop)
// Apply the tint color over the entire image
context.fill(.init(origin: .zero, size: size))
return newImage
}
}

View File

@ -1,4 +1,5 @@
import Foundation import Foundation
import GhosttyKit
extension OSColor { extension OSColor {
var isLightColor: Bool { var isLightColor: Bool {
@ -47,6 +48,37 @@ extension OSColor {
#endif #endif
} }
/// Create an OSColor from a hex string.
convenience init?(hex: String) {
var cleanedHex = hex.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove `#` if present
if cleanedHex.hasPrefix("#") {
cleanedHex.removeFirst()
}
guard cleanedHex.count == 6 || cleanedHex.count == 8 else { return nil }
let scanner = Scanner(string: cleanedHex)
var hexNumber: UInt64 = 0
guard scanner.scanHexInt64(&hexNumber) else { return nil }
let red, green, blue, alpha: CGFloat
if cleanedHex.count == 8 {
alpha = CGFloat((hexNumber & 0xFF000000) >> 24) / 255
red = CGFloat((hexNumber & 0x00FF0000) >> 16) / 255
green = CGFloat((hexNumber & 0x0000FF00) >> 8) / 255
blue = CGFloat(hexNumber & 0x000000FF) / 255
} else { // 6 characters
alpha = 1.0
red = CGFloat((hexNumber & 0xFF0000) >> 16) / 255
green = CGFloat((hexNumber & 0x00FF00) >> 8) / 255
blue = CGFloat(hexNumber & 0x0000FF) / 255
}
self.init(red: red, green: green, blue: blue, alpha: alpha)
}
func darken(by amount: CGFloat) -> OSColor { func darken(by amount: CGFloat) -> OSColor {
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
self.getHue(&h, saturation: &s, brightness: &b, alpha: &a) self.getHue(&h, saturation: &s, brightness: &b, alpha: &a)
@ -58,3 +90,15 @@ extension OSColor {
) )
} }
} }
// MARK: Ghostty Types
extension OSColor {
/// Create a color from a Ghostty color.
convenience init(ghostty: ghostty_config_color_s) {
let red = Double(ghostty.r) / 255
let green = Double(ghostty.g) / 255
let blue = Double(ghostty.b) / 255
self.init(red: red, green: green, blue: blue, alpha: 1)
}
}

View File

@ -31,7 +31,7 @@
glslang, glslang,
gtk4, gtk4,
libadwaita, libadwaita,
gnome, adwaita-icon-theme,
hicolor-icon-theme, hicolor-icon-theme,
harfbuzz, harfbuzz,
libpng, libpng,
@ -165,7 +165,7 @@ in
# is available (namely icons). # is available (namely icons).
# Minimal subset of env set by wrapGAppsHook4 for icons and global settings # Minimal subset of env set by wrapGAppsHook4 for icons and global settings
export XDG_DATA_DIRS=$XDG_DATA_DIRS:${hicolor-icon-theme}/share:${gnome.adwaita-icon-theme}/share export XDG_DATA_DIRS=$XDG_DATA_DIRS:${hicolor-icon-theme}/share:${adwaita-icon-theme}/share
export XDG_DATA_DIRS=$XDG_DATA_DIRS:$GSETTINGS_SCHEMAS_PATH # from glib setup hook export XDG_DATA_DIRS=$XDG_DATA_DIRS:$GSETTINGS_SCHEMAS_PATH # from glib setup hook
'') '')
+ (lib.optionalString stdenv.hostPlatform.isDarwin '' + (lib.optionalString stdenv.hostPlatform.isDarwin ''

View File

@ -157,7 +157,7 @@ in
chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
''; '';
outputs = ["out" "terminfo" "shell_integration"]; outputs = ["out" "terminfo" "shell_integration" "vim"];
postInstall = '' postInstall = ''
terminfo_src=${ terminfo_src=${
@ -177,6 +177,8 @@ in
mv "$out/share/ghostty/shell-integration" "$shell_integration/shell-integration" mv "$out/share/ghostty/shell-integration" "$shell_integration/shell-integration"
ln -sf "$shell_integration/shell-integration" "$out/share/ghostty/shell-integration" ln -sf "$shell_integration/shell-integration" "$out/share/ghostty/shell-integration"
echo "$shell_integration" >> "$out/nix-support/propagated-user-env-packages" echo "$shell_integration" >> "$out/nix-support/propagated-user-env-packages"
cp -r $out/share/vim/vimfiles "$vim"
''; '';
postFixup = '' postFixup = ''

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for # This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details. # more details.
"sha256-q9UDVryP50HfeeafgnrOd+D6K+cEy33/05K2TB5qiqw=" "sha256-vP8f8KQyM4CwKlw7Esmxv1q4ANu8pDXXsnVorgpWCr4="

View File

@ -3,7 +3,7 @@
.version = "2.14.2", .version = "2.14.2",
.dependencies = .{ .dependencies = .{
.fontconfig = .{ .fontconfig = .{
.url = "https://deps.files.ghostty.dev/fontconfig-2.14.2.tar.gz", .url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz",
.hash = "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7", .hash = "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7",
}, },

View File

@ -217,6 +217,10 @@ pub const FontOrientation = enum(c_uint) {
pub const FontTableTag = enum(u32) { pub const FontTableTag = enum(u32) {
svg = c.kCTFontTableSVG, svg = c.kCTFontTableSVG,
os2 = c.kCTFontTableOS2,
head = c.kCTFontTableHead,
hhea = c.kCTFontTableHhea,
post = c.kCTFontTablePost,
_, _,
pub fn init(v: *const [4]u8) FontTableTag { pub fn init(v: *const [4]u8) FontTableTag {

View File

@ -3298,9 +3298,10 @@ pub fn cursorPosCallback(
// No mouse point so we don't highlight links // No mouse point so we don't highlight links
self.renderer_state.mouse.point = null; self.renderer_state.mouse.point = null;
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
return; // Mark the link's row as dirty, but continue with updating the
// mouse state below so we can scroll when our position is negative.
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
} }
// Always show the mouse again if it is hidden // Always show the mouse again if it is hidden

View File

@ -23,6 +23,7 @@ const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig"); const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig"); const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook; const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const version = @import("version.zig"); const version = @import("version.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -35,7 +36,7 @@ window: *c.GtkWindow,
/// The header bar for the window. This is possibly null since it can be /// The header bar for the window. This is possibly null since it can be
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or /// disabled using gtk-titlebar. This is either an AdwHeaderBar or
/// GtkHeaderBar depending on if adw is enabled and linked. /// GtkHeaderBar depending on if adw is enabled and linked.
header: ?*c.GtkWidget, header: ?HeaderBar,
/// The tab overview for the window. This is possibly null since there is no /// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0). /// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
@ -120,9 +121,13 @@ pub fn init(self: *Window, app: *App) !void {
// Create our box which will hold our widgets in the main content area. // Create our box which will hold our widgets in the main content area.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
// Setup our notebook
self.notebook = Notebook.create(self);
// If we are using an AdwWindow then we can support the tab overview. // If we are using an AdwWindow then we can support the tab overview.
self.tab_overview = if (self.isAdwWindow()) overview: { self.tab_overview = if (self.isAdwWindow()) overview: {
const tab_overview = c.adw_tab_overview_new(); const tab_overview = c.adw_tab_overview_new();
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1); c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
tab_overview, tab_overview,
@ -149,49 +154,53 @@ pub fn init(self: *Window, app: *App) !void {
// are decorated or not because we can have a keybind to toggle the // are decorated or not because we can have a keybind to toggle the
// decorations. // decorations.
if (app.config.@"gtk-titlebar") { if (app.config.@"gtk-titlebar") {
const header: *c.GtkWidget = if (self.isAdwWindow()) const header = HeaderBar.init(self);
@ptrCast(c.adw_header_bar_new())
else
@ptrCast(c.gtk_header_bar_new());
{ {
const btn = c.gtk_menu_button_new(); const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu"); c.gtk_widget_set_tooltip_text(btn, "Main Menu");
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic"); c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu))); c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
if (self.isAdwWindow()) { header.packEnd(btn);
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
c.adw_header_bar_pack_end(@ptrCast(header), btn);
} else c.gtk_header_bar_pack_end(@ptrCast(header), btn);
} }
// If we're using an AdwWindow then we can support the tab overview. // If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| { if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.isAdwWindow()); assert(self.isAdwWindow());
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
_ = c.g_object_bind_property(
btn,
"active",
tab_overview,
"open",
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
);
break :btn btn;
},
.hidden => btn: {
const btn = c.adw_tab_button_new();
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw_tab_view);
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn;
},
};
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "Show Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
c.gtk_widget_set_focus_on_click(btn, c.FALSE); c.gtk_widget_set_focus_on_click(btn, c.FALSE);
c.adw_header_bar_pack_end(@ptrCast(header), btn); header.packEnd(btn);
_ = c.g_object_bind_property(
btn,
"active",
tab_overview,
"open",
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
);
} }
{ {
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic"); const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
c.gtk_widget_set_tooltip_text(btn, "New Tab"); c.gtk_widget_set_tooltip_text(btn, "New Tab");
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(&gtkTabNewClick), self, null, c.G_CONNECT_DEFAULT);
if (self.isAdwWindow()) header.packEnd(btn);
c.adw_header_bar_pack_end(@ptrCast(header), btn)
else
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
} }
self.header = header; self.header = header;
@ -225,9 +234,6 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_box_append(@ptrCast(box), warning_box); c.gtk_box_append(@ptrCast(box), warning_box);
} }
// Setup our notebook
self.notebook = Notebook.create(self);
// Setup our toast overlay if we have one // Setup our toast overlay if we have one
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: { self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
const toast_overlay = c.adw_toast_overlay_new(); const toast_overlay = c.adw_toast_overlay_new();
@ -277,18 +283,22 @@ pub fn init(self: *Window, app: *App) !void {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new()); const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
const header_widget: *c.GtkWidget = @ptrCast(@alignCast(self.header.?)); const header_widget: *c.GtkWidget = self.header.?.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget); c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
const tab_bar = c.adw_tab_bar_new();
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view);
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0); if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new();
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw_tab_view);
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar)); if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
switch (self.app.config.@"gtk-tabs-location") {
// left and right is not supported in libadwaita. const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget), switch (self.app.config.@"gtk-tabs-location") {
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), // left and right are not supported in libadwaita.
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
.hidden => unreachable,
}
} }
c.adw_toolbar_view_set_content(toolbar_view, box); c.adw_toolbar_view_set_content(toolbar_view, box);
@ -322,15 +332,16 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(toolbar_view)), @ptrCast(@alignCast(toolbar_view)),
); );
} }
} else { } else tab_bar: {
switch (self.notebook) { switch (self.notebook) {
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) { .adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
// In earlier adwaita versions, we need to add the tabbar manually since we do not use // In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView. // an AdwToolbarView.
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
switch (app.config.@"gtk-tabs-location") { switch (app.config.@"gtk-tabs-location") {
// left and right is not supported in libadwaita.
.top, .top,
.left, .left,
.right, .right,
@ -343,12 +354,11 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(box), @ptrCast(box),
@ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(tab_bar)),
), ),
.hidden => unreachable,
} }
c.adw_tab_bar_set_view(tab_bar, tab_view); c.adw_tab_bar_set_view(tab_bar, tab_view);
if (!app.config.@"gtk-wide-tabs") { if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
}
}, },
.gtk_notebook => {}, .gtk_notebook => {},
@ -356,7 +366,7 @@ pub fn init(self: *Window, app: *App) !void {
// The box is our main child // The box is our main child
c.gtk_window_set_child(gtk_window, box); c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, @ptrCast(@alignCast(h))); if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, h.asWidget());
} }
// Show the window // Show the window
@ -507,7 +517,7 @@ pub fn toggleWindowDecorations(self: *Window) void {
// and hides it with decorations, but libadwaita doesn't. This makes it // and hides it with decorations, but libadwaita doesn't. This makes it
// explicit. // explicit.
if (self.header) |v| { if (self.header) |v| {
const widget: *c.GtkWidget = @alignCast(@ptrCast(v)); const widget = v.asWidget();
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated)); c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
} }
} }

View File

@ -0,0 +1,69 @@
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else void;
pub const HeaderBar = union(enum) {
adw: *AdwHeaderBar,
gtk: *c.GtkHeaderBar,
pub fn init(window: *Window) HeaderBar {
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&window.app.config))
{
return initAdw();
}
return initGtk();
}
fn initAdw() HeaderBar {
const headerbar = c.adw_header_bar_new();
return .{ .adw = @ptrCast(headerbar) };
}
fn initGtk() HeaderBar {
const headerbar = c.gtk_header_bar_new();
return .{ .gtk = @ptrCast(headerbar) };
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),
.gtk => |headerbar| @ptrCast(@alignCast(headerbar)),
};
}
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(headerbar)),
widget,
);
},
.gtk => |headerbar| c.gtk_header_bar_pack_end(
@ptrCast(@alignCast(headerbar)),
widget,
),
}
}
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(headerbar)),
widget,
);
},
.gtk => |headerbar| c.gtk_header_bar_pack_start(
@ptrCast(@alignCast(headerbar)),
widget,
),
}
}
};

View File

@ -29,7 +29,7 @@ pub const Notebook = union(enum) {
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new(); const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget); const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") { const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
.top => c.GTK_POS_TOP, .top, .hidden => c.GTK_POS_TOP,
.bottom => c.GTK_POS_BOTTOM, .bottom => c.GTK_POS_BOTTOM,
.left => c.GTK_POS_LEFT, .left => c.GTK_POS_LEFT,
.right => c.GTK_POS_RIGHT, .right => c.GTK_POS_RIGHT,

View File

@ -0,0 +1,338 @@
const std = @import("std");
const Config = @import("../config/Config.zig");
const Action = @import("../cli/action.zig").Action;
/// A bash completions configuration that contains all the available commands
/// and options.
///
/// Notes: bash completion support for --<key>=<value> depends on setting the completion
/// system to _not_ print a space following each successful completion (see -o nospace).
/// This results leading or tailing spaces being necessary to move onto the next match.
///
/// bash completion will read = as it's own completiong word regardless of whether or not
/// it's part of an on going completion like --<key>=. Working around this requires looking
/// backward in the command line args to pretend the = is an empty string
/// see: https://www.gnu.org/software/gnuastro/manual/html_node/Bash-TAB-completion-tutorial.html
pub const bash_completions = comptimeGenerateBashCompletions();
fn comptimeGenerateBashCompletions() []const u8 {
comptime {
@setEvalBranchQuota(50000);
var counter = std.io.countingWriter(std.io.null_writer);
try writeBashCompletions(&counter.writer());
var buf: [counter.bytes_written]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try writeBashCompletions(stream.writer());
const final = buf;
return final[0..stream.getWritten().len];
}
}
fn writeBashCompletions(writer: anytype) !void {
const pad1 = " ";
const pad2 = pad1 ++ pad1;
const pad3 = pad2 ++ pad1;
const pad4 = pad3 ++ pad1;
const pad5 = pad4 ++ pad1;
try writer.writeAll(
\\_ghostty() {
\\
\\ # -o nospace requires we add back a space when a completion is finished
\\ # and not part of a --key= completion
\\ _add_spaces() {
\\ for idx in "${!COMPREPLY[@]}"; do
\\ [ -n "${COMPREPLY[idx]}" ] && COMPREPLY[idx]="${COMPREPLY[idx]} ";
\\ done
\\ }
\\
\\ _fonts() {
\\ local IFS=$'\n'
\\ mapfile -t COMPREPLY < <( compgen -P '"' -S '"' -W "$($ghostty +list-fonts | grep '^[A-Z]' )" -- "$cur")
\\ }
\\
\\ _themes() {
\\ local IFS=$'\n'
\\ mapfile -t COMPREPLY < <( compgen -P '"' -S '"' -W "$($ghostty +list-themes | sed -E 's/^(.*) \(.*$/\1/')" -- "$cur")
\\ }
\\
\\ _files() {
\\ mapfile -t COMPREPLY < <( compgen -o filenames -f -- "$cur" )
\\ for i in "${!COMPREPLY[@]}"; do
\\ if [[ -d "${COMPREPLY[i]}" ]]; then
\\ COMPREPLY[i]="${COMPREPLY[i]}/";
\\ fi
\\ if [[ -f "${COMPREPLY[i]}" ]]; then
\\ COMPREPLY[i]="${COMPREPLY[i]} ";
\\ fi
\\ done
\\ }
\\
\\ _dirs() {
\\ mapfile -t COMPREPLY < <( compgen -o dirnames -d -- "$cur" )
\\ for i in "${!COMPREPLY[@]}"; do
\\ if [[ -d "${COMPREPLY[i]}" ]]; then
\\ COMPREPLY[i]="${COMPREPLY[i]}/";
\\ fi
\\ done
\\ if [[ "${#COMPREPLY[@]}" == 0 && -d "$cur" ]]; then
\\ COMPREPLY=( "$cur " )
\\ fi
\\ }
\\
\\ _handle_config() {
\\ local config="--help"
\\ config+=" --version"
\\
);
for (@typeInfo(Config).Struct.fields) |field| {
if (field.name[0] == '_') continue;
switch (field.type) {
bool, ?bool => try writer.writeAll(pad2 ++ "config+=\" '--" ++ field.name ++ " '\"\n"),
else => try writer.writeAll(pad2 ++ "config+=\" --" ++ field.name ++ "=\"\n"),
}
}
try writer.writeAll(
\\
\\ case "$prev" in
\\
);
for (@typeInfo(Config).Struct.fields) |field| {
if (field.name[0] == '_') continue;
try writer.writeAll(pad3 ++ "--" ++ field.name ++ ") ");
if (std.mem.startsWith(u8, field.name, "font-family"))
try writer.writeAll("_fonts ;;")
else if (std.mem.eql(u8, "theme", field.name))
try writer.writeAll("_themes ;;")
else if (std.mem.eql(u8, "working-directory", field.name))
try writer.writeAll("_dirs ;;")
else if (field.type == Config.RepeatablePath)
try writer.writeAll("_files ;;")
else {
const compgenPrefix = "mapfile -t COMPREPLY < <( compgen -W \"";
const compgenSuffix = "\" -- \"$cur\" ); _add_spaces ;;";
switch (@typeInfo(field.type)) {
.Bool => try writer.writeAll("return ;;"),
.Enum => |info| {
try writer.writeAll(compgenPrefix);
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll(compgenSuffix);
},
.Struct => |info| {
if (!@hasDecl(field.type, "parseCLI") and info.layout == .@"packed") {
try writer.writeAll(compgenPrefix);
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name ++ " no-" ++ f.name);
}
try writer.writeAll(compgenSuffix);
} else {
try writer.writeAll("return ;;");
}
},
else => try writer.writeAll("return ;;"),
}
}
try writer.writeAll("\n");
}
try writer.writeAll(
\\ *) mapfile -t COMPREPLY < <( compgen -W "$config" -- "$cur" ) ;;
\\ esac
\\
\\ return 0
\\ }
\\
\\ _handle_actions() {
\\
);
for (@typeInfo(Action).Enum.fields) |field| {
if (std.mem.eql(u8, "help", field.name)) continue;
if (std.mem.eql(u8, "version", field.name)) continue;
const options = @field(Action, field.name).options();
// assumes options will never be created with only <_name> members
if (@typeInfo(options).Struct.fields.len == 0) continue;
var buffer: [field.name.len]u8 = undefined;
const bashName: []u8 = buffer[0..field.name.len];
@memcpy(bashName, field.name);
std.mem.replaceScalar(u8, bashName, '-', '_');
try writer.writeAll(pad2 ++ "local " ++ bashName ++ "=\"");
{
var count = 0;
for (@typeInfo(options).Struct.fields) |opt| {
if (opt.name[0] == '_') continue;
if (count > 0) try writer.writeAll(" ");
switch (opt.type) {
bool, ?bool => try writer.writeAll("'--" ++ opt.name ++ " '"),
else => try writer.writeAll("--" ++ opt.name ++ "="),
}
count += 1;
}
}
try writer.writeAll(" --help\"\n");
}
try writer.writeAll(
\\
\\ case "${COMP_WORDS[1]}" in
\\
);
for (@typeInfo(Action).Enum.fields) |field| {
if (std.mem.eql(u8, "help", field.name)) continue;
if (std.mem.eql(u8, "version", field.name)) continue;
const options = @field(Action, field.name).options();
if (@typeInfo(options).Struct.fields.len == 0) continue;
// bash doesn't allow variable names containing '-' so replace them
var buffer: [field.name.len]u8 = undefined;
const bashName: []u8 = buffer[0..field.name.len];
_ = std.mem.replace(u8, field.name, "-", "_", bashName);
try writer.writeAll(pad3 ++ "+" ++ field.name ++ ")\n");
try writer.writeAll(pad4 ++ "case $prev in\n");
for (@typeInfo(options).Struct.fields) |opt| {
if (opt.name[0] == '_') continue;
try writer.writeAll(pad5 ++ "--" ++ opt.name ++ ") ");
const compgenPrefix = "mapfile -t COMPREPLY < <( compgen -W \"";
const compgenSuffix = "\" -- \"$cur\" ); _add_spaces ;;";
switch (@typeInfo(opt.type)) {
.Bool => try writer.writeAll("return ;;"),
.Enum => |info| {
try writer.writeAll(compgenPrefix);
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll(compgenSuffix);
},
.Optional => |optional| {
switch (@typeInfo(optional.child)) {
.Enum => |info| {
try writer.writeAll(compgenPrefix);
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll(compgenSuffix);
},
else => {
if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll("return ;;");
} else try writer.writeAll("return;;");
},
}
},
else => {
if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll("_files ;;");
} else try writer.writeAll("return;;");
},
}
try writer.writeAll("\n");
}
try writer.writeAll(pad5 ++ "*) mapfile -t COMPREPLY < <( compgen -W \"$" ++ bashName ++ "\" -- \"$cur\" ) ;;\n");
try writer.writeAll(
\\ esac
\\ ;;
\\
);
}
try writer.writeAll(
\\ *) mapfile -t COMPREPLY < <( compgen -W "--help" -- "$cur" ) ;;
\\ esac
\\
\\ return 0
\\ }
\\
\\ # begin main logic
\\ local topLevel="-e"
\\ topLevel+=" --help"
\\ topLevel+=" --version"
\\
);
for (@typeInfo(Action).Enum.fields) |field| {
if (std.mem.eql(u8, "help", field.name)) continue;
if (std.mem.eql(u8, "version", field.name)) continue;
try writer.writeAll(pad1 ++ "topLevel+=\" +" ++ field.name ++ "\"\n");
}
try writer.writeAll(
\\
\\ local cur=""; local prev=""; local prevWasEq=false; COMPREPLY=()
\\ local ghostty="$1"
\\
\\ # script assumes default COMP_WORDBREAKS of roughly $' \t\n"\'><=;|&(:'
\\ # if = is missing this script will degrade to matching on keys only.
\\ # eg: --key=
\\ # this can be improved if needed see: https://github.com/ghostty-org/ghostty/discussions/2994
\\
\\ if [ "$2" = "=" ]; then cur=""
\\ else cur="$2"
\\ fi
\\
\\ if [ "$3" = "=" ]; then prev="${COMP_WORDS[COMP_CWORD-2]}"; prevWasEq=true;
\\ else prev="${COMP_WORDS[COMP_CWORD-1]}"
\\ fi
\\
\\ # current completion is double quoted add a space so the curor progresses
\\ if [[ "$2" == \"*\" ]]; then
\\ COMPREPLY=( "$cur " );
\\ return;
\\ fi
\\
\\ case "$COMP_CWORD" in
\\ 1)
\\ case "${COMP_WORDS[1]}" in
\\ -e | --help | --version) return 0 ;;
\\ --*) _handle_config ;;
\\ *) mapfile -t COMPREPLY < <( compgen -W "${topLevel}" -- "$cur" ); _add_spaces ;;
\\ esac
\\ ;;
\\ *)
\\ case "$prev" in
\\ -e | --help | --version) return 0 ;;
\\ *)
\\ if [[ "=" != "${COMP_WORDS[COMP_CWORD]}" && $prevWasEq != true ]]; then
\\ # must be completing with a space after the key eg: '--<key> '
\\ # clear out prev so we don't run any of the key specific completions
\\ prev=""
\\ fi
\\
\\ case "${COMP_WORDS[1]}" in
\\ --*) _handle_config ;;
\\ +*) _handle_actions ;;
\\ esac
\\ ;;
\\ esac
\\ ;;
\\ esac
\\
\\ return 0
\\}
\\
\\complete -o nospace -o bashdefault -F _ghostty ghostty
\\
);
}

View File

@ -117,12 +117,25 @@ fn writeFishCompletions(writer: anytype) !void {
.Bool => try writer.writeAll(" -a \"true false\""), .Bool => try writer.writeAll(" -a \"true false\""),
.Enum => |info| { .Enum => |info| {
try writer.writeAll(" -a \""); try writer.writeAll(" -a \"");
for (info.opts, 0..) |f, i| { for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" "); if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name); try writer.writeAll(f.name);
} }
try writer.writeAll("\""); try writer.writeAll("\"");
}, },
.Optional => |optional| {
switch (@typeInfo(optional.child)) {
.Enum => |info| {
try writer.writeAll(" -a \"");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll("\"");
},
else => {},
}
},
else => {}, else => {},
} }
try writer.writeAll("\n"); try writer.writeAll("\n");

View File

@ -0,0 +1,46 @@
const std = @import("std");
const help_strings = @import("help_strings");
const KeybindAction = @import("../../input/Binding.zig").Action;
pub fn main() !void {
const output = std.io.getStdOut().writer();
try genKeybindActions(output);
}
pub fn genKeybindActions(writer: anytype) !void {
// Write the header
try writer.writeAll(
\\---
\\title: Keybinding Action Reference
\\description: Reference of all Ghostty keybinding actions.
\\---
\\
\\This is a reference of all Ghostty keybinding actions.
\\
\\
);
@setEvalBranchQuota(5_000);
const fields = @typeInfo(KeybindAction).Union.fields;
inline for (fields) |field| {
if (field.name[0] == '_') continue;
// Write the field name.
try writer.writeAll("## `");
try writer.writeAll(field.name);
try writer.writeAll("`\n");
if (@hasDecl(help_strings.KeybindAction, field.name)) {
var iter = std.mem.splitScalar(
u8,
@field(help_strings.KeybindAction, field.name),
'\n',
);
while (iter.next()) |s| {
try writer.writeAll(s);
try writer.writeAll("\n");
}
try writer.writeAll("\n\n");
}
}
}

View File

@ -0,0 +1,131 @@
const std = @import("std");
const Config = @import("../../config/Config.zig");
const help_strings = @import("help_strings");
pub fn main() !void {
const output = std.io.getStdOut().writer();
try genConfig(output);
}
pub fn genConfig(writer: anytype) !void {
// Write the header
try writer.writeAll(
\\---
\\title: Reference
\\description: Reference of all Ghostty configuration options.
\\---
\\
\\This is a reference of all Ghostty configuration options. These
\\options are ordered roughly by how common they are to be used
\\and grouped with related options. I recommend utilizing your
\\browser's search functionality to find the option you're looking
\\for.
\\
\\In the future, we'll have a more user-friendly way to view and
\\organize these options.
\\
\\
);
@setEvalBranchQuota(50_000);
const fields = @typeInfo(Config).Struct.fields;
inline for (fields, 0..) |field, i| {
if (field.name[0] == '_') continue;
if (!@hasDecl(help_strings.Config, field.name)) continue;
// Write the field name.
try writer.writeAll("## `");
try writer.writeAll(field.name);
try writer.writeAll("`\n");
// For all subsequent fields with no docs, they are grouped
// with the previous field.
if (i + 1 < fields.len) {
inline for (fields[i + 1 ..]) |next_field| {
if (next_field.name[0] == '_') break;
if (@hasDecl(help_strings.Config, next_field.name)) break;
try writer.writeAll("## `");
try writer.writeAll(next_field.name);
try writer.writeAll("`\n");
}
}
// Newline after our headers
try writer.writeAll("\n");
var iter = std.mem.splitScalar(
u8,
@field(help_strings.Config, field.name),
'\n',
);
// We do some really rough markdown "parsing" here so that
// we can fix up some styles for what our website expects.
var block: ?enum {
/// Plaintext, do nothing.
text,
/// Code block, wrap in triple backticks. We use indented
/// code blocks in our comments but the website parser only
/// supports triple backticks.
code,
/// Callouts. We detect these based on paragraphs starting
/// with "Note:", "Warning:", etc. (case-insensitive).
callout_note,
callout_warning,
} = null;
while (iter.next()) |s| {
// Empty line resets our block
if (std.mem.eql(u8, s, "")) {
try endBlock(writer, block);
block = null;
try writer.writeAll("\n");
continue;
}
// If we don't have a block figure out our type.
const first: bool = block == null;
if (block == null) {
if (std.mem.startsWith(u8, s, " ")) {
block = .code;
try writer.writeAll("```\n");
} else if (std.ascii.startsWithIgnoreCase(s, "note:")) {
block = .callout_note;
try writer.writeAll("<Note>\n");
} else if (std.ascii.startsWithIgnoreCase(s, "warning:")) {
block = .callout_warning;
try writer.writeAll("<Warning>\n");
} else {
block = .text;
}
}
try writer.writeAll(switch (block.?) {
.text => s,
.callout_note => if (first) s["note:".len..] else s,
.callout_warning => if (first) s["warning:".len..] else s,
.code => if (std.mem.startsWith(u8, s, " "))
s[4..]
else
s,
});
try writer.writeAll("\n");
}
try endBlock(writer, block);
try writer.writeAll("\n");
}
}
fn endBlock(writer: anytype, block: anytype) !void {
if (block) |v| switch (v) {
.text => {},
.code => try writer.writeAll("```\n"),
.callout_note => try writer.writeAll("</Note>\n"),
.callout_warning => try writer.writeAll("</Warning>\n"),
};
}

View File

@ -9,7 +9,7 @@ pub const zsh_completions = comptimeGenerateZshCompletions();
fn comptimeGenerateZshCompletions() []const u8 { fn comptimeGenerateZshCompletions() []const u8 {
comptime { comptime {
@setEvalBranchQuota(19000); @setEvalBranchQuota(50000);
var counter = std.io.countingWriter(std.io.null_writer); var counter = std.io.countingWriter(std.io.null_writer);
try writeZshCompletions(&counter.writer()); try writeZshCompletions(&counter.writer());
@ -175,12 +175,29 @@ fn writeZshCompletions(writer: anytype) !void {
.Bool => try writer.writeAll("(true false)"), .Bool => try writer.writeAll("(true false)"),
.Enum => |info| { .Enum => |info| {
try writer.writeAll("("); try writer.writeAll("(");
for (info.opts, 0..) |f, i| { for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" "); if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name); try writer.writeAll(f.name);
} }
try writer.writeAll(")"); try writer.writeAll(")");
}, },
.Optional => |optional| {
switch (@typeInfo(optional.child)) {
.Enum => |info| {
try writer.writeAll("(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll(")");
},
else => {
if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll("_files");
} else try writer.writeAll("( )");
},
}
},
else => { else => {
if (std.mem.eql(u8, "config-file", opt.name)) { if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll("_files"); try writer.writeAll("_files");

View File

@ -58,6 +58,15 @@ pub const BuildConfig = struct {
"{}", "{}",
.{self.version}, .{self.version},
)); ));
step.addOption(
ReleaseChannel,
"release_channel",
channel: {
const pre = self.version.pre orelse break :channel .stable;
if (pre.len == 0) break :channel .stable;
break :channel .tip;
},
);
} }
/// Rehydrate our BuildConfig from the comptime options. Note that not all /// Rehydrate our BuildConfig from the comptime options. Note that not all
@ -82,6 +91,9 @@ pub const BuildConfig = struct {
pub const version = options.app_version; pub const version = options.app_version;
pub const version_string = options.app_version_string; pub const version_string = options.app_version_string;
/// The release channel for this build.
pub const release_channel = std.meta.stringToEnum(ReleaseChannel, @tagName(options.release_channel)).?;
/// The optimization mode as a string. /// The optimization mode as a string.
pub const mode_string = mode: { pub const mode_string = mode: {
const m = @tagName(builtin.mode); const m = @tagName(builtin.mode);
@ -172,9 +184,20 @@ pub const ExeEntrypoint = enum {
helpgen, helpgen,
mdgen_ghostty_1, mdgen_ghostty_1,
mdgen_ghostty_5, mdgen_ghostty_5,
webgen_config,
webgen_actions,
bench_parser, bench_parser,
bench_stream, bench_stream,
bench_codepoint_width, bench_codepoint_width,
bench_grapheme_break, bench_grapheme_break,
bench_page_init, bench_page_init,
}; };
/// The release channel for the build.
pub const ReleaseChannel = enum {
/// Unstable builds on every commit.
tip,
/// Stable tagged releases.
stable,
};

View File

@ -25,6 +25,10 @@ pub fn run(alloc: Allocator) !u8 {
try stdout.print("Ghostty {s}\n\n", .{build_config.version_string}); try stdout.print("Ghostty {s}\n\n", .{build_config.version_string});
if (tty) try stdout.print("\x1b]8;;\x1b\\", .{}); if (tty) try stdout.print("\x1b]8;;\x1b\\", .{});
try stdout.print("Version\n", .{});
try stdout.print(" - version: {s}\n", .{build_config.version_string});
try stdout.print(" - channel: {s}\n", .{@tagName(build_config.release_channel)});
try stdout.print("Build Config\n", .{}); try stdout.print("Build Config\n", .{});
try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string}); try stdout.print(" - Zig version: {s}\n", .{builtin.zig_version_string});
try stdout.print(" - build mode : {}\n", .{builtin.mode}); try stdout.print(" - build mode : {}\n", .{builtin.mode});

View File

@ -12,6 +12,7 @@ const Config = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
@ -138,7 +139,7 @@ const c = @cImport({
/// requested style, then the font will be used as-is since the style is /// requested style, then the font will be used as-is since the style is
/// not synthetic. /// not synthetic.
/// ///
/// Warning! An easy mistake is to disable `bold` or `italic` but not /// Warning: An easy mistake is to disable `bold` or `italic` but not
/// `bold-italic`. Disabling only `bold` or `italic` will NOT disable either /// `bold-italic`. Disabling only `bold` or `italic` will NOT disable either
/// in the `bold-italic` style. If you want to disable `bold-italic`, you must /// in the `bold-italic` style. If you want to disable `bold-italic`, you must
/// explicitly disable it. You cannot partially disable `bold-italic`. /// explicitly disable it. You cannot partially disable `bold-italic`.
@ -255,12 +256,28 @@ const c = @cImport({
/// that things like status lines continue to look aligned. /// that things like status lines continue to look aligned.
@"adjust-cell-width": ?MetricModifier = null, @"adjust-cell-width": ?MetricModifier = null,
@"adjust-cell-height": ?MetricModifier = null, @"adjust-cell-height": ?MetricModifier = null,
/// Distance in pixels from the bottom of the cell to the text baseline.
/// Increase to move baseline UP, decrease to move baseline DOWN.
@"adjust-font-baseline": ?MetricModifier = null, @"adjust-font-baseline": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the underline.
/// Increase to move underline DOWN, decrease to move underline UP.
@"adjust-underline-position": ?MetricModifier = null, @"adjust-underline-position": ?MetricModifier = null,
/// Thickness in pixels of the underline.
@"adjust-underline-thickness": ?MetricModifier = null, @"adjust-underline-thickness": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the strikethrough.
/// Increase to move strikethrough DOWN, decrease to move underline UP.
@"adjust-strikethrough-position": ?MetricModifier = null, @"adjust-strikethrough-position": ?MetricModifier = null,
/// Thickness in pixels of the strikethrough.
@"adjust-strikethrough-thickness": ?MetricModifier = null, @"adjust-strikethrough-thickness": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the overline.
/// Increase to move overline DOWN, decrease to move underline UP.
@"adjust-overline-position": ?MetricModifier = null,
/// Thickness in pixels of the overline.
@"adjust-overline-thickness": ?MetricModifier = null,
/// Thickness in pixels of the bar cursor and outlined rect cursor.
@"adjust-cursor-thickness": ?MetricModifier = null, @"adjust-cursor-thickness": ?MetricModifier = null,
/// Thickness in pixels of box drawing characters.
@"adjust-box-thickness": ?MetricModifier = null,
/// The method to use for calculating the cell width of a grapheme cluster. /// The method to use for calculating the cell width of a grapheme cluster.
/// The default value is `unicode` which uses the Unicode standard to determine /// The default value is `unicode` which uses the Unicode standard to determine
@ -270,16 +287,16 @@ const c = @cImport({
/// ///
/// Valid values are: /// Valid values are:
/// ///
/// * `legacy` - Use a legacy method to determine grapheme width, such as /// * `legacy` - Use a legacy method to determine grapheme width, such as
/// wcswidth This maximizes compatibility with legacy programs but may result /// wcswidth This maximizes compatibility with legacy programs but may result
/// in incorrect grapheme width for certain graphemes such as skin-tone /// in incorrect grapheme width for certain graphemes such as skin-tone
/// emoji, non-English characters, etc. /// emoji, non-English characters, etc.
/// ///
/// This is called "legacy" and not something more specific because the /// This is called "legacy" and not something more specific because the
/// behavior is undefined and we want to retain the ability to modify it. /// behavior is undefined and we want to retain the ability to modify it.
/// For example, we may or may not use libc `wcswidth` now or in the future. /// For example, we may or may not use libc `wcswidth` now or in the future.
/// ///
/// * `unicode` - Use the Unicode standard to determine grapheme width. /// * `unicode` - Use the Unicode standard to determine grapheme width.
/// ///
/// If a running program explicitly enables terminal mode 2027, then `unicode` /// If a running program explicitly enables terminal mode 2027, then `unicode`
/// width will be forced regardless of this configuration. When mode 2027 is /// width will be forced regardless of this configuration. When mode 2027 is
@ -602,6 +619,16 @@ command: ?[]const u8 = null,
/// process will exit when the command exits. Additionally, the /// process will exit when the command exits. Additionally, the
/// `quit-after-last-window-closed-delay` is unset. /// `quit-after-last-window-closed-delay` is unset.
/// ///
/// * `shell-integration=detect` (if not `none`) - This prevents forcibly
/// injecting any configured shell integration into the command's
/// environment. With `-e` its highly unlikely that you're executing a
/// shell and forced shell integration is likely to cause problems
/// (i.e. by wrapping your command in a shell, setting env vars, etc.).
/// This is a safety measure to prevent unexpected behavior. If you want
/// shell integration with a `-e`-executed command, you must either
/// name your binary appopriately or source the shell integration script
/// manually.
///
@"initial-command": ?[]const u8 = null, @"initial-command": ?[]const u8 = null,
/// If true, keep the terminal open after the command exits. Normally, the /// If true, keep the terminal open after the command exits. Normally, the
@ -677,6 +704,11 @@ fullscreen: bool = false,
/// window to be this title at all times and Ghostty will ignore any set title /// window to be this title at all times and Ghostty will ignore any set title
/// escape sequences programs (such as Neovim) may send. /// escape sequences programs (such as Neovim) may send.
/// ///
/// If you want a blank title, set this to one or more spaces by quoting
/// the value. For example, `title = " "`. This effectively hides the title.
/// This is necessary because setting a blank value resets the title to the
/// default value of the running program.
///
/// This configuration can be reloaded at runtime. If it is set, the title /// This configuration can be reloaded at runtime. If it is set, the title
/// will update for all windows. If it is unset, the next title change escape /// will update for all windows. If it is unset, the next title change escape
/// sequence will be honored but previous changes will not retroactively /// sequence will be honored but previous changes will not retroactively
@ -754,7 +786,7 @@ class: ?[:0]const u8 = null,
/// or the alias. When debugging keybinds, the non-aliased modifier will always /// or the alias. When debugging keybinds, the non-aliased modifier will always
/// be used in output. /// be used in output.
/// ///
/// Note that the fn or "globe" key on keyboards are not supported as a /// Note: The fn or "globe" key on keyboards are not supported as a
/// modifier. This is a limitation of the operating systems and GUI toolkits /// modifier. This is a limitation of the operating systems and GUI toolkits
/// that Ghostty uses. /// that Ghostty uses.
/// ///
@ -765,7 +797,7 @@ class: ?[:0]const u8 = null,
/// is sometimes called a leader key, a key chord, a key table, etc. There /// is sometimes called a leader key, a key chord, a key table, etc. There
/// is no hardcoded limit on the number of parts in a sequence. /// is no hardcoded limit on the number of parts in a sequence.
/// ///
/// Warning: if you define a sequence as a CLI argument to `ghostty`, /// Warning: If you define a sequence as a CLI argument to `ghostty`,
/// you probably have to quote the keybind since `>` is a special character /// you probably have to quote the keybind since `>` is a special character
/// in most shells. Example: ghostty --keybind='ctrl+a>n=new_window' /// in most shells. Example: ghostty --keybind='ctrl+a>n=new_window'
/// ///
@ -854,7 +886,7 @@ class: ?[:0]const u8 = null,
/// Since they are not associated with a specific terminal surface, /// Since they are not associated with a specific terminal surface,
/// they're never encoded. /// they're never encoded.
/// ///
/// Keybind trigger are not unique per prefix combination. For example, /// Keybind triggers are not unique per prefix combination. For example,
/// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind /// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind
/// set later will overwrite the keybind set earlier. In this case, the /// set later will overwrite the keybind set earlier. In this case, the
/// `global:` keybind will be used. /// `global:` keybind will be used.
@ -863,7 +895,7 @@ class: ?[:0]const u8 = null,
/// `global:unconsumed:ctrl+a=reload_config` will make the keybind global /// `global:unconsumed:ctrl+a=reload_config` will make the keybind global
/// and not consume the input to reload the config. /// and not consume the input to reload the config.
/// ///
/// A note on `global:`: this feature is only supported on macOS. On macOS, /// Note: `global:` is only supported on macOS. On macOS,
/// this feature requires accessibility permissions to be granted to Ghostty. /// this feature requires accessibility permissions to be granted to Ghostty.
/// When a `global:` keybind is specified and Ghostty is launched or reloaded, /// When a `global:` keybind is specified and Ghostty is launched or reloaded,
/// Ghostty will attempt to request these permissions. If the permissions are /// Ghostty will attempt to request these permissions. If the permissions are
@ -977,7 +1009,7 @@ keybind: Keybinds = .{},
/// * `false` - windows won't have native decorations, i.e. titlebar and /// * `false` - windows won't have native decorations, i.e. titlebar and
/// borders. On macOS this also disables tabs and tab overview. /// borders. On macOS this also disables tabs and tab overview.
/// ///
/// The "toggle_window_decoration" keybind action can be used to create /// The "toggle_window_decorations" keybind action can be used to create
/// a keybinding to toggle this setting at runtime. /// a keybinding to toggle this setting at runtime.
/// ///
/// Changing this configuration in your configuration and reloading will /// Changing this configuration in your configuration and reloading will
@ -1192,7 +1224,7 @@ keybind: Keybinds = .{},
@"clipboard-paste-bracketed-safe": bool = true, @"clipboard-paste-bracketed-safe": bool = true,
/// The total amount of bytes that can be used for image data (i.e. the Kitty /// The total amount of bytes that can be used for image data (i.e. the Kitty
/// image protocol) per terminal scren. The maximum value is 4,294,967,295 /// image protocol) per terminal screen. The maximum value is 4,294,967,295
/// (4GiB). The default is 320MB. If this is set to zero, then all image /// (4GiB). The default is 320MB. If this is set to zero, then all image
/// protocols will be disabled. /// protocols will be disabled.
/// ///
@ -1445,7 +1477,7 @@ keybind: Keybinds = .{},
/// Custom shaders to run after the default shaders. This is a file path /// Custom shaders to run after the default shaders. This is a file path
/// to a GLSL-syntax shader for all platforms. /// to a GLSL-syntax shader for all platforms.
/// ///
/// WARNING: Invalid shaders can cause Ghostty to become unusable such as by /// Warning: Invalid shaders can cause Ghostty to become unusable such as by
/// causing the window to be completely black. If this happens, you can /// causing the window to be completely black. If this happens, you can
/// unset this configuration to disable the shader. /// unset this configuration to disable the shader.
/// ///
@ -1643,6 +1675,73 @@ keybind: Keybinds = .{},
/// you may want to disable it. /// you may want to disable it.
@"macos-secure-input-indication": bool = true, @"macos-secure-input-indication": bool = true,
/// Customize the macOS app icon.
///
/// This only affects the icon that appears in the dock, application
/// switcher, etc. This does not affect the icon in Finder because
/// that is controlled by a hardcoded value in the signed application
/// bundle and can't be changed at runtime. For more details on what
/// exactly is affected, see the `NSApplication.icon` Apple documentation;
/// that is the API that is being used to set the icon.
///
/// Valid values:
///
/// * `official` - Use the official Ghostty icon.
/// * `custom-style` - Use the official Ghostty icon but with custom
/// styles applied to various layers. The custom styles must be
/// specified using the additional `macos-icon`-prefixed configurations.
/// The `macos-icon-ghost-color` and `macos-icon-screen-color`
/// configurations are required for this style.
///
/// WARNING: The `custom-style` option is _experimental_. We may change
/// the format of the custom styles in the future. We're still finalizing
/// the exact layers and customization options that will be available.
///
/// Other caveats:
///
/// * The icon in the update dialog will always be the official icon.
/// This is because the update dialog is managed through a
/// separate framework and cannot be customized without significant
/// effort.
///
@"macos-icon": MacAppIcon = .official,
/// The material to use for the frame of the macOS app icon.
///
/// Valid values:
///
/// * `aluminum` - A brushed aluminum frame. This is the default.
/// * `beige` - A classic 90's computer beige frame.
/// * `plastic` - A glossy, dark plastic frame.
/// * `chrome` - A shiny chrome frame.
///
/// This only has an effect when `macos-icon` is set to `custom-style`.
@"macos-icon-frame": MacAppIconFrame = .aluminum,
/// The color of the ghost in the macOS app icon.
///
/// The format of the color is the same as the `background` configuration;
/// see that for more information.
///
/// Note: This configuration is required when `macos-icon` is set to
/// `custom-style`.
///
/// This only has an effect when `macos-icon` is set to `custom-style`.
@"macos-icon-ghost-color": ?Color = null,
/// The color of the screen in the macOS app icon.
///
/// The screen is a gradient so you can specify multiple colors that
/// make up the gradient. Colors should be separated by commas. The
/// format of the color is the same as the `background` configuration;
/// see that for more information.
///
/// Note: This configuration is required when `macos-icon` is set to
/// `custom-style`.
///
/// This only has an effect when `macos-icon` is set to `custom-style`.
@"macos-icon-screen-color": ?ColorList = null,
/// Put every surface (tab, split, window) into a dedicated Linux cgroup. /// Put every surface (tab, split, window) into a dedicated Linux cgroup.
/// ///
/// This makes it so that resource management can be done on a per-surface /// This makes it so that resource management can be done on a per-surface
@ -1696,7 +1795,7 @@ keybind: Keybinds = .{},
/// If this is true, then any cgroup initialization failure will cause /// If this is true, then any cgroup initialization failure will cause
/// Ghostty to exit or new surfaces to not be created. /// Ghostty to exit or new surfaces to not be created.
/// ///
/// Note: this currently only affects cgroup initialization. Subprocesses /// Note: This currently only affects cgroup initialization. Subprocesses
/// must always be able to move themselves into an isolated cgroup. /// must always be able to move themselves into an isolated cgroup.
@"linux-cgroup-hard-fail": bool = false, @"linux-cgroup-hard-fail": bool = false,
@ -1727,10 +1826,17 @@ keybind: Keybinds = .{},
@"gtk-titlebar": bool = true, @"gtk-titlebar": bool = true,
/// Determines the side of the screen that the GTK tab bar will stick to. /// Determines the side of the screen that the GTK tab bar will stick to.
/// Top, bottom, left, and right are supported. The default is top. /// Top, bottom, left, right, and hidden are supported. The default is top.
/// ///
/// If this option has value `left` or `right` when using Adwaita, it falls /// If this option has value `left` or `right` when using Adwaita, it falls
/// back to `top`. /// back to `top`. `hidden`, meaning that tabs don't exist, is not supported
/// without using Adwaita, falling back to `top`.
///
/// When `hidden` is set and Adwaita is enabled, a tab button displaying the
/// number of tabs will appear in the title bar. It has the ability to open a
/// tab overview for displaying tabs. Alternatively, you can use the
/// `toggle_tab_overview` action in a keybind if your window doesn't have a
/// title bar, or you can switch tabs with keybinds.
@"gtk-tabs-location": GtkTabsLocation = .top, @"gtk-tabs-location": GtkTabsLocation = .top,
/// Determines the appearance of the top and bottom bars when using the /// Determines the appearance of the top and bottom bars when using the
@ -1810,6 +1916,28 @@ term: []const u8 = "xterm-ghostty",
/// Changing this value at runtime works after a small delay. /// Changing this value at runtime works after a small delay.
@"auto-update": AutoUpdate = .check, @"auto-update": AutoUpdate = .check,
/// The release channel to use for auto-updates.
///
/// The default value of this matches the release channel of the currently
/// running Ghostty version. If you download a pre-release version of Ghostty
/// then this will be set to `tip` and you will receive pre-release updates.
/// If you download a stable version of Ghostty then this will be set to
/// `stable` and you will receive stable updates.
///
/// Valid values are:
///
/// * `stable` - Stable, tagged releases such as "1.0.0".
/// * `tip` - Pre-release versions generated from each commit to the
/// main branch. This is the version that was in use during private
/// beta testing by thousands of people. It is generally stable but
/// will likely have more bugs than the stable channel.
///
/// Changing this configuration requires a full restart of
/// Ghostty to take effect.
///
/// This only works on macOS since only macOS has an auto-update feature.
@"auto-update-channel": ?build_config.ReleaseChannel = null,
/// This is set by the CLI parser for deinit. /// This is set by the CLI parser for deinit.
_arena: ?ArenaAllocator = null, _arena: ?ArenaAllocator = null,
@ -3036,6 +3164,12 @@ pub fn finalize(self: *Config) !void {
); );
} }
} }
// We can't set this as a struct default because our config is
// loaded in environments where a build config isn't available.
if (self.@"auto-update-channel" == null) {
self.@"auto-update-channel" = build_config.release_channel;
}
} }
/// Callback for src/cli/args.zig to allow us to handle special cases /// Callback for src/cli/args.zig to allow us to handle special cases
@ -3082,6 +3216,9 @@ pub fn parseManuallyHook(
self.@"gtk-single-instance" = .false; self.@"gtk-single-instance" = .false;
self.@"quit-after-last-window-closed" = true; self.@"quit-after-last-window-closed" = true;
self.@"quit-after-last-window-closed-delay" = null; self.@"quit-after-last-window-closed-delay" = null;
if (self.@"shell-integration" != .none) {
self.@"shell-integration" = .detect;
}
// Do not continue, we consumed everything. // Do not continue, we consumed everything.
return false; return false;
@ -3473,11 +3610,22 @@ pub const WindowPaddingColor = enum {
/// ///
/// This is a packed struct so that the C API to read color values just /// This is a packed struct so that the C API to read color values just
/// works by setting it to a C integer. /// works by setting it to a C integer.
pub const Color = packed struct(u24) { pub const Color = struct {
r: u8, r: u8,
g: u8, g: u8,
b: u8, b: u8,
/// ghostty_config_color_s
pub const C = extern struct {
r: u8,
g: u8,
b: u8,
};
pub fn cval(self: Color) Color.C {
return .{ .r = self.r, .g = self.g, .b = self.b };
}
/// Convert this to the terminal RGB struct /// Convert this to the terminal RGB struct
pub fn toTerminalRGB(self: Color) terminal.color.RGB { pub fn toTerminalRGB(self: Color) terminal.color.RGB {
return .{ .r = self.r, .g = self.g, .b = self.b }; return .{ .r = self.r, .g = self.g, .b = self.b };
@ -3510,14 +3658,19 @@ pub const Color = packed struct(u24) {
var buf: [128]u8 = undefined; var buf: [128]u8 = undefined;
try formatter.formatEntry( try formatter.formatEntry(
[]const u8, []const u8,
std.fmt.bufPrint( try self.formatBuf(&buf),
&buf,
"#{x:0>2}{x:0>2}{x:0>2}",
.{ self.r, self.g, self.b },
) catch return error.OutOfMemory,
); );
} }
/// Format the color as a string.
pub fn formatBuf(self: Color, buf: []u8) Allocator.Error![]const u8 {
return std.fmt.bufPrint(
buf,
"#{x:0>2}{x:0>2}{x:0>2}",
.{ self.r, self.g, self.b },
) catch error.OutOfMemory;
}
/// fromHex parses a color from a hex value such as #RRGGBB. The "#" /// fromHex parses a color from a hex value such as #RRGGBB. The "#"
/// is optional. /// is optional.
pub fn fromHex(input: []const u8) !Color { pub fn fromHex(input: []const u8) !Color {
@ -3570,6 +3723,133 @@ pub const Color = packed struct(u24) {
} }
}; };
pub const ColorList = struct {
const Self = @This();
colors: std.ArrayListUnmanaged(Color) = .{},
colors_c: std.ArrayListUnmanaged(Color.C) = .{},
/// ghostty_config_color_list_s
pub const C = extern struct {
colors: [*]Color.C,
len: usize,
};
pub fn cval(self: *const Self) C {
return .{
.colors = self.colors_c.items.ptr,
.len = self.colors_c.items.len,
};
}
pub fn parseCLI(
self: *Self,
alloc: Allocator,
input_: ?[]const u8,
) !void {
const input = input_ orelse return error.ValueRequired;
if (input.len == 0) return error.ValueRequired;
// Always reset on parse
self.* = .{};
// Split the input by commas and parse each color
var it = std.mem.tokenizeScalar(u8, input, ',');
var count: usize = 0;
while (it.next()) |raw| {
count += 1;
if (count > 64) return error.InvalidValue;
const color = try Color.parseCLI(raw);
try self.colors.append(alloc, color);
try self.colors_c.append(alloc, color.cval());
}
// If no colors were parsed, we need to return an error
if (self.colors.items.len == 0) return error.InvalidValue;
assert(self.colors.items.len == self.colors_c.items.len);
}
pub fn clone(
self: *const Self,
alloc: Allocator,
) Allocator.Error!Self {
return .{
.colors = try self.colors.clone(alloc),
};
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
const itemsA = self.colors.items;
const itemsB = other.colors.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |a, b| {
if (!a.equal(b)) return false;
} else return true;
}
/// Used by Formatter
pub fn formatEntry(
self: Self,
formatter: anytype,
) !void {
// If no items, we want to render an empty field.
if (self.colors.items.len == 0) {
try formatter.formatEntry(void, {});
return;
}
// Build up the value of our config. Our buffer size should be
// sized to contain all possible maximum values.
var buf: [1024]u8 = undefined;
var fbs = std.io.fixedBufferStream(&buf);
var writer = fbs.writer();
for (self.colors.items, 0..) |color, i| {
var color_buf: [128]u8 = undefined;
const color_str = try color.formatBuf(&color_buf);
if (i != 0) writer.writeByte(',') catch return error.OutOfMemory;
writer.writeAll(color_str) catch return error.OutOfMemory;
}
try formatter.formatEntry(
[]const u8,
fbs.getWritten(),
);
}
test "parseCLI" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var p: Self = .{};
try p.parseCLI(alloc, "black,white");
try testing.expectEqual(2, p.colors.items.len);
// Error cases
try testing.expectError(error.ValueRequired, p.parseCLI(alloc, null));
try testing.expectError(error.InvalidValue, p.parseCLI(alloc, " "));
}
test "format" {
const testing = std.testing;
var buf = std.ArrayList(u8).init(testing.allocator);
defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var p: Self = .{};
try p.parseCLI(alloc, "black,white");
try p.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
try std.testing.expectEqualSlices(u8, "a = #000000,#ffffff\n", buf.items);
}
};
/// Palette is the 256 color palette for 256-color mode. This is still /// Palette is the 256 color palette for 256-color mode. This is still
/// used by many terminal applications. /// used by many terminal applications.
pub const Palette = struct { pub const Palette = struct {
@ -3686,7 +3966,7 @@ pub const RepeatableString = struct {
return .{ .list = list }; return .{ .list = list };
} }
/// The number of itemsin the list /// The number of items in the list
pub fn count(self: Self) usize { pub fn count(self: Self) usize {
return self.list.items.len; return self.list.items.len;
} }
@ -4850,11 +5130,29 @@ pub const MacTitlebarStyle = enum {
}; };
/// See macos-titlebar-proxy-icon /// See macos-titlebar-proxy-icon
pub const MacTitlebarProxyIcon: type = enum { pub const MacTitlebarProxyIcon = enum {
visible, visible,
hidden, hidden,
}; };
/// See macos-icon
///
/// Note: future versions of Ghostty can support a custom icon with
/// path by changing this to a tagged union, which doesn't change our
/// format at all.
pub const MacAppIcon = enum {
official,
@"custom-style",
};
/// See macos-icon-frame
pub const MacAppIconFrame = enum {
aluminum,
beige,
plastic,
chrome,
};
/// See gtk-single-instance /// See gtk-single-instance
pub const GtkSingleInstance = enum { pub const GtkSingleInstance = enum {
desktop, desktop,
@ -4868,6 +5166,7 @@ pub const GtkTabsLocation = enum {
bottom, bottom,
left, left,
right, right,
hidden,
}; };
/// See adw-toolbar-style /// See adw-toolbar-style
@ -5189,9 +5488,8 @@ pub const Duration = struct {
} }
} }
pub fn c_get(self: Duration, ptr_raw: *anyopaque) void { pub fn cval(self: Duration) usize {
const ptr: *usize = @ptrCast(@alignCast(ptr_raw)); return @intCast(self.asMilliseconds());
ptr.* = @intCast(self.asMilliseconds());
} }
/// Convenience function to convert to milliseconds since many OS and /// Convenience function to convert to milliseconds since many OS and

View File

@ -60,9 +60,11 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
}, },
.Struct => |info| { .Struct => |info| {
// If the struct implements c_get then we call that // If the struct implements cval then we call then.
if (@hasDecl(@TypeOf(value), "c_get")) { if (@hasDecl(T, "cval")) {
value.c_get(ptr_raw); const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?;
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));
ptr.* = value.cval();
return true; return true;
} }
@ -100,7 +102,7 @@ fn fieldByKey(self: *const Config, comptime k: Key) Value(k) {
return @field(self, field.name); return @field(self, field.name);
} }
test "u8" { test "c_get: u8" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -113,7 +115,7 @@ test "u8" {
try testing.expectEqual(@as(f32, 24), cval); try testing.expectEqual(@as(f32, 24), cval);
} }
test "enum" { test "c_get: enum" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -128,7 +130,7 @@ test "enum" {
try testing.expectEqualStrings("dark", str); try testing.expectEqualStrings("dark", str);
} }
test "color" { test "c_get: color" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -136,12 +138,14 @@ test "color" {
defer c.deinit(); defer c.deinit();
c.background = .{ .r = 255, .g = 0, .b = 0 }; c.background = .{ .r = 255, .g = 0, .b = 0 };
var cval: c_uint = undefined; var cval: Color.C = undefined;
try testing.expect(get(&c, .background, @ptrCast(&cval))); try testing.expect(get(&c, .background, @ptrCast(&cval)));
try testing.expectEqual(@as(c_uint, 255), cval); try testing.expectEqual(255, cval.r);
try testing.expectEqual(0, cval.g);
try testing.expectEqual(0, cval.b);
} }
test "optional" { test "c_get: optional" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -150,14 +154,16 @@ test "optional" {
{ {
c.@"unfocused-split-fill" = null; c.@"unfocused-split-fill" = null;
var cval: c_uint = undefined; var cval: Color.C = undefined;
try testing.expect(!get(&c, .@"unfocused-split-fill", @ptrCast(&cval))); try testing.expect(!get(&c, .@"unfocused-split-fill", @ptrCast(&cval)));
} }
{ {
c.@"unfocused-split-fill" = .{ .r = 255, .g = 0, .b = 0 }; c.@"unfocused-split-fill" = .{ .r = 255, .g = 0, .b = 0 };
var cval: c_uint = undefined; var cval: Color.C = undefined;
try testing.expect(get(&c, .@"unfocused-split-fill", @ptrCast(&cval))); try testing.expect(get(&c, .@"unfocused-split-fill", @ptrCast(&cval)));
try testing.expectEqual(@as(c_uint, 255), cval); try testing.expectEqual(255, cval.r);
try testing.expectEqual(0, cval.g);
try testing.expectEqual(0, cval.b);
} }
} }

View File

@ -427,7 +427,10 @@ pub const DerivedConfig = struct {
@"adjust-underline-thickness": ?Metrics.Modifier, @"adjust-underline-thickness": ?Metrics.Modifier,
@"adjust-strikethrough-position": ?Metrics.Modifier, @"adjust-strikethrough-position": ?Metrics.Modifier,
@"adjust-strikethrough-thickness": ?Metrics.Modifier, @"adjust-strikethrough-thickness": ?Metrics.Modifier,
@"adjust-overline-position": ?Metrics.Modifier,
@"adjust-overline-thickness": ?Metrics.Modifier,
@"adjust-cursor-thickness": ?Metrics.Modifier, @"adjust-cursor-thickness": ?Metrics.Modifier,
@"adjust-box-thickness": ?Metrics.Modifier,
@"freetype-load-flags": font.face.FreetypeLoadFlags, @"freetype-load-flags": font.face.FreetypeLoadFlags,
/// Initialize a DerivedConfig. The config should be either a /// Initialize a DerivedConfig. The config should be either a
@ -462,7 +465,10 @@ pub const DerivedConfig = struct {
.@"adjust-underline-thickness" = config.@"adjust-underline-thickness", .@"adjust-underline-thickness" = config.@"adjust-underline-thickness",
.@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position", .@"adjust-strikethrough-position" = config.@"adjust-strikethrough-position",
.@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness", .@"adjust-strikethrough-thickness" = config.@"adjust-strikethrough-thickness",
.@"adjust-overline-position" = config.@"adjust-overline-position",
.@"adjust-overline-thickness" = config.@"adjust-overline-thickness",
.@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness",
.@"adjust-box-thickness" = config.@"adjust-box-thickness",
.@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {}, .@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {},
// This must be last so the arena contains all our allocations // This must be last so the arena contains all our allocations
@ -604,7 +610,10 @@ pub const Key = struct {
if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m);
if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m);
if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m);
if (config.@"adjust-overline-position") |m| try set.put(alloc, .overline_position, m);
if (config.@"adjust-overline-thickness") |m| try set.put(alloc, .overline_thickness, m);
if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m); if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m);
if (config.@"adjust-box-thickness") |m| try set.put(alloc, .box_thickness, m);
break :set set; break :set set;
}; };

View File

@ -292,31 +292,45 @@ pub const Face = struct {
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)}; var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
// Get the bounding rect for rendering this glyph. // Get the bounding rect for rendering this glyph.
const rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null); // This is in a coordinate space with (0.0, 0.0)
// in the bottom left and +Y pointing up.
var rect = self.font.getBoundingRectsForGlyphs(.horizontal, &glyphs, null);
// The x/y that we render the glyph at. The Y value has to be flipped // If we're rendering a synthetic bold then we will gain 50% of
// because our coordinates in 3D space are (0, 0) bottom left with // the line width on every edge, which means we should increase
// +y being up. // our width and height by the line width and subtract half from
const render_x = @floor(rect.origin.x); // our origin points.
const render_y = @ceil(-rect.origin.y); if (self.synthetic_bold) |line_width| {
rect.size.width += line_width;
rect.size.height += line_width;
rect.origin.x -= line_width / 2;
rect.origin.y -= line_width / 2;
}
// The ascent is the amount of pixels above the baseline this glyph // We make an assumption that font smoothing ("thicken")
// is rendered. The ascent can be calculated by adding the full // adds no more than 1 extra pixel to any edge. We don't
// glyph height to the origin. // add extra size if it's a sbix color font though, since
const glyph_ascent = @ceil(rect.size.height + rect.origin.y); // bitmaps aren't affected by smoothing.
const sbix = self.color != null and self.color.?.sbix;
if (opts.thicken and !sbix) {
rect.size.width += 2.0;
rect.size.height += 2.0;
rect.origin.x -= 1.0;
rect.origin.y -= 1.0;
}
// The glyph height is basically rect.size.height but we do the // We compute the minimum and maximum x and y values.
// ascent plus the descent because both are rounded elements that // We round our min points down and max points up.
// will make us more accurate. const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{
const height: u32 = @intFromFloat(glyph_ascent + render_y); @intFromFloat(@floor(rect.origin.x)),
@intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)),
// The glyph width is our advertised bounding with plus the rounding @intFromFloat(@floor(rect.origin.y)),
// difference from our rendering X. @intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)),
const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x))); };
// This bitmap is blank. I've seen it happen in a font, I don't know why. // This bitmap is blank. I've seen it happen in a font, I don't know why.
// If it is empty, we just return a valid glyph struct that does nothing. // If it is empty, we just return a valid glyph struct that does nothing.
if (width == 0 or height == 0) return font.Glyph{ if (x1 <= x0 or y1 <= y0) return font.Glyph{
.width = 0, .width = 0,
.height = 0, .height = 0,
.offset_x = 0, .offset_x = 0,
@ -326,25 +340,8 @@ pub const Face = struct {
.advance_x = 0, .advance_x = 0,
}; };
// Additional padding we need to add to the bitmap context itself const width: u32 = @intCast(x1 - x0);
// due to the glyph being larger than standard. const height: u32 = @intCast(y1 - y0);
const padding_ctx: u32 = padding_ctx: {
// If we're doing thicken, then getBoundsForGlyphs does not take
// into account the anti-aliasing that will be added to the glyph.
// We need to add some padding to allow that to happen. A padding of
// 2 is usually enough for anti-aliasing.
var result: u32 = if (opts.thicken) 2 else 0;
// If we have a synthetic bold, add padding for the stroke width
if (self.synthetic_bold) |line_width| {
// x2 for top and bottom padding
result += @intFromFloat(@ceil(line_width) * 2);
}
break :padding_ctx result;
};
const padded_width: u32 = width + (padding_ctx * 2);
const padded_height: u32 = height + (padding_ctx * 2);
// Settings that are specific to if we are rendering text or emoji. // Settings that are specific to if we are rendering text or emoji.
const color: struct { const color: struct {
@ -380,17 +377,17 @@ pub const Face = struct {
// usually stabilizes pretty quickly and is very infrequent so I think // usually stabilizes pretty quickly and is very infrequent so I think
// the allocation overhead is acceptable compared to the cost of // the allocation overhead is acceptable compared to the cost of
// caching it forever or having to deal with a cache lifetime. // caching it forever or having to deal with a cache lifetime.
const buf = try alloc.alloc(u8, padded_width * padded_height * color.depth); const buf = try alloc.alloc(u8, width * height * color.depth);
defer alloc.free(buf); defer alloc.free(buf);
@memset(buf, 0); @memset(buf, 0);
const context = macos.graphics.BitmapContext.context; const context = macos.graphics.BitmapContext.context;
const ctx = try macos.graphics.BitmapContext.create( const ctx = try macos.graphics.BitmapContext.create(
buf, buf,
padded_width, width,
padded_height, height,
8, 8,
padded_width * color.depth, width * color.depth,
color.space, color.space,
color.context_opts, color.context_opts,
); );
@ -405,8 +402,8 @@ pub const Face = struct {
context.fillRect(ctx, .{ context.fillRect(ctx, .{
.origin = .{ .x = 0, .y = 0 }, .origin = .{ .x = 0, .y = 0 },
.size = .{ .size = .{
.width = @floatFromInt(padded_width), .width = @floatFromInt(width),
.height = @floatFromInt(padded_height), .height = @floatFromInt(height),
}, },
}); });
@ -437,67 +434,57 @@ pub const Face = struct {
// We want to render the glyphs at (0,0), but the glyphs themselves // 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 // are offset by bearings, so we have to undo those bearings in order
// to get them to 0,0. We also add the padding so that they render // to get them to 0,0.
// slightly off the edge of the bitmap.
const padding_ctx_f64: f64 = @floatFromInt(padding_ctx);
self.font.drawGlyphs(&glyphs, &.{ self.font.drawGlyphs(&glyphs, &.{
.{ .{
.x = -1 * (render_x - padding_ctx_f64), .x = @floatFromInt(-x0),
.y = render_y + padding_ctx_f64, .y = @floatFromInt(-y0),
}, },
}, ctx); }, ctx);
const region = region: { const region = region: {
// We need to add a 1px padding to the font so that we don't // We reserve a region that's 1px wider and taller than we need
// get fuzzy issues when blending textures. // in order to create a 1px separation between adjacent glyphs
const padding = 1; // to prevent interpolation with adjacent glyphs while sampling
// from the atlas.
// Get the full padded region
var region = try atlas.reserve( var region = try atlas.reserve(
alloc, alloc,
padded_width + (padding * 2), // * 2 because left+right width + 1,
padded_height + (padding * 2), // * 2 because top+bottom height + 1,
); );
// Modify the region so that we remove the padding so that // We adjust the region width and height back down since we
// we write to the non-zero location. The data in an Altlas // don't need the extra pixel, we just needed to reserve it
// is always initialized to zero (Atlas.clear) so we don't // so that it isn't used for other glyphs in the future.
// need to worry about zero-ing that. region.width -= 1;
region.x += padding; region.height -= 1;
region.y += padding;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region; break :region region;
}; };
atlas.set(region, buf); atlas.set(region, buf);
const metrics = opts.grid_metrics orelse self.metrics; const metrics = opts.grid_metrics orelse self.metrics;
const offset_y: i32 = offset_y: {
// Our Y coordinate in 3D is (0, 0) bottom left, +y is UP.
// We need to calculate our baseline from the bottom of a cell.
const baseline_from_bottom: f64 = @floatFromInt(metrics.cell_baseline);
// Next we offset our baseline by the bearing in the font. We // This should be the distance from the bottom of
// ADD here because CoreText y is UP. // the cell to the top of the glyph's bounding box.
const baseline_with_offset = baseline_from_bottom + glyph_ascent; //
// The calculation is distance from bottom of cell to
// Add our context padding we may have created. // baseline plus distance from baseline to top of glyph.
const baseline_with_padding = baseline_with_offset + padding_ctx_f64; const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1;
break :offset_y @intFromFloat(@ceil(baseline_with_padding));
};
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: { const offset_x: i32 = offset_x: {
// Don't forget to apply our context padding if we have one var result: i32 = x0;
var result: i32 = @intFromFloat(render_x - padding_ctx_f64);
// If our cell was resized to be wider then we center our // If our cell was resized then we adjust our glyph's
// glyph in the cell. // position relative to the new center. This keeps glyphs
// centered in the cell whether it was made wider or narrower.
if (metrics.original_cell_width) |original_width| { if (metrics.original_cell_width) |original_width| {
if (original_width < metrics.cell_width) { const before: i32 = @intCast(original_width);
const diff = (metrics.cell_width - original_width) / 2; const after: i32 = @intCast(metrics.cell_width);
result += @intCast(diff); // Increase the offset by half of the difference
} // between the widths to keep things centered.
result += @divTrunc(after - before, 2);
} }
break :offset_x result; break :offset_x result;
@ -507,21 +494,9 @@ pub const Face = struct {
var advances: [glyphs.len]macos.graphics.Size = undefined; var advances: [glyphs.len]macos.graphics.Size = undefined;
_ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances);
// std.log.warn("renderGlyph rect={} width={} height={} render_x={} render_y={} offset_y={} ascent={} cell_height={} cell_baseline={}", .{
// rect,
// width,
// height,
// render_x,
// render_y,
// offset_y,
// glyph_ascent,
// self.metrics.cell_height,
// self.metrics.cell_baseline,
// });
return .{ return .{
.width = padded_width, .width = width,
.height = padded_height, .height = height,
.offset_x = offset_x, .offset_x = offset_x,
.offset_y = offset_y, .offset_y = offset_y,
.atlas_x = region.x, .atlas_x = region.x,
@ -534,8 +509,6 @@ pub const Face = struct {
CopyTableError, CopyTableError,
InvalidHeadTable, InvalidHeadTable,
InvalidPostTable, InvalidPostTable,
InvalidOS2Table,
OS2VersionNotSupported,
InvalidHheaTable, InvalidHheaTable,
}; };
@ -569,18 +542,16 @@ pub const Face = struct {
}; };
}; };
// Read the 'OS/2' table out of the font data. // Read the 'OS/2' table out of the font data if it's available.
const os2: opentype.OS2 = os2: { const os2_: ?opentype.OS2 = os2: {
const tag = macos.text.FontTableTag.init("OS/2"); const tag = macos.text.FontTableTag.init("OS/2");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError; const data = ct_font.copyTable(tag) orelse break :os2 null;
defer data.release(); defer data.release();
const ptr = data.getPointer(); const ptr = data.getPointer();
const len = data.getLength(); const len = data.getLength();
break :os2 opentype.OS2.init(ptr[0..len]) catch |err| { break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
return switch (err) { log.warn("error parsing OS/2 table: {}", .{err});
error.EndOfStream => error.InvalidOS2Table, break :os2 null;
error.OS2VersionNotSupported => error.OS2VersionNotSupported,
};
}; };
}; };
@ -603,54 +574,59 @@ pub const Face = struct {
const px_per_unit: f64 = px_per_em / units_per_em; const px_per_unit: f64 = px_per_em / units_per_em;
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); const hhea_descent: f64 = @floatFromInt(hhea.descender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
// If the font says to use typo metrics, trust it. if (os2_) |os2| {
if (os2.fsSelection.use_typo_metrics) { const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
break :vertical_metrics .{ const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
// If the font says to use typo metrics, trust it.
if (os2.fsSelection.use_typo_metrics) break :vertical_metrics .{
os2_ascent * px_per_unit, os2_ascent * px_per_unit,
os2_descent * px_per_unit, os2_descent * px_per_unit,
os2_line_gap * px_per_unit, os2_line_gap * px_per_unit,
}; };
}
// Otherwise we prefer the height metrics from 'hhea' if they // Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else // are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics. // fails then we use OS/2 usWin* metrics.
// //
// This is not "standard" behavior, but it's our best bet to // This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what // account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics. // FreeType does to get its generic ascent and descent metrics.
if (hhea.ascender != 0 or hhea.descender != 0) { if (hhea.ascender != 0 or hhea.descender != 0) break :vertical_metrics .{
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
const hhea_descent: f64 = @floatFromInt(hhea.descender);
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
break :vertical_metrics .{
hhea_ascent * px_per_unit, hhea_ascent * px_per_unit,
hhea_descent * px_per_unit, hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit, hhea_line_gap * px_per_unit,
}; };
}
if (os2_ascent != 0 or os2_descent != 0) { if (os2_ascent != 0 or os2_descent != 0) break :vertical_metrics .{
break :vertical_metrics .{
os2_ascent * px_per_unit, os2_ascent * px_per_unit,
os2_descent * px_per_unit, os2_descent * px_per_unit,
os2_line_gap * px_per_unit, os2_line_gap * px_per_unit,
}; };
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
break :vertical_metrics .{
win_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender
// and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
};
} }
const win_ascent: f64 = @floatFromInt(os2.usWinAscent); // If our font has no OS/2 table, then we just
const win_descent: f64 = @floatFromInt(os2.usWinDescent); // blindly use the metrics from the hhea table.
break :vertical_metrics .{ break :vertical_metrics .{
win_ascent * px_per_unit, hhea_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender hhea_descent * px_per_unit,
// and hhea.Descender, so we flip its sign to fix this. hhea_line_gap * px_per_unit,
-win_descent * px_per_unit,
0.0,
}; };
}; };
@ -672,30 +648,44 @@ pub const Face = struct {
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit; @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
// Similar logic to the underline above. // Similar logic to the underline above.
const has_broken_strikethrough = os2.yStrikeoutSize == 0; const strikethrough_position, const strikethrough_thickness = st: {
const os2 = os2_ orelse break :st .{ null, null };
const strikethrough_position: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0) const has_broken_strikethrough = os2.yStrikeoutSize == 0;
null
else
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const strikethrough_thickness: ?f64 = if (has_broken_strikethrough) const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
null null
else else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit; @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
// We fall back to whatever CoreText does if const thick: ?f64 = if (has_broken_strikethrough)
// the OS/2 table doesn't specify a cap height. null
const cap_height: f64 = if (os2.sCapHeight) |sCapHeight| else
@as(f64, @floatFromInt(sCapHeight)) * px_per_unit @as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
else
ct_font.getCapHeight();
// Ditto for ex height. break :st .{ pos, thick };
const ex_height: f64 = if (os2.sxHeight) |sxHeight| };
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
else // We fall back to whatever CoreText does if the
ct_font.getXHeight(); // OS/2 table doesn't specify a cap or ex height.
const cap_height: f64, const ex_height: f64 = heights: {
const os2 = os2_ orelse break :heights .{
ct_font.getCapHeight(),
ct_font.getXHeight(),
};
break :heights .{
if (os2.sCapHeight) |sCapHeight|
@as(f64, @floatFromInt(sCapHeight)) * px_per_unit
else
ct_font.getCapHeight(),
if (os2.sxHeight) |sxHeight|
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
else
ct_font.getXHeight(),
};
};
// Cell width is calculated by calculating the widest width of the // Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take // visible ASCII characters. Usually 'M' is widest but we just take

View File

@ -600,7 +600,6 @@ pub const Face = struct {
const CalcMetricsError = error{ const CalcMetricsError = error{
CopyTableError, CopyTableError,
MissingOS2Table,
}; };
/// Calculate the metrics associated with a face. This is not public because /// Calculate the metrics associated with a face. This is not public because
@ -629,70 +628,80 @@ pub const Face = struct {
const post = face.getSfntTable(.post) orelse return error.CopyTableError; const post = face.getSfntTable(.post) orelse return error.CopyTableError;
// Read the 'OS/2' table out of the font data. // Read the 'OS/2' table out of the font data.
const os2 = face.getSfntTable(.os2) orelse return error.CopyTableError; const os2_: ?*freetype.c.TT_OS2 = os2: {
const os2 = face.getSfntTable(.os2) orelse break :os2 null;
if (os2.version == 0xFFFF) break :os2 null;
break :os2 os2;
};
// Read the 'hhea' table out of the font data. // Read the 'hhea' table out of the font data.
const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError; const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError;
// Some fonts don't actually have an OS/2 table, which
// we need in order to do the metrics calculations, in
// such cases FreeType sets the version to 0xFFFF
if (os2.version == 0xFFFF) return error.MissingOS2Table;
const units_per_em = head.Units_Per_EM; const units_per_em = head.Units_Per_EM;
const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem); const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em)); const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em));
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: { const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender); const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender); const hhea_descent: f64 = @floatFromInt(hhea.Descender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap); const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
// If the font says to use typo metrics, trust it. if (os2_) |os2| {
// (The USE_TYPO_METRICS bit is bit 7) const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
if (os2.fsSelection & (1 << 7) != 0) { const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
// If the font says to use typo metrics, trust it.
// (The USE_TYPO_METRICS bit is bit 7)
if (os2.fsSelection & (1 << 7) != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
// Otherwise we prefer the height metrics from 'hhea' if they
// are available, or else OS/2 sTypo* metrics, and if all else
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if (hhea.Ascender != 0 or hhea.Descender != 0) {
break :vertical_metrics .{
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
};
}
if (os2_ascent != 0 or os2_descent != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
break :vertical_metrics .{ break :vertical_metrics .{
os2_ascent * px_per_unit, win_ascent * px_per_unit,
os2_descent * px_per_unit, // usWinDescent is *positive* -> down unlike sTypoDescender
os2_line_gap * px_per_unit, // and hhea.Descender, so we flip its sign to fix this.
-win_descent * px_per_unit,
0.0,
}; };
} }
// Otherwise we prefer the height metrics from 'hhea' if they // If our font has no OS/2 table, then we just
// are available, or else OS/2 sTypo* metrics, and if all else // blindly use the metrics from the hhea table.
// fails then we use OS/2 usWin* metrics.
//
// This is not "standard" behavior, but it's our best bet to
// account for fonts being... just weird. It's pretty much what
// FreeType does to get its generic ascent and descent metrics.
if (hhea.Ascender != 0 or hhea.Descender != 0) {
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
break :vertical_metrics .{
hhea_ascent * px_per_unit,
hhea_descent * px_per_unit,
hhea_line_gap * px_per_unit,
};
}
if (os2_ascent != 0 or os2_descent != 0) {
break :vertical_metrics .{
os2_ascent * px_per_unit,
os2_descent * px_per_unit,
os2_line_gap * px_per_unit,
};
}
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
break :vertical_metrics .{ break :vertical_metrics .{
win_ascent * px_per_unit, hhea_ascent * px_per_unit,
// usWinDescent is *positive* -> down unlike sTypoDescender hhea_descent * px_per_unit,
// and hhea.Descender, so we flip its sign to fix this. hhea_line_gap * px_per_unit,
-win_descent * px_per_unit,
0.0,
}; };
}; };
@ -714,17 +723,23 @@ pub const Face = struct {
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit; @as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
// Similar logic to the underline above. // Similar logic to the underline above.
const has_broken_strikethrough = os2.yStrikeoutSize == 0; const strikethrough_position, const strikethrough_thickness = st: {
const os2 = os2_ orelse break :st .{ null, null };
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0) const has_broken_strikethrough = os2.yStrikeoutSize == 0;
null
else
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const strikethrough_thickness = if (has_broken_strikethrough) const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
null null
else else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit; @as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
const thick: ?f64 = if (has_broken_strikethrough)
null
else
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
break :st .{ pos, thick };
};
// Cell width is calculated by calculating the widest width of the // Cell width is calculated by calculating the widest width of the
// visible ASCII characters. Usually 'M' is widest but we just take // visible ASCII characters. Usually 'M' is widest but we just take
@ -754,37 +769,37 @@ pub const Face = struct {
break :cell_width max; break :cell_width max;
}; };
// The OS/2 table does not include sCapHeight or sxHeight in version 1. // We use the cap and ex heights specified by the font if they're
const has_os2_height_metrics = os2.version >= 2; // available, otherwise we try to measure the `H` and `x` glyphs.
const cap_height: ?f64, const ex_height: ?f64 = heights: {
// We use the cap height specified by the font if it's if (os2_) |os2| {
// available, otherwise we try to measure the `H` glyph. // The OS/2 table does not include these metrics in version 1.
const cap_height: ?f64 = cap_height: { if (os2.version >= 2) {
if (has_os2_height_metrics) { break :heights .{
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit; @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit,
} @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit,
if (face.getCharIndex('H')) |glyph_index| { };
if (face.loadGlyph(glyph_index, .{ .render = true })) { }
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} else |_| {}
} }
break :cap_height null; break :heights .{
}; cap: {
if (face.getCharIndex('H')) |glyph_index| {
// We use the ex height specified by the font if it's if (face.loadGlyph(glyph_index, .{ .render = true })) {
// available, otherwise we try to measure the `x` glyph. break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
const ex_height: ?f64 = ex_height: { } else |_| {}
if (has_os2_height_metrics) { }
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit; break :cap null;
} },
if (face.getCharIndex('x')) |glyph_index| { ex: {
if (face.loadGlyph(glyph_index, .{ .render = true })) { if (face.getCharIndex('x')) |glyph_index| {
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height); if (face.loadGlyph(glyph_index, .{ .render = true })) {
} else |_| {} break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
} } else |_| {}
}
break :ex_height null; break :ex null;
},
};
}; };
var result = font.face.Metrics.calc(.{ var result = font.face.Metrics.calc(.{

View File

@ -2067,7 +2067,7 @@ test "legacy: f1" {
{ {
enc.event.key = .f3; enc.event.key = .f3;
const actual = try enc.legacy(&buf); const actual = try enc.legacy(&buf);
try testing.expectEqualStrings("\x1b[1;5R", actual); try testing.expectEqualStrings("\x1b[13;5~", actual);
} }
// F4 // F4

View File

@ -13,7 +13,7 @@ pub const CursorMode = enum { any, normal, application };
pub const KeypadMode = enum { any, normal, application }; pub const KeypadMode = enum { any, normal, application };
/// A bit confusing so I'll document this one: this is the "modify other keys" /// A bit confusing so I'll document this one: this is the "modify other keys"
/// setting. We only change behavior for "set_other" which is ESC [ 4; 2 m. /// setting. We only change behavior for "set_other" which is ESC [ > 4; 2 m.
/// So this can be "any" which means we don't care what's going on. Or it /// So this can be "any" which means we don't care what's going on. Or it
/// can be "set" which means modify keys must be set EXCEPT FOR "other keys" /// can be "set" which means modify keys must be set EXCEPT FOR "other keys"
/// mode, and "set_other" which means modify keys must be set to "other keys" /// mode, and "set_other" which means modify keys must be set to "other keys"
@ -89,7 +89,7 @@ pub const keys = keys: {
// Function Keys. todo: f13-f35 but we need to add to input.Key // Function Keys. todo: f13-f35 but we need to add to input.Key
result.set(.f1, pcStyle("\x1b[1;{}P") ++ .{.{ .sequence = "\x1BOP" }}); result.set(.f1, pcStyle("\x1b[1;{}P") ++ .{.{ .sequence = "\x1BOP" }});
result.set(.f2, pcStyle("\x1b[1;{}Q") ++ .{.{ .sequence = "\x1BOQ" }}); result.set(.f2, pcStyle("\x1b[1;{}Q") ++ .{.{ .sequence = "\x1BOQ" }});
result.set(.f3, pcStyle("\x1b[1;{}R") ++ .{.{ .sequence = "\x1BOR" }}); result.set(.f3, pcStyle("\x1b[13;{}~") ++ .{.{ .sequence = "\x1BOR" }});
result.set(.f4, pcStyle("\x1b[1;{}S") ++ .{.{ .sequence = "\x1BOS" }}); result.set(.f4, pcStyle("\x1b[1;{}S") ++ .{.{ .sequence = "\x1BOS" }});
result.set(.f5, pcStyle("\x1b[15;{}~") ++ .{.{ .sequence = "\x1B[15~" }}); result.set(.f5, pcStyle("\x1b[15;{}~") ++ .{.{ .sequence = "\x1B[15~" }});
result.set(.f6, pcStyle("\x1b[17;{}~") ++ .{.{ .sequence = "\x1B[17~" }}); result.set(.f6, pcStyle("\x1b[17;{}~") ++ .{.{ .sequence = "\x1B[17~" }});

View File

@ -7,6 +7,8 @@ const entrypoint = switch (build_config.exe_entrypoint) {
.helpgen => @import("helpgen.zig"), .helpgen => @import("helpgen.zig"),
.mdgen_ghostty_1 => @import("build/mdgen/main_ghostty_1.zig"), .mdgen_ghostty_1 => @import("build/mdgen/main_ghostty_1.zig"),
.mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"), .mdgen_ghostty_5 => @import("build/mdgen/main_ghostty_5.zig"),
.webgen_config => @import("build/webgen/main_config.zig"),
.webgen_actions => @import("build/webgen/main_actions.zig"),
.bench_parser => @import("bench/parser.zig"), .bench_parser => @import("bench/parser.zig"),
.bench_stream => @import("bench/stream.zig"), .bench_stream => @import("bench/stream.zig"),
.bench_codepoint_width => @import("bench/codepoint-width.zig"), .bench_codepoint_width => @import("bench/codepoint-width.zig"),

View File

@ -34,6 +34,23 @@ pub fn appendEnvAlways(
}); });
} }
/// Prepend a value to an environment variable such as PATH.
/// The returned value is always allocated so it must be freed.
pub fn prependEnv(
alloc: Allocator,
current: []const u8,
value: []const u8,
) ![]u8 {
// If there is no prior value, we return it as-is
if (current.len == 0) return try alloc.dupe(u8, value);
return try std.fmt.allocPrint(alloc, "{s}{c}{s}", .{
value,
std.fs.path.delimiter,
current,
});
}
/// The result of getenv, with a shared deinit to properly handle allocation /// The result of getenv, with a shared deinit to properly handle allocation
/// on Windows. /// on Windows.
pub const GetEnvResult = struct { pub const GetEnvResult = struct {
@ -110,3 +127,25 @@ test "appendEnv existing" {
try testing.expectEqualStrings(result, "a:b:foo"); try testing.expectEqualStrings(result, "a:b:foo");
} }
} }
test "prependEnv empty" {
const testing = std.testing;
const alloc = testing.allocator;
const result = try prependEnv(alloc, "", "foo");
defer alloc.free(result);
try testing.expectEqualStrings(result, "foo");
}
test "prependEnv existing" {
const testing = std.testing;
const alloc = testing.allocator;
const result = try prependEnv(alloc, "a:b", "foo");
defer alloc.free(result);
if (builtin.os.tag == .windows) {
try testing.expectEqualStrings(result, "foo;a:b");
} else {
try testing.expectEqualStrings(result, "foo:a:b");
}
}

View File

@ -27,6 +27,7 @@ pub const CFReleaseThread = @import("cf_release_thread.zig");
pub const TempDir = @import("TempDir.zig"); pub const TempDir = @import("TempDir.zig");
pub const appendEnv = env.appendEnv; pub const appendEnv = env.appendEnv;
pub const appendEnvAlways = env.appendEnvAlways; pub const appendEnvAlways = env.appendEnvAlways;
pub const prependEnv = env.prependEnv;
pub const getenv = env.getenv; pub const getenv = env.getenv;
pub const setenv = env.setenv; pub const setenv = env.setenv;
pub const unsetenv = env.unsetenv; pub const unsetenv = env.unsetenv;

View File

@ -256,9 +256,7 @@ vertex CellTextVertexOut cell_text_vertex(
offset.y = uniforms.cell_size.y - offset.y; offset.y = uniforms.cell_size.y - offset.y;
// If we're constrained then we need to scale the glyph. // If we're constrained then we need to scale the glyph.
// We also always constrain colored glyphs since we should have if (in.mode == MODE_TEXT_CONSTRAINED) {
// their scaled cell size exactly correct.
if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) {
float max_width = uniforms.cell_size.x * in.constraint_width; float max_width = uniforms.cell_size.x * in.constraint_width;
if (size.x > max_width) { if (size.x > max_width) {
float new_y = size.y * (max_width / size.x); float new_y = size.y * (max_width / size.x);

View File

@ -208,10 +208,8 @@ void main() {
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y; glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
// If this is a constrained mode, we need to constrain it! // If this is a constrained mode, we need to constrain it!
// We also always constrain colored glyphs since we should have
// their scaled cell size exactly correct.
vec2 glyph_size_calc = glyph_size; vec2 glyph_size_calc = glyph_size;
if (mode == MODE_FG_CONSTRAINED || mode == MODE_FG_COLOR) { if (mode == MODE_FG_CONSTRAINED) {
if (glyph_size.x > cell_size_scaled.x) { if (glyph_size.x > cell_size_scaled.x) {
float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x);
glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2); glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2);

View File

@ -18,9 +18,6 @@ our integration script (`bash/ghostty.bash`). This prevents Bash from loading
its normal startup files, which becomes our script's responsibility (along with its normal startup files, which becomes our script's responsibility (along with
disabling POSIX mode). disabling POSIX mode).
Because automatic Bash shell integration requires Bash version 4 or later, it
must be explicitly enabled (`shell-integration = bash`).
Bash shell integration can also be sourced manually from `bash/ghostty.bash`. Bash shell integration can also be sourced manually from `bash/ghostty.bash`.
This also works for older versions of Bash. This also works for older versions of Bash.
@ -31,6 +28,13 @@ if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
fi fi
``` ```
> [!NOTE]
>
> The version of Bash distributed with macOS (`/bin/bash`) does not support
> automatic shell integration. You'll need to manually source the shell
> integration script (as shown above). You can also install a standard
> version of Bash from Homebrew or elsewhere and set it as your shell.
### Elvish ### Elvish
For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration` For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration`

View File

@ -58,7 +58,8 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
# Arch, Debian, Ubuntu use /etc/bash.bashrc # Arch, Debian, Ubuntu use /etc/bash.bashrc
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC # Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_BASHRC
# Void Linux uses /etc/bash/bashrc # Void Linux uses /etc/bash/bashrc
for rcfile in /etc/bash.bashrc /etc/bash/bashrc ; do # Nixos uses /etc/bashrc
for rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do
[ -r "$rcfile" ] && { builtin source "$rcfile"; break; } [ -r "$rcfile" ] && { builtin source "$rcfile"; break; }
done done
if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi
@ -99,15 +100,11 @@ function __ghostty_precmd() {
PS1=$PS1'\[\e]133;B\a\]' PS1=$PS1'\[\e]133;B\a\]'
PS2=$PS2'\[\e]133;B\a\]' PS2=$PS2'\[\e]133;B\a\]'
# bash doesn't redraw the leading lines in a multiline prompt so
# mark the last line as a secondary prompt (k=s) to prevent the
# preceding lines from being erased by ghostty after a resize.
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
# bash doesn't redraw the leading lines in a multiline prompt so PS1=$PS1'\[\e]133;A;k=s\a\]'
# mark the last line as a secondary prompt (k=s) to prevent the
# preceding lines from being erased by ghostty after a resize.
builtin local oldval
oldval=$(builtin shopt -p extglob)
builtin shopt -s extglob
PS1=${PS1%@('\n'|$'\n')*}'\n\[\e]133;A;k=s\a\]'${PS1##*@('\n'|$'\n')}
builtin eval "$oldval"
fi fi
# Cursor # Cursor
@ -151,7 +148,7 @@ function __ghostty_precmd() {
if test "$_ghostty_executing" != ""; then if test "$_ghostty_executing" != ""; then
# End of current command. Report its status. # End of current command. Report its status.
builtin printf "\033]133;D;%s;aid=%s\007" "$ret" "$BASHPID" builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID"
fi fi
# unfortunately bash provides no hooks to detect cwd changes # unfortunately bash provides no hooks to detect cwd changes
@ -163,7 +160,7 @@ function __ghostty_precmd() {
fi fi
# Fresh line and start of prompt. # Fresh line and start of prompt.
builtin printf "\033]133;A;aid=%s\007" "$BASHPID" builtin printf "\e]133;A;aid=%s\a" "$BASHPID"
_ghostty_executing=0 _ghostty_executing=0
} }
@ -171,7 +168,7 @@ function __ghostty_preexec() {
PS0="$_GHOSTTY_SAVE_PS0" PS0="$_GHOSTTY_SAVE_PS0"
PS1="$_GHOSTTY_SAVE_PS1" PS1="$_GHOSTTY_SAVE_PS1"
PS2="$_GHOSTTY_SAVE_PS2" PS2="$_GHOSTTY_SAVE_PS2"
builtin printf "\033]133;C;\007" builtin printf "\e]133;C;\a"
_ghostty_executing=1 _ghostty_executing=1
} }

View File

@ -1315,8 +1315,13 @@ pub fn clearPrompt(self: *Screen) void {
switch (row.semantic_prompt) { switch (row.semantic_prompt) {
// We are at a prompt but we're not at the start of the prompt. // We are at a prompt but we're not at the start of the prompt.
// We mark our found value and continue because the prompt // We mark our found value and continue because the prompt
// may be multi-line. // may be multi-line, unless this is the second time we've
.input => found = p, // seen an .input marker, in which case we've run into an
// earlier prompt.
.input => {
if (found != null) break;
found = p;
},
// If we find the prompt then we're done. We are also done // If we find the prompt then we're done. We are also done
// if we find any prompt continuation, because the shells // if we find any prompt continuation, because the shells
@ -3565,6 +3570,32 @@ test "Screen: clearPrompt continuation" {
} }
} }
test "Screen: clearPrompt consecutive prompts" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 5, 3, 0);
defer s.deinit();
const str = "1ABCD\n2EFGH\n3IJKL";
try s.testWriteString(str);
// Set both rows to be prompts
{
s.cursorAbsolute(0, 1);
s.cursor.page_row.semantic_prompt = .input;
s.cursorAbsolute(0, 2);
s.cursor.page_row.semantic_prompt = .input;
}
s.clearPrompt();
{
const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} });
defer alloc.free(contents);
try testing.expectEqualStrings("1ABCD\n2EFGH", contents);
}
}
test "Screen: clearPrompt no prompt" { test "Screen: clearPrompt no prompt" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;

View File

@ -794,26 +794,42 @@ const Subprocess = struct {
} }
} }
// Add the man pages from our application bundle to MANPATH. // On macOS, export additional data directories from our
if (comptime builtin.target.isDarwin()) { // application bundle.
if (cfg.resources_dir) |resources_dir| man: { if (comptime builtin.target.isDarwin()) darwin: {
var buf: [std.fs.max_path_bytes]u8 = undefined; const resources_dir = cfg.resources_dir orelse break :darwin;
const dir = std.fmt.bufPrint(&buf, "{s}/../man", .{resources_dir}) catch |err| {
log.warn("error building manpath, man pages may not be available err={}", .{err});
break :man;
};
var buf: [std.fs.max_path_bytes]u8 = undefined;
const xdg_data_dir_key = "XDG_DATA_DIRS";
if (std.fmt.bufPrint(&buf, "{s}/..", .{resources_dir})) |data_dir| {
try env.put(
xdg_data_dir_key,
try internal_os.appendEnv(
alloc,
env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share",
data_dir,
),
);
} else |err| {
log.warn("error building {s}; err={}", .{ xdg_data_dir_key, err });
}
const manpath_key = "MANPATH";
if (std.fmt.bufPrint(&buf, "{s}/../man", .{resources_dir})) |man_dir| {
// Always append with colon in front, as it mean that if // Always append with colon in front, as it mean that if
// `MANPATH` is empty, then it should be treated as an extra // `MANPATH` is empty, then it should be treated as an extra
// path instead of overriding all paths set by OS. // path instead of overriding all paths set by OS.
try env.put( try env.put(
"MANPATH", manpath_key,
try internal_os.appendEnvAlways( try internal_os.appendEnvAlways(
alloc, alloc,
env.get("MATHPATH") orelse "", env.get(manpath_key) orelse "",
dir, man_dir,
), ),
); );
} else |err| {
log.warn("error building {s}; man pages may not be available; err={}", .{ manpath_key, err });
} }
} }
@ -1119,7 +1135,7 @@ const Subprocess = struct {
// This is important because our cwd can be set by the shell (OSC 7) // This is important because our cwd can be set by the shell (OSC 7)
// and we don't want to break new windows. // and we don't want to break new windows.
const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: { const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: {
if (std.fs.accessAbsolute(proposed, .{})) { if (std.fs.cwd().access(proposed, .{})) {
break :cwd proposed; break :cwd proposed;
} else |err| { } else |err| {
log.warn("cannot access cwd, ignoring: {}", .{err}); log.warn("cannot access cwd, ignoring: {}", .{err});

View File

@ -1,9 +1,11 @@
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator; const ArenaAllocator = std.heap.ArenaAllocator;
const EnvMap = std.process.EnvMap; const EnvMap = std.process.EnvMap;
const config = @import("../config.zig"); const config = @import("../config.zig");
const homedir = @import("../os/homedir.zig"); const homedir = @import("../os/homedir.zig");
const internal_os = @import("../os/main.zig");
const log = std.log.scoped(.shell_integration); const log = std.log.scoped(.shell_integration);
@ -57,11 +59,21 @@ pub fn setup(
}; };
const result: ShellIntegration = shell: { const result: ShellIntegration = shell: {
// For now, bash integration must be explicitly enabled via force_shell. if (std.mem.eql(u8, "bash", exe)) {
// Our automatic shell integration requires bash version 4 or later, // Apple distributes their own patched version of Bash 3.2
// and systems like macOS continue to ship bash version 3 by default. // on macOS that disables the ENV-based POSIX startup path.
// This approach avoids the cost of performing a runtime version check. // This means we're unable to perform our automatic shell
if (std.mem.eql(u8, "bash", exe) and force_shell == .bash) { // integration sequence in this specific environment.
//
// If we're running "/bin/bash" on Darwin, we can assume
// we're using Apple's Bash because /bin is non-writable
// on modern macOS due to System Integrity Protection.
if (comptime builtin.target.isDarwin()) {
if (std.mem.eql(u8, "/bin/bash", command)) {
return null;
}
}
const new_command = try setupBash( const new_command = try setupBash(
alloc_arena, alloc_arena,
command, command,
@ -424,8 +436,8 @@ test "bash: preserve ENV" {
/// Setup automatic shell integration for shells that include /// Setup automatic shell integration for shells that include
/// their modules from paths in `XDG_DATA_DIRS` env variable. /// their modules from paths in `XDG_DATA_DIRS` env variable.
/// ///
/// Path of shell-integration dir is prepended to `XDG_DATA_DIRS`. /// The shell-integration path is prepended to `XDG_DATA_DIRS`.
/// It is also saved in `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable /// It is also saved in the `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable
/// so that the shell can refer to it and safely remove this directory /// so that the shell can refer to it and safely remove this directory
/// from `XDG_DATA_DIRS` when integration is complete. /// from `XDG_DATA_DIRS` when integration is complete.
fn setupXdgDataDirs( fn setupXdgDataDirs(
@ -447,32 +459,60 @@ fn setupXdgDataDirs(
// so that our modifications don't interfere with other commands. // so that our modifications don't interfere with other commands.
try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir); try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir);
{ // We attempt to avoid allocating by using the stack up to 4K.
const xdg_data_dir_key = "XDG_DATA_DIRS"; // Max stack size is considerably larger on mac
// 4K is a reasonable size for this for most cases. However, env
// vars can be significantly larger so if we have to we fall
// back to a heap allocated value.
var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena);
const stack_alloc = stack_alloc_state.get();
// We attempt to avoid allocating by using the stack up to 4K. // If no XDG_DATA_DIRS set use the default value as specified.
// Max stack size is considerably larger on macOS and Linux but // This ensures that the default directories aren't lost by setting
// 4K is a reasonable size for this for most cases. However, env // our desired integration dir directly. See #2711.
// vars can be significantly larger so if we have to we fall // <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
// back to a heap allocated value. const xdg_data_dirs_key = "XDG_DATA_DIRS";
var stack_alloc_state = std.heap.stackFallback(4096, alloc_arena); try env.put(
const stack_alloc = stack_alloc_state.get(); xdg_data_dirs_key,
try internal_os.prependEnv(
// If no XDG_DATA_DIRS set use the default value as specified. stack_alloc,
// This ensures that the default directories aren't lost by setting env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share",
// our desired integration dir directly. See #2711.
// <https://specifications.freedesktop.org/basedir-spec/0.6/#variables>
const old = env.get(xdg_data_dir_key) orelse "/usr/local/share:/usr/share";
const prepended = try std.fmt.allocPrint(stack_alloc, "{s}{c}{s}", .{
integ_dir, integ_dir,
std.fs.path.delimiter, ),
old, );
}); }
defer stack_alloc.free(prepended);
try env.put(xdg_data_dir_key, prepended); test "xdg: empty XDG_DATA_DIRS" {
} const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var env = EnvMap.init(alloc);
defer env.deinit();
try setupXdgDataDirs(alloc, ".", &env);
try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?);
try testing.expectEqualStrings("./shell-integration:/usr/local/share:/usr/share", env.get("XDG_DATA_DIRS").?);
}
test "xdg: existing XDG_DATA_DIRS" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var env = EnvMap.init(alloc);
defer env.deinit();
try env.put("XDG_DATA_DIRS", "/opt/share");
try setupXdgDataDirs(alloc, ".", &env);
try testing.expectEqualStrings("./shell-integration", env.get("GHOSTTY_SHELL_INTEGRATION_XDG_DIR").?);
try testing.expectEqualStrings("./shell-integration:/opt/share", env.get("XDG_DATA_DIRS").?);
} }
/// Setup the zsh automatic shell integration. This works by setting /// Setup the zsh automatic shell integration. This works by setting

View File

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

35
website/.gitignore vendored
View File

@ -1,35 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@ -1 +0,0 @@
.next/

View File

@ -1,48 +0,0 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, install the necessary dependencies:
```bash
npm install
# or
yarn install
# or
pnpm install
# or
bun install
```
Then, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@ -1,20 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: #0e1324;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-rgb: #0e1324;
}
}
body {
color: rgb(var(--foreground-rgb));
background-color: var(--background-rgb);
}

View File

@ -1,35 +0,0 @@
import "./globals.css";
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-inter",
});
const jetbrains_mono = JetBrains_Mono({
subsets: ["latin"],
display: "swap",
weight: ["100", "600"],
variable: "--font-jetbrains-mono",
});
export const metadata: Metadata = {
title: "Ghostty",
description: "👻",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={`${inter.className} ${jetbrains_mono.variable}`}>
{children}
</body>
</html>
);
}

View File

@ -1,23 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"></div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:content-[''] before:lg:h-[360px] z-[-1]">
<p className="text-9xl">
<Image
src="/icon.png"
alt="Ghostty Icon"
width={250}
height={250}
priority
/>
</p>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left"></div>
</main>
);
}

View File

@ -1,26 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Bell (BEL)
<VTSequence sequence="BEL" />
The purpose of the bell sequence is to raise the attention
of the user. Historically, this would [ring a physical bell](https://en.wikipedia.org/wiki/Bell_character). Today, many alternate behaviors are
acceptable:
- An audible sound can be played through the speakers
- Background or border of a window can visually flash
- The terminal window can come into focus or be put on top
- Application icon can bounce or otherwise draw attention
- A desktop notification can be shown
Normally, the bell behavior is configurable and can be disabled.
## BEL as an OSC Terminator
The `BEL` character is also a valid terminating character for
OSC sequences, although `ST` is preferred. If `BEL` is the
terminating character for an OSC sequence, any responses should
also terminate with the `BEL` character.[^1]
[^1]: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html

View File

@ -1,9 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Backspace (BS)
<VTSequence sequence="BS" />
This sequence performs [cursor backward (CUB)](/vt/cub)
with `n = 1`. There is no additional or different behavior for
using `BS`.

View File

@ -1,83 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Backward Tabulation (CBT)
<VTSequence sequence={["CSI", "Pn", "Z"]} />
Move the cursor `n` tabs left.
The leftmost valid column for this operation is the first column. If
[origin mode](#TODO) is enabled, then the leftmost valid column for this
operation is the [left margin](#TODO).
Move the cursor left until the cursor position is on a tabstop. If the
cursor would move past the leftmost valid column, the cursor remains at
the leftmost valid column and the operation completes. Repeat this process
`n` times.
Tabstops are dynamic and can be set with escape sequences such as
[horizontal tab set (HTS)](/vt/hts), [tab clear (TBC)](/vt/tbc), etc.
A terminal emulator may default tabstops at any interval, though an interval
of 8 spaces is most common.
## Validation
### CBT V-1: Left Beyond First Column
```bash
printf "\033[?W" # reset tab stops
printf "\033[10Z"
printf "A"
```
```
|Ac________|
```
### CBT V-2: Left Starting After Tab Stop
```bash
printf "\033[?W" # reset tab stops
printf "\033[1;10H"
printf "X"
printf "\033[Z"
printf "A"
```
```
|________AX|
```
### CBT V-3: Left Starting on Tabstop
```bash
printf "\033[?W" # reset tab stops
printf "\033[1;9H"
printf "X"
printf "\033[1;9H"
printf "\033[Z"
printf "A"
```
```
|A_______X_|
```
### CBT V-4: Left Margin with Origin Mode
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?W" # reset tab stops
printf "\033[?6h" # enable origin mode
printf "\033[?69h" # enable left/right margins
printf "\033[3;6s" # scroll region left/right
printf "\033[1;2H" # move cursor in region
printf "X"
printf "\033[Z"
printf "A"
```
```
|__AX______|
```

View File

@ -1,70 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Horizontal Tabulation (CHT)
<VTSequence sequence={["CSI", "Pn", "I"]} />
Move the cursor `n` tabs right.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
The rightmost valid column for this operation is the rightmost column in
the terminal screen or the [right margin](#TODO), whichever is smaller.
This sequence does not change behavior with [origin mode](#TODO) set.
Move the cursor right until the cursor position is on a tabstop. If the
cursor would move past the rightmost valid column, the cursor remains at
the rightmost valid column and the operation completes. Repeat this process
`n` times.
Tabstops are dynamic and can be set with escape sequences such as
[horizontal tab set (HTS)](/vt/hts), [tab clear (TBC)](/vt/tbc), etc.
A terminal emulator may default tabstops at any interval, though an interval
of 8 spaces is most common.
## Validation
### CHT V-1: Right Beyond Last Column
```bash
printf "\033[?W" # reset tab stops
printf "\033[100I" # assuming the test terminal has less than 800 columns
printf "A"
```
```
|_________A|
```
### CHT V-2: Right From Before a Tabstop
```bash
printf "\033[?W" # reset tab stops
printf "\033[1;2H"
printf "A"
printf "\033[I"
printf "X"
```
```
|_A______X_|
```
### CHT V-3: Right Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?W" # reset tab stops
printf "\033[?69h" # enable left/right margins
printf "\033[3;6s" # scroll region left/right
printf "\033[1;1H" # move cursor in region
printf "X"
printf "\033[I"
printf "A"
```
```
|__AX______|
```

View File

@ -1,13 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Next Line (CNL)
<VTSequence sequence={["CSI", "Pn", "E"]} />
Move the cursor `n` cells down and to the beginning of the line.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
The logic of this sequence is identical to [Cursor Down (CUD)](/vt/cud)
followed by [Carriage Return (CR)](/vt/cr).

View File

@ -1,13 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Previous Line (CPL)
<VTSequence sequence={["CSI", "Pn", "F"]} />
Move the cursor `n` cells up and to the beginning of the line.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
The logic of this sequence is identical to [Cursor Up (CUU)](/vt/cuu)
followed by [Carriage Return (CR)](/vt/cr).

View File

@ -1,91 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Carriage Return (CR)
<VTSequence sequence="CR" />
Move the cursor to the leftmost column.
This sequence always unsets the pending wrap state.
If [origin mode (mode 6)](#TODO) is enabled, the cursor is set to the
[left margin](#TODO) of the scroll region and the operation is complete.
If origin mode is _not_ set and the cursor is on or to the right of the
left margin, the cursor is set to the left margin. If the cursor is to the left
of the left margin, the cursor is moved to the leftmost column in the terminal.
## Validation
### CR V-1: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\r"
printf "X"
echo
```
```
|X________A|
|c_________|
```
### CR V-2: Left Margin
```bash
cols=$(tput cols)
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margin mode
printf "\033[2;5s" # set left/right margin
printf "\033[4G"
printf "A"
printf "\r"
printf "X"
```
```
|_XcA______|
```
### CR V-3: Left of Left Margin
```bash
cols=$(tput cols)
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margin mode
printf "\033[2;5s" # set left/right margin
printf "\033[4G"
printf "A"
printf "\033[1G"
printf "\r"
printf "X"
```
```
|Xc_A______|
```
### CR V-3: Left Margin with Origin Mode
```bash
cols=$(tput cols)
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?6h" # enable origin mode
printf "\033[?69h" # enable left/right margin mode
printf "\033[2;5s" # set left/right margin
printf "\033[4G"
printf "A"
printf "\033[1G"
printf "\r"
printf "X"
```
```
|_XcA______|
```

View File

@ -1,182 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Backward (CUB)
<VTSequence sequence={["CSI", "Pn", "D"]} />
Move the cursor `n` cells left.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
This sequence always unsets the pending wrap state.
The leftmost boundary the cursor can move to is determined by the current
cursor column and the [left margin](#TODO). If the cursor begins to the left of the left margin, modify the left margin to be the leftmost column
for the duration of the sequence. The leftmost column the cursor can be on
is the left margin.
With the above in place, there are three different cursor backward behaviors
depending on the mode state of the terminal. The possible behaviors are listed
below. In the case of a conflict, the top-most behavior takes priority.
- **Extended reverse wrap**: [wraparound (mode 7)](#TODO) and [extended reverse wrap (mode 1045)](#TODO)
are **BOTH** enabled
- **Reverse wrap**: [wraparound (mode 7)](#TODO) and [reverse wrap (mode 45)](#TODO)
are **BOTH** enabled
- **No wrap**: The default behavior if the above wrapping behaviors
do not have their conditions met.
For the **no wrap** behavior, move the cursor to the left `n` cells while
respecting the aforementioned leftmost boundary. Upon reaching the leftmost
boundary, stop moving the cursor left regardless of the remaining value of `n`.
The cursor row remains unchanged.
For the **extended reverse wrap** behavior, move the cursor to the left `n`
cells while respecting the aforementioned leftmost boundary. Upon reaching the
leftmost boundary, if `n > 0` then move the cursor to the [right margin](#TODO)
of the line above the cursor. If the cursor is already on the
[top margin](#TODO), move the cursor to the right margin of the
[bottom margin](#TODO). Both the cursor column and row can change in this
mode. Compared to non-extended reverse wrap, the two critical differences are
that extended reverse wrap doesn't require the previous line to be wrapped
and extended reverse wrap will wrap around to the bottom margin.
For the **reverse wrap** (non-extended) behavior, move the cursor to the left `n`
cells while respecting the aforementioned leftmost boundary. Upon reaching the
leftmost boundary, if `n > 0` and the previous line was wrapped, then move the
cursor to the [right margin](#TODO) of the line above the cursor. If the previous
line was not wrapped, the cursor left operation is complete even if there
is a remaining value of `n`. If the cursor
is already on the [top margin](#TODO), do not move the cursor up.
This wrapping mode does not wrap the cursor row back to the bottom margin.
For **extended reverse wrap** or **reverse wrap** modes, if the pending
wrap state is set, decrease `n` by 1. In these modes, the initial cursor
backward count is consumed by the pending wrap state, as if you pressed
"backspace" on an empty newline and the cursor moved back to the previous line.
## Validation
### CUB V-1: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[D" # move back one
printf "XYZ"
```
```
|________XY|
|Zc________|
```
### CUB V-2: Leftmost Boundary with Reverse Wrap Disabled
```bash
printf "\033[?45l" # disable reverse wrap
echo "A"
printf "\033[10D" # back
printf "B"
```
```
|A_________|
|Bc________|
```
### CUB V-3: Reverse Wrap
```bash
cols=$(tput cols)
printf "\033[?7h" # enable wraparound
printf "\033[?45h" # enable reverse wrap
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[${cols}G" # move to end of line
printf "AB" # write and wrap
printf "\033[D" # move back two
printf "X"
```
```
|_________Xc
|B_________|
```
### CUB V-4: Extended Reverse Wrap Single Line
```bash
printf "\033[?7h" # enable wraparound
printf "\033[?1045h" # enable extended reverse wrap
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
echo "A"
printf "B"
printf "\033[2D" # move back two
printf "X"
```
```
|A________Xc
|B_________|
```
### CUB V-5: Extended Reverse Wrap Wraps to Bottom
```bash
cols=$(tput cols)
printf "\033[?7h" # enable wraparound
printf "\033[?1045h" # enable extended reverse wrap
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[1;3r" # set scrolling region
echo "A"
printf "B"
printf "\033[D" # move back one
printf "\033[${cols}D" # move back entire width
printf "\033[D" # move back one
printf "X"
```
```
|A_________|
|B_________|
|_________Xc
```
### CUB V-6: Reverse Wrap Outside of Margins
```bash
printf "\033[1;1H"
printf "\033[0J"
printf "\033[?45h"
printf "\033[3r"
printf "\b"
printf "X"
```
```
|__________|
|__________|
|Xc________|
```
### CUB V-7: Reverse Wrap with Pending Wrap State
```bash
cols=$(tput cols)
printf "\033[?45h"
printf "\033[${cols}G"
printf "\033[4D"
printf "ABCDE"
printf "\033[D"
printf "X"
```
```
|_____ABCDX|
```

View File

@ -1,75 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Down (CUD)
<VTSequence sequence={["CSI", "Pn", "B"]} />
Move the cursor `n` cells down.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
This sequence always unsets the pending wrap state.
If the current cursor position is at or above the [bottom margin](#TODO),
the lowest point the cursor can move is the bottom margin. If the current
cursor position is below the bottom margin, the lowest point the cursor
can move is the final row.
This sequence never triggers scrolling.
## Validation
### CUD V-1: Cursor Down
```bash
printf "A"
printf "\033[2B" # cursor down
printf "X"
```
```
|A_________|
|__________|
|_Xc_______|
```
### CUD V-2: Cursor Down Above Bottom Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\n\n\n\n" # screen is 4 high
printf "\033[1;3r" # set scrolling region
printf "A"
printf "\033[5B" # cursor down
printf "X"
```
```
|A_________|
|__________|
|_Xc_______|
|__________|
```
### CUD V-3: Cursor Down Below Bottom Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\n\n\n\n\n" # screen is 5 high
printf "\033[1;3r" # set scrolling region
printf "A"
printf "\033[4;1H" # move below region
printf "\033[5B" # cursor down
printf "X"
```
```
|A_________|
|__________|
|__________|
|__________|
|_Xc_______|
```

View File

@ -1,83 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Forward (CUF)
<VTSequence sequence={["CSI", "Pn", "C"]} />
Move the cursor `n` cells right.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
This sequence always unsets the pending wrap state.
The rightmost boundary the cursor can move to is determined by the current
cursor column and the [right margin](#TODO). If the cursor begins to the right
of the right margin, modify the right margin to be the rightmost column
of the screen for the duration of the sequence. The rightmost column the cursor
can be on is the right margin.
Move the cursor `n` cells to the right up to and including the rightmost boundary.
This sequence never wraps or modifies cell content. This sequence is not affected
by any terminal modes.
## Validation
### CUF V-1: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[C" # move forward one
printf "XYZ"
```
```
|_________X|
|YZ________|
```
### CUF V-2: Rightmost Boundary with Reverse Wrap Disabled
```bash
printf "A"
printf "\033[500C" # forward larger than screen width
printf "B"
```
```
|A________Bc
```
### CUF V-3: Left of the Right Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[1G" # move to left
printf "\033[500C" # forward larger than screen width
printf "X"
```
```
|____X_____|
```
### CUF V-4: Right of the Right Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[6G" # move to right of margin
printf "\033[500C" # forward larger than screen width
printf "X"
```
```
|_________X|
```

View File

@ -1,127 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Position (CUP)
<VTSequence sequence={["CSI", "Py", ";", "Px", "H"]} />
Move the cursor to row `y` and column `x`.
The parameters `y` and `x` must be integers greater than or equal to 1.
If either is less than or equal to 0, adjust that parameter to be 1.
The values `y` and `x` are both one-based. For example, the top row is row 1
and the leftmost column on the screen is column 1.
This sequence always unsets the pending wrap state.
If [origin mode](#TODO) is **NOT** set, the cursor is moved exactly to the
row and column specified by `y` and `x`. The maximum value for `y` is the
bottom row of the screen and the maximum value for `x` is the rightmost
column of the screen.
If [origin mode](#TODO) is set, the cursor position is set relative
to the top-left corner of the scroll region. `y = 1` corresponds to
the [top margin](#TODO) and `x = 1` corresponds to the [left margin](#TODO).
The maximum value for `y` is the [bottom margin](#TODO) and the maximum
value for `x` is the [right margin](#TODO).
When origin mode is set, it is impossible set a cursor position using
this sequence outside the boundaries of the scroll region.
## Validation
### CUP V-1: Normal Usage
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[2;3H"
printf "A"
```
```
|__________|
|__Ac______|
```
### CUP V-2: Off the Screen
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[500;500H"
printf "A"
```
```
|__________|
|__________|
|_________Ac
```
### CUP V-3: Relative to Origin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[1;1H" # move to top-left
printf "X"
```
```
|__________|
|X_________|
```
### CUP V-4: Relative to Origin with Left/Right Margins
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[1;1H" # move to top-left
printf "X"
```
```
|__________|
|__X_______|
```
### CUP V-5: Limits with Scroll Region and Origin Mode
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[2;3r" # scroll region top/bottom
printf "\033[?6h" # origin mode
printf "\033[500;500H" # move to top-left
printf "X"
```
```
|__________|
|__________|
|____X_____|
```
### CUP V-6: Pending Wrap is Unset
```bash
cols=$(tput cols)
printf "\033[${cols}G" # move to last column
printf "A" # set pending wrap state
printf "\033[1;1H"
printf "X"
```
```
|Xc_______X|
```

View File

@ -1,78 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Cursor Up (CUU)
<VTSequence sequence={["CSI", "Pn", "A"]} />
Move the cursor `n` cells up.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
This sequence always unsets the pending wrap state.
If the current cursor position is at or below the [top margin](#TODO),
the highest point the cursor can move is the top margin. If the current
cursor position is above the top margin, the highest point the cursor
can move is the first row.
## Validation
### CUU V-1: Cursor Up
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[3;1H"
printf "A"
printf "\033[2A" # cursor up
printf "X"
```
```
|_Xc_______|
|__________|
|A_________|
```
### CUU V-2: Cursor Up Below Top Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\n\n\n\n" # screen is 4 high
printf "\033[2;4r" # set scrolling region
printf "\033[3;1H"
printf "A"
printf "\033[5A" # cursor up
printf "X"
```
```
|__________|
|_Xc_______|
|A_________|
|__________|
```
### CUU V-3: Cursor Up Above Top Margin
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\n\n\n\n\n" # screen is 5 high
printf "\033[3;5r" # set scrolling region
printf "\033[3;1H"
printf "A"
printf "\033[2;1H" # move above region
printf "\033[5A" # cursor up
printf "X"
```
```
|Xc________|
|__________|
|A_________|
|__________|
|__________|
```

View File

@ -1,106 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Delete Character (DCH)
<VTSequence sequence={["CSI", "Pn", "P"]} />
Deletes `n` characters at the current cursor position and shifts existing
cell contents left.
The parameter `n` must be an integer greater than or equal to 1. If `n` is less than
or equal to 0, adjust `n` to be 1. If `n` is omitted, `n` defaults to 1.
If the current cursor position is outside of the current scroll region,
this sequence does nothing. The cursor is outside of the current scroll
region if it is left of the [left margin](#TODO), or right of the
[right margin](#TODO).
This sequence unsets the pending wrap state. This sequence does _not_ unset
the pending wrap state if the cursor position is outside of the current
scroll region. This has to be called out explicitly because this behavior
differs from [Insert Character (ICH)](/vt/ich).
Only cells within the scroll region are deleted or shifted. Cells to the
right of the right margin are unmodified.
The blank cells inserted from the right margin are blank with the background
color colored according to the current SGR state.
If a multi-cell character (such as "橋") is shifted so that the cell is split
in half, the multi-cell character can either be clipped or erased. Typical
behavior is to clip at the right edge of the screen and erase at a right
margin, but either behavior is acceptable.
## Validation
### DCH V-1: Simple Delete Character
```bash
printf "ABC123"
printf "\033[3G"
printf "\033[2P"
```
```
|AB23____|
```
### DCH V-2: SGR State
```bash
printf "ABC123"
printf "\033[3G"
printf "\033[41m"
printf "\033[2P"
```
```
|AB23____|
```
The two rightmost cells should have a red background color.
### DCH V-3: Outside Left/Right Scroll Region
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC123"
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[2G"
printf "\033[P"
```
```
|ABC123__|
```
### DCH V-4: Inside Left/Right Scroll Region
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "ABC123"
printf "\033[?69h" # enable left/right margins
printf "\033[3;5s" # scroll region left/right
printf "\033[4G"
printf "\033[P"
```
```
|ABC2_3__|
```
### DCH V-5: Split Wide Character
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "A橋123"
printf "\033[3G"
printf "\033[P"
```
```
|A_123_____|
```

View File

@ -1,45 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Screen Alignment Test (DECALN)
<VTSequence sequence={["ESC", "#", "8"]} />
Reset margins, move cursor to the top left, and fill the screen with `E`.
Reset the top, bottom, left, and right margins and unset [origin mode](#TODO).
The cursor is moved to the top-left corner of the screen.
All stylistic SGR attributes are unset, such as bold, blink, etc.
SGR foreground and background colors are preserved.
The [protected attribute](#TODO) is not unset.
The entire screen is filled with the character `E`. The letter `E` ignores
the current SGR settings and is written with no styling.
## Validation
### DECALN V-1: Simple Usage
```bash
printf "\033#8"
```
```
|EEEEEEEE|
|EEEEEEEE|
|EEEEEEEE|
```
### DECALN V-2: Reset Margins
```bash
printf "\033[2;3r" # scroll region top/bottom
printf "\033#8"
printf "\033[T"
```
```
|c_______|
|EEEEEEEE|
|EEEEEEEE|
```

View File

@ -1,7 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Keypad Application Mode (DECKPAM)
<VTSequence sequence={["ESC", "="]} />
Sets keypad application mode.

View File

@ -1,7 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Keypad Numeric Mode (DECKPNM)
<VTSequence sequence={["ESC", ">"]} />
Sets keypad numeric mode.

View File

@ -1,14 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Restore Cursor (DECRC)
<VTSequence sequence={["ESC", "8"]} />
Restore the cursor-related state saved via [Save Cursor (DECSC)](/vt/decsc).
If a cursor was never previously saved, this sets all the typically saved
values to their default values.
## Validation
Validation is shared with [Save Cursor (DECSC)](/vt/decsc).

View File

@ -1,83 +0,0 @@
import VTSequence from "@/components/VTSequence";
# Save Cursor (DECSC)
<VTSequence sequence={["ESC", "7"]} />
Save various cursor-related state that can be restored with
[Restore Cursor (DECRC)](/vt/decrc).
The following attributes are saved:
- Cursor row and column in absolute screen coordinates
- Character sets
- Pending wrap state
- SGR attributes
- [Origin mode (DEC Mode 6)](/vt/modes/origin)
Only one cursor can be saved at any time. If save cursor is repeated, the
previously save cursor is overwritten.
Primary and alternate screens have separate saved cursor state. A cursor
saved on the primary screen is inaccessible from the alternate screen
and vice versa.
## Validation
### SC V-1: Cursor Position
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[1;5H"
printf "A"
printf "\0337" # Save Cursor
printf "\033[1;1H"
printf "B"
printf "\0338" # Restore Cursor
printf "X"
```
```
|B___AX____|
```
### SC V-2: Pending Wrap State
```bash
cols=$(tput cols)
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[${cols}G"
printf "A"
printf "\0337" # Save Cursor
printf "\033[1;1H"
printf "B"
printf "\0338" # Restore Cursor
printf "X"
```
```
|B________A|
|X_________|
```
### SC V-3: SGR Attributes
```bash
printf "\033[1;1H" # move to top-left
printf "\033[0J" # clear screen
printf "\033[1;4;33;44m"
printf "A"
printf "\0337" # Save Cursor
printf "\033[0m"
printf "B"
printf "\0338" # Restore Cursor
printf "X"
```
```
|AX________|
```
The "A" and "X" should have identical styling.

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