Merge remote-tracking branch 'origin/main' into shaping-positions
commit
e28e4facf0
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env nu
|
||||
|
||||
# A command to review the changes made in the current Git branch.
|
||||
#
|
||||
# IMPORTANT: This command is prompted to NOT write any code and to ONLY
|
||||
# produce a review summary. You should still be vigilant when running this
|
||||
# but that is the expected behavior.
|
||||
#
|
||||
# The optional `<issue>` parameter can be an issue number, PR number,
|
||||
# or a full GitHub URL to provide additional context.
|
||||
def main [
|
||||
issue?: any, # Optional GitHub issue/PR number or URL for context
|
||||
] {
|
||||
let issueContext = if $issue != null {
|
||||
let data = gh issue view $issue --json author,title,number,body,comments | from json
|
||||
let comments = if ($data.comments? != null) {
|
||||
$data.comments | each { |comment|
|
||||
let author = if ($comment.author?.login? != null) { $comment.author.login } else { "unknown" }
|
||||
$"
|
||||
### Comment by ($author)
|
||||
($comment.body)
|
||||
" | str trim
|
||||
} | str join "\n\n"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
$"
|
||||
## Source Issue: ($data.title) \(#($data.number)\)
|
||||
|
||||
### Description
|
||||
($data.body)
|
||||
|
||||
### Comments
|
||||
($comments)
|
||||
"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
$"
|
||||
# Branch Review
|
||||
|
||||
Inspect the changes made in this Git branch. Identify any possible issues
|
||||
and suggest improvements. Do not write code. Explain the problems clearly
|
||||
and propose a brief plan for addressing them.
|
||||
($issueContext)
|
||||
## Your Tasks
|
||||
|
||||
You are an experienced software developer with expertise in code review.
|
||||
|
||||
Review the change history between the current branch and its
|
||||
base branch. Analyze all relevant code for possible issues, including but
|
||||
not limited to:
|
||||
|
||||
- Code quality and readability
|
||||
- Code style that matches or mimics the rest of the codebase
|
||||
- Potential bugs or logical errors
|
||||
- Edge cases that may not be handled
|
||||
- Performance considerations
|
||||
- Security vulnerabilities
|
||||
- Backwards compatibility \(if applicable\)
|
||||
- Test coverage and effectiveness
|
||||
|
||||
For test coverage, consider if the changes are in an area of the codebase
|
||||
that is testable. If so, check if there are appropriate tests added or
|
||||
modified. Consider if the code itself should be modified to be more
|
||||
testable.
|
||||
|
||||
Think deeply about the implications of the changes here and proposed.
|
||||
Consult the oracle if you have access to it.
|
||||
|
||||
**ONLY CREATE A SUMMARY. DO NOT WRITE ANY CODE.**
|
||||
" | str trim
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source-run-id:
|
||||
description: run id of the workflow that generated the artifact
|
||||
required: true
|
||||
type: string
|
||||
source-artifact-id:
|
||||
description: source tarball built during build-dist
|
||||
required: true
|
||||
type: string
|
||||
|
||||
name: Flatpak
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
name: "Flatpak"
|
||||
container:
|
||||
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47
|
||||
options: --privileged
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- arch: x86_64
|
||||
runner: namespace-profile-ghostty-md
|
||||
- arch: aarch64
|
||||
runner: namespace-profile-ghostty-md-arm64
|
||||
runs-on: ${{ matrix.variant.runner }}
|
||||
steps:
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
run-id: ${{ inputs.source-run-id }}
|
||||
artifact-ids: ${{ inputs.source-artifact-id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Extract tarball
|
||||
run: |
|
||||
mkdir dist
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
|
||||
with:
|
||||
bundle: com.mitchellh.ghostty
|
||||
manifest-path: dist/flatpak/com.mitchellh.ghostty.yml
|
||||
cache-key: flatpak-builder-${{ github.sha }}
|
||||
arch: ${{ matrix.variant.arch }}
|
||||
verbose: true
|
||||
|
|
@ -113,7 +113,7 @@ jobs:
|
|||
nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
path: |-
|
||||
|
|
@ -269,7 +269,7 @@ jobs:
|
|||
zip -9 -r --symlinks ../../../ghostty-macos-universal-dsym.zip Ghostty.app.dSYM/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: macos
|
||||
path: |-
|
||||
|
|
@ -286,7 +286,7 @@ jobs:
|
|||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ jobs:
|
|||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ jobs:
|
|||
mv appcast_new.xml appcast.xml
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: sparkle
|
||||
path: |-
|
||||
|
|
@ -357,17 +357,17 @@ jobs:
|
|||
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Download macOS Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: macos
|
||||
|
||||
- name: Download Sparkle Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: sparkle
|
||||
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
run-id: ${{ inputs.source-run-id }}
|
||||
artifact-ids: ${{ inputs.source-artifact-id }}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- build-linux-libghostty
|
||||
- build-nix
|
||||
- build-macos
|
||||
- build-macos-matrix
|
||||
- build-macos-freetype
|
||||
- build-snap
|
||||
- build-windows
|
||||
- test
|
||||
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
- test-debian-13
|
||||
- valgrind
|
||||
- zig-fmt
|
||||
- flatpak
|
||||
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
|
|
@ -397,7 +397,7 @@ jobs:
|
|||
|
||||
- name: Upload artifact
|
||||
id: upload-artifact
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
path: |-
|
||||
|
|
@ -421,6 +421,24 @@ jobs:
|
|||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
trigger-flatpak:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: namespace-profile-ghostty-xsm
|
||||
needs: [build-dist, build-flatpak]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Trigger Flatpak workflow
|
||||
run: |
|
||||
gh workflow run \
|
||||
flatpak.yml \
|
||||
--ref ${{ github.ref_name || 'main' }} \
|
||||
--field source-run-id=${{ github.run_id }} \
|
||||
--field source-artifact-id=${{ needs.build-dist.outputs.artifact-id }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
build-macos:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
|
|
@ -464,7 +482,7 @@ jobs:
|
|||
cd macos
|
||||
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
|
||||
|
||||
build-macos-matrix:
|
||||
build-macos-freetype:
|
||||
runs-on: namespace-profile-ghostty-macos-tahoe
|
||||
needs: test
|
||||
steps:
|
||||
|
|
@ -493,18 +511,10 @@ jobs:
|
|||
- name: Test All
|
||||
run: |
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=freetype
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_freetype
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_harfbuzz
|
||||
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Drenderer=metal -Dfont-backend=coretext_noshape
|
||||
|
||||
- name: Build All
|
||||
run: |
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=freetype
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_freetype
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_harfbuzz
|
||||
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false -Drenderer=metal -Dfont-backend=coretext_noshape
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-2022
|
||||
|
|
@ -1075,7 +1085,7 @@ jobs:
|
|||
uses: namespacelabs/nscloud-setup-buildx-action@91c2e6537780e3b092cb8476406be99a8f91bd5e # v0.0.20
|
||||
|
||||
- name: Download Source Tarball Artifacts
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: source-tarball
|
||||
|
||||
|
|
@ -1092,32 +1102,6 @@ jobs:
|
|||
build-args: |
|
||||
DISTRO_VERSION=13
|
||||
|
||||
flatpak:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
name: "Flatpak"
|
||||
container:
|
||||
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47
|
||||
options: --privileged
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
variant:
|
||||
- arch: x86_64
|
||||
runner: namespace-profile-ghostty-md
|
||||
- arch: aarch64
|
||||
runner: namespace-profile-ghostty-md-arm64
|
||||
runs-on: ${{ matrix.variant.runner }}
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: flatpak/flatpak-github-actions/flatpak-builder@92ae9851ad316786193b1fd3f40c4b51eb5cb101 # v6.6
|
||||
with:
|
||||
bundle: com.mitchellh.ghostty
|
||||
manifest-path: flatpak/com.mitchellh.ghostty.yml
|
||||
cache-key: flatpak-builder-${{ github.sha }}
|
||||
arch: ${{ matrix.variant.arch }}
|
||||
verbose: true
|
||||
|
||||
valgrind:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-lg
|
||||
|
|
|
|||
|
|
@ -37,16 +37,33 @@ jobs:
|
|||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Run zig fetch
|
||||
id: zig_fetch
|
||||
- name: Download colorschemes
|
||||
id: download
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Get the latest release from iTerm2-Color-Schemes
|
||||
RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest)
|
||||
TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name')
|
||||
nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz"
|
||||
FILENAME="ghostty-themes-${TAG_NAME}.tgz"
|
||||
mkdir -p upload
|
||||
curl -L -o "upload/${FILENAME}" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz"
|
||||
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
|
||||
echo "filename=$FILENAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@b801a390acbdeb034c5e684ff5e1361c06639e7c # v1.4
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_DEPS_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_DEPS_AWS_KEY }}
|
||||
r2-secret-access-key: ${{ secrets.CF_R2_DEPS_SECRET_KEY }}
|
||||
r2-bucket: ghostty-deps
|
||||
source-dir: upload
|
||||
destination-dir: ./
|
||||
|
||||
- name: Run zig fetch
|
||||
run: |
|
||||
nix develop -c zig fetch --save="iterm2_themes" "https://deps.files.ghostty.org/${{ steps.download.outputs.filename }}"
|
||||
|
||||
- name: Update zig cache hash
|
||||
run: |
|
||||
|
|
@ -75,5 +92,5 @@ jobs:
|
|||
build.zig.zon.json
|
||||
flatpak/zig-packages.json
|
||||
body: |
|
||||
Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.zig_fetch.outputs.tag_name }}
|
||||
Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.download.outputs.tag_name }}
|
||||
labels: dependencies
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ zig-cache/
|
|||
.zig-cache/
|
||||
zig-out/
|
||||
/result*
|
||||
/.nixos-test-history
|
||||
example/*.wasm
|
||||
test/ghostty
|
||||
test/cases/**/*.actual.png
|
||||
|
|
|
|||
323
CONTRIBUTING.md
323
CONTRIBUTING.md
|
|
@ -17,14 +17,12 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
|
|||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Ghostty,
|
||||
> it must be disclosed in the pull request.
|
||||
> The Ghostty project allows AI-**assisted** _code contributions_, which
|
||||
> must be properly disclosed in the pull request.
|
||||
|
||||
If you are using any kind of AI assistance while contributing to Ghostty,
|
||||
**this must be disclosed in the pull request**, along with the extent to
|
||||
which AI assistance was used (e.g. docs only vs. code generation).
|
||||
As a small exception, trivial tab-completion doesn't need to be disclosed,
|
||||
so long as it is limited to single keywords or short phrases.
|
||||
|
||||
The submitter must have also tested the pull request on all impacted
|
||||
platforms, and it's **highly discouraged** to code for an unfamiliar platform
|
||||
|
|
@ -32,10 +30,49 @@ with AI assistance alone: if you only have a macOS machine, do **not** ask AI
|
|||
to write the equivalent GTK code, and vice versa — someone else with more
|
||||
expertise will eventually get to it and do it for you.
|
||||
|
||||
Even though using AI to generate responses on a PR is allowed when properly
|
||||
disclosed, **we do not encourage you to do so**. Often, the positive impact
|
||||
of genuine, responsive human interaction more than makes up for any language
|
||||
barrier. ❤️
|
||||
> [!WARNING]
|
||||
> **Note that AI _assistance_ does not equal AI _generation_**. We require
|
||||
> a significant amount of human accountability, involvement and interaction
|
||||
> even within AI-assisted contributions. Contributors are required to be able
|
||||
> to understand the AI-assisted output, reason with it and answer critical
|
||||
> questions about it. Should a PR see no visible human accountability and
|
||||
> involvement, or it is so broken that it requires significant rework to be
|
||||
> acceptable, **we reserve the right to close it without hesitation**.
|
||||
|
||||
**In addition, we currently restrict AI assistance to code changes only.**
|
||||
No AI-generated media, e.g. artwork, icons, videos and other assets is
|
||||
allowed, as it goes against the methodology and ethos behind Ghostty.
|
||||
While AI-assisted code can help with productive prototyping, creative
|
||||
inspiration and even automated bugfinding, we have currently found zero
|
||||
benefit to AI-generated assets. Instead, we are far more interested and
|
||||
invested in funding professional work done by human designers and artists.
|
||||
If you intend to submit AI-generated assets to Ghostty, sorry,
|
||||
we are not interested.
|
||||
|
||||
Likewise, all community interactions, including all comments on issues and
|
||||
discussions and all PR titles and descriptions **must be composed by a human**.
|
||||
Community moderators and Ghostty maintainers reserve the right to mark
|
||||
AI-generated responses as spam or disruptive content, and ban users who have
|
||||
been repeatedly caught relying entirely on LLMs during interactions.
|
||||
|
||||
> [!NOTE]
|
||||
> If your English isn't the best and you are currently relying on an LLM to
|
||||
> translate your responses, don't fret — usually we maintainers will be able
|
||||
> to understand your messages well enough. We'd like to encourage real humans
|
||||
> to interact with each other more, and the positive impact of genuine,
|
||||
> responsive yet imperfect human interaction more than makes up for any
|
||||
> language barrier.
|
||||
>
|
||||
> Please write your responses yourself, to the best of your ability.
|
||||
> If you do feel the need to polish your sentences, however, please use
|
||||
> dedicated translation software rather than an LLM.
|
||||
>
|
||||
> We greatly appreciate it. Thank you. ❤️
|
||||
|
||||
Minor exceptions to this policy include trivial AI-generated tab completion
|
||||
functionality, as it usually does not impact the quality of the code and
|
||||
do not need to be disclosed, and commit titles and messages, which are often
|
||||
generated by AI coding agents.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
|
|
@ -60,13 +97,6 @@ work than any human. That isn't the world we live in today, and in most cases
|
|||
it's generating slop. I say this despite being a fan of and using them
|
||||
successfully myself (with heavy supervision)!
|
||||
|
||||
When using AI assistance, we expect a fairly high level of accountability
|
||||
and responsibility from contributors, and expect them to understand the code
|
||||
that is produced and be able to answer critical questions about it. It
|
||||
isn't a maintainers job to review a PR so broken that it requires
|
||||
significant rework to be acceptable, and we **reserve the right to close
|
||||
these PRs without hesitation**.
|
||||
|
||||
Please be respectful to maintainers and disclose AI assistance.
|
||||
|
||||
## Quick Guide
|
||||
|
|
@ -202,3 +232,266 @@ pull request will be accepted with a high degree of certainty.
|
|||
> **Pull requests are NOT a place to discuss feature design.** Please do
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> and link to your branch.
|
||||
|
||||
# Developer Guide
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **The remainder of this file is dedicated to developers actively
|
||||
> working on Ghostty.** If you're a user reporting an issue, you can
|
||||
> ignore the rest of this document.
|
||||
|
||||
## Including and Updating Translations
|
||||
|
||||
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
|
||||
|
||||
## Checking for Memory Leaks
|
||||
|
||||
While Zig does an amazing job of finding and preventing memory leaks,
|
||||
Ghostty uses many third-party libraries that are written in C. Improper usage
|
||||
of those libraries or bugs in those libraries can cause memory leaks that
|
||||
Zig cannot detect by itself.
|
||||
|
||||
### On Linux
|
||||
|
||||
On Linux the recommended tool to check for memory leaks is Valgrind. The
|
||||
recommended way to run Valgrind is via `zig build`:
|
||||
|
||||
```sh
|
||||
zig build run-valgrind
|
||||
```
|
||||
|
||||
This builds a Ghostty executable with Valgrind support and runs Valgrind
|
||||
with the proper flags to ensure we're suppressing known false positives.
|
||||
|
||||
You can combine the same build args with `run-valgrind` that you can with
|
||||
`run`, such as specifying additional configurations after a trailing `--`.
|
||||
|
||||
## Input Stack Testing
|
||||
|
||||
The input stack is the part of the codebase that starts with a
|
||||
key event and ends with text encoding being sent to the pty (it
|
||||
does not include _rendering_ the text, which is part of the
|
||||
font or rendering stack).
|
||||
|
||||
If you modify any part of the input stack, you must manually verify
|
||||
all the following input cases work properly. We unfortunately do
|
||||
not automate this in any way, but if we can do that one day that'd
|
||||
save a LOT of grief and time.
|
||||
|
||||
Note: this list may not be exhaustive, I'm still working on it.
|
||||
|
||||
### Linux IME
|
||||
|
||||
IME (Input Method Editors) are a common source of bugs in the input stack,
|
||||
especially on Linux since there are multiple different IME systems
|
||||
interacting with different windowing systems and application frameworks
|
||||
all written by different organizations.
|
||||
|
||||
The following matrix should be tested to ensure that all IME input works
|
||||
properly:
|
||||
|
||||
1. Wayland, X11
|
||||
2. ibus, fcitx, none
|
||||
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
|
||||
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a **work in progress**. I'm still working on this list and it
|
||||
> is not complete. As I find more test cases, I will add them here.
|
||||
|
||||
#### Dead Key Input
|
||||
|
||||
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press `a`
|
||||
4. Verify that `á` is displayed
|
||||
|
||||
Note that the dead key may or may not show a preedit state visually.
|
||||
For ibus and fcitx it does but for the "none" case it does not. Importantly,
|
||||
the text should be correct when it is sent to the pty.
|
||||
|
||||
We should also test canceling dead key input:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press escape
|
||||
4. Press `a`
|
||||
5. Verify that `a` is displayed (no diacritic)
|
||||
|
||||
#### CJK Input
|
||||
|
||||
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
|
||||
exact layout doesn't matter.
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Enter`
|
||||
5. Verify that `こん` is displayed in the terminal.
|
||||
|
||||
We should also test switching input methods while preedit is active, which
|
||||
should commit the text:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Ctrl+Shift` to switch to another layout (any)
|
||||
5. Verify that `こん` is displayed in the terminal as committed text.
|
||||
|
||||
## Nix Virtual Machines
|
||||
|
||||
Several Nix virtual machine definitions are provided by the project for testing
|
||||
and developing Ghostty against multiple different Linux desktop environments.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
VMs should only be run on your local desktop and then powered off when not in
|
||||
use, which will discard any changes to the VM.
|
||||
|
||||
The VM definitions provide minimal software "out of the box" but additional
|
||||
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
|
||||
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
|
||||
with `common` or `create`.
|
||||
3. The VM will build and then launch. Depending on the speed of your system, this
|
||||
can take a while, but eventually you should get a new VM window.
|
||||
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
|
||||
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
|
||||
writable by the VM user, so be careful!
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a VM.
|
||||
|
||||
### Custom VMs
|
||||
|
||||
To easily create a custom VM without modifying the Ghostty source, create a new
|
||||
directory, then create a file called `flake.nix` with the following text in the
|
||||
new directory.
|
||||
|
||||
```
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
ghostty.url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
ghostty,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
|
||||
nixpkgs = nixpkgs;
|
||||
system = "x86_64-linux";
|
||||
overlay = ghostty.overlays.releasefast;
|
||||
# module = ./configuration.nix # also works
|
||||
module = {pkgs, ...}: {
|
||||
environment.systemPackages = [
|
||||
pkgs.btop
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The custom VM can then be run with a command like this:
|
||||
|
||||
```
|
||||
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
|
||||
```
|
||||
|
||||
A file named `ghostty.qcow2` will be created that is used to persist any changes
|
||||
made in the VM. To "reset" the VM to default delete the file and it will be
|
||||
recreated the next time you run the VM.
|
||||
|
||||
### Contributing new VM definitions
|
||||
|
||||
#### VM Acceptance Criteria
|
||||
|
||||
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
|
||||
|
||||
1. They should be different enough from existing VM definitions that they represent a distinct
|
||||
user (and developer) experience.
|
||||
2. There's a significant Ghostty user population that uses a similar environment.
|
||||
3. The VMs can be built using only packages from the current stable NixOS release.
|
||||
|
||||
#### VM Definition Criteria
|
||||
|
||||
1. VMs should be as minimal as possible so that they build and launch quickly.
|
||||
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
|
||||
2. VMs should not expose any services to the network, or run any remote access
|
||||
software like SSH daemons, VNC or RDP.
|
||||
3. VMs should auto-login using the "ghostty" user.
|
||||
|
||||
## Nix VM Integration Tests
|
||||
|
||||
Several Nix VM tests are provided by the project for testing Ghostty in a "live"
|
||||
environment rather than just unit tests.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#check.<system>.<test-name>.driver`. `<system>` should be
|
||||
`x86_64-linux` or `aarch64-linux` (even on macOS, this launches a Linux
|
||||
VM, not a macOS one). `<test-name>` should be one of the tests defined in
|
||||
`nix/tests.nix`. The test will build and then launch. Depending on the speed
|
||||
of your system, this can take a while. Eventually though the test should
|
||||
complete. Hopefully successfully, but if not error messages should be printed
|
||||
out that can be used to diagnose the issue.
|
||||
3. To run _all_ of the tests, run `nix flake check`.
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a test.
|
||||
|
||||
### Interactively Running Test VMs
|
||||
|
||||
To run a test interactively, run `nix run
|
||||
.#check.<system>.<test-name>.driverInteractive`. This will load a Python console
|
||||
that can be used to manage the test VMs. In this console run `start_all()` to
|
||||
start the VM(s). The VMs should boot up and a window should appear showing the
|
||||
VM's console.
|
||||
|
||||
For more information about the Nix test console, see [the NixOS manual](https://nixos.org/manual/nixos/stable/index.html#sec-call-nixos-test-outside-nixos)
|
||||
|
||||
### SSH Access to Test VMs
|
||||
|
||||
Some test VMs are configured to allow outside SSH access for debugging. To
|
||||
access the VM, use a command like the following:
|
||||
|
||||
```
|
||||
ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 root@192.168.122.1
|
||||
ssh -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null -p 2222 ghostty@192.168.122.1
|
||||
```
|
||||
|
||||
The SSH options are important because the SSH host keys will be regenerated
|
||||
every time the test is started. Without them, your personal SSH known hosts file
|
||||
will become difficult to manage. The port that is needed to access the VM may
|
||||
change depending on the test.
|
||||
|
||||
None of the users in the VM have passwords so do not expose these VMs to the Internet.
|
||||
|
|
|
|||
30
HACKING.md
30
HACKING.md
|
|
@ -93,6 +93,36 @@ produced.
|
|||
> may ask you to fix it and close the issue. It isn't a maintainers job to
|
||||
> review a PR so broken that it requires significant rework to be acceptable.
|
||||
|
||||
## Logging
|
||||
|
||||
Ghostty can write logs to a number of destinations. On all platforms, logging to
|
||||
`stderr` is available. Depending on the platform and how Ghostty was launched,
|
||||
logs sent to `stderr` may be stored by the system and made available for later
|
||||
retrieval.
|
||||
|
||||
On Linux if Ghostty is launched by the default `systemd` user service, you can use
|
||||
`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`.
|
||||
|
||||
On macOS logging to the macOS unified log is available and enabled by default.
|
||||
Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`.
|
||||
|
||||
Ghostty's logging can be configured in two ways. The first is by what
|
||||
optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug`
|
||||
optimizations debug logs will be output to `stderr`. If Ghostty is compiled with
|
||||
any other optimization the debug logs will not be output to `stderr`.
|
||||
|
||||
Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used
|
||||
to control which destinations receive logs. Ghostty currently defines two
|
||||
destinations:
|
||||
|
||||
- `stderr` - logging to `stderr`.
|
||||
- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
|
||||
|
||||
Combine values with a comma to enable multiple destinations. Prefix a
|
||||
destination with `no-` to disable it. Enabling and disabling destinations
|
||||
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
|
||||
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
|
||||
|
||||
## Linting
|
||||
|
||||
### Prettier
|
||||
|
|
|
|||
|
|
@ -318,8 +318,3 @@ pub fn build(b: *std.Build) !void {
|
|||
try translations_step.addError("cannot update translations when i18n is disabled", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker used by Config.zig to detect if ghostty is the build root.
|
||||
/// This avoids running logic such as Git tag checking when Ghostty
|
||||
/// is used as a dependency.
|
||||
pub const _ghostty_build_root = true;
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
},
|
||||
.vaxis = .{
|
||||
// rockorager/libvaxis
|
||||
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
.url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
.lazy = true,
|
||||
},
|
||||
.z2d = .{
|
||||
// vancluever/z2d
|
||||
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
.url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
.lazy = true,
|
||||
},
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
.uucode = .{
|
||||
// jacobsandlund/uucode
|
||||
.url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
.url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
|
|
@ -50,14 +50,14 @@
|
|||
},
|
||||
.zf = .{
|
||||
// natecraddock/zf
|
||||
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
.url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
.lazy = true,
|
||||
},
|
||||
.gobject = .{
|
||||
// https://github.com/ghostty-org/zig-gobject based on zig_gobject
|
||||
// Temporary until we generate them at build time automatically.
|
||||
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
.url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
.lazy = true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,124 @@
|
|||
.{
|
||||
.name = .ghostty,
|
||||
.version = "1.3.0-dev",
|
||||
.paths = .{""},
|
||||
.fingerprint = 0x64407a2a0b4147e5,
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
|
||||
.libxev = .{
|
||||
// mitchellh/libxev
|
||||
.url = "https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
// rockorager/libvaxis
|
||||
.url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
.hash = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
.lazy = true,
|
||||
},
|
||||
.z2d = .{
|
||||
// vancluever/z2d
|
||||
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
.hash = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
// mitchellh/zig-objc
|
||||
.url = "https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_Sp5gTAQCvxxR7oVIrPXxXwsfKgVP7_wqoOQrZjFeK",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_js = .{
|
||||
// mitchellh/zig-js
|
||||
.url = "https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz",
|
||||
.hash = "zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi",
|
||||
.lazy = true,
|
||||
},
|
||||
.uucode = .{
|
||||
// jacobsandlund/uucode
|
||||
.url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
.hash = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
// codeberg ifreund/zig-wayland
|
||||
.url = "https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz",
|
||||
.hash = "wayland-0.5.0-dev-lQa1khrMAQDJDwYFKpdH3HizherB7sHo5dKMECfvxQHe",
|
||||
.lazy = true,
|
||||
},
|
||||
.zf = .{
|
||||
// natecraddock/zf
|
||||
.url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
.hash = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
.lazy = true,
|
||||
},
|
||||
.gobject = .{
|
||||
// https://github.com/ghostty-org/zig-gobject based on zig_gobject
|
||||
// Temporary until we generate them at build time automatically.
|
||||
.url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
// C libs
|
||||
.cimgui = .{ .path = "./pkg/cimgui", .lazy = true },
|
||||
.fontconfig = .{ .path = "./pkg/fontconfig", .lazy = true },
|
||||
.freetype = .{ .path = "./pkg/freetype", .lazy = true },
|
||||
.gtk4_layer_shell = .{ .path = "./pkg/gtk4-layer-shell", .lazy = true },
|
||||
.harfbuzz = .{ .path = "./pkg/harfbuzz", .lazy = true },
|
||||
.highway = .{ .path = "./pkg/highway", .lazy = true },
|
||||
.libintl = .{ .path = "./pkg/libintl", .lazy = true },
|
||||
.libpng = .{ .path = "./pkg/libpng", .lazy = true },
|
||||
.macos = .{ .path = "./pkg/macos", .lazy = true },
|
||||
.oniguruma = .{ .path = "./pkg/oniguruma", .lazy = true },
|
||||
.opengl = .{ .path = "./pkg/opengl", .lazy = true },
|
||||
.sentry = .{ .path = "./pkg/sentry", .lazy = true },
|
||||
.simdutf = .{ .path = "./pkg/simdutf", .lazy = true },
|
||||
.utfcpp = .{ .path = "./pkg/utfcpp", .lazy = true },
|
||||
.wuffs = .{ .path = "./pkg/wuffs", .lazy = true },
|
||||
.zlib = .{ .path = "./pkg/zlib", .lazy = true },
|
||||
|
||||
// Shader translation
|
||||
.glslang = .{ .path = "./pkg/glslang", .lazy = true },
|
||||
.spirv_cross = .{ .path = "./pkg/spirv-cross", .lazy = true },
|
||||
|
||||
// Wayland
|
||||
.wayland = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
|
||||
.hash = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t",
|
||||
.lazy = true,
|
||||
},
|
||||
.wayland_protocols = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
|
||||
.hash = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S",
|
||||
.lazy = true,
|
||||
},
|
||||
.plasma_wayland_protocols = .{
|
||||
.url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
|
||||
.hash = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
// Fonts
|
||||
.jetbrains_mono = .{
|
||||
.url = "https://deps.files.ghostty.org/JetBrainsMono-2.304.tar.gz",
|
||||
.hash = "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x",
|
||||
.lazy = true,
|
||||
},
|
||||
.nerd_fonts_symbols_only = .{
|
||||
.url = "https://deps.files.ghostty.org/NerdFontsSymbolsOnly-3.4.0.tar.gz",
|
||||
.hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO26s",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz",
|
||||
.hash = "N-V-__8AANFEAwCzzNzNs3Gaq8pzGNl2BbeyFBwTyO5iZJL-",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
},
|
||||
"gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-": {
|
||||
"name": "gobject",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
"hash": "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg="
|
||||
},
|
||||
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
|
||||
|
|
@ -116,12 +116,12 @@
|
|||
},
|
||||
"uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E": {
|
||||
"name": "uucode",
|
||||
"url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"hash": "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4="
|
||||
},
|
||||
"vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS": {
|
||||
"name": "vaxis",
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"hash": "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY="
|
||||
},
|
||||
"N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t": {
|
||||
|
|
@ -141,12 +141,12 @@
|
|||
},
|
||||
"z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T": {
|
||||
"name": "z2d",
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
"hash": "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA="
|
||||
},
|
||||
"zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh": {
|
||||
"name": "zf",
|
||||
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"hash": "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg="
|
||||
},
|
||||
"zig_js-0.0.0-rjCAV-6GAADxFug7rDmPH-uM_XcnJ5NmuAMJCAscMjhi": {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ in
|
|||
name = "gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-";
|
||||
path = fetchZigArtifact {
|
||||
name = "gobject";
|
||||
url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst";
|
||||
url = "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst";
|
||||
hash = "sha256-2b1DBvAIHY5LhItq3+q9L6tJgi7itnnrSAHc7fXWDEg=";
|
||||
};
|
||||
}
|
||||
|
|
@ -270,7 +270,7 @@ in
|
|||
name = "uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E";
|
||||
path = fetchZigArtifact {
|
||||
name = "uucode";
|
||||
url = "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
|
||||
url = "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz";
|
||||
hash = "sha256-SzpYGhgG4B6Luf8eT35sKLobCxjmwEuo1Twk0jeu9g4=";
|
||||
};
|
||||
}
|
||||
|
|
@ -278,7 +278,7 @@ in
|
|||
name = "vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS";
|
||||
path = fetchZigArtifact {
|
||||
name = "vaxis";
|
||||
url = "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
|
||||
url = "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz";
|
||||
hash = "sha256-LnIzK8icW1Qexua9SHaeHz+3V8QAbz0a+UC1T5sIjvY=";
|
||||
};
|
||||
}
|
||||
|
|
@ -310,7 +310,7 @@ in
|
|||
name = "z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T";
|
||||
path = fetchZigArtifact {
|
||||
name = "z2d";
|
||||
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz";
|
||||
url = "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz";
|
||||
hash = "sha256-+QqCRoXwrFA1/l+oWvYVyAVebGQitAFQNhi9U3EVrxA=";
|
||||
};
|
||||
}
|
||||
|
|
@ -318,7 +318,7 @@ in
|
|||
name = "zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh";
|
||||
path = fetchZigArtifact {
|
||||
name = "zf";
|
||||
url = "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
|
||||
url = "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz";
|
||||
hash = "sha256-OwFdkorwTp4mJyvBXrTbtNmp1GnrbSkKDdrmc7d8RWg=";
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
|
|||
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
|
||||
https://deps.files.ghostty.org/gettext-0.24.tar.gz
|
||||
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
|
||||
https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst
|
||||
https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz
|
||||
https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz
|
||||
https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz
|
||||
|
|
@ -19,17 +20,16 @@ https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e
|
|||
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
|
||||
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
|
||||
https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
|
||||
https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
|
||||
https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
|
||||
https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
|
||||
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
|
||||
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
|
||||
https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz
|
||||
https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
|
||||
https://deps.files.ghostty.org/zig_js-04db83c617da1956ac5adc1cb9ba1e434c1cb6fd.tar.gz
|
||||
https://deps.files.ghostty.org/zig_objc-f356ed02833f0f1b8e84d50bed9e807bf7cdc0ae.tar.gz
|
||||
https://deps.files.ghostty.org/zig_wayland-1b5c038ec10da20ed3a15b0b2a6db1c21383e8ea.tar.gz
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst
|
||||
https://github.com/ivanstepanovftw/zigimg/archive/d7b7ab0ba0899643831ef042bd73289510b39906.tar.gz
|
||||
https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20251201-150531-bfb3ee1/ghostty-themes.tgz
|
||||
https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz
|
||||
https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz
|
||||
|
|
|
|||
22
flake.lock
22
flake.lock
|
|
@ -34,6 +34,27 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1755776884,
|
||||
"narHash": "sha256-CPM7zm6csUx7vSfKvzMDIjepEJv1u/usmaT7zydzbuI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "4fb695d10890e9fc6a19deadf85ff79ffb78da86",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.05",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1763191728,
|
||||
|
|
@ -51,6 +72,7 @@
|
|||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"zig": "zig",
|
||||
"zon2nix": "zon2nix"
|
||||
|
|
|
|||
22
flake.nix
22
flake.nix
|
|
@ -33,6 +33,13 @@
|
|||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager?ref=release-25.05";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs = {
|
||||
|
|
@ -40,6 +47,7 @@
|
|||
nixpkgs,
|
||||
zig,
|
||||
zon2nix,
|
||||
home-manager,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs.lib.recursiveUpdate {} (
|
||||
|
|
@ -47,7 +55,7 @@
|
|||
system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs.callPackage ./nix/devShell.nix {
|
||||
devShells.${system}.default = pkgs.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.15.2";
|
||||
wraptest = pkgs.callPackage ./nix/pkgs/wraptest.nix {};
|
||||
zon2nix = zon2nix;
|
||||
|
|
@ -79,6 +87,10 @@
|
|||
|
||||
formatter.${system} = pkgs.alejandra;
|
||||
|
||||
checks.${system} = import ./nix/tests.nix {
|
||||
inherit home-manager nixpkgs self system;
|
||||
};
|
||||
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
|
|
@ -95,6 +107,9 @@
|
|||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
meta = {
|
||||
description = "start a vm from ${toString module}";
|
||||
};
|
||||
}
|
||||
);
|
||||
in {
|
||||
|
|
@ -120,11 +135,6 @@
|
|||
ghostty = self.packages.${prev.stdenv.hostPlatform.system}.ghostty-debug;
|
||||
};
|
||||
};
|
||||
create-vm = import ./nix/vm/create.nix;
|
||||
create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
|
||||
create-gnome-vm = import ./nix/vm/create-gnome.nix;
|
||||
create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
|
||||
create-xfce-vm = import ./nix/vm/create-xfce.nix;
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/ghostty-org/zig-gobject/releases/download/0.7.0-2025-11-08-23-1/ghostty-gobject-0.7.0-2025-11-08-23-1.tar.zst",
|
||||
"url": "https://deps.files.ghostty.org/gobject-2025-11-08-23-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ANLnwDvEfIpVmohcppXgOvg_I6YOJFmPIsKfXk-",
|
||||
"sha256": "d9bd4306f0081d8e4b848b6adfeabd2fab49822ee2b679eb4801dcedf5d60c48"
|
||||
},
|
||||
|
|
@ -139,13 +139,13 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/jacobsandlund/uucode/archive/31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/uucode-31655fba3c638229989cc524363ef5e3c7b580c1.tar.gz",
|
||||
"dest": "vendor/p/uucode-0.1.0-ZZjBPicPTQDlG6OClzn2bPu7ICkkkyWrTB6aRsBr-A1E",
|
||||
"sha256": "4b3a581a1806e01e8bb9ff1e4f7e6c28ba1b0b18e6c04ba8d53c24d237aef60e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/rockorager/libvaxis/archive/7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/vaxis-7dbb9fd3122e4ffad262dd7c151d80d863b68558.tar.gz",
|
||||
"dest": "vendor/p/vaxis-0.5.1-BWNV_LosCQAGmCCNOLljCIw6j6-yt53tji6n6rwJ2BhS",
|
||||
"sha256": "2e72332bc89c5b541ec6e6bd48769e1f3fb757c4006f3d1af940b54f9b088ef6"
|
||||
},
|
||||
|
|
@ -169,13 +169,13 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.9.0.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.9.0-j5P_Hu-WFgA_JEfRpiFss6gdvcvS47cgOc0Via2eKD_T",
|
||||
"sha256": "f90a824685f0ac5035fe5fa85af615c8055e6c6422b401503618bd537115af10"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/natecraddock/zf/archive/3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"url": "https://deps.files.ghostty.org/zf-3c52637b7e937c5ae61fd679717da3e276765b23.tar.gz",
|
||||
"dest": "vendor/p/zf-0.10.3-OIRy8RuJAACKA3Lohoumrt85nRbHwbpMcUaLES8vxDnh",
|
||||
"sha256": "3b015d928af04e9e26272bc15eb4dbb4d9a9d469eb6d290a0ddae673b77c4568"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -512,6 +512,12 @@ typedef enum {
|
|||
GHOSTTY_GOTO_SPLIT_RIGHT,
|
||||
} ghostty_action_goto_split_e;
|
||||
|
||||
// apprt.action.GotoWindow
|
||||
typedef enum {
|
||||
GHOSTTY_GOTO_WINDOW_PREVIOUS,
|
||||
GHOSTTY_GOTO_WINDOW_NEXT,
|
||||
} ghostty_action_goto_window_e;
|
||||
|
||||
// apprt.action.ResizeSplit.Direction
|
||||
typedef enum {
|
||||
GHOSTTY_RESIZE_SPLIT_UP,
|
||||
|
|
@ -573,6 +579,12 @@ typedef enum {
|
|||
GHOSTTY_QUIT_TIMER_STOP,
|
||||
} ghostty_action_quit_timer_e;
|
||||
|
||||
// apprt.action.Readonly
|
||||
typedef enum {
|
||||
GHOSTTY_READONLY_OFF,
|
||||
GHOSTTY_READONLY_ON,
|
||||
} ghostty_action_readonly_e;
|
||||
|
||||
// apprt.action.DesktopNotification.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
|
|
@ -791,9 +803,11 @@ typedef enum {
|
|||
GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL,
|
||||
GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE,
|
||||
GHOSTTY_ACTION_TOGGLE_VISIBILITY,
|
||||
GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY,
|
||||
GHOSTTY_ACTION_MOVE_TAB,
|
||||
GHOSTTY_ACTION_GOTO_TAB,
|
||||
GHOSTTY_ACTION_GOTO_SPLIT,
|
||||
GHOSTTY_ACTION_GOTO_WINDOW,
|
||||
GHOSTTY_ACTION_RESIZE_SPLIT,
|
||||
GHOSTTY_ACTION_EQUALIZE_SPLITS,
|
||||
GHOSTTY_ACTION_TOGGLE_SPLIT_ZOOM,
|
||||
|
|
@ -837,6 +851,7 @@ typedef enum {
|
|||
GHOSTTY_ACTION_END_SEARCH,
|
||||
GHOSTTY_ACTION_SEARCH_TOTAL,
|
||||
GHOSTTY_ACTION_SEARCH_SELECTED,
|
||||
GHOSTTY_ACTION_READONLY,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
|
|
@ -845,6 +860,7 @@ typedef union {
|
|||
ghostty_action_move_tab_s move_tab;
|
||||
ghostty_action_goto_tab_e goto_tab;
|
||||
ghostty_action_goto_split_e goto_split;
|
||||
ghostty_action_goto_window_e goto_window;
|
||||
ghostty_action_resize_split_s resize_split;
|
||||
ghostty_action_size_limit_s size_limit;
|
||||
ghostty_action_initial_size_s initial_size;
|
||||
|
|
@ -874,6 +890,7 @@ typedef union {
|
|||
ghostty_action_start_search_s start_search;
|
||||
ghostty_action_search_total_s search_total;
|
||||
ghostty_action_search_selected_s search_selected;
|
||||
ghostty_action_readonly_e readonly;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
Features/QuickTerminal/QuickTerminal.xib,
|
||||
Features/QuickTerminal/QuickTerminalController.swift,
|
||||
Features/QuickTerminal/QuickTerminalPosition.swift,
|
||||
Features/QuickTerminal/QuickTerminalRestorableState.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreen.swift,
|
||||
Features/QuickTerminal/QuickTerminalScreenStateCache.swift,
|
||||
Features/QuickTerminal/QuickTerminalSize.swift,
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ class AppDelegate: NSObject,
|
|||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTitle: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTabTitle: NSMenuItem?
|
||||
@IBOutlet private var menuReadonly: NSMenuItem?
|
||||
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||
@IBOutlet private var menuCommandPalette: NSMenuItem?
|
||||
|
|
@ -98,11 +99,35 @@ class AppDelegate: NSObject,
|
|||
/// The global undo manager for app-level state such as window restoration.
|
||||
lazy var undoManager = ExpiringUndoManager()
|
||||
|
||||
/// The current state of the quick terminal.
|
||||
private var quickTerminalControllerState: QuickTerminalState = .uninitialized
|
||||
|
||||
/// Our quick terminal. This starts out uninitialized and only initializes if used.
|
||||
private(set) lazy var quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: derivedConfig.quickTerminalPosition
|
||||
)
|
||||
var quickController: QuickTerminalController {
|
||||
switch quickTerminalControllerState {
|
||||
case .initialized(let controller):
|
||||
return controller
|
||||
|
||||
case .pendingRestore(let state):
|
||||
let controller = QuickTerminalController(
|
||||
ghostty,
|
||||
position: derivedConfig.quickTerminalPosition,
|
||||
baseConfig: state.baseConfig,
|
||||
restorationState: state
|
||||
)
|
||||
quickTerminalControllerState = .initialized(controller)
|
||||
return controller
|
||||
|
||||
case .uninitialized:
|
||||
let controller = QuickTerminalController(
|
||||
ghostty,
|
||||
position: derivedConfig.quickTerminalPosition,
|
||||
restorationState: nil
|
||||
)
|
||||
quickTerminalControllerState = .initialized(controller)
|
||||
return controller
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages updates
|
||||
let updateController = UpdateController()
|
||||
|
|
@ -544,6 +569,7 @@ class AppDelegate: NSObject,
|
|||
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
|
||||
self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
|
||||
self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill")
|
||||
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
|
||||
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
|
||||
self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right")
|
||||
|
|
@ -994,10 +1020,31 @@ class AppDelegate: NSObject,
|
|||
|
||||
func application(_ app: NSApplication, willEncodeRestorableState coder: NSCoder) {
|
||||
Self.logger.debug("application will save window state")
|
||||
|
||||
guard ghostty.config.windowSaveState != "never" else { return }
|
||||
|
||||
// Encode our quick terminal state if we have it.
|
||||
switch quickTerminalControllerState {
|
||||
case .initialized(let controller) where controller.restorable:
|
||||
let data = QuickTerminalRestorableState(from: controller)
|
||||
data.encode(with: coder)
|
||||
|
||||
case .pendingRestore(let state):
|
||||
state.encode(with: coder)
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ app: NSApplication, didDecodeRestorableState coder: NSCoder) {
|
||||
Self.logger.debug("application will restore window state")
|
||||
|
||||
// Decode our quick terminal state.
|
||||
if ghostty.config.windowSaveState != "never",
|
||||
let state = QuickTerminalRestorableState(coder: coder) {
|
||||
quickTerminalControllerState = .pendingRestore(state)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - UNUserNotificationCenterDelegate
|
||||
|
|
@ -1271,6 +1318,16 @@ extension AppDelegate: NSMenuItemValidation {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the state of the quick terminal controller.
|
||||
private enum QuickTerminalState {
|
||||
/// Controller has not been initialized and has no pending restoration state.
|
||||
case uninitialized
|
||||
/// Restoration state is pending; controller will use this when first accessed.
|
||||
case pendingRestore(QuickTerminalRestorableState)
|
||||
/// Controller has been initialized.
|
||||
case initialized(QuickTerminalController)
|
||||
}
|
||||
|
||||
@globalActor
|
||||
fileprivate actor AppIconActor: GlobalActor {
|
||||
static let shared = AppIconActor()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
<outlet property="menuReadonly" destination="xpe-ia-Yjw" id="MMT-Sl-AfD"/>
|
||||
<outlet property="menuRedo" destination="EX8-lB-4s7" id="wON-2J-yT1"/>
|
||||
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
|
||||
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
|
||||
|
|
@ -328,6 +329,12 @@
|
|||
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Terminal Read-only" id="xpe-ia-Yjw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleReadonly:" target="-1" id="Gqx-wT-K9v"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Vkj-tP-dMZ"/>
|
||||
<menuItem title="Quick Terminal" id="1pv-LF-NBJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
private var previousActiveSpace: CGSSpace? = nil
|
||||
|
||||
/// Cache for per-screen window state.
|
||||
private let screenStateCache = QuickTerminalScreenStateCache()
|
||||
let screenStateCache: QuickTerminalScreenStateCache
|
||||
|
||||
/// Non-nil if we have hidden dock state.
|
||||
private var hiddenDock: HiddenDock? = nil
|
||||
|
|
@ -32,15 +32,27 @@ class QuickTerminalController: BaseTerminalController {
|
|||
|
||||
/// Tracks if we're currently handling a manual resize to prevent recursion
|
||||
private var isHandlingResize: Bool = false
|
||||
|
||||
|
||||
/// This is set to false by init if the window managed by this controller should not be restorable.
|
||||
/// For example, terminals executing custom scripts are not restorable.
|
||||
let restorable: Bool
|
||||
private var restorationState: QuickTerminalRestorableState?
|
||||
|
||||
init(_ ghostty: Ghostty.App,
|
||||
position: QuickTerminalPosition = .top,
|
||||
baseConfig base: Ghostty.SurfaceConfiguration? = nil,
|
||||
surfaceTree tree: SplitTree<Ghostty.SurfaceView>? = nil
|
||||
restorationState: QuickTerminalRestorableState? = nil,
|
||||
) {
|
||||
self.position = position
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
|
||||
// The window we manage is not restorable if we've specified a command
|
||||
// to execute. We do this because the restored window is meaningless at the
|
||||
// time of writing this: it'd just restore to a shell in the same directory
|
||||
// as the script. We may want to revisit this behavior when we have scrollback
|
||||
// restoration.
|
||||
restorable = (base?.command ?? "") == ""
|
||||
self.restorationState = restorationState
|
||||
self.screenStateCache = QuickTerminalScreenStateCache(stateByDisplay: restorationState?.screenStateEntries ?? [:])
|
||||
// Important detail here: we initialize with an empty surface tree so
|
||||
// that we don't start a terminal process. This gets started when the
|
||||
// first terminal is shown in `animateIn`.
|
||||
|
|
@ -104,8 +116,8 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// window close so we can animate out.
|
||||
window.delegate = self
|
||||
|
||||
// The quick window is not restorable (yet!). "Yet" because in theory we can
|
||||
// make this restorable, but it isn't currently implemented.
|
||||
// The quick window is restored by `screenStateCache`.
|
||||
// We disable this for better control
|
||||
window.isRestorable = false
|
||||
|
||||
// Setup our configured appearance that we support.
|
||||
|
|
@ -342,16 +354,33 @@ class QuickTerminalController: BaseTerminalController {
|
|||
// animate out.
|
||||
if surfaceTree.isEmpty,
|
||||
let ghostty_app = ghostty.app {
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1"
|
||||
|
||||
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
surfaceTree = SplitTree(view: view)
|
||||
focusedSurface = view
|
||||
if let tree = restorationState?.surfaceTree, !tree.isEmpty {
|
||||
surfaceTree = tree
|
||||
let view = tree.first(where: { $0.id.uuidString == restorationState?.focusedSurface }) ?? tree.first!
|
||||
focusedSurface = view
|
||||
// Add a short delay to check if the correct surface is focused.
|
||||
// Each SurfaceWrapper defaults its FocusedValue to itself; without this delay,
|
||||
// the tree often focuses the first surface instead of the intended one.
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
if !view.focused {
|
||||
self.focusedSurface = view
|
||||
self.makeWindowKey(window)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1"
|
||||
|
||||
let view = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
surfaceTree = SplitTree(view: view)
|
||||
focusedSurface = view
|
||||
}
|
||||
}
|
||||
|
||||
// Animate the window in
|
||||
animateWindowIn(window: window, from: position)
|
||||
// Clear the restoration state after first use
|
||||
restorationState = nil
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
|
|
@ -370,6 +399,22 @@ class QuickTerminalController: BaseTerminalController {
|
|||
animateWindowOut(window: window, to: position)
|
||||
}
|
||||
|
||||
func saveScreenState(exitFullscreen: Bool) {
|
||||
// If we are in fullscreen, then we exit fullscreen. We do this immediately so
|
||||
// we have th correct window.frame for the save state below.
|
||||
if exitFullscreen, let fullscreenStyle, fullscreenStyle.isFullscreen {
|
||||
fullscreenStyle.exit()
|
||||
}
|
||||
guard let window else { return }
|
||||
// Save the current window frame before animating out. This preserves
|
||||
// the user's preferred window size and position for when the quick
|
||||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
// would reset the window to its minimum content size.
|
||||
if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen {
|
||||
screenStateCache.save(frame: window.frame, for: screen)
|
||||
}
|
||||
}
|
||||
|
||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
|
|
@ -494,19 +539,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// If we are in fullscreen, then we exit fullscreen. We do this immediately so
|
||||
// we have th correct window.frame for the save state below.
|
||||
if let fullscreenStyle, fullscreenStyle.isFullscreen {
|
||||
fullscreenStyle.exit()
|
||||
}
|
||||
|
||||
// Save the current window frame before animating out. This preserves
|
||||
// the user's preferred window size and position for when the quick
|
||||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
// would reset the window to its minimum content size.
|
||||
if window.frame.width > 0 && window.frame.height > 0, let screen = window.screen {
|
||||
screenStateCache.save(frame: window.frame, for: screen)
|
||||
}
|
||||
saveScreenState(exitFullscreen: true)
|
||||
|
||||
// If we hid the dock then we unhide it.
|
||||
hiddenDock = nil
|
||||
|
|
@ -563,7 +596,7 @@ class QuickTerminalController: BaseTerminalController {
|
|||
})
|
||||
}
|
||||
|
||||
private func syncAppearance() {
|
||||
override func syncAppearance() {
|
||||
guard let window else { return }
|
||||
|
||||
defer { updateColorSchemeForSurfaceTree() }
|
||||
|
|
@ -575,7 +608,8 @@ class QuickTerminalController: BaseTerminalController {
|
|||
guard window.isVisible else { return }
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (self.derivedConfig.backgroundOpacity < 1) {
|
||||
// Also check if the user has overridden transparency to be fully opaque.
|
||||
if !isBackgroundOpaque && self.derivedConfig.backgroundOpacity < 1 {
|
||||
window.isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import Cocoa
|
||||
|
||||
struct QuickTerminalRestorableState: TerminalRestorable {
|
||||
static var version: Int { 1 }
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
let screenStateEntries: QuickTerminalScreenStateCache.Entries
|
||||
|
||||
init(from controller: QuickTerminalController) {
|
||||
controller.saveScreenState(exitFullscreen: true)
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
self.surfaceTree = controller.surfaceTree
|
||||
self.screenStateEntries = controller.screenStateCache.stateByDisplay
|
||||
}
|
||||
|
||||
init(copy other: QuickTerminalRestorableState) {
|
||||
self = other
|
||||
}
|
||||
|
||||
var baseConfig: Ghostty.SurfaceConfiguration? {
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
config.environmentVariables["GHOSTTY_QUICK_TERMINAL"] = "1"
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import Cocoa
|
|||
/// to restore to its previous size and position when reopened. It uses stable display UUIDs
|
||||
/// to survive NSScreen garbage collection and automatically prunes stale entries.
|
||||
class QuickTerminalScreenStateCache {
|
||||
typealias Entries = [UUID: DisplayEntry]
|
||||
|
||||
/// The maximum number of saved screen states we retain. This is to avoid some kind of
|
||||
/// pathological memory growth in case we get our screen state serializing wrong. I don't
|
||||
/// know anyone with more than 10 screens, so let's just arbitrarily go with that.
|
||||
|
|
@ -16,9 +18,10 @@ class QuickTerminalScreenStateCache {
|
|||
private static let screenStaleTTL: TimeInterval = 14 * 24 * 60 * 60
|
||||
|
||||
/// Keyed by display UUID to survive NSScreen garbage collection.
|
||||
private var stateByDisplay: [UUID: DisplayEntry] = [:]
|
||||
|
||||
init() {
|
||||
private(set) var stateByDisplay: Entries = [:]
|
||||
|
||||
init(stateByDisplay: Entries = [:]) {
|
||||
self.stateByDisplay = stateByDisplay
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(onScreensChanged(_:)),
|
||||
|
|
@ -96,7 +99,7 @@ class QuickTerminalScreenStateCache {
|
|||
}
|
||||
}
|
||||
|
||||
private struct DisplayEntry {
|
||||
struct DisplayEntry: Codable {
|
||||
var frame: NSRect
|
||||
var screenSize: CGSize
|
||||
var scale: CGFloat
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
/// Track whether background is forced opaque (true) or using config transparency (false)
|
||||
var isBackgroundOpaque: Bool = false
|
||||
|
||||
/// The cancellables related to our focused surface.
|
||||
private var focusedSurfaceCancellables: Set<AnyCancellable> = []
|
||||
|
||||
|
|
@ -621,9 +624,14 @@ class BaseTerminalController: NSWindowController,
|
|||
return
|
||||
}
|
||||
|
||||
// Remove the zoomed state for this surface tree.
|
||||
if surfaceTree.zoomed != nil {
|
||||
surfaceTree = .init(root: surfaceTree.root, zoomed: nil)
|
||||
if derivedConfig.splitPreserveZoom.contains(.navigation) {
|
||||
surfaceTree = SplitTree(
|
||||
root: surfaceTree.root,
|
||||
zoomed: surfaceTree.root?.node(view: nextSurface))
|
||||
} else {
|
||||
surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Move focus to the next surface
|
||||
|
|
@ -807,6 +815,35 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Appearance
|
||||
|
||||
/// Toggle the background opacity between transparent and opaque states.
|
||||
/// Do nothing if the configured background-opacity is >= 1 (already opaque).
|
||||
/// Subclasses should override this to add platform-specific checks and sync appearance.
|
||||
func toggleBackgroundOpacity() {
|
||||
// Do nothing if config is already fully opaque
|
||||
guard ghostty.config.backgroundOpacity < 1 else { return }
|
||||
|
||||
// Do nothing if in fullscreen (transparency doesn't apply in fullscreen)
|
||||
guard let window, !window.styleMask.contains(.fullScreen) else { return }
|
||||
|
||||
// Toggle between transparent and opaque
|
||||
isBackgroundOpaque.toggle()
|
||||
|
||||
// Update our appearance
|
||||
syncAppearance()
|
||||
}
|
||||
|
||||
/// Override this to resync any appearance related properties. This will be called automatically
|
||||
/// when certain window properties change that affect appearance. The list below should be updated
|
||||
/// as we add new things:
|
||||
///
|
||||
/// - ``toggleBackgroundOpacity``
|
||||
func syncAppearance() {
|
||||
// Purposely a no-op. This lets subclasses override this and we can call
|
||||
// it virtually from here.
|
||||
}
|
||||
|
||||
// MARK: Fullscreen
|
||||
|
||||
/// Toggle fullscreen for the given mode.
|
||||
|
|
@ -867,6 +904,9 @@ class BaseTerminalController: NSWindowController,
|
|||
} else {
|
||||
updateOverlayIsVisible = defaultUpdateOverlayVisibility()
|
||||
}
|
||||
|
||||
// Always resync our appearance
|
||||
syncAppearance()
|
||||
}
|
||||
|
||||
// MARK: Clipboard Confirmation
|
||||
|
|
@ -1188,17 +1228,20 @@ class BaseTerminalController: NSWindowController,
|
|||
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
|
||||
let windowStepResize: Bool
|
||||
let focusFollowsMouse: Bool
|
||||
let splitPreserveZoom: Ghostty.Config.SplitPreserveZoom
|
||||
|
||||
init() {
|
||||
self.macosTitlebarProxyIcon = .visible
|
||||
self.windowStepResize = false
|
||||
self.focusFollowsMouse = false
|
||||
self.splitPreserveZoom = .init()
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
|
||||
self.windowStepResize = config.windowStepResize
|
||||
self.focusFollowsMouse = config.focusFollowsMouse
|
||||
self.splitPreserveZoom = config.splitPreserveZoom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,17 +165,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override func fullscreenDidChange() {
|
||||
super.fullscreenDidChange()
|
||||
|
||||
// When our fullscreen state changes, we resync our appearance because some
|
||||
// properties change when fullscreen or not.
|
||||
guard let focusedSurface else { return }
|
||||
|
||||
syncAppearance(focusedSurface.derivedConfig)
|
||||
}
|
||||
|
||||
// MARK: Terminal Creation
|
||||
|
||||
/// Returns all the available terminal controllers present in the app currently.
|
||||
|
|
@ -489,6 +478,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
|||
tabWindowsHash = v
|
||||
self.relabelTabs()
|
||||
}
|
||||
|
||||
override func syncAppearance() {
|
||||
// When our focus changes, we update our window appearance based on the
|
||||
// currently focused surface.
|
||||
guard let focusedSurface else { return }
|
||||
syncAppearance(focusedSurface.derivedConfig)
|
||||
}
|
||||
|
||||
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
// Let our window handle its own appearance
|
||||
|
|
|
|||
|
|
@ -1,10 +1,47 @@
|
|||
import Cocoa
|
||||
|
||||
protocol TerminalRestorable: Codable {
|
||||
static var selfKey: String { get }
|
||||
static var versionKey: String { get }
|
||||
static var version: Int { get }
|
||||
init(copy other: Self)
|
||||
|
||||
/// Returns a base configuration to use when restoring terminal surfaces.
|
||||
/// Override this to provide custom environment variables or other configuration.
|
||||
var baseConfig: Ghostty.SurfaceConfiguration? { get }
|
||||
}
|
||||
|
||||
extension TerminalRestorable {
|
||||
static var selfKey: String { "state" }
|
||||
static var versionKey: String { "version" }
|
||||
|
||||
/// Default implementation returns nil (no custom base config).
|
||||
var baseConfig: Ghostty.SurfaceConfiguration? { nil }
|
||||
|
||||
init?(coder aDecoder: NSCoder) {
|
||||
// If the version doesn't match then we can't decode. In the future we can perform
|
||||
// version upgrading or something but for now we only have one version so we
|
||||
// don't bother.
|
||||
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(copy: v.value)
|
||||
}
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(Self.version, forKey: Self.versionKey)
|
||||
coder.encode(CodableBridge(self), forKey: Self.selfKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// The state stored for terminal window restoration.
|
||||
class TerminalRestorableState: Codable {
|
||||
static let selfKey = "state"
|
||||
static let versionKey = "version"
|
||||
static let version: Int = 7
|
||||
class TerminalRestorableState: TerminalRestorable {
|
||||
class var version: Int { 7 }
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
|
|
@ -20,28 +57,12 @@ class TerminalRestorableState: Codable {
|
|||
self.titleOverride = controller.titleOverride
|
||||
}
|
||||
|
||||
init?(coder aDecoder: NSCoder) {
|
||||
// If the version doesn't match then we can't decode. In the future we can perform
|
||||
// version upgrading or something but for now we only have one version so we
|
||||
// don't bother.
|
||||
guard aDecoder.decodeInteger(forKey: Self.versionKey) == Self.version else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let v = aDecoder.decodeObject(of: CodableBridge<Self>.self, forKey: Self.selfKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.surfaceTree = v.value.surfaceTree
|
||||
self.focusedSurface = v.value.focusedSurface
|
||||
self.effectiveFullscreenMode = v.value.effectiveFullscreenMode
|
||||
self.tabColor = v.value.tabColor
|
||||
self.titleOverride = v.value.titleOverride
|
||||
}
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(Self.version, forKey: Self.versionKey)
|
||||
coder.encode(CodableBridge(self), forKey: Self.selfKey)
|
||||
required init(copy other: TerminalRestorableState) {
|
||||
self.surfaceTree = other.surfaceTree
|
||||
self.focusedSurface = other.focusedSurface
|
||||
self.effectiveFullscreenMode = other.effectiveFullscreenMode
|
||||
self.tabColor = other.tabColor
|
||||
self.titleOverride = other.titleOverride
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -170,3 +191,5 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ class TerminalWindow: NSWindow {
|
|||
true
|
||||
}
|
||||
|
||||
/// Glass effect view for liquid glass background when transparency is enabled
|
||||
private var glassEffectView: NSView?
|
||||
|
||||
/// Gets the terminal controller from the window controller.
|
||||
var terminalController: TerminalController? {
|
||||
windowController as? TerminalController
|
||||
|
|
@ -466,7 +469,11 @@ class TerminalWindow: NSWindow {
|
|||
// Window transparency only takes effect if our window is not native fullscreen.
|
||||
// In native fullscreen we disable transparency/opacity because the background
|
||||
// becomes gray and widgets show through.
|
||||
//
|
||||
// Also check if the user has overridden transparency to be fully opaque.
|
||||
let forceOpaque = terminalController?.isBackgroundOpaque ?? false
|
||||
if !styleMask.contains(.fullScreen) &&
|
||||
!forceOpaque &&
|
||||
surfaceConfig.backgroundOpacity < 1
|
||||
{
|
||||
isOpaque = false
|
||||
|
|
@ -476,7 +483,15 @@ class TerminalWindow: NSWindow {
|
|||
// Terminal.app more easily.
|
||||
backgroundColor = .white.withAlphaComponent(0.001)
|
||||
|
||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
// Add liquid glass behind terminal content
|
||||
if #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle {
|
||||
setupGlassLayer()
|
||||
} else if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
// If we had a prior glass layer we should remove it
|
||||
if #available(macOS 26.0, *) {
|
||||
removeGlassLayer()
|
||||
}
|
||||
|
||||
ghostty_set_window_background_blur(
|
||||
appDelegate.ghostty.app,
|
||||
Unmanaged.passUnretained(self).toOpaque())
|
||||
|
|
@ -484,6 +499,11 @@ class TerminalWindow: NSWindow {
|
|||
} else {
|
||||
isOpaque = true
|
||||
|
||||
// Remove liquid glass when not transparent
|
||||
if #available(macOS 26.0, *) {
|
||||
removeGlassLayer()
|
||||
}
|
||||
|
||||
let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor)
|
||||
self.backgroundColor = backgroundColor.withAlphaComponent(1)
|
||||
}
|
||||
|
|
@ -562,19 +582,69 @@ class TerminalWindow: NSWindow {
|
|||
}
|
||||
}
|
||||
|
||||
#if compiler(>=6.2)
|
||||
// MARK: Glass
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private func setupGlassLayer() {
|
||||
// Remove existing glass effect view
|
||||
removeGlassLayer()
|
||||
|
||||
// Get the window content view (parent of the NSHostingView)
|
||||
guard let contentView else { return }
|
||||
guard let windowContentView = contentView.superview else { return }
|
||||
|
||||
// Create NSGlassEffectView for native glass effect
|
||||
let effectView = NSGlassEffectView()
|
||||
|
||||
// Map Ghostty config to NSGlassEffectView style
|
||||
switch derivedConfig.backgroundBlur {
|
||||
case .macosGlassRegular:
|
||||
effectView.style = NSGlassEffectView.Style.regular
|
||||
case .macosGlassClear:
|
||||
effectView.style = NSGlassEffectView.Style.clear
|
||||
default:
|
||||
// Should not reach here since we check for glass style before calling
|
||||
// setupGlassLayer()
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
effectView.cornerRadius = derivedConfig.windowCornerRadius
|
||||
effectView.tintColor = preferredBackgroundColor
|
||||
effectView.frame = windowContentView.bounds
|
||||
effectView.autoresizingMask = [.width, .height]
|
||||
|
||||
// Position BELOW the terminal content to act as background
|
||||
windowContentView.addSubview(effectView, positioned: .below, relativeTo: contentView)
|
||||
glassEffectView = effectView
|
||||
}
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
private func removeGlassLayer() {
|
||||
glassEffectView?.removeFromSuperview()
|
||||
glassEffectView = nil
|
||||
}
|
||||
#endif // compiler(>=6.2)
|
||||
|
||||
// MARK: Config
|
||||
|
||||
struct DerivedConfig {
|
||||
let title: String?
|
||||
let backgroundBlur: Ghostty.Config.BackgroundBlur
|
||||
let backgroundColor: NSColor
|
||||
let backgroundOpacity: Double
|
||||
let macosWindowButtons: Ghostty.MacOSWindowButtons
|
||||
let macosTitlebarStyle: String
|
||||
let windowCornerRadius: CGFloat
|
||||
|
||||
init() {
|
||||
self.title = nil
|
||||
self.backgroundColor = NSColor.windowBackgroundColor
|
||||
self.backgroundOpacity = 1
|
||||
self.macosWindowButtons = .visible
|
||||
self.backgroundBlur = .disabled
|
||||
self.macosTitlebarStyle = "transparent"
|
||||
self.windowCornerRadius = 16
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
|
|
@ -582,6 +652,18 @@ class TerminalWindow: NSWindow {
|
|||
self.backgroundColor = NSColor(config.backgroundColor)
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
self.macosWindowButtons = config.macosWindowButtons
|
||||
self.backgroundBlur = config.backgroundBlur
|
||||
self.macosTitlebarStyle = config.macosTitlebarStyle
|
||||
|
||||
// Set corner radius based on macos-titlebar-style
|
||||
// Native, transparent, and hidden styles use 16pt radius
|
||||
// Tabs style uses 20pt radius
|
||||
switch config.macosTitlebarStyle {
|
||||
case "tabs":
|
||||
self.windowCornerRadius = 20
|
||||
default:
|
||||
self.windowCornerRadius = 16
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -708,8 +790,8 @@ extension TerminalWindow {
|
|||
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
|
||||
guard NSApp.keyWindow === self else { return false }
|
||||
|
||||
// These are the target selectors, at least for macOS 26.
|
||||
let tabContextSelectors: Set<String> = [
|
||||
// These selectors must all exist for it to be a tab context menu.
|
||||
let requiredSelectors: Set<String> = [
|
||||
"performClose:",
|
||||
"performCloseOtherTabs:",
|
||||
"moveTabToNewWindow:",
|
||||
|
|
@ -717,7 +799,7 @@ extension TerminalWindow {
|
|||
]
|
||||
|
||||
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
|
||||
return !selectorNames.isDisjoint(with: tabContextSelectors)
|
||||
return requiredSelectors.isSubset(of: selectorNames)
|
||||
}
|
||||
|
||||
private func appendTabModifierSection(to menu: NSMenu, target: TerminalController?) {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
|||
// color of the titlebar in native fullscreen view.
|
||||
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
|
||||
titlebarView.wantsLayer = true
|
||||
titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
||||
|
||||
// For glass background styles, use a transparent titlebar to let the glass effect show through
|
||||
// Only apply this for transparent and tabs titlebar styles
|
||||
let isGlassStyle = derivedConfig.backgroundBlur.isGlassStyle
|
||||
let isTransparentTitlebar = derivedConfig.macosTitlebarStyle == "transparent" ||
|
||||
derivedConfig.macosTitlebarStyle == "tabs"
|
||||
|
||||
titlebarView.layer?.backgroundColor = (isGlassStyle && isTransparentTitlebar)
|
||||
? NSColor.clear.cgColor
|
||||
: preferredBackgroundColor?.cgColor
|
||||
}
|
||||
|
||||
// In all cases, we have to hide the background view since this has multiple subviews
|
||||
|
|
|
|||
|
|
@ -501,6 +501,9 @@ extension Ghostty {
|
|||
case GHOSTTY_ACTION_GOTO_SPLIT:
|
||||
return gotoSplit(app, target: target, direction: action.action.goto_split)
|
||||
|
||||
case GHOSTTY_ACTION_GOTO_WINDOW:
|
||||
return gotoWindow(app, target: target, direction: action.action.goto_window)
|
||||
|
||||
case GHOSTTY_ACTION_RESIZE_SPLIT:
|
||||
resizeSplit(app, target: target, resize: action.action.resize_split)
|
||||
|
||||
|
|
@ -570,6 +573,9 @@ extension Ghostty {
|
|||
case GHOSTTY_ACTION_TOGGLE_VISIBILITY:
|
||||
toggleVisibility(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_BACKGROUND_OPACITY:
|
||||
toggleBackgroundOpacity(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
keySequence(app, target: target, v: action.action.key_sequence)
|
||||
|
||||
|
|
@ -588,6 +594,9 @@ extension Ghostty {
|
|||
case GHOSTTY_ACTION_RING_BELL:
|
||||
ringBell(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_READONLY:
|
||||
setReadonly(app, target: target, v: action.action.readonly)
|
||||
|
||||
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
||||
checkForUpdates(app)
|
||||
|
||||
|
|
@ -1010,6 +1019,31 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
private static func setReadonly(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_readonly_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set readonly does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyDidChangeReadonly,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.ReadonlyKey: v == GHOSTTY_READONLY_ON,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func moveTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
|
@ -1121,6 +1155,64 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
private static func gotoWindow(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
direction: ghostty_action_goto_window_e
|
||||
) -> Bool {
|
||||
// Collect candidate windows: visible terminal windows that are either
|
||||
// standalone or the currently selected tab in their tab group. This
|
||||
// treats each native tab group as a single "window" for navigation
|
||||
// purposes, since goto_tab handles per-tab navigation.
|
||||
let candidates: [NSWindow] = NSApplication.shared.windows.filter { window in
|
||||
guard window.windowController is BaseTerminalController else { return false }
|
||||
guard window.isVisible, !window.isMiniaturized else { return false }
|
||||
// For native tabs, only include the selected tab in each group
|
||||
if let group = window.tabGroup, group.selectedWindow !== window {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Need at least two windows to navigate between
|
||||
guard candidates.count > 1 else { return false }
|
||||
|
||||
// Find starting index from the current key/main window
|
||||
let startIndex = candidates.firstIndex(where: { $0.isKeyWindow })
|
||||
?? candidates.firstIndex(where: { $0.isMainWindow })
|
||||
?? 0
|
||||
|
||||
let step: Int
|
||||
switch direction {
|
||||
case GHOSTTY_GOTO_WINDOW_NEXT:
|
||||
step = 1
|
||||
case GHOSTTY_GOTO_WINDOW_PREVIOUS:
|
||||
step = -1
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// Iterate with wrap-around until we find a valid window or return to start
|
||||
let count = candidates.count
|
||||
var index = (startIndex + step + count) % count
|
||||
|
||||
while index != startIndex {
|
||||
let candidate = candidates[index]
|
||||
if candidate.isVisible, !candidate.isMiniaturized {
|
||||
candidate.makeKeyAndOrderFront(nil)
|
||||
// Also focus the terminal surface within the window
|
||||
if let controller = candidate.windowController as? BaseTerminalController,
|
||||
let surface = controller.focusedSurface {
|
||||
Ghostty.moveFocus(to: surface)
|
||||
}
|
||||
return true
|
||||
}
|
||||
index = (index + step + count) % count
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private static func resizeSplit(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
|
@ -1286,6 +1378,27 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
private static func toggleBackgroundOpacity(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s
|
||||
) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("toggle background opacity does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface,
|
||||
let surfaceView = self.surfaceView(from: surface),
|
||||
let controller = surfaceView.window?.windowController as? BaseTerminalController else { return }
|
||||
|
||||
controller.toggleBackgroundOpacity()
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleSecureInput(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,14 @@ extension Ghostty {
|
|||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var splitPreserveZoom: SplitPreserveZoom {
|
||||
guard let config = self.config else { return .init() }
|
||||
var v: CUnsignedInt = 0
|
||||
let key = "split-preserve-zoom"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() }
|
||||
return .init(rawValue: v)
|
||||
}
|
||||
|
||||
var initialWindow: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = true;
|
||||
|
|
@ -402,12 +410,12 @@ extension Ghostty {
|
|||
return v;
|
||||
}
|
||||
|
||||
var backgroundBlurRadius: Int {
|
||||
guard let config = self.config else { return 1 }
|
||||
var v: Int = 0
|
||||
var backgroundBlur: BackgroundBlur {
|
||||
guard let config = self.config else { return .disabled }
|
||||
var v: Int16 = 0
|
||||
let key = "background-blur"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
|
||||
return v;
|
||||
return BackgroundBlur(fromCValue: v)
|
||||
}
|
||||
|
||||
var unfocusedSplitOpacity: Double {
|
||||
|
|
@ -626,6 +634,60 @@ extension Ghostty.Config {
|
|||
case download
|
||||
}
|
||||
|
||||
/// Background blur configuration that maps from the C API values.
|
||||
/// Positive values represent blur radius, special negative values
|
||||
/// represent macOS-specific glass effects.
|
||||
enum BackgroundBlur: Equatable {
|
||||
case disabled
|
||||
case radius(Int)
|
||||
case macosGlassRegular
|
||||
case macosGlassClear
|
||||
|
||||
init(fromCValue value: Int16) {
|
||||
switch value {
|
||||
case 0:
|
||||
self = .disabled
|
||||
case -1:
|
||||
self = .macosGlassRegular
|
||||
case -2:
|
||||
self = .macosGlassClear
|
||||
default:
|
||||
self = .radius(Int(value))
|
||||
}
|
||||
}
|
||||
|
||||
var isEnabled: Bool {
|
||||
switch self {
|
||||
case .disabled:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if this is a macOS glass style (regular or clear).
|
||||
var isGlassStyle: Bool {
|
||||
switch self {
|
||||
case .macosGlassRegular, .macosGlassClear:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the blur radius if applicable, nil for glass effects.
|
||||
var radius: Int? {
|
||||
switch self {
|
||||
case .disabled:
|
||||
return nil
|
||||
case .radius(let r):
|
||||
return r
|
||||
case .macosGlassRegular, .macosGlassClear:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BellFeatures: OptionSet {
|
||||
let rawValue: CUnsignedInt
|
||||
|
||||
|
|
@ -635,6 +697,12 @@ extension Ghostty.Config {
|
|||
static let title = BellFeatures(rawValue: 1 << 3)
|
||||
static let border = BellFeatures(rawValue: 1 << 4)
|
||||
}
|
||||
|
||||
struct SplitPreserveZoom: OptionSet {
|
||||
let rawValue: CUnsignedInt
|
||||
|
||||
static let navigation = SplitPreserveZoom(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
enum MacDockDropBehavior: String {
|
||||
case new_tab = "new-tab"
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ extension Ghostty {
|
|||
case app
|
||||
case zig_run
|
||||
}
|
||||
|
||||
|
||||
/// Returns the mechanism that launched the app. This is based on an env var so
|
||||
/// its up to the env var being set in the correct circumstance.
|
||||
static var launchSource: LaunchSource {
|
||||
|
|
@ -65,7 +65,7 @@ extension Ghostty {
|
|||
// source. If its unset we assume we're in a CLI environment.
|
||||
return .cli
|
||||
}
|
||||
|
||||
|
||||
// If the env var is set but its unknown then we default back to the app.
|
||||
return LaunchSource(rawValue: envValue) ?? .app
|
||||
}
|
||||
|
|
@ -76,17 +76,17 @@ extension Ghostty {
|
|||
extension Ghostty {
|
||||
class AllocatedString {
|
||||
private let cString: ghostty_string_s
|
||||
|
||||
|
||||
init(_ c: ghostty_string_s) {
|
||||
self.cString = c
|
||||
}
|
||||
|
||||
|
||||
var string: String {
|
||||
guard let ptr = cString.ptr else { return "" }
|
||||
let data = Data(bytes: ptr, count: Int(cString.len))
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
ghostty_string_free(cString)
|
||||
}
|
||||
|
|
@ -391,6 +391,10 @@ extension Notification.Name {
|
|||
|
||||
/// Ring the bell
|
||||
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
|
||||
|
||||
/// Readonly mode changed
|
||||
static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly")
|
||||
static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly"
|
||||
static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle")
|
||||
|
||||
/// Toggle maximize of current window
|
||||
|
|
|
|||
|
|
@ -116,6 +116,13 @@ extension Ghostty {
|
|||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Readonly indicator badge
|
||||
if surfaceView.readonly {
|
||||
ReadonlyBadge {
|
||||
surfaceView.toggleReadonly(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in the middle of a key sequence, then we show a visual element. We only
|
||||
// support this on macOS currently although in theory we can support mobile with keyboards!
|
||||
if !surfaceView.keySequence.isEmpty {
|
||||
|
|
@ -757,6 +764,96 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Readonly Badge
|
||||
|
||||
/// A badge overlay that indicates a surface is in readonly mode.
|
||||
/// Positioned in the top-right corner and styled to be noticeable but unobtrusive.
|
||||
struct ReadonlyBadge: View {
|
||||
let onDisable: () -> Void
|
||||
|
||||
@State private var showingPopover = false
|
||||
|
||||
private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8)
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "eye.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("Read-only")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(badgeBackground)
|
||||
.foregroundStyle(badgeColor)
|
||||
.onTapGesture {
|
||||
showingPopover = true
|
||||
}
|
||||
.backport.pointerStyle(.link)
|
||||
.popover(isPresented: $showingPopover, arrowEdge: .bottom) {
|
||||
ReadonlyPopoverView(onDisable: onDisable, isPresented: $showingPopover)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Read-only terminal")
|
||||
}
|
||||
|
||||
private var badgeBackground: some View {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(.regularMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color.orange.opacity(0.6), lineWidth: 1.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadonlyPopoverView: View {
|
||||
let onDisable: () -> Void
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 13))
|
||||
Text("Read-Only Mode")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
}
|
||||
|
||||
Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Disable") {
|
||||
onDisable()
|
||||
isPresented = false
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(width: 280)
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
/// When changing the split state, or going full screen (native or non), the terminal view
|
||||
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ extension Ghostty {
|
|||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published private(set) var bell: Bool = false
|
||||
|
||||
/// True when the surface is in readonly mode.
|
||||
@Published private(set) var readonly: Bool = false
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
|
|
@ -333,6 +336,11 @@ extension Ghostty {
|
|||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
name: .ghosttyBellDidRing,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidChangeReadonly(_:)),
|
||||
name: .ghosttyDidChangeReadonly,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
|
|
@ -703,6 +711,11 @@ extension Ghostty {
|
|||
bell = true
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidChangeReadonly(_ notification: SwiftUI.Notification) {
|
||||
guard let value = notification.userInfo?[SwiftUI.Notification.Name.ReadonlyKey] as? Bool else { return }
|
||||
readonly = value
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
|
|
@ -1416,6 +1429,9 @@ extension Ghostty {
|
|||
item.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise")
|
||||
item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
|
||||
item.setImageIfDesired(systemSymbolName: "scope")
|
||||
item = menu.addItem(withTitle: "Terminal Read-only", action: #selector(toggleReadonly(_:)), keyEquivalent: "")
|
||||
item.setImageIfDesired(systemSymbolName: "eye.fill")
|
||||
item.state = readonly ? .on : .off
|
||||
menu.addItem(.separator())
|
||||
item = menu.addItem(withTitle: "Change Tab Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "")
|
||||
item.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
|
|
@ -1499,6 +1515,14 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleReadonly(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "toggle_readonly"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
|
|
@ -1975,6 +1999,10 @@ extension Ghostty.SurfaceView: NSMenuItemValidation {
|
|||
case #selector(findHide):
|
||||
return searchState != nil
|
||||
|
||||
case #selector(toggleReadonly):
|
||||
item.state = readonly ? .on : .off
|
||||
return true
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ extension Ghostty {
|
|||
|
||||
// The current search state. When non-nil, the search overlay should be shown.
|
||||
@Published var searchState: SearchState? = nil
|
||||
|
||||
/// True when the surface is in readonly mode.
|
||||
@Published private(set) var readonly: Bool = false
|
||||
|
||||
// Returns sizing information for the surface. This is the raw C
|
||||
// structure because I'm lazy.
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@
|
|||
wayland-scanner,
|
||||
wayland-protocols,
|
||||
zon2nix,
|
||||
system,
|
||||
pkgs,
|
||||
# needed by GTK for loading SVG icons while running from within the
|
||||
# developer shell
|
||||
|
|
@ -100,7 +99,7 @@ in
|
|||
scdoc
|
||||
zig
|
||||
zip
|
||||
zon2nix.packages.${system}.zon2nix
|
||||
zon2nix.packages.${stdenv.hostPlatform.system}.zon2nix
|
||||
|
||||
# For web and wasm stuff
|
||||
nodejs
|
||||
|
|
|
|||
|
|
@ -0,0 +1,283 @@
|
|||
{
|
||||
self,
|
||||
system,
|
||||
nixpkgs,
|
||||
home-manager,
|
||||
...
|
||||
}: let
|
||||
nixos-version = nixpkgs.lib.trivial.release;
|
||||
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
self.overlays.debug
|
||||
];
|
||||
};
|
||||
|
||||
pink_value = "#FF0087";
|
||||
|
||||
color_test = ''
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
def check_for_pink(final=False) -> bool:
|
||||
with tempfile.NamedTemporaryFile() as tmpin:
|
||||
machine.send_monitor_command("screendump {}".format(tmpin.name))
|
||||
|
||||
cmd = 'convert {} -define histogram:unique-colors=true -format "%c" histogram:info:'.format(
|
||||
tmpin.name
|
||||
)
|
||||
ret = subprocess.run(cmd, shell=True, capture_output=True)
|
||||
if ret.returncode != 0:
|
||||
raise Exception(
|
||||
"image analysis failed with exit code {}".format(ret.returncode)
|
||||
)
|
||||
|
||||
text = ret.stdout.decode("utf-8")
|
||||
return "${pink_value}" in text
|
||||
'';
|
||||
|
||||
mkNodeGnome = {
|
||||
config,
|
||||
pkgs,
|
||||
settings,
|
||||
sshPort ? null,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./vm/wayland-gnome.nix
|
||||
settings
|
||||
];
|
||||
|
||||
virtualisation = {
|
||||
forwardPorts = pkgs.lib.optionals (sshPort != null) [
|
||||
{
|
||||
from = "host";
|
||||
host.port = sshPort;
|
||||
guest.port = 22;
|
||||
}
|
||||
];
|
||||
|
||||
vmVariant = {
|
||||
virtualisation.host.pkgs = pkgs;
|
||||
};
|
||||
};
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "yes";
|
||||
PermitEmptyPasswords = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
security.pam.services.sshd.allowNullPassword = true;
|
||||
|
||||
users.groups.ghostty = {
|
||||
gid = 1000;
|
||||
};
|
||||
|
||||
users.users.ghostty = {
|
||||
uid = 1000;
|
||||
};
|
||||
|
||||
home-manager = {
|
||||
users = {
|
||||
ghostty = {
|
||||
home = {
|
||||
username = config.users.users.ghostty.name;
|
||||
homeDirectory = config.users.users.ghostty.home;
|
||||
stateVersion = nixos-version;
|
||||
};
|
||||
programs.ssh = {
|
||||
enable = true;
|
||||
extraOptionOverrides = {
|
||||
StrictHostKeyChecking = "accept-new";
|
||||
UserKnownHostsFile = "/dev/null";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
system.stateVersion = nixos-version;
|
||||
};
|
||||
|
||||
mkTestGnome = {
|
||||
name,
|
||||
settings,
|
||||
testScript,
|
||||
ocr ? false,
|
||||
}:
|
||||
pkgs.testers.runNixOSTest {
|
||||
name = name;
|
||||
|
||||
enableOCR = ocr;
|
||||
|
||||
extraBaseModules = {
|
||||
imports = [
|
||||
home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
|
||||
nodes = {
|
||||
machine = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
mkNodeGnome {
|
||||
inherit config pkgs settings;
|
||||
sshPort = 2222;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = testScript;
|
||||
};
|
||||
in {
|
||||
basic-version-check = pkgs.testers.runNixOSTest {
|
||||
name = "basic-version-check";
|
||||
nodes = {
|
||||
machine = {pkgs, ...}: {
|
||||
users.groups.ghostty = {};
|
||||
users.users.ghostty = {
|
||||
isNormalUser = true;
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
hashedPassword = "";
|
||||
packages = [
|
||||
pkgs.ghostty
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = {...}: ''
|
||||
machine.succeed("su - ghostty -c 'ghostty +version'")
|
||||
'';
|
||||
};
|
||||
|
||||
basic-window-check-gnome = mkTestGnome {
|
||||
name = "basic-window-check-gnome";
|
||||
settings = {
|
||||
home-manager.users.ghostty = {
|
||||
xdg.configFile = {
|
||||
"ghostty/config".text = ''
|
||||
background = ${pink_value}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
ocr = true;
|
||||
testScript = {nodes, ...}: let
|
||||
user = nodes.machine.users.users.ghostty;
|
||||
bus_path = "/run/user/${toString user.uid}/bus";
|
||||
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=${bus_path}";
|
||||
gdbus = "${bus} gdbus";
|
||||
ghostty = "${bus} ghostty";
|
||||
su = command: "su - ${user.name} -c '${command}'";
|
||||
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
|
||||
wm_class = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
|
||||
in ''
|
||||
${color_test}
|
||||
|
||||
with subtest("wait for x"):
|
||||
start_all()
|
||||
machine.wait_for_x()
|
||||
|
||||
machine.wait_for_file("${bus_path}")
|
||||
|
||||
with subtest("Ensuring no pink is present without the terminal."):
|
||||
assert (
|
||||
check_for_pink() == False
|
||||
), "Pink was present on the screen before we even launched a terminal!"
|
||||
|
||||
machine.systemctl("enable app-com.mitchellh.ghostty-debug.service", user="${user.name}")
|
||||
machine.succeed("${su "${ghostty} +new-window"}")
|
||||
machine.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'")
|
||||
|
||||
machine.sleep(2)
|
||||
|
||||
with subtest("Have the terminal display a color."):
|
||||
assert(
|
||||
check_for_pink() == True
|
||||
), "Pink was not found on the screen!"
|
||||
|
||||
machine.systemctl("stop app-com.mitchellh.ghostty-debug.service", user="${user.name}")
|
||||
'';
|
||||
};
|
||||
|
||||
ssh-integration-test = pkgs.testers.runNixOSTest {
|
||||
name = "ssh-integration-test";
|
||||
extraBaseModules = {
|
||||
imports = [
|
||||
home-manager.nixosModules.home-manager
|
||||
];
|
||||
};
|
||||
nodes = {
|
||||
server = {...}: {
|
||||
users.groups.ghostty = {};
|
||||
users.users.ghostty = {
|
||||
isNormalUser = true;
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
hashedPassword = "";
|
||||
packages = [];
|
||||
};
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "yes";
|
||||
PermitEmptyPasswords = "yes";
|
||||
};
|
||||
};
|
||||
security.pam.services.sshd.allowNullPassword = true;
|
||||
};
|
||||
client = {
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
mkNodeGnome {
|
||||
inherit config pkgs;
|
||||
settings = {
|
||||
home-manager.users.ghostty = {
|
||||
xdg.configFile = {
|
||||
"ghostty/config".text = let
|
||||
in ''
|
||||
shell-integration-features = ssh-terminfo
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
sshPort = 2222;
|
||||
};
|
||||
};
|
||||
testScript = {nodes, ...}: let
|
||||
user = nodes.client.users.users.ghostty;
|
||||
bus_path = "/run/user/${toString user.uid}/bus";
|
||||
bus = "DBUS_SESSION_BUS_ADDRESS=unix:path=${bus_path}";
|
||||
gdbus = "${bus} gdbus";
|
||||
ghostty = "${bus} ghostty";
|
||||
su = command: "su - ${user.name} -c '${command}'";
|
||||
gseval = "call --session -d org.gnome.Shell -o /org/gnome/Shell -m org.gnome.Shell.Eval";
|
||||
wm_class = su "${gdbus} ${gseval} global.display.focus_window.wm_class";
|
||||
in ''
|
||||
with subtest("Start server and wait for ssh to be ready."):
|
||||
server.start()
|
||||
server.wait_for_open_port(22)
|
||||
|
||||
with subtest("Start client and wait for ghostty window."):
|
||||
client.start()
|
||||
client.wait_for_x()
|
||||
client.wait_for_file("${bus_path}")
|
||||
client.systemctl("enable app-com.mitchellh.ghostty-debug.service", user="${user.name}")
|
||||
client.succeed("${su "${ghostty} +new-window"}")
|
||||
client.wait_until_succeeds("${wm_class} | grep -q 'com.mitchellh.ghostty-debug'")
|
||||
|
||||
with subtest("SSH from client to server and verify that the Ghostty terminfo is copied.")
|
||||
client.sleep(2)
|
||||
client.send_chars("ssh ghostty@server\n")
|
||||
server.wait_for_file("${user.home}/.terminfo/x/xterm-ghostty", timeout=30)
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
@ -22,6 +22,19 @@
|
|||
};
|
||||
};
|
||||
|
||||
systemd.user.services = {
|
||||
"org.gnome.Shell@wayland" = {
|
||||
serviceConfig = {
|
||||
ExecStart = [
|
||||
# Clear the list before overriding it.
|
||||
""
|
||||
# Eval API is now internal so Shell needs to run in unsafe mode.
|
||||
"${pkgs.gnome-shell}/bin/gnome-shell --unsafe-mode"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.gnomeExtensions.no-overview
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
|
||||
documentation.nixos.enable = false;
|
||||
|
||||
networking.hostName = "ghostty";
|
||||
networking.domain = "mitchellh.com";
|
||||
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.memorySize = 2048;
|
||||
};
|
||||
|
|
@ -28,17 +25,11 @@
|
|||
users.groups.ghostty = {};
|
||||
|
||||
users.users.ghostty = {
|
||||
isNormalUser = true;
|
||||
description = "Ghostty";
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
isNormalUser = true;
|
||||
initialPassword = "ghostty";
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"xdg/autostart/com.mitchellh.ghostty.desktop" = {
|
||||
source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop";
|
||||
};
|
||||
hashedPassword = "";
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
|
|
@ -61,6 +52,7 @@
|
|||
|
||||
services.displayManager = {
|
||||
autoLogin = {
|
||||
enable = true;
|
||||
user = "ghostty";
|
||||
};
|
||||
};
|
||||
|
|
|
|||
161
src/Surface.zig
161
src/Surface.zig
|
|
@ -145,6 +145,12 @@ focused: bool = true,
|
|||
/// Used to determine whether to continuously scroll.
|
||||
selection_scroll_active: bool = false,
|
||||
|
||||
/// True if the surface is in read-only mode. When read-only, no input
|
||||
/// is sent to the PTY but terminal-level operations like selections,
|
||||
/// (native) scrolling, and copy keybinds still work. Warn before quit is
|
||||
/// always enabled in this state.
|
||||
readonly: bool = false,
|
||||
|
||||
/// Used to send notifications that long running commands have finished.
|
||||
/// Requires that shell integration be active. Should represent a nanosecond
|
||||
/// precision timestamp. It does not necessarily need to correspond to the
|
||||
|
|
@ -601,6 +607,9 @@ pub fn init(
|
|||
};
|
||||
errdefer env.deinit();
|
||||
|
||||
// don't leak GHOSTTY_LOG to any subprocesses
|
||||
env.remove("GHOSTTY_LOG");
|
||||
|
||||
// Initialize our IO backend
|
||||
var io_exec = try termio.Exec.init(alloc, .{
|
||||
.command = command,
|
||||
|
|
@ -812,6 +821,30 @@ inline fn surfaceMailbox(self: *Surface) Mailbox {
|
|||
};
|
||||
}
|
||||
|
||||
/// Queue a message for the IO thread.
|
||||
///
|
||||
/// We centralize all our logic into this spot so we can intercept
|
||||
/// messages for example in readonly mode.
|
||||
fn queueIo(
|
||||
self: *Surface,
|
||||
msg: termio.Message,
|
||||
mutex: termio.Termio.MutexState,
|
||||
) void {
|
||||
// In readonly mode, we don't allow any writes through to the pty.
|
||||
if (self.readonly) {
|
||||
switch (msg) {
|
||||
.write_small,
|
||||
.write_stable,
|
||||
.write_alloc,
|
||||
=> return,
|
||||
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.io.queueMessage(msg, mutex);
|
||||
}
|
||||
|
||||
/// Forces the surface to render. This is useful for when the surface
|
||||
/// is in the middle of animation (such as a resize, etc.) or when
|
||||
/// the render timer is managed manually by the apprt.
|
||||
|
|
@ -843,7 +876,7 @@ pub fn activateInspector(self: *Surface) !void {
|
|||
|
||||
// Notify our components we have an inspector active
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
|
||||
self.io.queueMessage(.{ .inspector = true }, .unlocked);
|
||||
self.queueIo(.{ .inspector = true }, .unlocked);
|
||||
}
|
||||
|
||||
/// Deactivate the inspector and stop collecting any information.
|
||||
|
|
@ -860,7 +893,7 @@ pub fn deactivateInspector(self: *Surface) void {
|
|||
|
||||
// Notify our components we have deactivated inspector
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
|
||||
self.io.queueMessage(.{ .inspector = false }, .unlocked);
|
||||
self.queueIo(.{ .inspector = false }, .unlocked);
|
||||
|
||||
// Deinit the inspector
|
||||
insp.deinit();
|
||||
|
|
@ -871,6 +904,9 @@ pub fn deactivateInspector(self: *Surface) void {
|
|||
/// True if the surface requires confirmation to quit. This should be called
|
||||
/// by apprt to determine if the surface should confirm before quitting.
|
||||
pub fn needsConfirmQuit(self: *Surface) bool {
|
||||
// If the surface is in read-only mode, always require confirmation
|
||||
if (self.readonly) return true;
|
||||
|
||||
// If the child has exited, then our process is certainly not alive.
|
||||
// We check this first to avoid the locking overhead below.
|
||||
if (self.child_exited) return false;
|
||||
|
|
@ -929,7 +965,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||
// We always use an allocating message because we don't know
|
||||
// the length of the title and this isn't a performance critical
|
||||
// path.
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.write_alloc = .{
|
||||
.alloc = self.alloc,
|
||||
.data = data,
|
||||
|
|
@ -1121,7 +1157,7 @@ fn selectionScrollTick(self: *Surface) !void {
|
|||
// If our screen changed while this is happening, we stop our
|
||||
// selection scroll.
|
||||
if (self.mouse.left_click_screen != t.screens.active_key) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -1353,7 +1389,7 @@ fn reportColorScheme(self: *Surface, force: bool) void {
|
|||
.dark => "\x1B[?997;1n",
|
||||
};
|
||||
|
||||
self.io.queueMessage(.{ .write_stable = output }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = output }, .unlocked);
|
||||
}
|
||||
|
||||
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
|
||||
|
|
@ -1726,7 +1762,7 @@ pub fn updateConfig(
|
|||
errdefer termio_config_ptr.deinit();
|
||||
|
||||
_ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} });
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.change_config = .{
|
||||
.alloc = self.alloc,
|
||||
.ptr = termio_config_ptr,
|
||||
|
|
@ -2001,6 +2037,23 @@ pub fn pwd(
|
|||
return try alloc.dupe(u8, terminal_pwd);
|
||||
}
|
||||
|
||||
/// Resolves a relative file path to an absolute path using the terminal's pwd.
|
||||
fn resolvePathForOpening(
|
||||
self: *Surface,
|
||||
path: []const u8,
|
||||
) Allocator.Error!?[]const u8 {
|
||||
if (!std.fs.path.isAbsolute(path)) {
|
||||
const terminal_pwd = self.io.terminal.getPwd() orelse {
|
||||
return null;
|
||||
};
|
||||
|
||||
const resolved = try std.fs.path.resolve(self.alloc, &.{ terminal_pwd, path });
|
||||
return resolved;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the x/y coordinate of where the IME (Input Method Editor)
|
||||
/// keyboard should be rendered.
|
||||
pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
|
|
@ -2292,7 +2345,7 @@ fn setCellSize(self: *Surface, size: rendererpkg.CellSize) !void {
|
|||
self.balancePaddingIfNeeded();
|
||||
|
||||
// Notify the terminal
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
self.queueIo(.{ .resize = self.size }, .unlocked);
|
||||
|
||||
// Update our terminal default size if necessary.
|
||||
self.recomputeInitialSize() catch |err| {
|
||||
|
|
@ -2395,7 +2448,7 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void {
|
|||
}
|
||||
|
||||
// Mail the IO thread
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
self.queueIo(.{ .resize = self.size }, .unlocked);
|
||||
}
|
||||
|
||||
/// Recalculate the balanced padding if needed.
|
||||
|
|
@ -2671,7 +2724,7 @@ pub fn keyCallback(
|
|||
}
|
||||
|
||||
errdefer write_req.deinit();
|
||||
self.io.queueMessage(switch (write_req) {
|
||||
self.queueIo(switch (write_req) {
|
||||
.small => |v| .{ .write_small = v },
|
||||
.stable => |v| .{ .write_stable = v },
|
||||
.alloc => |v| .{ .write_alloc = v },
|
||||
|
|
@ -2900,7 +2953,7 @@ fn endKeySequence(
|
|||
if (self.keyboard.queued.items.len > 0) {
|
||||
switch (action) {
|
||||
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||
self.io.queueMessage(switch (write_req) {
|
||||
self.queueIo(switch (write_req) {
|
||||
.small => |v| .{ .write_small = v },
|
||||
.stable => |v| .{ .write_stable = v },
|
||||
.alloc => |v| .{ .write_alloc = v },
|
||||
|
|
@ -3126,7 +3179,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
|||
self.renderer_state.mutex.lock();
|
||||
self.io.terminal.flags.focused = focused;
|
||||
self.renderer_state.mutex.unlock();
|
||||
self.io.queueMessage(.{ .focused = focused }, .unlocked);
|
||||
self.queueIo(.{ .focused = focused }, .unlocked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3290,7 +3343,7 @@ pub fn scrollCallback(
|
|||
};
|
||||
};
|
||||
for (0..y.magnitude()) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = seq }, .locked);
|
||||
self.queueIo(.{ .write_stable = seq }, .locked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3511,7 +3564,7 @@ fn mouseReport(
|
|||
data[5] = 32 + @as(u8, @intCast(viewport_point.y)) + 1;
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = 6,
|
||||
} }, .locked);
|
||||
|
|
@ -3534,7 +3587,7 @@ fn mouseReport(
|
|||
i += try std.unicode.utf8Encode(@intCast(32 + viewport_point.y + 1), data[i..]);
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(i),
|
||||
} }, .locked);
|
||||
|
|
@ -3555,7 +3608,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3572,7 +3625,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3601,7 +3654,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3753,7 +3806,7 @@ pub fn mouseButtonCallback(
|
|||
// Stop selection scrolling when releasing the left mouse button
|
||||
// but only when selection scrolling is active.
|
||||
if (self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.unlocked,
|
||||
);
|
||||
|
|
@ -4110,7 +4163,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
|||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B";
|
||||
};
|
||||
for (0..@abs(path.y)) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = arrow }, .locked);
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
}
|
||||
if (path.x != 0) {
|
||||
|
|
@ -4120,7 +4173,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
|||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C";
|
||||
};
|
||||
for (0..@abs(path.x)) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = arrow }, .locked);
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4229,7 +4282,12 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
|
|||
.trim = false,
|
||||
});
|
||||
defer self.alloc.free(str);
|
||||
try self.openUrl(.{ .kind = .unknown, .url = str });
|
||||
|
||||
const resolved_path = try self.resolvePathForOpening(str);
|
||||
defer if (resolved_path) |p| self.alloc.free(p);
|
||||
|
||||
const url_to_open = resolved_path orelse str;
|
||||
try self.openUrl(.{ .kind = .unknown, .url = url_to_open });
|
||||
},
|
||||
|
||||
._open_osc8 => {
|
||||
|
|
@ -4393,7 +4451,7 @@ pub fn cursorPosCallback(
|
|||
// Stop selection scrolling when inside the viewport within a 1px buffer
|
||||
// for fullscreen windows, but only when selection scrolling is active.
|
||||
if (pos.y >= 1 and self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -4493,7 +4551,7 @@ pub fn cursorPosCallback(
|
|||
if ((pos.y <= 1 or pos.y > max_y - 1) and
|
||||
!self.selection_scroll_active)
|
||||
{
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -4869,7 +4927,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
.esc => try std.fmt.bufPrint(&buf, "\x1b{s}", .{data}),
|
||||
else => unreachable,
|
||||
};
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
full_data,
|
||||
), .unlocked);
|
||||
|
|
@ -4896,7 +4954,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
);
|
||||
return true;
|
||||
};
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
text,
|
||||
), .unlocked);
|
||||
|
|
@ -4929,9 +4987,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
};
|
||||
|
||||
if (normal) {
|
||||
self.io.queueMessage(.{ .write_stable = ck.normal }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = ck.normal }, .unlocked);
|
||||
} else {
|
||||
self.io.queueMessage(.{ .write_stable = ck.application }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = ck.application }, .unlocked);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -5204,19 +5262,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
if (self.io.terminal.screens.active_key == .alternate) return false;
|
||||
}
|
||||
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.clear_screen = .{ .history = true },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_to_top => {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .top = {} },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_to_bottom => {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .bottom = {} },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5246,14 +5304,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
|
||||
.scroll_page_up => {
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = -1 * rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_page_down => {
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5261,19 +5319,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
.scroll_page_fractional => |fraction| {
|
||||
const rows: f32 = @floatFromInt(self.size.grid().rows);
|
||||
const delta: isize = @intFromFloat(@trunc(fraction * rows));
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = delta },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_page_lines => |lines| {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = lines },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.jump_to_prompt => |delta| {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.jump_to_prompt = @intCast(delta),
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5357,6 +5415,15 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
},
|
||||
),
|
||||
|
||||
.goto_window => |direction| return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.goto_window,
|
||||
switch (direction) {
|
||||
.previous => .previous,
|
||||
.next => .next,
|
||||
},
|
||||
),
|
||||
|
||||
.resize_split => |value| return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.resize_split,
|
||||
|
|
@ -5383,6 +5450,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
{},
|
||||
),
|
||||
|
||||
.toggle_readonly => {
|
||||
self.readonly = !self.readonly;
|
||||
_ = try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.readonly,
|
||||
if (self.readonly) .on else .off,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
||||
.reset_window_size => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.reset_window_size,
|
||||
|
|
@ -5441,6 +5518,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
{},
|
||||
),
|
||||
|
||||
.toggle_background_opacity => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_background_opacity,
|
||||
{},
|
||||
),
|
||||
|
||||
.show_on_screen_keyboard => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.show_on_screen_keyboard,
|
||||
|
|
@ -5488,7 +5571,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
};
|
||||
},
|
||||
|
||||
.io => self.io.queueMessage(.{ .crash = {} }, .unlocked),
|
||||
.io => self.queueIo(.{ .crash = {} }, .unlocked),
|
||||
},
|
||||
|
||||
.adjust_selection => |direction| {
|
||||
|
|
@ -5686,7 +5769,7 @@ fn writeScreenFile(
|
|||
},
|
||||
.url = path,
|
||||
}),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
.paste => self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
path,
|
||||
), .unlocked),
|
||||
|
|
@ -5826,7 +5909,7 @@ fn completeClipboardPaste(
|
|||
};
|
||||
|
||||
for (vecs) |vec| if (vec.len > 0) {
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
vec,
|
||||
), .unlocked);
|
||||
|
|
@ -5872,7 +5955,7 @@ fn completeClipboardReadOSC52(
|
|||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .unlocked);
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ pub const Action = union(Key) {
|
|||
/// Toggle the visibility of all Ghostty terminal windows.
|
||||
toggle_visibility,
|
||||
|
||||
/// Toggle the window background opacity. This only has an effect
|
||||
/// if the window started as transparent (non-opaque), and toggles
|
||||
/// it between fully opaque and the configured background opacity.
|
||||
toggle_background_opacity,
|
||||
|
||||
/// Moves a tab by a relative offset.
|
||||
///
|
||||
/// Adjusts the tab position based on `offset` (e.g., -1 for left, +1
|
||||
|
|
@ -129,6 +134,9 @@ pub const Action = union(Key) {
|
|||
/// Jump to a specific split.
|
||||
goto_split: GotoSplit,
|
||||
|
||||
/// Jump to next/previous window.
|
||||
goto_window: GotoWindow,
|
||||
|
||||
/// Resize the split in the given direction.
|
||||
resize_split: ResizeSplit,
|
||||
|
||||
|
|
@ -314,6 +322,9 @@ pub const Action = union(Key) {
|
|||
/// The currently selected search match index (1-based).
|
||||
search_selected: SearchSelected,
|
||||
|
||||
/// The readonly state of the surface has changed.
|
||||
readonly: Readonly,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
|
|
@ -329,9 +340,11 @@ pub const Action = union(Key) {
|
|||
toggle_quick_terminal,
|
||||
toggle_command_palette,
|
||||
toggle_visibility,
|
||||
toggle_background_opacity,
|
||||
move_tab,
|
||||
goto_tab,
|
||||
goto_split,
|
||||
goto_window,
|
||||
resize_split,
|
||||
equalize_splits,
|
||||
toggle_split_zoom,
|
||||
|
|
@ -375,6 +388,7 @@ pub const Action = union(Key) {
|
|||
end_search,
|
||||
search_total,
|
||||
search_selected,
|
||||
readonly,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
|
|
@ -470,6 +484,13 @@ pub const GotoSplit = enum(c_int) {
|
|||
right,
|
||||
};
|
||||
|
||||
// This is made extern (c_int) to make interop easier with our embedded
|
||||
// runtime. The small size cost doesn't make a difference in our union.
|
||||
pub const GotoWindow = enum(c_int) {
|
||||
previous,
|
||||
next,
|
||||
};
|
||||
|
||||
/// The amount to resize the split by and the direction to resize it in.
|
||||
pub const ResizeSplit = extern struct {
|
||||
amount: u16,
|
||||
|
|
@ -532,6 +553,11 @@ pub const QuitTimer = enum(c_int) {
|
|||
stop,
|
||||
};
|
||||
|
||||
pub const Readonly = enum(c_int) {
|
||||
off,
|
||||
on,
|
||||
};
|
||||
|
||||
pub const MouseVisibility = enum(c_int) {
|
||||
visible,
|
||||
hidden,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ const glib = @import("glib");
|
|||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const state = &@import("../../../global.zig").state;
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const cgroup = @import("../cgroup.zig");
|
||||
|
|
@ -659,6 +661,8 @@ pub const Application = extern struct {
|
|||
|
||||
.goto_split => return Action.gotoSplit(target, value),
|
||||
|
||||
.goto_window => return Action.gotoWindow(value),
|
||||
|
||||
.goto_tab => return Action.gotoTab(target, value),
|
||||
|
||||
.initial_size => return Action.initialSize(target, value),
|
||||
|
|
@ -737,6 +741,7 @@ pub const Application = extern struct {
|
|||
.close_all_windows,
|
||||
.float_window,
|
||||
.toggle_visibility,
|
||||
.toggle_background_opacity,
|
||||
.cell_size,
|
||||
.key_sequence,
|
||||
.render_inspector,
|
||||
|
|
@ -746,6 +751,7 @@ pub const Application = extern struct {
|
|||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.readonly,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
|
|
@ -2013,6 +2019,69 @@ const Action = struct {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn gotoWindow(direction: apprt.action.GotoWindow) bool {
|
||||
const glist = gtk.Window.listToplevels();
|
||||
defer glist.free();
|
||||
|
||||
// The window we're starting from is typically our active window.
|
||||
const starting: *glib.List = @as(?*glib.List, glist.findCustom(
|
||||
null,
|
||||
findActiveWindow,
|
||||
)) orelse glist;
|
||||
|
||||
// Go forward or backwards in the list until we find a valid
|
||||
// window that is visible.
|
||||
var current_: ?*glib.List = starting;
|
||||
while (current_) |node| : (current_ = switch (direction) {
|
||||
.next => node.f_next,
|
||||
.previous => node.f_prev,
|
||||
}) {
|
||||
const data = node.f_data orelse continue;
|
||||
const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
|
||||
if (gotoWindowMaybe(gtk_window)) return true;
|
||||
}
|
||||
|
||||
// If we reached here, we didn't find a valid window to focus.
|
||||
// Wrap around.
|
||||
current_ = switch (direction) {
|
||||
.next => glist,
|
||||
.previous => last: {
|
||||
var end: *glib.List = glist;
|
||||
while (end.f_next) |next| end = next;
|
||||
break :last end;
|
||||
},
|
||||
};
|
||||
while (current_) |node| : (current_ = switch (direction) {
|
||||
.next => node.f_next,
|
||||
.previous => node.f_prev,
|
||||
}) {
|
||||
if (current_ == starting) break;
|
||||
const data = node.f_data orelse continue;
|
||||
const gtk_window: *gtk.Window = @ptrCast(@alignCast(data));
|
||||
if (gotoWindowMaybe(gtk_window)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn gotoWindowMaybe(gtk_window: *gtk.Window) bool {
|
||||
// If it is already active skip it.
|
||||
if (gtk_window.isActive() != 0) return false;
|
||||
// If it is hidden, skip it.
|
||||
if (gtk_window.as(gtk.Widget).isVisible() == 0) return false;
|
||||
// If it isn't a Ghostty window, skip it.
|
||||
const window = gobject.ext.cast(
|
||||
Window,
|
||||
gtk_window,
|
||||
) orelse return false;
|
||||
|
||||
// Focus our active surface
|
||||
const surface = window.getActiveSurface() orelse return false;
|
||||
gtk.Window.present(gtk_window);
|
||||
surface.grabFocus();
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initialSize(
|
||||
target: apprt.Target,
|
||||
value: apprt.action.InitialSize,
|
||||
|
|
@ -2611,7 +2680,9 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
|
|||
/// disable it.
|
||||
@"vulkan-disable": bool = false,
|
||||
} = .{
|
||||
.opengl = config.@"gtk-opengl-debug",
|
||||
// `gtk-opengl-debug` dumps logs directly to stderr so both must be true
|
||||
// to enable OpenGL debugging.
|
||||
.opengl = state.logging.stderr and config.@"gtk-opengl-debug",
|
||||
};
|
||||
|
||||
var gdk_disable: struct {
|
||||
|
|
|
|||
|
|
@ -340,6 +340,35 @@ pub const SplitTree = extern struct {
|
|||
const surface = tree.nodes[target.idx()].leaf;
|
||||
surface.grabFocus();
|
||||
|
||||
// We also need to setup our last_focused to this because if we
|
||||
// trigger a tree change like below, the grab focus above never
|
||||
// actually triggers in time to set this and this ensures we
|
||||
// grab focus to the right thing.
|
||||
const old_last_focused = self.private().last_focused.get();
|
||||
defer if (old_last_focused) |v| v.unref(); // unref strong ref from get
|
||||
self.private().last_focused.set(surface);
|
||||
errdefer self.private().last_focused.set(old_last_focused);
|
||||
|
||||
if (tree.zoomed != null) {
|
||||
const app = Application.default();
|
||||
const config_obj = app.getConfig();
|
||||
defer config_obj.unref();
|
||||
const config = config_obj.get();
|
||||
|
||||
if (!config.@"split-preserve-zoom".navigation) {
|
||||
tree.zoomed = null;
|
||||
} else {
|
||||
tree.zoom(target);
|
||||
}
|
||||
|
||||
// When the zoom state changes our tree state changes and
|
||||
// we need to send the proper notifications to trigger
|
||||
// relayout.
|
||||
const object = self.as(gobject.Object);
|
||||
object.notifyByPspec(properties.tree.impl.param_spec);
|
||||
object.notifyByPspec(properties.@"is-zoomed".impl.param_spec);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1658,13 +1658,7 @@ pub const Surface = extern struct {
|
|||
};
|
||||
priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len);
|
||||
|
||||
// Initialize our GLArea. We only set the values we can't set
|
||||
// in our blueprint file.
|
||||
const gl_area = priv.gl_area;
|
||||
gl_area.setRequiredVersion(
|
||||
renderer.OpenGL.MIN_VERSION_MAJOR,
|
||||
renderer.OpenGL.MIN_VERSION_MINOR,
|
||||
);
|
||||
// Setup properties we can't set from our Blueprint file.
|
||||
self.as(gtk.Widget).setCursorFromName("text");
|
||||
|
||||
// Initialize our config
|
||||
|
|
|
|||
|
|
@ -793,7 +793,7 @@ pub const Window = extern struct {
|
|||
|
||||
/// Get the currently active surface. See the "active-surface" property.
|
||||
/// This does not ref the value.
|
||||
fn getActiveSurface(self: *Self) ?*Surface {
|
||||
pub fn getActiveSurface(self: *Self) ?*Surface {
|
||||
const tab = self.getSelectedTab() orelse return null;
|
||||
return tab.getActiveSurface();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,12 @@ pub const Window = struct {
|
|||
|
||||
blur_region: Region = .{},
|
||||
|
||||
// Cache last applied values to avoid redundant X11 property updates.
|
||||
// Redundant property updates seem to cause some visual glitches
|
||||
// with some window managers: https://github.com/ghostty-org/ghostty/pull/8075
|
||||
last_applied_blur_region: ?Region = null,
|
||||
last_applied_decoration_hints: ?MotifWMHints = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
|
|
@ -255,30 +261,42 @@ pub const Window = struct {
|
|||
const gtk_widget = self.apprt_window.as(gtk.Widget);
|
||||
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
// When blur is disabled, remove the property if it was previously set
|
||||
const blur = config.@"background-blur";
|
||||
if (!blur.enabled()) {
|
||||
if (self.last_applied_blur_region != null) {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
self.last_applied_blur_region = null;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = gtk_widget.getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = config.@"background-blur";
|
||||
// Only update X11 properties when the blur region actually changes
|
||||
if (self.last_applied_blur_region) |last| {
|
||||
if (std.meta.eql(self.blur_region, last)) return;
|
||||
}
|
||||
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur.enabled()) {
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
self.last_applied_blur_region = self.blur_region;
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
|
|
@ -307,6 +325,11 @@ pub const Window = struct {
|
|||
.auto, .client, .none => false,
|
||||
};
|
||||
|
||||
// Only update decoration hints when they actually change
|
||||
if (self.last_applied_decoration_hints) |last| {
|
||||
if (std.meta.eql(hints, last)) return;
|
||||
}
|
||||
|
||||
try self.changeProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
|
|
@ -315,6 +338,7 @@ pub const Window = struct {
|
|||
.{ .mode = .replace },
|
||||
&hints,
|
||||
);
|
||||
self.last_applied_decoration_hints = hints;
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
//! This benchmark tests the throughput of the OSC parser.
|
||||
const OscParser = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Benchmark = @import("Benchmark.zig");
|
||||
const options = @import("options.zig");
|
||||
const Parser = @import("../terminal/osc.zig").Parser;
|
||||
const log = std.log.scoped(.@"osc-parser-bench");
|
||||
|
||||
opts: Options,
|
||||
|
||||
/// The file, opened in the setup function.
|
||||
data_f: ?std.fs.File = null,
|
||||
|
||||
parser: Parser,
|
||||
|
||||
pub const Options = struct {
|
||||
/// The data to read as a filepath. If this is "-" then
|
||||
/// we will read stdin. If this is unset, then we will
|
||||
/// do nothing (benchmark is a noop). It'd be more unixy to
|
||||
/// use stdin by default but I find that a hanging CLI command
|
||||
/// with no interaction is a bit annoying.
|
||||
data: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Create a new terminal stream handler for the given arguments.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
opts: Options,
|
||||
) !*OscParser {
|
||||
const ptr = try alloc.create(OscParser);
|
||||
errdefer alloc.destroy(ptr);
|
||||
ptr.* = .{
|
||||
.opts = opts,
|
||||
.data_f = null,
|
||||
.parser = .init(alloc),
|
||||
};
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *OscParser, alloc: Allocator) void {
|
||||
self.parser.deinit();
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
pub fn benchmark(self: *OscParser) Benchmark {
|
||||
return .init(self, .{
|
||||
.stepFn = step,
|
||||
.setupFn = setup,
|
||||
.teardownFn = teardown,
|
||||
});
|
||||
}
|
||||
|
||||
fn setup(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
// Open our data file to prepare for reading. We can do more
|
||||
// validation here eventually.
|
||||
assert(self.data_f == null);
|
||||
self.data_f = options.dataFile(self.opts.data) catch |err| {
|
||||
log.warn("error opening data file err={}", .{err});
|
||||
return error.BenchmarkFailed;
|
||||
};
|
||||
self.parser.reset();
|
||||
}
|
||||
|
||||
fn teardown(ptr: *anyopaque) void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
if (self.data_f) |f| {
|
||||
f.close();
|
||||
self.data_f = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn step(ptr: *anyopaque) Benchmark.Error!void {
|
||||
const self: *OscParser = @ptrCast(@alignCast(ptr));
|
||||
|
||||
const f = self.data_f orelse return;
|
||||
var read_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
var r = f.reader(&read_buf);
|
||||
|
||||
var osc_buf: [4096]u8 align(std.atomic.cache_line) = undefined;
|
||||
while (true) {
|
||||
r.interface.fill(@bitSizeOf(usize) / 8) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
const len = r.interface.takeInt(usize, .little) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
|
||||
if (len > osc_buf.len) return error.BenchmarkFailed;
|
||||
|
||||
r.interface.readSliceAll(osc_buf[0..len]) catch |err| switch (err) {
|
||||
error.EndOfStream => return,
|
||||
error.ReadFailed => return error.BenchmarkFailed,
|
||||
};
|
||||
|
||||
for (osc_buf[0..len]) |c| self.parser.next(c);
|
||||
_ = self.parser.end(std.ascii.control_code.bel);
|
||||
self.parser.reset();
|
||||
}
|
||||
}
|
||||
|
||||
test OscParser {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const impl: *OscParser = try .create(alloc, .{});
|
||||
defer impl.destroy(alloc);
|
||||
|
||||
const bench = impl.benchmark();
|
||||
_ = try bench.run(.once);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ pub const Action = enum {
|
|||
@"terminal-parser",
|
||||
@"terminal-stream",
|
||||
@"is-symbol",
|
||||
@"osc-parser",
|
||||
|
||||
/// Returns the struct associated with the action. The struct
|
||||
/// should have a few decls:
|
||||
|
|
@ -29,6 +30,7 @@ pub const Action = enum {
|
|||
.@"grapheme-break" => @import("GraphemeBreak.zig"),
|
||||
.@"terminal-parser" => @import("TerminalParser.zig"),
|
||||
.@"is-symbol" => @import("IsSymbol.zig"),
|
||||
.@"osc-parser" => @import("OscParser.zig"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -219,20 +219,14 @@ pub fn init(b: *std.Build, appVersion: []const u8) !Config {
|
|||
else version: {
|
||||
const app_version = try std.SemanticVersion.parse(appVersion);
|
||||
|
||||
// Detect if ghostty is being built as a dependency by checking if the
|
||||
// build root has our marker. When used as a dependency, we skip git
|
||||
// detection entirely to avoid reading the downstream project's git state.
|
||||
const is_dependency = !@hasDecl(
|
||||
@import("root"),
|
||||
"_ghostty_build_root",
|
||||
);
|
||||
if (is_dependency) {
|
||||
break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
};
|
||||
}
|
||||
// Is ghostty a dependency? If so, skip git detection.
|
||||
// @src().file won't resolve from b.build_root unless ghostty
|
||||
// is the project being built.
|
||||
b.build_root.handle.access(@src().file, .{}) catch break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
};
|
||||
|
||||
// If no explicit version is given, we try to detect it from git.
|
||||
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ pub fn init(
|
|||
|
||||
// This overrides our default behavior and forces logs to show
|
||||
// up on stderr (in addition to the centralized macOS log).
|
||||
open.setEnvironmentVariable("GHOSTTY_LOG", "1");
|
||||
open.setEnvironmentVariable("GHOSTTY_LOG", "stderr,macos");
|
||||
|
||||
// Configure how we're launching
|
||||
open.setEnvironmentVariable("GHOSTTY_MAC_LAUNCH_SOURCE", "zig_run");
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ precedence over the XDG environment locations.
|
|||
|
||||
: **WINDOWS ONLY:** alternate location to search for configuration files.
|
||||
|
||||
**GHOSTTY_LOG**
|
||||
|
||||
: The `GHOSTTY_LOG` environment variable can be used to control which
|
||||
destinations receive logs. Ghostty currently defines two destinations:
|
||||
|
||||
: - `stderr` - logging to `stderr`.
|
||||
: - `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
|
||||
|
||||
: Combine values with a comma to enable multiple destinations. Prefix a
|
||||
destination with `no-` to disable it. Enabling and disabling destinations
|
||||
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
|
||||
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
|
||||
|
||||
# BUGS
|
||||
|
||||
See GitHub issues: <https://github.com/ghostty-org/ghostty/issues>
|
||||
|
|
|
|||
|
|
@ -73,16 +73,12 @@ the public config files of many Ghostty users for examples and inspiration.
|
|||
## 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 with `log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`.
|
||||
them and move on. Configuration errors will be logged.
|
||||
|
||||
## 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.
|
||||
debug output of Ghostty.
|
||||
|
||||
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
|
||||
|
|
@ -93,3 +89,34 @@ 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.
|
||||
|
||||
## Logging
|
||||
|
||||
Ghostty can write logs to a number of destinations. On all platforms, logging to
|
||||
`stderr` is available. Depending on the platform and how Ghostty was launched,
|
||||
logs sent to `stderr` may be stored by the system and made available for later
|
||||
retrieval.
|
||||
|
||||
On Linux if Ghostty is launched by the default `systemd` user service, you can use
|
||||
`journald` to see Ghostty's logs: `journalctl --user --unit app-com.mitchellh.ghostty.service`.
|
||||
|
||||
On macOS logging to the macOS unified log is available and enabled by default.
|
||||
--Use the system `log` CLI to view Ghostty's logs: `sudo log stream --level debug
|
||||
--predicate 'subsystem=="com.mitchellh.ghostty"'`.
|
||||
|
||||
Ghostty's logging can be configured in two ways. The first is by what
|
||||
optimization level Ghostty is compiled with. If Ghostty is compiled with `Debug`
|
||||
optimizations debug logs will be output to `stderr`. If Ghostty is compiled with
|
||||
any other optimization the debug logs will not be output to `stderr`.
|
||||
|
||||
Ghostty also checks the `GHOSTTY_LOG` environment variable. It can be used
|
||||
to control which destinations receive logs. Ghostty currently defines two
|
||||
destinations:
|
||||
|
||||
- `stderr` - logging to `stderr`.
|
||||
- `macos` - logging to macOS's unified log (has no effect on non-macOS platforms).
|
||||
|
||||
Combine values with a comma to enable multiple destinations. Prefix a
|
||||
destination with `no-` to disable it. Enabling and disabling destinations
|
||||
can be done at the same time. Setting `GHOSTTY_LOG` to `true` will enable all
|
||||
destinations. Setting `GHOSTTY_LOG` to `false` will disable all destinations.
|
||||
|
|
|
|||
|
|
@ -604,7 +604,7 @@ pub fn parseAutoStruct(
|
|||
return result;
|
||||
}
|
||||
|
||||
fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
pub fn parsePackedStruct(comptime T: type, v: []const u8) !T {
|
||||
const info = @typeInfo(T).@"struct";
|
||||
comptime assert(info.layout == .@"packed");
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||
const args = @import("args.zig");
|
||||
const Action = @import("ghostty.zig").Action;
|
||||
const Config = @import("../config/Config.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const themepkg = @import("../config/theme.zig");
|
||||
const tui = @import("tui.zig");
|
||||
const global_state = &@import("../global.zig").state;
|
||||
|
|
@ -196,6 +197,31 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
|
|||
return 0;
|
||||
}
|
||||
|
||||
fn resolveAutoThemePath(alloc: std.mem.Allocator) ![]u8 {
|
||||
const main_cfg_path = try configpkg.preferredDefaultFilePath(alloc);
|
||||
defer alloc.free(main_cfg_path);
|
||||
|
||||
const base_dir = std.fs.path.dirname(main_cfg_path) orelse return error.BadPathName;
|
||||
return try std.fs.path.join(alloc, &.{ base_dir, "auto", "theme.ghostty" });
|
||||
}
|
||||
|
||||
fn writeAutoThemeFile(alloc: std.mem.Allocator, theme_name: []const u8) !void {
|
||||
const auto_path = try resolveAutoThemePath(alloc);
|
||||
defer alloc.free(auto_path);
|
||||
|
||||
if (std.fs.path.dirname(auto_path)) |dir| {
|
||||
try std.fs.cwd().makePath(dir);
|
||||
}
|
||||
|
||||
var f = try std.fs.createFileAbsolute(auto_path, .{ .truncate = true });
|
||||
defer f.close();
|
||||
|
||||
var buf: [128]u8 = undefined;
|
||||
var w = f.writer(&buf);
|
||||
try w.interface.print("theme = {s}\n", .{theme_name});
|
||||
try w.interface.flush();
|
||||
}
|
||||
|
||||
const Event = union(enum) {
|
||||
key_press: vaxis.Key,
|
||||
mouse: vaxis.Mouse,
|
||||
|
|
@ -487,6 +513,9 @@ const Preview = struct {
|
|||
self.should_quit = true;
|
||||
if (key.matchesAny(&.{ vaxis.Key.escape, vaxis.Key.enter, vaxis.Key.kp_enter }, .{}))
|
||||
self.mode = .normal;
|
||||
if (key.matches('w', .{})) {
|
||||
self.saveSelectedTheme();
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
|
|
@ -698,7 +727,7 @@ const Preview = struct {
|
|||
.help => {
|
||||
win.hideCursor();
|
||||
const width = 60;
|
||||
const height = 20;
|
||||
const height = 22;
|
||||
const child = win.child(
|
||||
.{
|
||||
.x_off = win.width / 2 -| width / 2,
|
||||
|
|
@ -733,6 +762,7 @@ const Preview = struct {
|
|||
.{ .keys = "/", .help = "Start search." },
|
||||
.{ .keys = "^X, ^/", .help = "Clear search." },
|
||||
.{ .keys = "⏎", .help = "Save theme or close search window." },
|
||||
.{ .keys = "w", .help = "Write theme to auto config file." },
|
||||
};
|
||||
|
||||
for (key_help, 0..) |help, captured_i| {
|
||||
|
|
@ -786,8 +816,8 @@ const Preview = struct {
|
|||
.save => {
|
||||
const theme = self.themes[self.filtered.items[self.current]];
|
||||
|
||||
const width = 90;
|
||||
const height = 12;
|
||||
const width = 92;
|
||||
const height = 17;
|
||||
const child = win.child(
|
||||
.{
|
||||
.x_off = win.width / 2 -| width / 2,
|
||||
|
|
@ -809,6 +839,12 @@ const Preview = struct {
|
|||
try std.fmt.allocPrint(alloc, "theme = {s}", .{theme.theme}),
|
||||
"",
|
||||
"Save the configuration file and then reload it to apply the new theme.",
|
||||
"",
|
||||
"Or press 'w' to write an auto theme file to your system's preferred default config path.",
|
||||
"Then add the following line to your Ghostty configuration and reload:",
|
||||
"",
|
||||
"config-file = ?auto/theme.ghostty",
|
||||
"",
|
||||
"For more details on configuration and themes, visit the Ghostty documentation:",
|
||||
"",
|
||||
"https://ghostty.org/docs/config/reference",
|
||||
|
|
@ -1657,6 +1693,18 @@ const Preview = struct {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn saveSelectedTheme(self: *Preview) void {
|
||||
if (self.filtered.items.len == 0)
|
||||
return;
|
||||
|
||||
const idx = self.filtered.items[self.current];
|
||||
const theme = self.themes[idx];
|
||||
|
||||
writeAutoThemeFile(self.allocator, theme.theme) catch {
|
||||
return;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
fn color(config: Config, palette: usize) vaxis.Color {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ pub const compatibility = std.StaticStringMap(
|
|||
// Ghostty 1.2 removed the "desktop" option and renamed it to "detect".
|
||||
// The semantics also changed slightly but this is the correct mapping.
|
||||
.{ "gtk-single-instance", compatGtkSingleInstance },
|
||||
|
||||
// Ghostty 1.3 rename the "window" option to "new-window".
|
||||
// See: https://github.com/ghostty-org/ghostty/pull/9764
|
||||
.{ "macos-dock-drop-behavior", compatMacOSDockDropBehavior },
|
||||
});
|
||||
|
||||
/// The font families to use.
|
||||
|
|
@ -927,6 +931,15 @@ palette: Palette = .{},
|
|||
/// reasonable for a good looking blur. Higher blur intensities may
|
||||
/// cause strange rendering and performance issues.
|
||||
///
|
||||
/// On macOS 26.0 and later, there are additional special values that
|
||||
/// can be set to use the native macOS glass effects:
|
||||
///
|
||||
/// * `macos-glass-regular` - Standard glass effect with some opacity
|
||||
/// * `macos-glass-clear` - Highly transparent glass effect
|
||||
///
|
||||
/// If the macOS values are set, then this implies `background-blur = true`
|
||||
/// on non-macOS platforms.
|
||||
///
|
||||
/// Supported on macOS and on some Linux desktop environments, including:
|
||||
///
|
||||
/// * KDE Plasma (Wayland and X11)
|
||||
|
|
@ -976,6 +989,22 @@ palette: Palette = .{},
|
|||
/// Available since: 1.1.0
|
||||
@"split-divider-color": ?Color = null,
|
||||
|
||||
/// Control when Ghostty preserves a zoomed split. Under normal circumstances,
|
||||
/// any operation that changes focus or layout of the split tree in a window
|
||||
/// will unzoom any zoomed split. This configuration allows you to control
|
||||
/// this behavior.
|
||||
///
|
||||
/// This can be set to `navigation` to preserve the zoomed split state
|
||||
/// when navigating to another split (e.g. via `goto_split`). This will
|
||||
/// change the zoomed split to the newly focused split instead of unzooming.
|
||||
///
|
||||
/// Any options can also be prefixed with `no-` to disable that option.
|
||||
///
|
||||
/// Example: `split-preserve-zoom = navigation`
|
||||
///
|
||||
/// Available since: 1.3.0
|
||||
@"split-preserve-zoom": SplitPreserveZoom = .{},
|
||||
|
||||
/// The foreground and background color for search matches. This only applies
|
||||
/// to non-focused search matches, also known as candidate matches.
|
||||
///
|
||||
|
|
@ -1329,7 +1358,7 @@ maximize: bool = false,
|
|||
/// new windows, not just the first one.
|
||||
///
|
||||
/// On macOS, this setting does not work if window-decoration is set to
|
||||
/// "false", because native fullscreen on macOS requires window decorations
|
||||
/// "none", because native fullscreen on macOS requires window decorations
|
||||
/// to be set.
|
||||
fullscreen: bool = false,
|
||||
|
||||
|
|
@ -2825,7 +2854,7 @@ keybind: Keybinds = .{},
|
|||
/// also known as the traffic lights, that allow you to close, miniaturize, and
|
||||
/// zoom the window.
|
||||
///
|
||||
/// This setting has no effect when `window-decoration = false` or
|
||||
/// This setting has no effect when `window-decoration = none` or
|
||||
/// `macos-titlebar-style = hidden`, as the window buttons are always hidden in
|
||||
/// these modes.
|
||||
///
|
||||
|
|
@ -2866,7 +2895,7 @@ keybind: Keybinds = .{},
|
|||
/// macOS 14 does not have this issue and any other macOS version has not
|
||||
/// been tested.
|
||||
///
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = none`,
|
||||
/// however, it does not remove the frame from the window or cause it to have
|
||||
/// squared corners. Changing to or from this option at run-time may affect
|
||||
/// existing windows in buggy ways.
|
||||
|
|
@ -2911,7 +2940,7 @@ keybind: Keybinds = .{},
|
|||
///
|
||||
/// * `new-tab` - Create a new tab in the current window, or open
|
||||
/// a new window if none exist.
|
||||
/// * `window` - Create a new window unconditionally.
|
||||
/// * `new-window` - Create a new window unconditionally.
|
||||
///
|
||||
/// The default value is `new-tab`.
|
||||
///
|
||||
|
|
@ -3205,7 +3234,7 @@ else
|
|||
/// manager's simple titlebar. The behavior of this option will vary with your
|
||||
/// window manager.
|
||||
///
|
||||
/// This option does nothing when `window-decoration` is false or when running
|
||||
/// This option does nothing when `window-decoration` is none or when running
|
||||
/// under macOS.
|
||||
@"gtk-titlebar": bool = true,
|
||||
|
||||
|
|
@ -4445,6 +4474,23 @@ fn compatBoldIsBright(
|
|||
return true;
|
||||
}
|
||||
|
||||
fn compatMacOSDockDropBehavior(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "macos-dock-drop-behavior"));
|
||||
|
||||
if (std.mem.eql(u8, value orelse "", "window")) {
|
||||
self.@"macos-dock-drop-behavior" = .@"new-window";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Add a diagnostic message to the config with the given string.
|
||||
/// This is always added with a location of "none".
|
||||
pub fn addDiagnosticFmt(
|
||||
|
|
@ -7414,6 +7460,10 @@ pub const ShellIntegrationFeatures = packed struct {
|
|||
path: bool = true,
|
||||
};
|
||||
|
||||
pub const SplitPreserveZoom = packed struct {
|
||||
navigation: bool = false,
|
||||
};
|
||||
|
||||
pub const RepeatableCommand = struct {
|
||||
value: std.ArrayListUnmanaged(inputpkg.Command) = .empty,
|
||||
|
||||
|
|
@ -7875,7 +7925,7 @@ pub const WindowNewTabPosition = enum {
|
|||
/// See macos-dock-drop-behavior
|
||||
pub const MacOSDockDropBehavior = enum {
|
||||
@"new-tab",
|
||||
window,
|
||||
@"new-window",
|
||||
};
|
||||
|
||||
/// See window-show-tab-bar
|
||||
|
|
@ -8315,6 +8365,8 @@ pub const AutoUpdate = enum {
|
|||
pub const BackgroundBlur = union(enum) {
|
||||
false,
|
||||
true,
|
||||
@"macos-glass-regular",
|
||||
@"macos-glass-clear",
|
||||
radius: u8,
|
||||
|
||||
pub fn parseCLI(self: *BackgroundBlur, input: ?[]const u8) !void {
|
||||
|
|
@ -8324,14 +8376,35 @@ pub const BackgroundBlur = union(enum) {
|
|||
return;
|
||||
};
|
||||
|
||||
self.* = if (cli.args.parseBool(input_)) |b|
|
||||
if (b) .true else .false
|
||||
else |_|
|
||||
.{ .radius = std.fmt.parseInt(
|
||||
u8,
|
||||
input_,
|
||||
0,
|
||||
) catch return error.InvalidValue };
|
||||
// Try to parse normal bools
|
||||
if (cli.args.parseBool(input_)) |b| {
|
||||
self.* = if (b) .true else .false;
|
||||
return;
|
||||
} else |_| {}
|
||||
|
||||
// Try to parse enums
|
||||
if (std.meta.stringToEnum(
|
||||
std.meta.Tag(BackgroundBlur),
|
||||
input_,
|
||||
)) |v| switch (v) {
|
||||
inline else => |tag| tag: {
|
||||
// We can only parse void types
|
||||
const info = std.meta.fieldInfo(BackgroundBlur, tag);
|
||||
if (info.type != void) break :tag;
|
||||
self.* = @unionInit(
|
||||
BackgroundBlur,
|
||||
@tagName(tag),
|
||||
{},
|
||||
);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
self.* = .{ .radius = std.fmt.parseInt(
|
||||
u8,
|
||||
input_,
|
||||
0,
|
||||
) catch return error.InvalidValue };
|
||||
}
|
||||
|
||||
pub fn enabled(self: BackgroundBlur) bool {
|
||||
|
|
@ -8339,14 +8412,24 @@ pub const BackgroundBlur = union(enum) {
|
|||
.false => false,
|
||||
.true => true,
|
||||
.radius => |v| v > 0,
|
||||
|
||||
// We treat these as true because they both imply some blur!
|
||||
// This has the effect of making the standard blur happen on
|
||||
// Linux.
|
||||
.@"macos-glass-regular", .@"macos-glass-clear" => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cval(self: BackgroundBlur) u8 {
|
||||
pub fn cval(self: BackgroundBlur) i16 {
|
||||
return switch (self) {
|
||||
.false => 0,
|
||||
.true => 20,
|
||||
.radius => |v| v,
|
||||
// I hate sentinel values like this but this is only for
|
||||
// our macOS application currently. We can switch to a proper
|
||||
// tagged union if we ever need to.
|
||||
.@"macos-glass-regular" => -1,
|
||||
.@"macos-glass-clear" => -2,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -8358,6 +8441,8 @@ pub const BackgroundBlur = union(enum) {
|
|||
.false => try formatter.formatEntry(bool, false),
|
||||
.true => try formatter.formatEntry(bool, true),
|
||||
.radius => |v| try formatter.formatEntry(u8, v),
|
||||
.@"macos-glass-regular" => try formatter.formatEntry([]const u8, "macos-glass-regular"),
|
||||
.@"macos-glass-clear" => try formatter.formatEntry([]const u8, "macos-glass-clear"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -8377,6 +8462,12 @@ pub const BackgroundBlur = union(enum) {
|
|||
try v.parseCLI("42");
|
||||
try testing.expectEqual(42, v.radius);
|
||||
|
||||
try v.parseCLI("macos-glass-regular");
|
||||
try testing.expectEqual(.@"macos-glass-regular", v);
|
||||
|
||||
try v.parseCLI("macos-glass-clear");
|
||||
try testing.expectEqual(.@"macos-glass-clear", v);
|
||||
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI(""));
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI("aaaa"));
|
||||
try testing.expectError(error.InvalidValue, v.parseCLI("420"));
|
||||
|
|
@ -9491,3 +9582,22 @@ test "compatibility: removed bold-is-bright" {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "compatibility: window new-window" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var cfg = try Config.default(alloc);
|
||||
defer cfg.deinit();
|
||||
var it: TestIterator = .{ .data = &.{
|
||||
"--macos-dock-drop-behavior=window",
|
||||
} };
|
||||
try cfg.loadIter(alloc, &it);
|
||||
try cfg.finalize();
|
||||
try testing.expectEqual(
|
||||
MacOSDockDropBehavior.@"new-window",
|
||||
cfg.@"macos-dock-drop-behavior",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -193,20 +193,48 @@ test "c_get: background-blur" {
|
|||
|
||||
{
|
||||
c.@"background-blur" = .false;
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(0, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .true;
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(20, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .{ .radius = 42 };
|
||||
var cval: u8 = undefined;
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(42, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .@"macos-glass-regular";
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(-1, cval);
|
||||
}
|
||||
{
|
||||
c.@"background-blur" = .@"macos-glass-clear";
|
||||
var cval: i16 = undefined;
|
||||
try testing.expect(get(&c, .@"background-blur", @ptrCast(&cval)));
|
||||
try testing.expectEqual(-2, cval);
|
||||
}
|
||||
}
|
||||
|
||||
test "c_get: split-preserve-zoom" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c = try Config.default(alloc);
|
||||
defer c.deinit();
|
||||
|
||||
var bits: c_uint = undefined;
|
||||
try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits)));
|
||||
try testing.expectEqual(@as(c_uint, 0), bits);
|
||||
|
||||
c.@"split-preserve-zoom".navigation = true;
|
||||
try testing.expect(get(&c, .@"split-preserve-zoom", @ptrCast(&bits)));
|
||||
try testing.expectEqual(@as(c_uint, 1), bits);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ pub const regex =
|
|||
"(?:" ++ url_schemes ++
|
||||
\\)(?:
|
||||
++ ipv6_url_pattern ++
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)[\w\-.~:\/?#@!$&*+,;=%]+(?:\/[\w\-.~:\/?#@!$&*+,;=%]*)*
|
||||
\\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(?<![,.])|(?:\.\.\/|\.\/|\/)(?:(?=[\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]*[\/.])*(?: +(?= *$))?|(?![\w\-.~:\/?#@!$&*+,;=%]*\.)[\w\-.~:\/?#@!$&*+,;=%]+(?: [\w\-.~:\/?#@!$&*+,;=%]+)*(?: +(?= *$))?)
|
||||
;
|
||||
const url_schemes =
|
||||
\\https?://|mailto:|ftp://|file:|ssh:|git://|ssh://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:
|
||||
|
|
@ -194,7 +194,7 @@ test "url regex" {
|
|||
},
|
||||
.{
|
||||
.input = "../example.py ",
|
||||
.expect = "../example.py",
|
||||
.expect = "../example.py ",
|
||||
},
|
||||
.{
|
||||
.input = "first time ../example.py contributor ",
|
||||
|
|
@ -253,6 +253,23 @@ test "url regex" {
|
|||
.input = "IPv6 in markdown [link](http://[2001:db8::1]/docs)",
|
||||
.expect = "http://[2001:db8::1]/docs",
|
||||
},
|
||||
// File paths with spaces
|
||||
.{
|
||||
.input = "./spaces-end. ",
|
||||
.expect = "./spaces-end. ",
|
||||
},
|
||||
.{
|
||||
.input = "./space middle",
|
||||
.expect = "./space middle",
|
||||
},
|
||||
.{
|
||||
.input = "../test folder/file.txt",
|
||||
.expect = "../test folder/file.txt",
|
||||
},
|
||||
.{
|
||||
.input = "/tmp/test folder/file.txt",
|
||||
.expect = "/tmp/test folder/file.txt",
|
||||
},
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
|
|
|
|||
|
|
@ -39,9 +39,13 @@ pub const GlobalState = struct {
|
|||
resources_dir: internal_os.ResourcesDir,
|
||||
|
||||
/// Where logging should go
|
||||
pub const Logging = union(enum) {
|
||||
disabled: void,
|
||||
stderr: void,
|
||||
pub const Logging = packed struct {
|
||||
/// Whether to log to stderr. For lib mode we always disable stderr
|
||||
/// logging by default. Otherwise it's enabled by default.
|
||||
stderr: bool = build_config.app_runtime != .none,
|
||||
/// Whether to log to macOS's unified logging. Enabled by default
|
||||
/// on macOS.
|
||||
macos: bool = builtin.os.tag.isDarwin(),
|
||||
};
|
||||
|
||||
/// Initialize the global state.
|
||||
|
|
@ -61,7 +65,7 @@ pub const GlobalState = struct {
|
|||
.gpa = null,
|
||||
.alloc = undefined,
|
||||
.action = null,
|
||||
.logging = .{ .stderr = {} },
|
||||
.logging = .{},
|
||||
.rlimits = .{},
|
||||
.resources_dir = .{},
|
||||
};
|
||||
|
|
@ -100,12 +104,7 @@ pub const GlobalState = struct {
|
|||
// If we have an action executing, we disable logging by default
|
||||
// since we write to stderr we don't want logs messing up our
|
||||
// output.
|
||||
if (self.action != null) self.logging = .{ .disabled = {} };
|
||||
|
||||
// For lib mode we always disable stderr logging by default.
|
||||
if (comptime build_config.app_runtime == .none) {
|
||||
self.logging = .{ .disabled = {} };
|
||||
}
|
||||
if (self.action != null) self.logging.stderr = false;
|
||||
|
||||
// I don't love the env var name but I don't have it in my heart
|
||||
// to parse CLI args 3 times (once for actions, once for config,
|
||||
|
|
@ -114,9 +113,7 @@ pub const GlobalState = struct {
|
|||
// easy to set.
|
||||
if ((try internal_os.getenv(self.alloc, "GHOSTTY_LOG"))) |v| {
|
||||
defer v.deinit(self.alloc);
|
||||
if (v.value.len > 0) {
|
||||
self.logging = .{ .stderr = {} };
|
||||
}
|
||||
self.logging = cli.args.parsePackedStruct(Logging, v.value) catch .{};
|
||||
}
|
||||
|
||||
// Setup our signal handlers before logging
|
||||
|
|
|
|||
|
|
@ -545,6 +545,9 @@ pub const Action = union(enum) {
|
|||
/// (`previous` and `next`).
|
||||
goto_split: SplitFocusDirection,
|
||||
|
||||
/// Focus on either the previous window or the next one ('previous', 'next')
|
||||
goto_window: GotoWindow,
|
||||
|
||||
/// Zoom in or out of the current split.
|
||||
///
|
||||
/// When a split is zoomed into, it will take up the entire space in
|
||||
|
|
@ -552,6 +555,16 @@ pub const Action = union(enum) {
|
|||
/// reflect this by displaying an icon indicating the zoomed state.
|
||||
toggle_split_zoom,
|
||||
|
||||
/// Toggle read-only mode for the current surface.
|
||||
///
|
||||
/// When a surface is in read-only mode:
|
||||
/// - No input is sent to the PTY (mouse events, key encoding)
|
||||
/// - Input can still be used at the terminal level to make selections,
|
||||
/// copy/paste (keybinds), scroll, etc.
|
||||
/// - Warn before quit is always enabled in this state even if an active
|
||||
/// process is not running
|
||||
toggle_readonly,
|
||||
|
||||
/// Resize the current split in the specified direction and amount in
|
||||
/// pixels. The two arguments should be joined with a comma (`,`),
|
||||
/// like in `resize_split:up,10`.
|
||||
|
|
@ -742,6 +755,16 @@ pub const Action = union(enum) {
|
|||
/// Only implemented on macOS.
|
||||
toggle_visibility,
|
||||
|
||||
/// Toggle the window background opacity between transparent and opaque.
|
||||
///
|
||||
/// This does nothing when `background-opacity` is set to 1 or above.
|
||||
///
|
||||
/// When `background-opacity` is less than 1, this action will either make
|
||||
/// the window transparent or not depending on its current transparency state.
|
||||
///
|
||||
/// Only implemented on macOS.
|
||||
toggle_background_opacity,
|
||||
|
||||
/// Check for updates.
|
||||
///
|
||||
/// Only implemented on macOS.
|
||||
|
|
@ -921,6 +944,11 @@ pub const Action = union(enum) {
|
|||
right,
|
||||
};
|
||||
|
||||
pub const GotoWindow = enum {
|
||||
previous,
|
||||
next,
|
||||
};
|
||||
|
||||
pub const SplitResizeParameter = struct {
|
||||
SplitResizeDirection,
|
||||
u16,
|
||||
|
|
@ -1222,6 +1250,7 @@ pub const Action = union(enum) {
|
|||
.toggle_secure_input,
|
||||
.toggle_mouse_reporting,
|
||||
.toggle_command_palette,
|
||||
.toggle_background_opacity,
|
||||
.show_on_screen_keyboard,
|
||||
.reset_window_size,
|
||||
.crash,
|
||||
|
|
@ -1240,7 +1269,9 @@ pub const Action = union(enum) {
|
|||
.toggle_tab_overview,
|
||||
.new_split,
|
||||
.goto_split,
|
||||
.goto_window,
|
||||
.toggle_split_zoom,
|
||||
.toggle_readonly,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.inspector,
|
||||
|
|
|
|||
|
|
@ -479,12 +479,31 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
},
|
||||
},
|
||||
|
||||
.goto_window => comptime &.{
|
||||
.{
|
||||
.action = .{ .goto_window = .previous },
|
||||
.title = "Focus Window: Previous",
|
||||
.description = "Focus the previous window, if any.",
|
||||
},
|
||||
.{
|
||||
.action = .{ .goto_window = .next },
|
||||
.title = "Focus Window: Next",
|
||||
.description = "Focus the next window, if any.",
|
||||
},
|
||||
},
|
||||
|
||||
.toggle_split_zoom => comptime &.{.{
|
||||
.action = .toggle_split_zoom,
|
||||
.title = "Toggle Split Zoom",
|
||||
.description = "Toggle the zoom state of the current split.",
|
||||
}},
|
||||
|
||||
.toggle_readonly => comptime &.{.{
|
||||
.action = .toggle_readonly,
|
||||
.title = "Toggle Read-Only Mode",
|
||||
.description = "Toggle read-only mode for the current surface.",
|
||||
}},
|
||||
|
||||
.equalize_splits => comptime &.{.{
|
||||
.action = .equalize_splits,
|
||||
.title = "Equalize Splits",
|
||||
|
|
@ -599,6 +618,12 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.description = "Toggle whether mouse events are reported to terminal applications.",
|
||||
}},
|
||||
|
||||
.toggle_background_opacity => comptime &.{.{
|
||||
.action = .toggle_background_opacity,
|
||||
.title = "Toggle Background Opacity",
|
||||
.description = "Toggle the background opacity of a window that started transparent.",
|
||||
}},
|
||||
|
||||
.check_for_updates => comptime &.{.{
|
||||
.action = .check_for_updates,
|
||||
.title = "Check for Updates",
|
||||
|
|
|
|||
|
|
@ -178,7 +178,7 @@ fn kitty(
|
|||
// Quote ("report all" mode):
|
||||
// Note that all keys are reported as escape codes, including Enter,
|
||||
// Tab, Backspace etc.
|
||||
if (effective_mods.empty()) {
|
||||
if (binding_mods.empty()) {
|
||||
switch (event.key) {
|
||||
.enter => return try writer.writeByte('\r'),
|
||||
.tab => return try writer.writeByte('\t'),
|
||||
|
|
@ -1311,7 +1311,48 @@ test "kitty: enter, backspace, tab" {
|
|||
try testing.expectEqualStrings("\x1b[9;1:3u", writer.buffered());
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
test "kitty: shift+backspace emits CSI u" {
|
||||
// Backspace with shift modifier should emit CSI u sequence, not raw 0x7F.
|
||||
// This is important for programs that want to distinguish shift+backspace.
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try kitty(&writer, .{
|
||||
.key = .backspace,
|
||||
.mods = .{ .shift = true },
|
||||
.utf8 = "",
|
||||
}, .{
|
||||
.kitty_flags = .{ .disambiguate = true },
|
||||
});
|
||||
try testing.expectEqualStrings("\x1b[127;2u", writer.buffered());
|
||||
}
|
||||
|
||||
test "kitty: shift+enter emits CSI u" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try kitty(&writer, .{
|
||||
.key = .enter,
|
||||
.mods = .{ .shift = true },
|
||||
.utf8 = "",
|
||||
}, .{
|
||||
.kitty_flags = .{ .disambiguate = true },
|
||||
});
|
||||
try testing.expectEqualStrings("\x1b[13;2u", writer.buffered());
|
||||
}
|
||||
|
||||
test "kitty: shift+tab emits CSI u" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
try kitty(&writer, .{
|
||||
.key = .tab,
|
||||
.mods = .{ .shift = true },
|
||||
.utf8 = "",
|
||||
}, .{
|
||||
.kitty_flags = .{ .disambiguate = true },
|
||||
});
|
||||
try testing.expectEqualStrings("\x1b[9;2u", writer.buffered());
|
||||
}
|
||||
|
||||
test "kitty: enter with all flags" {
|
||||
var buf: [128]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
|
|
|
|||
|
|
@ -118,19 +118,17 @@ fn logFn(
|
|||
comptime format: []const u8,
|
||||
args: anytype,
|
||||
) void {
|
||||
// Stuff we can do before the lock
|
||||
const level_txt = comptime level.asText();
|
||||
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
|
||||
|
||||
// Lock so we are thread-safe
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
// On Mac, we use unified logging. To view this:
|
||||
//
|
||||
// sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'
|
||||
//
|
||||
if (builtin.target.os.tag.isDarwin()) {
|
||||
// macOS logging is thread safe so no need for locks/mutexes
|
||||
macos: {
|
||||
if (comptime !builtin.target.os.tag.isDarwin()) break :macos;
|
||||
if (!state.logging.macos) break :macos;
|
||||
|
||||
const prefix = if (scope == .default) "" else @tagName(scope) ++ ": ";
|
||||
|
||||
// Convert our levels to Mac levels
|
||||
const mac_level: macos.os.LogType = switch (level) {
|
||||
.debug => .debug,
|
||||
|
|
@ -143,26 +141,35 @@ fn logFn(
|
|||
// but we shouldn't be logging too much.
|
||||
const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope));
|
||||
defer logger.release();
|
||||
logger.log(std.heap.c_allocator, mac_level, format, args);
|
||||
logger.log(std.heap.c_allocator, mac_level, prefix ++ format, args);
|
||||
}
|
||||
|
||||
switch (state.logging) {
|
||||
.disabled => {},
|
||||
stderr: {
|
||||
// don't log debug messages to stderr unless we are a debug build
|
||||
if (comptime builtin.mode != .Debug and level == .debug) break :stderr;
|
||||
|
||||
.stderr => {
|
||||
// Always try default to send to stderr
|
||||
var buffer: [1024]u8 = undefined;
|
||||
var stderr = std.fs.File.stderr().writer(&buffer);
|
||||
const writer = &stderr.interface;
|
||||
nosuspend writer.print(level_txt ++ prefix ++ format ++ "\n", args) catch return;
|
||||
// TODO: Do we want to use flushless stderr in the future?
|
||||
writer.flush() catch {};
|
||||
},
|
||||
// skip if we are not logging to stderr
|
||||
if (!state.logging.stderr) break :stderr;
|
||||
|
||||
// Lock so we are thread-safe
|
||||
var buf: [64]u8 = undefined;
|
||||
const stderr = std.debug.lockStderrWriter(&buf);
|
||||
defer std.debug.unlockStderrWriter();
|
||||
|
||||
const level_txt = comptime level.asText();
|
||||
const prefix = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
|
||||
nosuspend stderr.print(level_txt ++ prefix ++ format ++ "\n", args) catch break :stderr;
|
||||
nosuspend stderr.flush() catch break :stderr;
|
||||
}
|
||||
}
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
// Our log level is always at least info in every build mode.
|
||||
//
|
||||
// Note, we don't lower this to debug even with conditional logging
|
||||
// via GHOSTTY_LOG because our debug logs are very expensive to
|
||||
// calculate and we want to make sure they're optimized out in
|
||||
// builds.
|
||||
.log_level = switch (builtin.mode) {
|
||||
.Debug => .debug,
|
||||
else => .info,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,84 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Writer = std.Io.Writer;
|
||||
|
||||
/// Builder for constructing space-separated shell command strings.
|
||||
/// Uses a caller-provided allocator (typically with stackFallback).
|
||||
pub const ShellCommandBuilder = struct {
|
||||
buffer: std.Io.Writer.Allocating,
|
||||
|
||||
pub fn init(allocator: Allocator) ShellCommandBuilder {
|
||||
return .{ .buffer = .init(allocator) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ShellCommandBuilder) void {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
/// Append an argument to the command with automatic space separation.
|
||||
pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void {
|
||||
if (arg.len == 0) return;
|
||||
if (self.buffer.written().len > 0) {
|
||||
try self.buffer.writer.writeByte(' ');
|
||||
}
|
||||
try self.buffer.writer.writeAll(arg);
|
||||
}
|
||||
|
||||
/// Get the final null-terminated command string, transferring ownership to caller.
|
||||
/// Calling deinit() after this is safe but unnecessary.
|
||||
pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 {
|
||||
return try self.buffer.toOwnedSliceSentinel(0);
|
||||
}
|
||||
};
|
||||
|
||||
test ShellCommandBuilder {
|
||||
// Empty command
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try testing.expectEqualStrings("", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Single arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Multiple args
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
try cmd.appendArg("-l");
|
||||
try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Empty arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// toOwnedSlice
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
const result = try cmd.toOwnedSlice();
|
||||
defer testing.allocator.free(result);
|
||||
try testing.expectEqualStrings("bash --posix", result);
|
||||
try testing.expectEqual(@as(u8, 0), result[result.len]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer that escapes characters that shells treat specially to reduce the
|
||||
/// risk of injection attacks or other such weirdness. Specifically excludes
|
||||
/// linefeeds so that they can be used to delineate lists of file paths.
|
||||
|
|
|
|||
|
|
@ -265,3 +265,16 @@ test "percent 7" {
|
|||
@memcpy(&src, s);
|
||||
try std.testing.expectError(error.DecodeError, urlPercentDecode(&src));
|
||||
}
|
||||
|
||||
/// Is the given character valid in URI percent encoding?
|
||||
fn isValidChar(c: u8) bool {
|
||||
return switch (c) {
|
||||
' ', ';', '=' => false,
|
||||
else => return std.ascii.isPrint(c),
|
||||
};
|
||||
}
|
||||
|
||||
/// Write data to the writer after URI percent encoding.
|
||||
pub fn urlPercentEncode(writer: *std.Io.Writer, data: []const u8) std.Io.Writer.Error!void {
|
||||
try std.Uri.Component.percentEncode(writer, data, isValidChar);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,15 +137,7 @@ fn prepareContext(getProcAddress: anytype) !void {
|
|||
errdefer gl.glad.unload();
|
||||
log.info("loaded OpenGL {}.{}", .{ major, minor });
|
||||
|
||||
// Enable debug output for the context.
|
||||
try gl.enable(gl.c.GL_DEBUG_OUTPUT);
|
||||
|
||||
// Register our debug message callback with the OpenGL context.
|
||||
gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null);
|
||||
|
||||
// Enable SRGB framebuffer for linear blending support.
|
||||
try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
|
||||
|
||||
// Need to check version before trying to enable it
|
||||
if (major < MIN_VERSION_MAJOR or
|
||||
(major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR))
|
||||
{
|
||||
|
|
@ -155,6 +147,15 @@ fn prepareContext(getProcAddress: anytype) !void {
|
|||
);
|
||||
return error.OpenGLOutdated;
|
||||
}
|
||||
|
||||
// Enable debug output for the context.
|
||||
try gl.enable(gl.c.GL_DEBUG_OUTPUT);
|
||||
|
||||
// Register our debug message callback with the OpenGL context.
|
||||
gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null);
|
||||
|
||||
// Enable SRGB framebuffer for linear blending support.
|
||||
try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB);
|
||||
}
|
||||
|
||||
/// This is called early right after surface creation.
|
||||
|
|
|
|||
|
|
@ -561,6 +561,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
vsync: bool,
|
||||
colorspace: configpkg.Config.WindowColorspace,
|
||||
blending: configpkg.Config.AlphaBlending,
|
||||
background_blur: configpkg.Config.BackgroundBlur,
|
||||
|
||||
pub fn init(
|
||||
alloc_gpa: Allocator,
|
||||
|
|
@ -633,6 +634,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.vsync = config.@"window-vsync",
|
||||
.colorspace = config.@"window-colorspace",
|
||||
.blending = config.@"alpha-blending",
|
||||
.background_blur = config.@"background-blur",
|
||||
.arena = arena,
|
||||
};
|
||||
}
|
||||
|
|
@ -716,6 +718,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
options.config.background.r,
|
||||
options.config.background.g,
|
||||
options.config.background.b,
|
||||
// Note that if we're on macOS with glass effects
|
||||
// we'll disable background opacity but we handle
|
||||
// that in updateFrame.
|
||||
@intFromFloat(@round(options.config.background_opacity * 255.0)),
|
||||
},
|
||||
.bools = .{
|
||||
|
|
@ -1295,6 +1300,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
self.terminal_state.colors.background.b,
|
||||
@intFromFloat(@round(self.config.background_opacity * 255.0)),
|
||||
};
|
||||
|
||||
// If we're on macOS and have glass styles, we remove
|
||||
// the background opacity because the glass effect handles
|
||||
// it.
|
||||
if (comptime builtin.os.tag == .macos) switch (self.config.background_blur) {
|
||||
.@"macos-glass-regular",
|
||||
.@"macos-glass-clear",
|
||||
=> self.uniforms.bg_color[3] = 0,
|
||||
|
||||
else => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,15 @@ pub const Size = struct {
|
|||
self.grid(),
|
||||
self.cell,
|
||||
);
|
||||
|
||||
// The top/bottom padding is interesting. Subjectively, lots of padding
|
||||
// at the top looks bad. So instead of always being equal (like left/right),
|
||||
// we force the top padding to be at most equal to the maximum left padding,
|
||||
// which is the balanced explicit horizontal padding plus half a cell width.
|
||||
const max_padding_left = (explicit.left + explicit.right + self.cell.width) / 2;
|
||||
const vshift = self.padding.top -| max_padding_left;
|
||||
self.padding.top -= vshift;
|
||||
self.padding.bottom += vshift;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -258,16 +267,12 @@ pub const Padding = struct {
|
|||
const space_right = @as(f32, @floatFromInt(screen.width)) - grid_width;
|
||||
const space_bot = @as(f32, @floatFromInt(screen.height)) - grid_height;
|
||||
|
||||
// The left/right padding is just an equal split.
|
||||
// The padding is split equally along both axes.
|
||||
const padding_right = @floor(space_right / 2);
|
||||
const padding_left = padding_right;
|
||||
|
||||
// The top/bottom padding is interesting. Subjectively, lots of padding
|
||||
// at the top looks bad. So instead of always being equal (like left/right),
|
||||
// we force the top padding to be at most equal to the left, and the bottom
|
||||
// padding is the difference thereafter.
|
||||
const padding_top = @min(padding_left, @floor(space_bot / 2));
|
||||
const padding_bot = space_bot - padding_top;
|
||||
const padding_bot = @floor(space_bot / 2);
|
||||
const padding_top = padding_bot;
|
||||
|
||||
const zero = @as(f32, 0);
|
||||
return .{
|
||||
|
|
|
|||
|
|
@ -78,10 +78,16 @@ on the Fish startup process, see the
|
|||
|
||||
### Zsh
|
||||
|
||||
For `zsh`, Ghostty sets `ZDOTDIR` so that it loads our configuration
|
||||
from the `zsh` directory. The existing `ZDOTDIR` is retained so that
|
||||
after loading the Ghostty shell integration the normal Zsh loading
|
||||
sequence occurs.
|
||||
Automatic [Zsh](https://www.zsh.org/) integration works by temporarily setting
|
||||
`ZDOTDIR` to our `zsh` directory. An existing `ZDOTDIR` environment variable
|
||||
value will be retained and restored after our shell integration scripts are
|
||||
run.
|
||||
|
||||
However, if `ZDOTDIR` is set in a system-wide file like `/etc/zshenv`, it will
|
||||
override Ghostty's `ZDOTDIR` value, preventing the shell integration from being
|
||||
loaded. In this case, the shell integration needs to be loaded manually.
|
||||
|
||||
To load the Zsh shell integration manually:
|
||||
|
||||
```zsh
|
||||
if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then
|
|||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@";
|
||||
builtin command sudo --preserve-env=TERMINFO "$@";
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@
|
|||
if (not (has-value $arg =)) { break }
|
||||
}
|
||||
|
||||
if (not $sudoedit) { set args = [ TERMINFO=$E:TERMINFO $@args ] }
|
||||
if (not $sudoedit) { set args = [ --preserve-env=TERMINFO $@args ] }
|
||||
(external sudo) $@args
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration"
|
|||
if test "$sudo_has_sudoedit_flags" = "yes"
|
||||
command sudo $argv
|
||||
else
|
||||
command sudo TERMINFO="$TERMINFO" $argv
|
||||
command sudo --preserve-env=TERMINFO $argv
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -93,9 +93,6 @@ _entrypoint() {
|
|||
_ghostty_deferred_init() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
# The directory where ghostty-integration is located: /../shell-integration/zsh.
|
||||
builtin local self_dir="${functions_source[_ghostty_deferred_init]:A:h}"
|
||||
|
||||
# Enable semantic markup with OSC 133.
|
||||
_ghostty_precmd() {
|
||||
builtin local -i cmd_status=$?
|
||||
|
|
@ -255,7 +252,7 @@ _ghostty_deferred_init() {
|
|||
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
|
||||
builtin command sudo "$@";
|
||||
else
|
||||
builtin command sudo TERMINFO="$TERMINFO" "$@";
|
||||
builtin command sudo --preserve-env=TERMINFO "$@";
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,12 +5,23 @@ const std = @import("std");
|
|||
const assert = std.debug.assert;
|
||||
const Generator = @import("Generator.zig");
|
||||
const Bytes = @import("Bytes.zig");
|
||||
const urlPercentEncode = @import("../os/string_encoding.zig").urlPercentEncode;
|
||||
|
||||
/// Valid OSC request kinds that can be generated.
|
||||
pub const ValidKind = enum {
|
||||
change_window_title,
|
||||
prompt_start,
|
||||
prompt_end,
|
||||
end_of_input,
|
||||
end_of_command,
|
||||
rxvt_notify,
|
||||
mouse_shape,
|
||||
clipboard_operation,
|
||||
report_pwd,
|
||||
hyperlink_start,
|
||||
hyperlink_end,
|
||||
conemu_progress,
|
||||
iterm2_notification,
|
||||
};
|
||||
|
||||
/// Invalid OSC request kinds that can be generated.
|
||||
|
|
@ -55,6 +66,9 @@ fn checkOscAlphabet(c: u8) bool {
|
|||
|
||||
/// The alphabet for random bytes in OSCs (omitting 0x1B and 0x07).
|
||||
pub const osc_alphabet = Bytes.generateAlphabet(checkOscAlphabet);
|
||||
pub const ascii_alphabet = Bytes.generateAlphabet(std.ascii.isPrint);
|
||||
pub const alphabetic_alphabet = Bytes.generateAlphabet(std.ascii.isAlphabetic);
|
||||
pub const alphanumeric_alphabet = Bytes.generateAlphabet(std.ascii.isAlphanumeric);
|
||||
|
||||
pub fn generator(self: *Osc) Generator {
|
||||
return .init(self, next);
|
||||
|
|
@ -143,6 +157,115 @@ fn nextUnwrappedValidExact(self: *const Osc, writer: *std.Io.Writer, k: ValidKin
|
|||
if (max_len < 4) break :prompt_end;
|
||||
try writer.writeAll("133;B"); // End prompt
|
||||
},
|
||||
|
||||
.end_of_input => end_of_input: {
|
||||
if (max_len < 5) break :end_of_input;
|
||||
var remaining = max_len;
|
||||
try writer.writeAll("133;C"); // End prompt
|
||||
remaining -= 5;
|
||||
if (self.rand.boolean()) cmdline: {
|
||||
const prefix = ";cmdline_url=";
|
||||
if (remaining < prefix.len + 1) break :cmdline;
|
||||
try writer.writeAll(prefix);
|
||||
remaining -= prefix.len;
|
||||
var buf: [128]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
try self.bytes().newAlphabet(ascii_alphabet).atMost(@min(remaining, buf.len)).format(&w);
|
||||
try urlPercentEncode(writer, w.buffered());
|
||||
remaining -= w.buffered().len;
|
||||
}
|
||||
},
|
||||
|
||||
.end_of_command => end_of_command: {
|
||||
if (max_len < 4) break :end_of_command;
|
||||
try writer.writeAll("133;D"); // End prompt
|
||||
if (self.rand.boolean()) exit_code: {
|
||||
if (max_len < 7) break :exit_code;
|
||||
try writer.print(";{d}", .{self.rand.int(u8)});
|
||||
}
|
||||
},
|
||||
|
||||
.mouse_shape => mouse_shape: {
|
||||
if (max_len < 4) break :mouse_shape;
|
||||
try writer.print("22;{f}", .{self.bytes().newAlphabet(alphabetic_alphabet).atMost(@min(32, max_len - 3))}); // Start prompt
|
||||
},
|
||||
|
||||
.rxvt_notify => rxvt_notify: {
|
||||
const prefix = "777;notify;";
|
||||
if (max_len < prefix.len) break :rxvt_notify;
|
||||
var remaining = max_len;
|
||||
try writer.writeAll(prefix);
|
||||
remaining -= prefix.len;
|
||||
remaining -= try self.bytes().newAlphabet(kv_alphabet).atMost(@min(remaining - 2, 32)).write(writer);
|
||||
try writer.writeByte(';');
|
||||
remaining -= 1;
|
||||
remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer);
|
||||
},
|
||||
|
||||
.clipboard_operation => {
|
||||
try writer.writeAll("52;");
|
||||
var remaining = max_len - 3;
|
||||
if (self.rand.boolean()) {
|
||||
remaining -= try self.bytes().newAlphabet(alphabetic_alphabet).atMost(1).write(writer);
|
||||
}
|
||||
try writer.writeByte(';');
|
||||
remaining -= 1;
|
||||
if (self.rand.boolean()) {
|
||||
remaining -= try self.bytes().newAlphabet(osc_alphabet).atMost(remaining).write(writer);
|
||||
}
|
||||
},
|
||||
|
||||
.report_pwd => report_pwd: {
|
||||
const prefix = "7;file://localhost";
|
||||
if (max_len < prefix.len) break :report_pwd;
|
||||
var remaining = max_len;
|
||||
try writer.writeAll(prefix);
|
||||
remaining -= prefix.len;
|
||||
for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| {
|
||||
try writer.writeByte('/');
|
||||
remaining -= 1;
|
||||
remaining -= try self.bytes().newAlphabet(alphanumeric_alphabet).atMost(@min(16, remaining)).write(writer);
|
||||
}
|
||||
},
|
||||
|
||||
.hyperlink_start => {
|
||||
try writer.writeAll("8;");
|
||||
if (self.rand.boolean()) {
|
||||
try writer.print("id={f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)});
|
||||
}
|
||||
try writer.writeAll(";https://localhost");
|
||||
for (0..self.rand.intRangeAtMost(usize, 2, 5)) |_| {
|
||||
try writer.print("/{f}", .{self.bytes().newAlphabet(alphanumeric_alphabet).atMost(16)});
|
||||
}
|
||||
},
|
||||
|
||||
.hyperlink_end => hyperlink_end: {
|
||||
if (max_len < 3) break :hyperlink_end;
|
||||
try writer.writeAll("8;;");
|
||||
},
|
||||
|
||||
.conemu_progress => {
|
||||
try writer.writeAll("9;");
|
||||
switch (self.rand.intRangeAtMost(u3, 0, 4)) {
|
||||
0, 3 => |c| {
|
||||
try writer.print(";{d}", .{c});
|
||||
},
|
||||
1, 2, 4 => |c| {
|
||||
if (self.rand.boolean()) {
|
||||
try writer.print(";{d}", .{c});
|
||||
} else {
|
||||
try writer.print(";{d};{d}", .{ c, self.rand.intRangeAtMost(u8, 0, 100) });
|
||||
}
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
|
||||
.iterm2_notification => iterm2_notification: {
|
||||
if (max_len < 3) break :iterm2_notification;
|
||||
// add a prefix to ensure that this is not interpreted as a ConEmu OSC
|
||||
try writer.print("9;_{f}", .{self.bytes().newAlphabet(ascii_alphabet).atMost(max_len - 3)});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,10 +36,12 @@ pub fn run(_: *Ascii, writer: *std.Io.Writer, rand: std.Random) !void {
|
|||
var gen: Bytes = .{
|
||||
.rand = rand,
|
||||
.alphabet = ascii,
|
||||
.min_len = 1024,
|
||||
.max_len = 1024,
|
||||
};
|
||||
|
||||
while (true) {
|
||||
gen.next(writer, 1024) catch |err| {
|
||||
_ = gen.write(writer) catch |err| {
|
||||
const Error = error{ WriteFailed, BrokenPipe } || @TypeOf(err);
|
||||
switch (@as(Error, err)) {
|
||||
error.BrokenPipe => return, // stdout closed
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ const log = std.log.scoped(.@"terminal-stream-bench");
|
|||
pub const Options = struct {
|
||||
/// Probability of generating a valid value.
|
||||
@"p-valid": f64 = 0.5,
|
||||
|
||||
style: enum {
|
||||
/// Write all OSC data, including ESC ] and ST for end-to-end tests
|
||||
streaming,
|
||||
/// Only write data, prefixed with a length, used for testing just the
|
||||
/// OSC parser.
|
||||
parser,
|
||||
} = .streaming,
|
||||
};
|
||||
|
||||
opts: Options,
|
||||
|
|
@ -40,9 +48,21 @@ pub fn run(self: *Osc, writer: *std.Io.Writer, rand: std.Random) !void {
|
|||
var fixed: std.Io.Writer = .fixed(&buf);
|
||||
try gen.next(&fixed, buf.len);
|
||||
const data = fixed.buffered();
|
||||
writer.writeAll(data) catch |err| switch (err) {
|
||||
error.WriteFailed => return,
|
||||
};
|
||||
switch (self.opts.style) {
|
||||
.streaming => {
|
||||
writer.writeAll(data) catch |err| switch (err) {
|
||||
error.WriteFailed => return,
|
||||
};
|
||||
},
|
||||
.parser => {
|
||||
writer.writeInt(usize, data.len - 3, .little) catch |err| switch (err) {
|
||||
error.WriteFailed => return,
|
||||
};
|
||||
writer.writeAll(data[2 .. data.len - 1]) catch |err| switch (err) {
|
||||
error.WriteFailed => return,
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1219,7 +1219,7 @@ pub fn index(self: *Terminal) !void {
|
|||
// this check.
|
||||
!self.screens.active.blankCell().isZero())
|
||||
{
|
||||
self.scrollUp(1);
|
||||
try self.scrollUp(1);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1398,7 +1398,7 @@ pub fn scrollDown(self: *Terminal, count: usize) void {
|
|||
/// The new lines are created according to the current SGR state.
|
||||
///
|
||||
/// Does not change the (absolute) cursor position.
|
||||
pub fn scrollUp(self: *Terminal, count: usize) void {
|
||||
pub fn scrollUp(self: *Terminal, count: usize) !void {
|
||||
// Preserve our x/y to restore.
|
||||
const old_x = self.screens.active.cursor.x;
|
||||
const old_y = self.screens.active.cursor.y;
|
||||
|
|
@ -1408,6 +1408,32 @@ pub fn scrollUp(self: *Terminal, count: usize) void {
|
|||
self.screens.active.cursor.pending_wrap = old_wrap;
|
||||
}
|
||||
|
||||
// If our scroll region is at the top and we have no left/right
|
||||
// margins then we move the scrolled out text into the scrollback.
|
||||
if (self.scrolling_region.top == 0 and
|
||||
self.scrolling_region.left == 0 and
|
||||
self.scrolling_region.right == self.cols - 1)
|
||||
{
|
||||
// Scrolling dirties the images because it updates their placements pins.
|
||||
if (comptime build_options.kitty_graphics) {
|
||||
self.screens.active.kitty_images.dirty = true;
|
||||
}
|
||||
|
||||
// Clamp count to the scroll region height.
|
||||
const region_height = self.scrolling_region.bottom + 1;
|
||||
const adjusted_count = @min(count, region_height);
|
||||
|
||||
// TODO: Create an optimized version that can scroll N times
|
||||
// This isn't critical because in most cases, scrollUp is used
|
||||
// with count=1, but it's still a big optimization opportunity.
|
||||
|
||||
// Move our cursor to the bottom of the scroll region so we can
|
||||
// use the cursorScrollAbove function to create scrollback
|
||||
self.screens.active.cursorAbsolute(0, self.scrolling_region.bottom);
|
||||
for (0..adjusted_count) |_| try self.screens.active.cursorScrollAbove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move to the top of the scroll region
|
||||
self.screens.active.cursorAbsolute(self.scrolling_region.left, self.scrolling_region.top);
|
||||
self.deleteLines(count);
|
||||
|
|
@ -5635,14 +5661,16 @@ test "Terminal: scrollUp simple" {
|
|||
t.setCursorPos(2, 2);
|
||||
|
||||
const cursor = t.screens.active.cursor;
|
||||
t.clearDirty();
|
||||
t.scrollUp(1);
|
||||
const viewport_before = t.screens.active.pages.getTopLeft(.viewport);
|
||||
try t.scrollUp(1);
|
||||
try testing.expectEqual(cursor.x, t.screens.active.cursor.x);
|
||||
try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
|
||||
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } }));
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } }));
|
||||
// Viewport should have moved. Our entire page should've scrolled!
|
||||
// The viewport moving will cause our render state to make the full
|
||||
// frame as dirty.
|
||||
const viewport_after = t.screens.active.pages.getTopLeft(.viewport);
|
||||
try testing.expect(!viewport_before.eql(viewport_after));
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
|
|
@ -5666,7 +5694,7 @@ test "Terminal: scrollUp moves hyperlink" {
|
|||
try t.linefeed();
|
||||
try t.printString("GHI");
|
||||
t.setCursorPos(2, 2);
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
|
|
@ -5717,7 +5745,7 @@ test "Terminal: scrollUp clears hyperlink" {
|
|||
try t.linefeed();
|
||||
try t.printString("GHI");
|
||||
t.setCursorPos(2, 2);
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
|
|
@ -5755,7 +5783,7 @@ test "Terminal: scrollUp top/bottom scroll region" {
|
|||
t.setCursorPos(1, 1);
|
||||
|
||||
t.clearDirty();
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
|
||||
// This is dirty because the cursor moves from this row
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
|
|
@ -5787,7 +5815,7 @@ test "Terminal: scrollUp left/right scroll region" {
|
|||
|
||||
const cursor = t.screens.active.cursor;
|
||||
t.clearDirty();
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
try testing.expectEqual(cursor.x, t.screens.active.cursor.x);
|
||||
try testing.expectEqual(cursor.y, t.screens.active.cursor.y);
|
||||
|
||||
|
|
@ -5819,7 +5847,7 @@ test "Terminal: scrollUp left/right scroll region hyperlink" {
|
|||
t.scrolling_region.left = 1;
|
||||
t.scrolling_region.right = 3;
|
||||
t.setCursorPos(2, 2);
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
|
||||
{
|
||||
const str = try t.plainString(testing.allocator);
|
||||
|
|
@ -5919,7 +5947,7 @@ test "Terminal: scrollUp preserves pending wrap" {
|
|||
try t.print('B');
|
||||
t.setCursorPos(3, 5);
|
||||
try t.print('C');
|
||||
t.scrollUp(1);
|
||||
try t.scrollUp(1);
|
||||
try t.print('X');
|
||||
|
||||
{
|
||||
|
|
@ -5940,7 +5968,7 @@ test "Terminal: scrollUp full top/bottom region" {
|
|||
t.setTopAndBottomMargin(2, 5);
|
||||
|
||||
t.clearDirty();
|
||||
t.scrollUp(4);
|
||||
try t.scrollUp(4);
|
||||
|
||||
// This is dirty because the cursor moves from this row
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
|
|
@ -5966,7 +5994,7 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" {
|
|||
t.setLeftAndRightMargin(2, 4);
|
||||
|
||||
t.clearDirty();
|
||||
t.scrollUp(4);
|
||||
try t.scrollUp(4);
|
||||
|
||||
// This is dirty because the cursor moves from this row
|
||||
try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } }));
|
||||
|
|
@ -5982,6 +6010,143 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" {
|
|||
}
|
||||
}
|
||||
|
||||
test "Terminal: scrollUp creates scrollback in primary screen" {
|
||||
// When in primary screen with full-width scroll region at top,
|
||||
// scrollUp (CSI S) should push lines into scrollback like xterm.
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 10 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
// Fill the screen with content
|
||||
try t.printString("AAAAA");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("BBBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("CCCCC");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("DDDDD");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("EEEEE");
|
||||
|
||||
t.clearDirty();
|
||||
|
||||
// Scroll up by 1, which should push "AAAAA" into scrollback
|
||||
try t.scrollUp(1);
|
||||
|
||||
// The cursor row (new empty row) should be dirty
|
||||
try testing.expect(t.screens.active.cursor.page_row.dirty);
|
||||
|
||||
// The active screen should now show BBBBB through EEEEE plus one blank line
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings("BBBBB\nCCCCC\nDDDDD\nEEEEE", str);
|
||||
}
|
||||
|
||||
// Now scroll to the top to see scrollback - AAAAA should be there
|
||||
t.screens.active.scroll(.{ .top = {} });
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
// Should see AAAAA in scrollback
|
||||
try testing.expectEqualStrings("AAAAA\nBBBBB\nCCCCC\nDDDDD\nEEEEE", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: scrollUp with max_scrollback zero" {
|
||||
// When max_scrollback is 0, scrollUp should still work but not retain history
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
try t.printString("AAAAA");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("BBBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("CCCCC");
|
||||
|
||||
try t.scrollUp(1);
|
||||
|
||||
// Active screen should show scrolled content
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings("BBBBB\nCCCCC", str);
|
||||
}
|
||||
|
||||
// Scroll to top - should be same as active since no scrollback
|
||||
t.screens.active.scroll(.{ .top = {} });
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
try testing.expectEqualStrings("BBBBB\nCCCCC", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: scrollUp with max_scrollback zero and top margin" {
|
||||
// When max_scrollback is 0 and top margin is set, should use deleteLines path
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 5, .max_scrollback = 0 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
try t.printString("AAAAA");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("BBBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("CCCCC");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("DDDDD");
|
||||
|
||||
// Set top margin (not at row 0)
|
||||
t.setTopAndBottomMargin(2, 5);
|
||||
|
||||
try t.scrollUp(1);
|
||||
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
// First row preserved, rest scrolled
|
||||
try testing.expectEqualStrings("AAAAA\nCCCCC\nDDDDD", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: scrollUp with max_scrollback zero and left/right margin" {
|
||||
// When max_scrollback is 0 with left/right margins, uses deleteLines path
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 10, .max_scrollback = 0 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
try t.printString("AAAAABBBBB");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("CCCCCDDDDD");
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("EEEEEFFFFF");
|
||||
|
||||
// Set left/right margins (columns 2-6, 1-indexed = indices 1-5)
|
||||
t.modes.set(.enable_left_and_right_margin, true);
|
||||
t.setLeftAndRightMargin(2, 6);
|
||||
|
||||
try t.scrollUp(1);
|
||||
|
||||
{
|
||||
const str = try t.plainString(alloc);
|
||||
defer alloc.free(str);
|
||||
// cols 1-5 scroll, col 0 and cols 6+ preserved
|
||||
try testing.expectEqualStrings("ACCCCDBBBB\nCEEEEFDDDD\nE FFFF", str);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: scrollDown simple" {
|
||||
const alloc = testing.allocator;
|
||||
var t = try init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ pub const Handler = struct {
|
|||
.insert_lines => self.terminal.insertLines(value),
|
||||
.insert_blanks => self.terminal.insertBlanks(value),
|
||||
.delete_lines => self.terminal.deleteLines(value),
|
||||
.scroll_up => self.terminal.scrollUp(value),
|
||||
.scroll_up => try self.terminal.scrollUp(value),
|
||||
.scroll_down => self.terminal.scrollDown(value),
|
||||
.horizontal_tab => try self.horizontalTab(value),
|
||||
.horizontal_tab_back => try self.horizontalTabBack(value),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const configpkg = @import("../config.zig");
|
|||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
/// Mutex state argument for queueMessage.
|
||||
pub const MutexState = enum { locked, unlocked };
|
||||
|
||||
/// Allocator
|
||||
alloc: Allocator,
|
||||
|
||||
|
|
@ -380,7 +383,7 @@ pub fn threadExit(self: *Termio, data: *ThreadData) void {
|
|||
pub fn queueMessage(
|
||||
self: *Termio,
|
||||
msg: termio.Message,
|
||||
mutex: enum { locked, unlocked },
|
||||
mutex: MutexState,
|
||||
) void {
|
||||
self.mailbox.send(msg, switch (mutex) {
|
||||
.locked => self.renderer_state.mutex,
|
||||
|
|
|
|||
|
|
@ -259,8 +259,9 @@ fn setupBash(
|
|||
resource_dir: []const u8,
|
||||
env: *EnvMap,
|
||||
) !?config.Command {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 2);
|
||||
defer args.deinit(alloc);
|
||||
var stack_fallback = std.heap.stackFallback(4096, alloc);
|
||||
var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get());
|
||||
defer cmd.deinit();
|
||||
|
||||
// Iterator that yields each argument in the original command line.
|
||||
// This will allocate once proportionate to the command line length.
|
||||
|
|
@ -269,9 +270,9 @@ fn setupBash(
|
|||
|
||||
// Start accumulating arguments with the executable and initial flags.
|
||||
if (iter.next()) |exe| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, exe));
|
||||
try cmd.appendArg(exe);
|
||||
} else return null;
|
||||
try args.append(alloc, "--posix");
|
||||
try cmd.appendArg("--posix");
|
||||
|
||||
// Stores the list of intercepted command line flags that will be passed
|
||||
// to our shell integration script: --norc --noprofile
|
||||
|
|
@ -304,17 +305,17 @@ fn setupBash(
|
|||
if (std.mem.indexOfScalar(u8, arg, 'c') != null) {
|
||||
return null;
|
||||
}
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
} else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
|
||||
// All remaining arguments should be passed directly to the shell
|
||||
// command. We shouldn't perform any further option processing.
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
while (iter.next()) |remaining_arg| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, remaining_arg));
|
||||
try cmd.appendArg(remaining_arg);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
}
|
||||
}
|
||||
try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]);
|
||||
|
|
@ -352,8 +353,11 @@ fn setupBash(
|
|||
);
|
||||
try env.put("ENV", integ_dir);
|
||||
|
||||
// Join the accumulated arguments to form the final command string.
|
||||
return .{ .shell = try std.mem.joinZ(alloc, " ", args.items) };
|
||||
// Get the command string from the builder, then copy it to the arena
|
||||
// allocator. The stackFallback allocator's memory becomes invalid after
|
||||
// this function returns, so we must copy to the arena.
|
||||
const cmd_str = try cmd.toOwnedSlice();
|
||||
return .{ .shell = try alloc.dupeZ(u8, cmd_str) };
|
||||
}
|
||||
|
||||
test "bash" {
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ pub const StreamHandler = struct {
|
|||
.insert_lines => self.terminal.insertLines(value),
|
||||
.insert_blanks => self.terminal.insertBlanks(value),
|
||||
.delete_lines => self.terminal.deleteLines(value),
|
||||
.scroll_up => self.terminal.scrollUp(value),
|
||||
.scroll_up => try self.terminal.scrollUp(value),
|
||||
.scroll_down => self.terminal.scrollDown(value),
|
||||
.tab_clear_current => self.terminal.tabClear(.current),
|
||||
.tab_clear_all => self.terminal.tabClear(.all),
|
||||
|
|
|
|||
Loading…
Reference in New Issue