Merge branch 'ghostty-org:main' into alt-keybindings-copy-and-paste
383
.github/workflows/release-tag.yml
vendored
Normal 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: ./
|
38
.github/workflows/release-tip.yml
vendored
@ -111,7 +111,7 @@ jobs:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
- 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
|
||||
run: |
|
||||
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
|
||||
@ -239,7 +239,18 @@ jobs:
|
||||
# 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: "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:
|
||||
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
|
||||
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
|
||||
@ -250,22 +261,18 @@ jobs:
|
||||
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"
|
||||
|
||||
# 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.
|
||||
# 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 app"
|
||||
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
|
||||
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.
|
||||
# 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
|
||||
@ -283,7 +290,9 @@ jobs:
|
||||
prerelease: true
|
||||
tag_name: tip
|
||||
target_commitish: ${{ github.sha }}
|
||||
files: ghostty-macos-universal.zip
|
||||
files: |
|
||||
ghostty-macos-universal.zip
|
||||
Ghostty.dmg
|
||||
token: ${{ secrets.GH_RELEASE_TOKEN }}
|
||||
|
||||
# Create our appcast for Sparkle
|
||||
@ -292,8 +301,8 @@ jobs:
|
||||
SPARKLE_KEY: ${{ secrets.PROD_MACOS_SPARKLE_KEY }}
|
||||
run: |
|
||||
echo $SPARKLE_KEY > signing.key
|
||||
sign_update -f signing.key ghostty-macos-universal.zip > sign_update.txt
|
||||
curl -L https://tip.files.ghostty.dev/appcast.xml > appcast.xml
|
||||
sign_update -f signing.key Ghostty.dmg > sign_update.txt
|
||||
curl -L https://tip.files.ghostty.org/appcast.xml > appcast.xml
|
||||
python3 ./dist/macos/update_appcast_tip.py
|
||||
test -f appcast_new.xml
|
||||
|
||||
@ -304,6 +313,7 @@ jobs:
|
||||
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
|
||||
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.dmg blob/${GHOSTTY_COMMIT_LONG}/Ghostty.dmg
|
||||
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
|
22
PACKAGING.md
@ -14,17 +14,29 @@ package Ghostty for distribution.
|
||||
|
||||
## Source Tarballs
|
||||
|
||||
Source tarballs with stable checksums are available on the
|
||||
[GitHub releases page](https://github.com/ghostty-org/ghostty/releases).
|
||||
Use the `ghostty-source.tar.gz` asset and _not the GitHub auto-generated
|
||||
source tarball_.
|
||||
Source tarballs with stable checksums are available for tagged releases
|
||||
at `release.files.ghostty.org` in the following URL format where
|
||||
`VERSION` is the version number with no prefix such as `1.0.0`:
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
**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](https://ziglang.org) is required to build Ghostty. Prior to Zig 1.0,
|
||||
|
628
README.md
@ -9,29 +9,29 @@
|
||||
<br />
|
||||
<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>
|
||||
</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>
|
||||
|
||||
## About
|
||||
|
||||
Ghostty is a cross-platform, GPU-accelerated terminal emulator that aims 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.
|
||||
Ghostty is a terminal emulator that differentiates itself by being
|
||||
fast, feature-rich, and native. While there are many excellent terminal
|
||||
emulators available, they all force you to choose between speed,
|
||||
features, or native UIs. Ghostty provides all three.
|
||||
|
||||
There are a number of excellent terminal emulator options that exist
|
||||
today. The unique goal of Ghostty is to have a platform for experimenting
|
||||
with modern, optional, non-standards-compliant features to enhance the
|
||||
capabilities of CLI applications. We aim to be the best in this category,
|
||||
and competitive in the rest.
|
||||
In all categories, I am not trying to claim that Ghostty is the
|
||||
best (i.e. the fastest, most feature-rich, or most native). But
|
||||
Ghostty is competitive in all three categories and Ghostty
|
||||
doesn't make you choose between them.
|
||||
|
||||
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
|
||||
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
|
||||
as a drop-in replacement for your existing terminal emulator.
|
||||
|
||||
**Project Status:** Ghostty is still in beta but implements most of the
|
||||
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).
|
||||
For more details, see [About Ghostty](https://ghostty.org/docs/about).
|
||||
|
||||
## Download
|
||||
|
||||
| Platform / Package | Links | Notes |
|
||||
| ------------------ | -------------------------------------------------------------------------- | -------------------------- |
|
||||
| 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) |
|
||||
See the [download page](https://ghostty.org/download) on the Ghostty website.
|
||||
|
||||
### Configuration
|
||||
## Documentation
|
||||
|
||||
To configure Ghostty, you must use a configuration file. GUI-based configuration
|
||||
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.
|
||||
See the [documentation](https://ghostty.org/docs) on the Ghostty website.
|
||||
|
||||
## Roadmap and Status
|
||||
|
||||
@ -425,8 +108,6 @@ feature rich.
|
||||
|
||||
> [!NOTE]
|
||||
> 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
|
||||
|
||||
@ -506,40 +187,9 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
|
||||
|
||||
## Developing Ghostty
|
||||
|
||||
To build Ghostty, you need [Zig 0.13](https://ziglang.org/) installed.
|
||||
|
||||
On Linux, you may need to install additional dependencies. See
|
||||
[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`.
|
||||
See the documentation on the Ghostty website for
|
||||
[building Ghostty from source](http://ghostty.org/docs/install/build).
|
||||
For development, omit the `-Doptimize` flag to build a debug build.
|
||||
|
||||
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
|
||||
@ -556,189 +206,6 @@ Other useful commands:
|
||||
in the current running terminal emulator so if you want to check the
|
||||
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
|
||||
|
||||
#### 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).
|
||||
|
||||
### 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
|
||||
|
||||
The Nix package depends on a [fixed-output
|
||||
|
@ -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.
|
90
build.zig
@ -13,6 +13,7 @@ const config_vim = @import("src/config/vim.zig");
|
||||
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
||||
const fish_completions = @import("src/build/fish_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 BuildConfig = build_config.BuildConfig;
|
||||
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;
|
||||
};
|
||||
|
||||
const emit_webdata = b.option(
|
||||
bool,
|
||||
"emit-webdata",
|
||||
"Build the website data for the website.",
|
||||
) orelse false;
|
||||
|
||||
const emit_xcframework = b.option(
|
||||
bool,
|
||||
"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
|
||||
{
|
||||
const wf = b.addWriteFiles();
|
||||
@ -575,6 +594,11 @@ pub fn build(b: *std.Build) !void {
|
||||
b.getInstallStep().dependOn(&b.addInstallFile(placeholder, path).step);
|
||||
}
|
||||
|
||||
// Web data
|
||||
if (emit_webdata) {
|
||||
try buildWebData(b, config);
|
||||
}
|
||||
|
||||
// App (Linux)
|
||||
if (target.result.os.tag == .linux and config.app_runtime != .none) {
|
||||
// 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(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
|
@ -22,7 +22,7 @@
|
||||
.hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc",
|
||||
},
|
||||
.ziglyph = .{
|
||||
.url = "https://deps.files.ghostty.dev/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||
},
|
||||
|
||||
@ -49,8 +49,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5fd82e34a349e36a5b3422d8225c4e044c8b3b4b.tar.gz",
|
||||
.hash = "122083713c189f1ceab516efd494123386f3a29132a68a6896b651319a8c57d747e4",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/d6c42066b3045292e0b1154ad84ff22d6863ebf7.tar.gz",
|
||||
.hash = "12204358b2848ffd993d3425055bff0a5ba9b1b60bead763a6dea0517965d7290a6c",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
|
105
dist/macos/update_appcast_tag.py
vendored
Normal 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")
|
4
dist/macos/update_appcast_tip.py
vendored
@ -80,7 +80,7 @@ elem.text = build
|
||||
elem = ET.SubElement(item, "sparkle:shortVersionString")
|
||||
elem.text = f"{commit} ({now.strftime('%Y-%m-%d')})"
|
||||
elem = ET.SubElement(item, "sparkle:minimumSystemVersion")
|
||||
elem.text = "12.0.0"
|
||||
elem.text = "13.0.0"
|
||||
elem = ET.SubElement(item, "description")
|
||||
elem.text = f"""
|
||||
<p>
|
||||
@ -94,7 +94,7 @@ commit history <a href="{repo}">on GitHub</a> for all changes.
|
||||
</p>
|
||||
"""
|
||||
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")
|
||||
for key, value in attrs.items():
|
||||
elem.set(key, value)
|
||||
|
@ -333,6 +333,21 @@ typedef struct {
|
||||
uint32_t cell_height_px;
|
||||
} 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
|
||||
typedef enum {
|
||||
GHOSTTY_TARGET_APP,
|
||||
|
6
macos/Assets.xcassets/Custom Icon/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseAluminum.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "base.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseAluminum.imageset/base.png
vendored
Normal file
After Width: | Height: | Size: 145 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseBeige.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "beige.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseBeige.imageset/beige.png
vendored
Normal file
After Width: | Height: | Size: 349 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBaseChrome.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "chrome.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBaseChrome.imageset/chrome.png
vendored
Normal file
After Width: | Height: | Size: 124 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconBasePlastic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "plastic.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconBasePlastic.imageset/plastic.png
vendored
Normal file
After Width: | Height: | Size: 97 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "crt-effect.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconCRT.imageset/crt-effect.png
vendored
Normal file
After Width: | Height: | Size: 85 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ghosty.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconGhost.imageset/ghosty.png
vendored
Normal file
After Width: | Height: | Size: 63 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "gloss.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconGloss.imageset/gloss.png
vendored
Normal file
After Width: | Height: | Size: 3.4 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "screen-dark.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconScreen.imageset/screen-dark.png
vendored
Normal file
After Width: | Height: | Size: 8.2 KiB |
15
macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "screen-mask.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
BIN
macos/Assets.xcassets/Custom Icon/CustomIconScreenMask.imageset/screen-mask.png
vendored
Normal file
After Width: | Height: | Size: 4.3 KiB |
@ -42,6 +42,8 @@
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GhosttyBuild</key>
|
||||
<string></string>
|
||||
<key>GhosttyCommit</key>
|
||||
<string></string>
|
||||
<key>LSEnvironment</key>
|
||||
|
@ -39,6 +39,10 @@
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.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 */; };
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.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 */; };
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -122,6 +128,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -180,6 +190,8 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -233,6 +245,7 @@
|
||||
A57D79252C9C8782001D522E /* Secure Input */,
|
||||
A534263E2A7DCC5800EBB7A2 /* Settings */,
|
||||
A51BFC1C2B2FB5AB00E92F16 /* About */,
|
||||
A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */,
|
||||
A51BFC292B30F69F00E92F16 /* Update */,
|
||||
);
|
||||
path = Features;
|
||||
@ -252,6 +265,7 @@
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
@ -303,6 +317,16 @@
|
||||
path = macOS;
|
||||
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 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -371,12 +395,14 @@
|
||||
A5A1F8862A489D7400D1E8BC /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */,
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
A5985CE52C33060F00C57AD3 /* man */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -539,6 +565,7 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */,
|
||||
A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */,
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */,
|
||||
@ -547,6 +574,7 @@
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */,
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */,
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
|
||||
@ -572,8 +600,10 @@
|
||||
files = (
|
||||
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
@ -612,6 +642,8 @@
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||
|
@ -98,6 +98,13 @@ class AppDelegate: NSObject,
|
||||
/// The observer for the app appearance.
|
||||
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() {
|
||||
terminalManager = TerminalManager(ghostty)
|
||||
updaterController = SPUStandardUpdaterController(
|
||||
@ -519,6 +526,22 @@ class AppDelegate: NSObject,
|
||||
} else {
|
||||
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.
|
||||
|
@ -4,6 +4,7 @@ struct AboutView: View {
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
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.
|
||||
private var build: String? { Bundle.main.infoDictionary?["CFBundleVersion"] as? String }
|
||||
@ -43,7 +44,7 @@ struct AboutView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Image("AppIconImage")
|
||||
ghosttyIconImage()
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 128)
|
||||
@ -77,12 +78,16 @@ struct AboutView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if let url = docsURL {
|
||||
Button("Docs") {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
if let url = githubURL {
|
||||
Button("GitHub") {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if let copy = self.copyright {
|
||||
|
@ -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,
|
||||
])
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -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()!)
|
||||
}
|
||||
}
|
@ -3,11 +3,17 @@ import Cocoa
|
||||
|
||||
class UpdaterDelegate: NSObject, SPUUpdaterDelegate {
|
||||
func feedURLString(for updater: SPUUpdater) -> String? {
|
||||
// Eventually w want to support multiple channels. Sparkle itself supports
|
||||
// channels but we probably don't want some appcasts in the same file (i.e.
|
||||
// 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"
|
||||
guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
@ -252,6 +252,46 @@ extension Ghostty {
|
||||
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 {
|
||||
guard let config = self.config else { return false }
|
||||
var v = false;
|
||||
@ -261,9 +301,9 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var backgroundColor: Color {
|
||||
var rgb: UInt32 = 0
|
||||
var color: ghostty_config_color_s = .init();
|
||||
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)
|
||||
return Color(NSColor.windowBackgroundColor)
|
||||
#elseif os(iOS)
|
||||
@ -273,14 +313,10 @@ extension Ghostty {
|
||||
#endif
|
||||
}
|
||||
|
||||
let red = Double(rgb & 0xff)
|
||||
let green = Double((rgb >> 8) & 0xff)
|
||||
let blue = Double((rgb >> 16) & 0xff)
|
||||
|
||||
return Color(
|
||||
red: red / 255,
|
||||
green: green / 255,
|
||||
blue: blue / 255
|
||||
return .init(
|
||||
red: Double(color.r) / 255,
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
@ -311,21 +347,17 @@ extension Ghostty {
|
||||
var unfocusedSplitFill: Color {
|
||||
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"
|
||||
if (!ghostty_config_get(config, &rgb, key, UInt(key.count))) {
|
||||
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
|
||||
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)
|
||||
let green = Double((rgb >> 8) & 0xff)
|
||||
let blue = Double((rgb >> 16) & 0xff)
|
||||
|
||||
return Color(
|
||||
red: red / 255,
|
||||
green: green / 255,
|
||||
blue: blue / 255
|
||||
return .init(
|
||||
red: Double(color.r),
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
@ -408,6 +440,17 @@ extension Ghostty {
|
||||
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 {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
|
@ -195,12 +195,31 @@ 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 MacOSTitlebarProxyIcon: String {
|
||||
case visible
|
||||
case hidden
|
||||
}
|
||||
|
||||
/// Enum for auto-update-channel config option
|
||||
enum AutoUpdateChannel: String {
|
||||
case tip
|
||||
case stable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Surface Notification
|
||||
|
90
macos/Sources/Helpers/NSImage+Extension.swift
Normal 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
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import GhosttyKit
|
||||
|
||||
extension OSColor {
|
||||
var isLightColor: Bool {
|
||||
@ -47,6 +48,37 @@ extension OSColor {
|
||||
#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 {
|
||||
var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
||||
glslang,
|
||||
gtk4,
|
||||
libadwaita,
|
||||
gnome,
|
||||
adwaita-icon-theme,
|
||||
hicolor-icon-theme,
|
||||
harfbuzz,
|
||||
libpng,
|
||||
@ -165,7 +165,7 @@ in
|
||||
# is available (namely icons).
|
||||
|
||||
# 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
|
||||
'')
|
||||
+ (lib.optionalString stdenv.hostPlatform.isDarwin ''
|
||||
|
@ -157,7 +157,7 @@ in
|
||||
chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
|
||||
'';
|
||||
|
||||
outputs = ["out" "terminfo" "shell_integration"];
|
||||
outputs = ["out" "terminfo" "shell_integration" "vim"];
|
||||
|
||||
postInstall = ''
|
||||
terminfo_src=${
|
||||
@ -177,6 +177,8 @@ in
|
||||
mv "$out/share/ghostty/shell-integration" "$shell_integration/shell-integration"
|
||||
ln -sf "$shell_integration/shell-integration" "$out/share/ghostty/shell-integration"
|
||||
echo "$shell_integration" >> "$out/nix-support/propagated-user-env-packages"
|
||||
|
||||
cp -r $out/share/vim/vimfiles "$vim"
|
||||
'';
|
||||
|
||||
postFixup = ''
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-q9UDVryP50HfeeafgnrOd+D6K+cEy33/05K2TB5qiqw="
|
||||
"sha256-vP8f8KQyM4CwKlw7Esmxv1q4ANu8pDXXsnVorgpWCr4="
|
||||
|
@ -3,7 +3,7 @@
|
||||
.version = "2.14.2",
|
||||
.dependencies = .{
|
||||
.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",
|
||||
},
|
||||
|
||||
|
@ -217,6 +217,10 @@ pub const FontOrientation = enum(c_uint) {
|
||||
|
||||
pub const FontTableTag = enum(u32) {
|
||||
svg = c.kCTFontTableSVG,
|
||||
os2 = c.kCTFontTableOS2,
|
||||
head = c.kCTFontTableHead,
|
||||
hhea = c.kCTFontTableHhea,
|
||||
post = c.kCTFontTablePost,
|
||||
_,
|
||||
|
||||
pub fn init(v: *const [4]u8) FontTableTag {
|
||||
|
@ -3298,9 +3298,10 @@ pub fn cursorPosCallback(
|
||||
|
||||
// No mouse point so we don't highlight links
|
||||
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
|
||||
|
@ -23,6 +23,7 @@ const c = @import("c.zig").c;
|
||||
const adwaita = @import("adwaita.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
const version = @import("version.zig");
|
||||
|
||||
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
|
||||
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
|
||||
/// 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
|
||||
/// 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.
|
||||
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.
|
||||
self.tab_overview = if (self.isAdwWindow()) overview: {
|
||||
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.g_signal_connect_data(
|
||||
tab_overview,
|
||||
@ -149,32 +154,25 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
// are decorated or not because we can have a keybind to toggle the
|
||||
// decorations.
|
||||
if (app.config.@"gtk-titlebar") {
|
||||
const header: *c.GtkWidget = if (self.isAdwWindow())
|
||||
@ptrCast(c.adw_header_bar_new())
|
||||
else
|
||||
@ptrCast(c.gtk_header_bar_new());
|
||||
const header = HeaderBar.init(self);
|
||||
|
||||
{
|
||||
const btn = c.gtk_menu_button_new();
|
||||
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_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
if (self.isAdwWindow()) {
|
||||
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);
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
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, "Show Open Tabs");
|
||||
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
|
||||
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn);
|
||||
_ = c.g_object_bind_property(
|
||||
btn,
|
||||
"active",
|
||||
@ -182,16 +180,27 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
"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;
|
||||
},
|
||||
};
|
||||
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
{
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
if (self.isAdwWindow())
|
||||
c.adw_header_bar_pack_end(@ptrCast(header), btn)
|
||||
else
|
||||
c.gtk_header_bar_pack_end(@ptrCast(header), btn);
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
self.header = header;
|
||||
@ -225,9 +234,6 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_box_append(@ptrCast(box), warning_box);
|
||||
}
|
||||
|
||||
// Setup our notebook
|
||||
self.notebook = Notebook.create(self);
|
||||
|
||||
// Setup our toast overlay if we have one
|
||||
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
|
||||
const toast_overlay = c.adw_toast_overlay_new();
|
||||
@ -277,8 +283,10 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
@ -286,9 +294,11 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
|
||||
switch (self.app.config.@"gtk-tabs-location") {
|
||||
// left and right is not supported in libadwaita.
|
||||
// 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);
|
||||
|
||||
@ -322,15 +332,16 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
@ptrCast(@alignCast(toolbar_view)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
} else tab_bar: {
|
||||
switch (self.notebook) {
|
||||
.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
|
||||
// an AdwToolbarView.
|
||||
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
|
||||
switch (app.config.@"gtk-tabs-location") {
|
||||
// left and right is not supported in libadwaita.
|
||||
.top,
|
||||
.left,
|
||||
.right,
|
||||
@ -343,12 +354,11 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
@ptrCast(box),
|
||||
@ptrCast(@alignCast(tab_bar)),
|
||||
),
|
||||
.hidden => unreachable,
|
||||
}
|
||||
c.adw_tab_bar_set_view(tab_bar, tab_view);
|
||||
|
||||
if (!app.config.@"gtk-wide-tabs") {
|
||||
c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||
}
|
||||
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
|
||||
},
|
||||
|
||||
.gtk_notebook => {},
|
||||
@ -356,7 +366,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// The box is our main child
|
||||
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
|
||||
@ -507,7 +517,7 @@ pub fn toggleWindowDecorations(self: *Window) void {
|
||||
// and hides it with decorations, but libadwaita doesn't. This makes it
|
||||
// explicit.
|
||||
if (self.header) |v| {
|
||||
const widget: *c.GtkWidget = @alignCast(@ptrCast(v));
|
||||
const widget = v.asWidget();
|
||||
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
|
||||
}
|
||||
}
|
||||
|
69
src/apprt/gtk/headerbar.zig
Normal 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,
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
@ -29,7 +29,7 @@ pub const Notebook = union(enum) {
|
||||
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
|
||||
const notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
|
||||
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,
|
||||
.left => c.GTK_POS_LEFT,
|
||||
.right => c.GTK_POS_RIGHT,
|
||||
|
338
src/build/bash_completions.zig
Normal 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
|
||||
\\
|
||||
);
|
||||
}
|
@ -117,7 +117,17 @@ fn writeFishCompletions(writer: anytype) !void {
|
||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll(" -a \"");
|
||||
for (info.opts, 0..) |f, i| {
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@ -125,6 +135,9 @@ fn writeFishCompletions(writer: anytype) !void {
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
46
src/build/webgen/main_actions.zig
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
131
src/build/webgen/main_config.zig
Normal 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"),
|
||||
};
|
||||
}
|
@ -9,7 +9,7 @@ pub const zsh_completions = comptimeGenerateZshCompletions();
|
||||
|
||||
fn comptimeGenerateZshCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(19000);
|
||||
@setEvalBranchQuota(50000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeZshCompletions(&counter.writer());
|
||||
|
||||
@ -175,12 +175,29 @@ fn writeZshCompletions(writer: anytype) !void {
|
||||
.Bool => try writer.writeAll("(true false)"),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll("(");
|
||||
for (info.opts, 0..) |f, i| {
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
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 => {
|
||||
if (std.mem.eql(u8, "config-file", opt.name)) {
|
||||
try writer.writeAll("_files");
|
||||
|
@ -58,6 +58,15 @@ pub const BuildConfig = struct {
|
||||
"{}",
|
||||
.{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
|
||||
@ -82,6 +91,9 @@ pub const BuildConfig = struct {
|
||||
pub const version = options.app_version;
|
||||
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.
|
||||
pub const mode_string = mode: {
|
||||
const m = @tagName(builtin.mode);
|
||||
@ -172,9 +184,20 @@ pub const ExeEntrypoint = enum {
|
||||
helpgen,
|
||||
mdgen_ghostty_1,
|
||||
mdgen_ghostty_5,
|
||||
webgen_config,
|
||||
webgen_actions,
|
||||
bench_parser,
|
||||
bench_stream,
|
||||
bench_codepoint_width,
|
||||
bench_grapheme_break,
|
||||
bench_page_init,
|
||||
};
|
||||
|
||||
/// The release channel for the build.
|
||||
pub const ReleaseChannel = enum {
|
||||
/// Unstable builds on every commit.
|
||||
tip,
|
||||
|
||||
/// Stable tagged releases.
|
||||
stable,
|
||||
};
|
||||
|
@ -25,6 +25,10 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
try stdout.print("Ghostty {s}\n\n", .{build_config.version_string});
|
||||
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(" - Zig version: {s}\n", .{builtin.zig_version_string});
|
||||
try stdout.print(" - build mode : {}\n", .{builtin.mode});
|
||||
|
@ -12,6 +12,7 @@ const Config = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_config = @import("../build_config.zig");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
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
|
||||
/// 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
|
||||
/// in the `bold-italic` style. If you want to disable `bold-italic`, you must
|
||||
/// 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.
|
||||
@"adjust-cell-width": ?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,
|
||||
/// 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,
|
||||
/// Thickness in pixels of the underline.
|
||||
@"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,
|
||||
/// Thickness in pixels of the strikethrough.
|
||||
@"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,
|
||||
/// 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 default value is `unicode` which uses the Unicode standard to determine
|
||||
@ -602,6 +619,16 @@ command: ?[]const u8 = null,
|
||||
/// process will exit when the command exits. Additionally, the
|
||||
/// `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,
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
/// will update for all windows. If it is unset, the next title change escape
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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 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
|
||||
/// 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,
|
||||
/// 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
|
||||
/// set later will overwrite the keybind set earlier. In this case, the
|
||||
/// `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
|
||||
/// 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.
|
||||
/// When a `global:` keybind is specified and Ghostty is launched or reloaded,
|
||||
/// 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
|
||||
/// 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.
|
||||
///
|
||||
/// Changing this configuration in your configuration and reloading will
|
||||
@ -1192,7 +1224,7 @@ keybind: Keybinds = .{},
|
||||
@"clipboard-paste-bracketed-safe": bool = true,
|
||||
|
||||
/// 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
|
||||
/// protocols will be disabled.
|
||||
///
|
||||
@ -1445,7 +1477,7 @@ keybind: Keybinds = .{},
|
||||
/// Custom shaders to run after the default shaders. This is a file path
|
||||
/// 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
|
||||
/// unset this configuration to disable the shader.
|
||||
///
|
||||
@ -1643,6 +1675,73 @@ keybind: Keybinds = .{},
|
||||
/// you may want to disable it.
|
||||
@"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.
|
||||
///
|
||||
/// 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
|
||||
/// 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.
|
||||
@"linux-cgroup-hard-fail": bool = false,
|
||||
|
||||
@ -1727,10 +1826,17 @@ keybind: Keybinds = .{},
|
||||
@"gtk-titlebar": bool = true,
|
||||
|
||||
/// 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
|
||||
/// 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,
|
||||
|
||||
/// 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.
|
||||
@"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.
|
||||
_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
|
||||
@ -3082,6 +3216,9 @@ pub fn parseManuallyHook(
|
||||
self.@"gtk-single-instance" = .false;
|
||||
self.@"quit-after-last-window-closed" = true;
|
||||
self.@"quit-after-last-window-closed-delay" = null;
|
||||
if (self.@"shell-integration" != .none) {
|
||||
self.@"shell-integration" = .detect;
|
||||
}
|
||||
|
||||
// Do not continue, we consumed everything.
|
||||
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
|
||||
/// works by setting it to a C integer.
|
||||
pub const Color = packed struct(u24) {
|
||||
pub const Color = struct {
|
||||
r: u8,
|
||||
g: 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
|
||||
pub fn toTerminalRGB(self: Color) terminal.color.RGB {
|
||||
return .{ .r = self.r, .g = self.g, .b = self.b };
|
||||
@ -3510,12 +3658,17 @@ pub const Color = packed struct(u24) {
|
||||
var buf: [128]u8 = undefined;
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
std.fmt.bufPrint(
|
||||
&buf,
|
||||
try self.formatBuf(&buf),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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 return error.OutOfMemory,
|
||||
);
|
||||
) catch error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// fromHex parses a color from a hex value such as #RRGGBB. The "#"
|
||||
@ -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
|
||||
/// used by many terminal applications.
|
||||
pub const Palette = struct {
|
||||
@ -4850,11 +5130,29 @@ pub const MacTitlebarStyle = enum {
|
||||
};
|
||||
|
||||
/// See macos-titlebar-proxy-icon
|
||||
pub const MacTitlebarProxyIcon: type = enum {
|
||||
pub const MacTitlebarProxyIcon = enum {
|
||||
visible,
|
||||
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
|
||||
pub const GtkSingleInstance = enum {
|
||||
desktop,
|
||||
@ -4868,6 +5166,7 @@ pub const GtkTabsLocation = enum {
|
||||
bottom,
|
||||
left,
|
||||
right,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// See adw-toolbar-style
|
||||
@ -5189,9 +5488,8 @@ pub const Duration = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn c_get(self: Duration, ptr_raw: *anyopaque) void {
|
||||
const ptr: *usize = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = @intCast(self.asMilliseconds());
|
||||
pub fn cval(self: Duration) usize {
|
||||
return @intCast(self.asMilliseconds());
|
||||
}
|
||||
|
||||
/// Convenience function to convert to milliseconds since many OS and
|
||||
|
@ -60,9 +60,11 @@ fn getValue(ptr_raw: *anyopaque, value: anytype) bool {
|
||||
},
|
||||
|
||||
.Struct => |info| {
|
||||
// If the struct implements c_get then we call that
|
||||
if (@hasDecl(@TypeOf(value), "c_get")) {
|
||||
value.c_get(ptr_raw);
|
||||
// If the struct implements cval then we call then.
|
||||
if (@hasDecl(T, "cval")) {
|
||||
const PtrT = @typeInfo(@TypeOf(T.cval)).Fn.return_type.?;
|
||||
const ptr: *PtrT = @ptrCast(@alignCast(ptr_raw));
|
||||
ptr.* = value.cval();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -100,7 +102,7 @@ fn fieldByKey(self: *const Config, comptime k: Key) Value(k) {
|
||||
return @field(self, field.name);
|
||||
}
|
||||
|
||||
test "u8" {
|
||||
test "c_get: u8" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -113,7 +115,7 @@ test "u8" {
|
||||
try testing.expectEqual(@as(f32, 24), cval);
|
||||
}
|
||||
|
||||
test "enum" {
|
||||
test "c_get: enum" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -128,7 +130,7 @@ test "enum" {
|
||||
try testing.expectEqualStrings("dark", str);
|
||||
}
|
||||
|
||||
test "color" {
|
||||
test "c_get: color" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
@ -136,12 +138,14 @@ test "color" {
|
||||
defer c.deinit();
|
||||
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.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 alloc = testing.allocator;
|
||||
|
||||
@ -150,14 +154,16 @@ test "optional" {
|
||||
|
||||
{
|
||||
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)));
|
||||
}
|
||||
|
||||
{
|
||||
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.expectEqual(@as(c_uint, 255), cval);
|
||||
try testing.expectEqual(255, cval.r);
|
||||
try testing.expectEqual(0, cval.g);
|
||||
try testing.expectEqual(0, cval.b);
|
||||
}
|
||||
}
|
||||
|
@ -427,7 +427,10 @@ pub const DerivedConfig = struct {
|
||||
@"adjust-underline-thickness": ?Metrics.Modifier,
|
||||
@"adjust-strikethrough-position": ?Metrics.Modifier,
|
||||
@"adjust-strikethrough-thickness": ?Metrics.Modifier,
|
||||
@"adjust-overline-position": ?Metrics.Modifier,
|
||||
@"adjust-overline-thickness": ?Metrics.Modifier,
|
||||
@"adjust-cursor-thickness": ?Metrics.Modifier,
|
||||
@"adjust-box-thickness": ?Metrics.Modifier,
|
||||
@"freetype-load-flags": font.face.FreetypeLoadFlags,
|
||||
|
||||
/// 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-strikethrough-position" = config.@"adjust-strikethrough-position",
|
||||
.@"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-box-thickness" = config.@"adjust-box-thickness",
|
||||
.@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {},
|
||||
|
||||
// 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-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-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-box-thickness") |m| try set.put(alloc, .box_thickness, m);
|
||||
break :set set;
|
||||
};
|
||||
|
||||
|
@ -292,31 +292,45 @@ pub const Face = struct {
|
||||
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};
|
||||
|
||||
// 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
|
||||
// because our coordinates in 3D space are (0, 0) bottom left with
|
||||
// +y being up.
|
||||
const render_x = @floor(rect.origin.x);
|
||||
const render_y = @ceil(-rect.origin.y);
|
||||
// If we're rendering a synthetic bold then we will gain 50% of
|
||||
// the line width on every edge, which means we should increase
|
||||
// our width and height by the line width and subtract half from
|
||||
// our origin points.
|
||||
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
|
||||
// is rendered. The ascent can be calculated by adding the full
|
||||
// glyph height to the origin.
|
||||
const glyph_ascent = @ceil(rect.size.height + rect.origin.y);
|
||||
// We make an assumption that font smoothing ("thicken")
|
||||
// adds no more than 1 extra pixel to any edge. We don't
|
||||
// add extra size if it's a sbix color font though, since
|
||||
// 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
|
||||
// ascent plus the descent because both are rounded elements that
|
||||
// will make us more accurate.
|
||||
const height: u32 = @intFromFloat(glyph_ascent + render_y);
|
||||
|
||||
// The glyph width is our advertised bounding with plus the rounding
|
||||
// difference from our rendering X.
|
||||
const width: u32 = @intFromFloat(@ceil(rect.size.width + (rect.origin.x - render_x)));
|
||||
// We compute the minimum and maximum x and y values.
|
||||
// We round our min points down and max points up.
|
||||
const x0: i32, const x1: i32, const y0: i32, const y1: i32 = .{
|
||||
@intFromFloat(@floor(rect.origin.x)),
|
||||
@intFromFloat(@ceil(rect.origin.x) + @ceil(rect.size.width)),
|
||||
@intFromFloat(@floor(rect.origin.y)),
|
||||
@intFromFloat(@ceil(rect.origin.y) + @ceil(rect.size.height)),
|
||||
};
|
||||
|
||||
// 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 (width == 0 or height == 0) return font.Glyph{
|
||||
if (x1 <= x0 or y1 <= y0) return font.Glyph{
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.offset_x = 0,
|
||||
@ -326,25 +340,8 @@ pub const Face = struct {
|
||||
.advance_x = 0,
|
||||
};
|
||||
|
||||
// Additional padding we need to add to the bitmap context itself
|
||||
// due to the glyph being larger than standard.
|
||||
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);
|
||||
const width: u32 = @intCast(x1 - x0);
|
||||
const height: u32 = @intCast(y1 - y0);
|
||||
|
||||
// Settings that are specific to if we are rendering text or emoji.
|
||||
const color: struct {
|
||||
@ -380,17 +377,17 @@ pub const Face = struct {
|
||||
// usually stabilizes pretty quickly and is very infrequent so I think
|
||||
// the allocation overhead is acceptable compared to the cost of
|
||||
// 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);
|
||||
@memset(buf, 0);
|
||||
|
||||
const context = macos.graphics.BitmapContext.context;
|
||||
const ctx = try macos.graphics.BitmapContext.create(
|
||||
buf,
|
||||
padded_width,
|
||||
padded_height,
|
||||
width,
|
||||
height,
|
||||
8,
|
||||
padded_width * color.depth,
|
||||
width * color.depth,
|
||||
color.space,
|
||||
color.context_opts,
|
||||
);
|
||||
@ -405,8 +402,8 @@ pub const Face = struct {
|
||||
context.fillRect(ctx, .{
|
||||
.origin = .{ .x = 0, .y = 0 },
|
||||
.size = .{
|
||||
.width = @floatFromInt(padded_width),
|
||||
.height = @floatFromInt(padded_height),
|
||||
.width = @floatFromInt(width),
|
||||
.height = @floatFromInt(height),
|
||||
},
|
||||
});
|
||||
|
||||
@ -437,67 +434,57 @@ pub const Face = struct {
|
||||
|
||||
// We want to render the glyphs at (0,0), but the glyphs themselves
|
||||
// are offset by bearings, so we have to undo those bearings in order
|
||||
// to get them to 0,0. We also add the padding so that they render
|
||||
// slightly off the edge of the bitmap.
|
||||
const padding_ctx_f64: f64 = @floatFromInt(padding_ctx);
|
||||
// to get them to 0,0.
|
||||
self.font.drawGlyphs(&glyphs, &.{
|
||||
.{
|
||||
.x = -1 * (render_x - padding_ctx_f64),
|
||||
.y = render_y + padding_ctx_f64,
|
||||
.x = @floatFromInt(-x0),
|
||||
.y = @floatFromInt(-y0),
|
||||
},
|
||||
}, ctx);
|
||||
|
||||
const region = region: {
|
||||
// We need to add a 1px padding to the font so that we don't
|
||||
// get fuzzy issues when blending textures.
|
||||
const padding = 1;
|
||||
|
||||
// Get the full padded region
|
||||
// We reserve a region that's 1px wider and taller than we need
|
||||
// in order to create a 1px separation between adjacent glyphs
|
||||
// to prevent interpolation with adjacent glyphs while sampling
|
||||
// from the atlas.
|
||||
var region = try atlas.reserve(
|
||||
alloc,
|
||||
padded_width + (padding * 2), // * 2 because left+right
|
||||
padded_height + (padding * 2), // * 2 because top+bottom
|
||||
width + 1,
|
||||
height + 1,
|
||||
);
|
||||
|
||||
// Modify the region so that we remove the padding so that
|
||||
// we write to the non-zero location. The data in an Altlas
|
||||
// is always initialized to zero (Atlas.clear) so we don't
|
||||
// need to worry about zero-ing that.
|
||||
region.x += padding;
|
||||
region.y += padding;
|
||||
region.width -= padding * 2;
|
||||
region.height -= padding * 2;
|
||||
// We adjust the region width and height back down since we
|
||||
// don't need the extra pixel, we just needed to reserve it
|
||||
// so that it isn't used for other glyphs in the future.
|
||||
region.width -= 1;
|
||||
region.height -= 1;
|
||||
break :region region;
|
||||
};
|
||||
atlas.set(region, buf);
|
||||
|
||||
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
|
||||
// ADD here because CoreText y is UP.
|
||||
const baseline_with_offset = baseline_from_bottom + glyph_ascent;
|
||||
|
||||
// Add our context padding we may have created.
|
||||
const baseline_with_padding = baseline_with_offset + padding_ctx_f64;
|
||||
|
||||
break :offset_y @intFromFloat(@ceil(baseline_with_padding));
|
||||
};
|
||||
// This should be the distance from the bottom of
|
||||
// the cell to the top of the glyph's bounding box.
|
||||
//
|
||||
// The calculation is distance from bottom of cell to
|
||||
// baseline plus distance from baseline to top of glyph.
|
||||
const offset_y: i32 = @as(i32, @intCast(metrics.cell_baseline)) + y1;
|
||||
|
||||
// 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: {
|
||||
// Don't forget to apply our context padding if we have one
|
||||
var result: i32 = @intFromFloat(render_x - padding_ctx_f64);
|
||||
var result: i32 = x0;
|
||||
|
||||
// If our cell was resized to be wider then we center our
|
||||
// glyph in the cell.
|
||||
// If our cell was resized then we adjust our glyph's
|
||||
// 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 (original_width < metrics.cell_width) {
|
||||
const diff = (metrics.cell_width - original_width) / 2;
|
||||
result += @intCast(diff);
|
||||
}
|
||||
const before: i32 = @intCast(original_width);
|
||||
const after: i32 = @intCast(metrics.cell_width);
|
||||
// Increase the offset by half of the difference
|
||||
// between the widths to keep things centered.
|
||||
result += @divTrunc(after - before, 2);
|
||||
}
|
||||
|
||||
break :offset_x result;
|
||||
@ -507,21 +494,9 @@ pub const Face = struct {
|
||||
var advances: [glyphs.len]macos.graphics.Size = undefined;
|
||||
_ = 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 .{
|
||||
.width = padded_width,
|
||||
.height = padded_height,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.offset_x = offset_x,
|
||||
.offset_y = offset_y,
|
||||
.atlas_x = region.x,
|
||||
@ -534,8 +509,6 @@ pub const Face = struct {
|
||||
CopyTableError,
|
||||
InvalidHeadTable,
|
||||
InvalidPostTable,
|
||||
InvalidOS2Table,
|
||||
OS2VersionNotSupported,
|
||||
InvalidHheaTable,
|
||||
};
|
||||
|
||||
@ -569,18 +542,16 @@ pub const Face = struct {
|
||||
};
|
||||
};
|
||||
|
||||
// Read the 'OS/2' table out of the font data.
|
||||
const os2: opentype.OS2 = os2: {
|
||||
// Read the 'OS/2' table out of the font data if it's available.
|
||||
const os2_: ?opentype.OS2 = os2: {
|
||||
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();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream => error.InvalidOS2Table,
|
||||
error.OS2VersionNotSupported => error.OS2VersionNotSupported,
|
||||
};
|
||||
log.warn("error parsing OS/2 table: {}", .{err});
|
||||
break :os2 null;
|
||||
};
|
||||
};
|
||||
|
||||
@ -603,18 +574,21 @@ pub const Face = struct {
|
||||
const px_per_unit: f64 = px_per_em / units_per_em;
|
||||
|
||||
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
|
||||
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
||||
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
||||
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
||||
|
||||
if (os2_) |os2| {
|
||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||
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 .{
|
||||
if (os2.fsSelection.use_typo_metrics) 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
|
||||
@ -624,24 +598,17 @@ pub const Face = struct {
|
||||
// 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.lineGap);
|
||||
break :vertical_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 .{
|
||||
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);
|
||||
@ -652,6 +619,15 @@ pub const Face = struct {
|
||||
-win_descent * px_per_unit,
|
||||
0.0,
|
||||
};
|
||||
}
|
||||
|
||||
// If our font has no OS/2 table, then we just
|
||||
// blindly use the metrics from the hhea table.
|
||||
break :vertical_metrics .{
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
};
|
||||
};
|
||||
|
||||
// Some fonts have degenerate 'post' tables where the underline
|
||||
@ -672,30 +648,44 @@ pub const Face = struct {
|
||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||
|
||||
// Similar logic to the underline above.
|
||||
const strikethrough_position, const strikethrough_thickness = st: {
|
||||
const os2 = os2_ orelse break :st .{ null, null };
|
||||
|
||||
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||
|
||||
const strikethrough_position: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||
|
||||
const strikethrough_thickness: ?f64 = if (has_broken_strikethrough)
|
||||
const thick: ?f64 = if (has_broken_strikethrough)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
|
||||
|
||||
// We fall back to whatever CoreText does if
|
||||
// the OS/2 table doesn't specify a cap height.
|
||||
const cap_height: f64 = if (os2.sCapHeight) |sCapHeight|
|
||||
break :st .{ pos, thick };
|
||||
};
|
||||
|
||||
// We fall back to whatever CoreText does if the
|
||||
// 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();
|
||||
ct_font.getCapHeight(),
|
||||
|
||||
// Ditto for ex height.
|
||||
const ex_height: f64 = if (os2.sxHeight) |sxHeight|
|
||||
if (os2.sxHeight) |sxHeight|
|
||||
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
|
||||
else
|
||||
ct_font.getXHeight();
|
||||
ct_font.getXHeight(),
|
||||
};
|
||||
};
|
||||
|
||||
// Cell width is calculated by calculating the widest width of the
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
|
@ -600,7 +600,6 @@ pub const Face = struct {
|
||||
|
||||
const CalcMetricsError = error{
|
||||
CopyTableError,
|
||||
MissingOS2Table,
|
||||
};
|
||||
|
||||
/// Calculate the metrics associated with a face. This is not public because
|
||||
@ -629,21 +628,25 @@ pub const Face = struct {
|
||||
const post = face.getSfntTable(.post) orelse return error.CopyTableError;
|
||||
|
||||
// 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.
|
||||
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 px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
|
||||
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 hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
|
||||
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
||||
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
||||
|
||||
if (os2_) |os2| {
|
||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
||||
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
||||
@ -667,9 +670,6 @@ pub const Face = struct {
|
||||
// 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,
|
||||
@ -694,6 +694,15 @@ pub const Face = struct {
|
||||
-win_descent * px_per_unit,
|
||||
0.0,
|
||||
};
|
||||
}
|
||||
|
||||
// If our font has no OS/2 table, then we just
|
||||
// blindly use the metrics from the hhea table.
|
||||
break :vertical_metrics .{
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
};
|
||||
};
|
||||
|
||||
// Some fonts have degenerate 'post' tables where the underline
|
||||
@ -714,18 +723,24 @@ pub const Face = struct {
|
||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||
|
||||
// Similar logic to the underline above.
|
||||
const strikethrough_position, const strikethrough_thickness = st: {
|
||||
const os2 = os2_ orelse break :st .{ null, null };
|
||||
|
||||
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||
|
||||
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
const pos: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||
|
||||
const strikethrough_thickness = if (has_broken_strikethrough)
|
||||
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
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
@ -754,37 +769,37 @@ pub const Face = struct {
|
||||
break :cell_width max;
|
||||
};
|
||||
|
||||
// The OS/2 table does not include sCapHeight or sxHeight in version 1.
|
||||
const has_os2_height_metrics = os2.version >= 2;
|
||||
|
||||
// We use the cap height specified by the font if it's
|
||||
// available, otherwise we try to measure the `H` glyph.
|
||||
const cap_height: ?f64 = cap_height: {
|
||||
if (has_os2_height_metrics) {
|
||||
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit;
|
||||
// We use the cap and ex heights specified by the font if they're
|
||||
// available, otherwise we try to measure the `H` and `x` glyphs.
|
||||
const cap_height: ?f64, const ex_height: ?f64 = heights: {
|
||||
if (os2_) |os2| {
|
||||
// The OS/2 table does not include these metrics in version 1.
|
||||
if (os2.version >= 2) {
|
||||
break :heights .{
|
||||
@as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit,
|
||||
@as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
break :heights .{
|
||||
cap: {
|
||||
if (face.getCharIndex('H')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
break :cap_height null;
|
||||
};
|
||||
|
||||
// We use the ex height specified by the font if it's
|
||||
// available, otherwise we try to measure the `x` glyph.
|
||||
const ex_height: ?f64 = ex_height: {
|
||||
if (has_os2_height_metrics) {
|
||||
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit;
|
||||
}
|
||||
break :cap null;
|
||||
},
|
||||
ex: {
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
break :ex_height null;
|
||||
break :ex null;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
var result = font.face.Metrics.calc(.{
|
||||
|
@ -2067,7 +2067,7 @@ test "legacy: f1" {
|
||||
{
|
||||
enc.event.key = .f3;
|
||||
const actual = try enc.legacy(&buf);
|
||||
try testing.expectEqualStrings("\x1b[1;5R", actual);
|
||||
try testing.expectEqualStrings("\x1b[13;5~", actual);
|
||||
}
|
||||
|
||||
// F4
|
||||
|
@ -13,7 +13,7 @@ pub const CursorMode = 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"
|
||||
/// 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
|
||||
/// 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"
|
||||
@ -89,7 +89,7 @@ pub const keys = keys: {
|
||||
// Function Keys. todo: f13-f35 but we need to add to input.Key
|
||||
result.set(.f1, pcStyle("\x1b[1;{}P") ++ .{.{ .sequence = "\x1BOP" }});
|
||||
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(.f5, pcStyle("\x1b[15;{}~") ++ .{.{ .sequence = "\x1B[15~" }});
|
||||
result.set(.f6, pcStyle("\x1b[17;{}~") ++ .{.{ .sequence = "\x1B[17~" }});
|
||||
|
@ -7,6 +7,8 @@ const entrypoint = switch (build_config.exe_entrypoint) {
|
||||
.helpgen => @import("helpgen.zig"),
|
||||
.mdgen_ghostty_1 => @import("build/mdgen/main_ghostty_1.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_stream => @import("bench/stream.zig"),
|
||||
.bench_codepoint_width => @import("bench/codepoint-width.zig"),
|
||||
|
@ -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
|
||||
/// on Windows.
|
||||
pub const GetEnvResult = struct {
|
||||
@ -110,3 +127,25 @@ test "appendEnv existing" {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ pub const CFReleaseThread = @import("cf_release_thread.zig");
|
||||
pub const TempDir = @import("TempDir.zig");
|
||||
pub const appendEnv = env.appendEnv;
|
||||
pub const appendEnvAlways = env.appendEnvAlways;
|
||||
pub const prependEnv = env.prependEnv;
|
||||
pub const getenv = env.getenv;
|
||||
pub const setenv = env.setenv;
|
||||
pub const unsetenv = env.unsetenv;
|
||||
|
@ -256,9 +256,7 @@ vertex CellTextVertexOut cell_text_vertex(
|
||||
offset.y = uniforms.cell_size.y - offset.y;
|
||||
|
||||
// If we're constrained then we need to scale the glyph.
|
||||
// We also always constrain colored glyphs since we should have
|
||||
// their scaled cell size exactly correct.
|
||||
if (in.mode == MODE_TEXT_CONSTRAINED || in.mode == MODE_TEXT_COLOR) {
|
||||
if (in.mode == MODE_TEXT_CONSTRAINED) {
|
||||
float max_width = uniforms.cell_size.x * in.constraint_width;
|
||||
if (size.x > max_width) {
|
||||
float new_y = size.y * (max_width / size.x);
|
||||
|
@ -208,10 +208,8 @@ void main() {
|
||||
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
|
||||
|
||||
// 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;
|
||||
if (mode == MODE_FG_CONSTRAINED || mode == MODE_FG_COLOR) {
|
||||
if (mode == MODE_FG_CONSTRAINED) {
|
||||
if (glyph_size.x > cell_size_scaled.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);
|
||||
|
@ -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
|
||||
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`.
|
||||
This also works for older versions of Bash.
|
||||
|
||||
@ -31,6 +28,13 @@ if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
|
||||
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
|
||||
|
||||
For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration`
|
||||
|
@ -58,7 +58,8 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
|
||||
# Arch, Debian, Ubuntu use /etc/bash.bashrc
|
||||
# Fedora uses /etc/bashrc sourced from ~/.bashrc instead of SYS_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; }
|
||||
done
|
||||
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\]'
|
||||
PS2=$PS2'\[\e]133;B\a\]'
|
||||
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
# 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.
|
||||
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"
|
||||
if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then
|
||||
PS1=$PS1'\[\e]133;A;k=s\a\]'
|
||||
fi
|
||||
|
||||
# Cursor
|
||||
@ -151,7 +148,7 @@ function __ghostty_precmd() {
|
||||
|
||||
if test "$_ghostty_executing" != ""; then
|
||||
# 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
|
||||
|
||||
# unfortunately bash provides no hooks to detect cwd changes
|
||||
@ -163,7 +160,7 @@ function __ghostty_precmd() {
|
||||
fi
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
@ -171,7 +168,7 @@ function __ghostty_preexec() {
|
||||
PS0="$_GHOSTTY_SAVE_PS0"
|
||||
PS1="$_GHOSTTY_SAVE_PS1"
|
||||
PS2="$_GHOSTTY_SAVE_PS2"
|
||||
builtin printf "\033]133;C;\007"
|
||||
builtin printf "\e]133;C;\a"
|
||||
_ghostty_executing=1
|
||||
}
|
||||
|
||||
|
@ -1315,8 +1315,13 @@ pub fn clearPrompt(self: *Screen) void {
|
||||
switch (row.semantic_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
|
||||
// may be multi-line.
|
||||
.input => found = p,
|
||||
// may be multi-line, unless this is the second time we've
|
||||
// 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 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" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
@ -794,26 +794,42 @@ const Subprocess = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the man pages from our application bundle to MANPATH.
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
if (cfg.resources_dir) |resources_dir| man: {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
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;
|
||||
};
|
||||
// On macOS, export additional data directories from our
|
||||
// application bundle.
|
||||
if (comptime builtin.target.isDarwin()) darwin: {
|
||||
const resources_dir = cfg.resources_dir orelse break :darwin;
|
||||
|
||||
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
|
||||
// `MANPATH` is empty, then it should be treated as an extra
|
||||
// path instead of overriding all paths set by OS.
|
||||
try env.put(
|
||||
"MANPATH",
|
||||
manpath_key,
|
||||
try internal_os.appendEnvAlways(
|
||||
alloc,
|
||||
env.get("MATHPATH") orelse "",
|
||||
dir,
|
||||
env.get(manpath_key) orelse "",
|
||||
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)
|
||||
// and we don't want to break new windows.
|
||||
const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: {
|
||||
if (std.fs.accessAbsolute(proposed, .{})) {
|
||||
if (std.fs.cwd().access(proposed, .{})) {
|
||||
break :cwd proposed;
|
||||
} else |err| {
|
||||
log.warn("cannot access cwd, ignoring: {}", .{err});
|
||||
|
@ -1,9 +1,11 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const config = @import("../config.zig");
|
||||
const homedir = @import("../os/homedir.zig");
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.shell_integration);
|
||||
|
||||
@ -57,11 +59,21 @@ pub fn setup(
|
||||
};
|
||||
|
||||
const result: ShellIntegration = shell: {
|
||||
// For now, bash integration must be explicitly enabled via force_shell.
|
||||
// Our automatic shell integration requires bash version 4 or later,
|
||||
// and systems like macOS continue to ship bash version 3 by default.
|
||||
// This approach avoids the cost of performing a runtime version check.
|
||||
if (std.mem.eql(u8, "bash", exe) and force_shell == .bash) {
|
||||
if (std.mem.eql(u8, "bash", exe)) {
|
||||
// Apple distributes their own patched version of Bash 3.2
|
||||
// on macOS that disables the ENV-based POSIX startup path.
|
||||
// This means we're unable to perform our automatic shell
|
||||
// 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(
|
||||
alloc_arena,
|
||||
command,
|
||||
@ -424,8 +436,8 @@ test "bash: preserve ENV" {
|
||||
/// Setup automatic shell integration for shells that include
|
||||
/// their modules from paths in `XDG_DATA_DIRS` env variable.
|
||||
///
|
||||
/// Path of shell-integration dir is prepended to `XDG_DATA_DIRS`.
|
||||
/// It is also saved in `GHOSTTY_SHELL_INTEGRATION_XDG_DIR` variable
|
||||
/// The shell-integration path is prepended to `XDG_DATA_DIRS`.
|
||||
/// 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
|
||||
/// from `XDG_DATA_DIRS` when integration is complete.
|
||||
fn setupXdgDataDirs(
|
||||
@ -447,11 +459,8 @@ fn setupXdgDataDirs(
|
||||
// so that our modifications don't interfere with other commands.
|
||||
try env.put("GHOSTTY_SHELL_INTEGRATION_XDG_DIR", integ_dir);
|
||||
|
||||
{
|
||||
const xdg_data_dir_key = "XDG_DATA_DIRS";
|
||||
|
||||
// We attempt to avoid allocating by using the stack up to 4K.
|
||||
// Max stack size is considerably larger on macOS and Linux but
|
||||
// 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.
|
||||
@ -462,17 +471,48 @@ fn setupXdgDataDirs(
|
||||
// This ensures that the default directories aren't lost by setting
|
||||
// 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}", .{
|
||||
const xdg_data_dirs_key = "XDG_DATA_DIRS";
|
||||
try env.put(
|
||||
xdg_data_dirs_key,
|
||||
try internal_os.prependEnv(
|
||||
stack_alloc,
|
||||
env.get(xdg_data_dirs_key) orelse "/usr/local/share:/usr/share",
|
||||
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
|
||||
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
35
website/.gitignore
vendored
@ -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
|
@ -1 +0,0 @@
|
||||
.next/
|
@ -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.
|
Before Width: | Height: | Size: 124 KiB |
@ -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);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
@ -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`.
|
@ -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______|
|
||||
```
|
@ -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______|
|
||||
```
|
@ -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).
|
@ -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).
|
@ -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______|
|
||||
```
|
@ -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|
|
||||
```
|
@ -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_______|
|
||||
```
|
@ -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|
|
||||
```
|
@ -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|
|
||||
```
|
@ -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_________|
|
||||
|__________|
|
||||
|__________|
|
||||
```
|
@ -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_____|
|
||||
```
|
@ -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|
|
||||
```
|
@ -1,7 +0,0 @@
|
||||
import VTSequence from "@/components/VTSequence";
|
||||
|
||||
# Keypad Application Mode (DECKPAM)
|
||||
|
||||
<VTSequence sequence={["ESC", "="]} />
|
||||
|
||||
Sets keypad application mode.
|
@ -1,7 +0,0 @@
|
||||
import VTSequence from "@/components/VTSequence";
|
||||
|
||||
# Keypad Numeric Mode (DECKPNM)
|
||||
|
||||
<VTSequence sequence={["ESC", ">"]} />
|
||||
|
||||
Sets keypad numeric mode.
|
@ -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).
|
@ -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.
|