1 Commits
tip ... booids

Author SHA1 Message Date
Mitchell Hashimoto
b0573a40e7 BOO! Ids. (Booids, rhymes with "squids")
This introduces an ID format for Ghostty objects, lovingly called the
"booid" (rhyming with "squids" because "guids"). I'm not a huge fan of
overly cute naming -- I prefer obvious names -- but all the obvious
names like "unique ID" or "global ID" are too similar to other, very
real, standard ID formats.

These IDs will be used in the short term by app IDs to reference
surfaces for things like notifications. And in the long term, we'll use
these for the still-to-be-designed Ghostty API. And super long term,
we'll use these for session attach/detach.

There's no concurrency for our ID generation today, so this commit only
has a simple non-thread-safe implementation. That's all we need.

Ghostty's use case has slightly different requirements than most unique
ID requirements, so I believe a custom format is the way to go here.
These requirements are noted here:

The booid is a 64-bit ID to make it easy to send across C ABIs and local
transports like D-Bus, both of which are very important for a desktop
application like Ghostty.

We follow the same timestamp, machine, sequence number format as
Snowflake IDs, but we use the full 64 bits for the ID, rather than 63,
since can explicitly note its an unsigned ID format. Our bit width
breakdown allows for 1,024 nodes in a cluter producing 4,096 IDs per
millisecond.

"A cluster?! What the @#!* are you talking about? This is a local
terminal emulator." Yes, yes indeed. 42-bits of timestamp and 12 bits
of sequence are more than enough for a local terminal emulator, so we
have 10 bits left over (arguably, we need even less sequence bits). For
now, we designate this as a "machine ID" and always set it to "0" since
we are local-only.

But I think this will be useful in the future if/when we introduce the
ability to attach to remote Ghostty instances for session sharing,
restore, etc. This is a use case and feature I have always planned. 

The question of how we assign machine IDs in that future is left to the
future. I propose a simple idea in the comments in this commit: assign
and map them locally! All local IDs can produce machine ID 0 and then
the CLIENT side can replace those bits with some value. This eliminates
a distributed consensus problem and allows a client terminal to attach
to up to 1,024 remote instances at once (unlikely lol).
2025-08-15 11:08:16 -07:00
258 changed files with 16042 additions and 7525 deletions

View File

@@ -1,64 +0,0 @@
#!/usr/bin/env nu
# A command to generate an agent prompt to diagnose and formulate
# a plan for resolving a GitHub issue.
#
# IMPORTANT: This command is prompted to NOT write any code and to ONLY
# produce a plan. You should still be vigilant when running this but that
# is the expected behavior.
#
# The `<issue>` parameter can be either an issue number or a full GitHub
# issue URL.
def main [
issue: any, # Ghostty issue number or URL
--repo: string = "ghostty-org/ghostty" # GitHub repository in the format "owner/repo"
] {
# TODO: This whole script doesn't handle errors very well. I actually
# don't know Nu well enough to know the proper way to handle it all.
let issueData = gh issue view $issue --json author,title,number,body,comments | from json
let comments = $issueData.comments | each { |comment|
$"
### Comment by ($comment.author.login)
($comment.body)
" | str trim
} | str join "\n\n"
$"
Deep-dive on this GitHub issue. Find the problem and generate a plan.
Do not write code. Explain the problem clearly and propose a comprehensive plan
to solve it.
# ($issueData.title) \(($issueData.number)\)
## Description
($issueData.body)
## Comments
($comments)
## Your Tasks
You are an experienced software developer tasked with diagnosing issues.
1. Review the issue context and details.
2. Examine the relevant parts of the codebase. Analyze the code thoroughly
until you have a solid understanding of how it works.
3. Explain the issue in detail, including the problem and its root cause.
4. Create a comprehensive plan to solve the issue. The plan should include:
- Required code changes
- Potential impacts on other parts of the system
- Necessary tests to be written or updated
- Documentation updates
- Performance considerations
- Security implications
- Backwards compatibility \(if applicable\)
- Include the reference link to the source issue and any related discussions
4. Think deeply about all aspects of the task. Consider edge cases, potential
challenges, and best practices for addressing the issue. Review the plan
with the oracle and adjust it based on its feedback.
**ONLY CREATE A PLAN. DO NOT WRITE ANY CODE.** Your task is to create
a thorough, comprehensive strategy for understanding and resolving the issue.
" | str trim
}

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env nu
# Check if a given commit SHA has a corresponding tip release.
#
# This does not validate that the commit SHA is valid for the
# Ghostty repository, only that a tip release exists for it.
def main [
commit: string, # The full length commit SHA
] {
let url = $"https://tip.files.ghostty.org/($commit)/ghostty-macos-universal.zip"
try {
http head $url
exit 0
} catch {
print -e $"The SHA ($commit) does not have a corresponding tip release."
exit 1
}
}

View File

@@ -36,13 +36,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16

View File

@@ -47,7 +47,7 @@ jobs:
sentry-cli dif upload --project ghostty --wait dsym.zip
build-macos:
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
steps:
- name: Checkout code
@@ -57,9 +57,9 @@ jobs:
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -68,7 +68,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -95,7 +95,6 @@ jobs:
run: |
cd macos
sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -version
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
@@ -202,7 +201,7 @@ jobs:
destination-dir: ./
build-macos-debug:
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
steps:
- name: Checkout code
@@ -212,9 +211,9 @@ jobs:
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -223,7 +222,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.5.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -250,7 +249,6 @@ jobs:
run: |
cd macos
sudo xcode-select -s /Applications/Xcode_26.0.app
xcodebuild -version
xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.

View File

@@ -83,13 +83,13 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
@@ -120,7 +120,7 @@ jobs:
build-macos:
needs: [setup]
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
@@ -130,9 +130,9 @@ jobs:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: DeterminateSystems/nix-installer-action@main
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -141,12 +141,9 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.4.app
- name: Xcode Version
run: xcodebuild -version
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -294,7 +291,7 @@ jobs:
appcast:
needs: [setup, build-macos]
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
@@ -311,7 +308,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -15,57 +15,9 @@ concurrency:
cancel-in-progress: false
jobs:
setup:
if: |
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
runs-on: namespace-profile-ghostty-sm
outputs:
should_skip: ${{ steps.check.outputs.should_skip }}
build: ${{ steps.extract_build_info.outputs.build }}
commit: ${{ steps.extract_build_info.outputs.commit }}
commit_long: ${{ steps.extract_build_info.outputs.commit_long }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Extract build info
id: extract_build_info
run: |
GHOSTTY_BUILD=$(git rev-list --count HEAD)
GHOSTTY_COMMIT=$(git rev-parse --short HEAD)
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
echo "build=$GHOSTTY_BUILD" >> $GITHUB_OUTPUT
echo "commit=$GHOSTTY_COMMIT" >> $GITHUB_OUTPUT
echo "commit_long=$GHOSTTY_COMMIT_LONG" >> $GITHUB_OUTPUT
- name: Check if tip already exists
id: check
run: |
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
if nix develop -c nu .github/scripts/ghostty-tip $GHOSTTY_COMMIT_LONG; then
echo "Tip release already exists for commit $GHOSTTY_COMMIT_LONG"
echo "should_skip=true" >> $GITHUB_OUTPUT
else
echo "No tip release found for commit $GHOSTTY_COMMIT_LONG"
echo "should_skip=false" >> $GITHUB_OUTPUT
fi
tag:
runs-on: namespace-profile-ghostty-sm
needs: [setup, build-macos]
if: needs.setup.outputs.should_skip != 'true'
needs: [build-macos]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Tip Tag
@@ -77,10 +29,7 @@ jobs:
sentry-dsym-debug-slow:
runs-on: namespace-profile-ghostty-sm
needs: [setup, build-macos-debug-slow]
if: needs.setup.outputs.should_skip != 'true'
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
needs: [build-macos-debug-slow]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -90,6 +39,7 @@ jobs:
- name: Download dSYM
run: |
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-slow-dsym.zip > dsym.zip
- name: Upload dSYM to Sentry
@@ -100,10 +50,7 @@ jobs:
sentry-dsym-debug-fast:
runs-on: namespace-profile-ghostty-sm
needs: [setup, build-macos-debug-fast]
if: needs.setup.outputs.should_skip != 'true'
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
needs: [build-macos-debug-fast]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -113,6 +60,7 @@ jobs:
- name: Download dSYM
run: |
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-debug-fast-dsym.zip > dsym.zip
- name: Upload dSYM to Sentry
@@ -123,10 +71,7 @@ jobs:
sentry-dsym:
runs-on: namespace-profile-ghostty-sm
needs: [setup, build-macos]
if: needs.setup.outputs.should_skip != 'true'
env:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
needs: [build-macos]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -136,6 +81,7 @@ jobs:
- name: Download dSYM
run: |
GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)
curl -L https://tip.files.ghostty.dev/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip > dsym.zip
- name: Upload dSYM to Sentry
@@ -145,17 +91,15 @@ jobs:
sentry-cli dif upload --project ghostty --wait dsym.zip
source-tarball:
needs: [setup]
if: |
needs.setup.outputs.should_skip != 'true' &&
(
${{
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
)
}}
runs-on: namespace-profile-ghostty-md
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
@@ -163,12 +107,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -200,24 +144,18 @@ jobs:
token: ${{ secrets.GH_RELEASE_TOKEN }}
build-macos:
needs: [setup]
if: |
needs.setup.outputs.should_skip != 'true' &&
(
${{
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
)
}}
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
env:
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -225,10 +163,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -243,7 +181,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -251,6 +189,13 @@ jobs:
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# Load Build Number
- name: Build Number
run: |
echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit
@@ -419,24 +364,18 @@ jobs:
destination-dir: ./
build-macos-debug-slow:
needs: [setup]
if: |
needs.setup.outputs.should_skip != 'true' &&
(
${{
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
)
}}
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
env:
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -444,10 +383,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -462,7 +401,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.5.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -470,6 +409,13 @@ jobs:
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# Load Build Number
- name: Build Number
run: |
echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit
@@ -598,24 +544,18 @@ jobs:
destination-dir: ./
build-macos-debug-fast:
needs: [setup]
if: |
needs.setup.outputs.should_skip != 'true' &&
(
${{
github.event_name == 'workflow_dispatch' ||
(
github.event.workflow_run.conclusion == 'success' &&
github.repository_owner == 'ghostty-org' &&
github.ref_name == 'main'
)
)
}}
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
timeout-minutes: 90
env:
GHOSTTY_BUILD: ${{ needs.setup.outputs.build }}
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -623,10 +563,10 @@ jobs:
# Important so that build number generation works
fetch-depth: 0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -641,7 +581,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.7.1
SPARKLE_VERSION: 2.5.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -649,6 +589,13 @@ jobs:
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# Load Build Number
- name: Build Number
run: |
echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT_LONG=$(git rev-parse HEAD)" >> $GITHUB_ENV
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit

View File

@@ -13,16 +13,19 @@ jobs:
- build-bench
- build-dist
- build-flatpak
- build-freebsd
- build-linux
- build-linux-libghostty
- build-nix
- build-snap
- build-macos
- build-macos-tahoe
- build-macos-matrix
- build-windows
- flatpak-check-zig-cache
- flatpak
- test
- test-gtk
- test-gtk-ng
- test-sentry-linux
- test-macos
- pinact
@@ -34,9 +37,7 @@ jobs:
- blueprint-compiler
- test-pkg-linux
- test-debian-13
- valgrind
- zig-fmt
- flatpak
steps:
- id: status
name: Determine status
@@ -69,14 +70,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -100,14 +101,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -136,14 +137,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -165,14 +166,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -198,14 +199,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -242,14 +243,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -271,16 +272,16 @@ jobs:
ghostty-source.tar.gz
build-macos:
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -313,7 +314,7 @@ jobs:
cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-macos-matrix:
build-macos-tahoe:
runs-on: namespace-profile-ghostty-macos-tahoe
needs: test
steps:
@@ -332,8 +333,45 @@ jobs:
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Xcode Version
run: xcodebuild -version
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access.
- name: Build GhosttyKit
run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Demit-macos-app=false
# The native app is built with native Xcode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild -target Ghostty
# Build the iOS target without code signing just to verify it works.
- name: Build Ghostty iOS
run: |
cd macos
xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO"
build-macos-matrix:
runs-on: namespace-profile-ghostty-macos-sequoia
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: get the Zig deps
id: deps
@@ -362,7 +400,6 @@ jobs:
os:
[namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
needs: [test, build-dist]
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
@@ -377,7 +414,7 @@ jobs:
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@@ -397,7 +434,6 @@ jobs:
runs-on: windows-2022
# this will not stop other jobs from running
continue-on-error: true
timeout-minutes: 45
needs: test
steps:
- name: Checkout code
@@ -473,14 +509,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -494,6 +530,9 @@ jobs:
- name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs -Demit-webdata
- name: Test GTK-NG Build
run: nix develop -c zig build -Dapp-runtime=gtk-ng -Demit-docs -Demit-webdata
# This relies on the cache being populated by the commands above.
- name: Test System Build
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
@@ -515,14 +554,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -547,6 +586,55 @@ jobs:
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}
test-gtk-ng:
strategy:
fail-fast: false
matrix:
x11: ["true", "false"]
wayland: ["true", "false"]
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Test
run: |
nix develop -c \
zig build \
-Dapp-runtime=gtk-ng \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }} \
test
- name: Build
run: |
nix develop -c \
zig build \
-Dapp-runtime=gtk-ng \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}
test-sentry-linux:
strategy:
fail-fast: false
@@ -563,14 +651,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -583,16 +671,16 @@ jobs:
nix develop -c zig build -Dsentry=${{ matrix.sentry }}
test-macos:
runs-on: namespace-profile-ghostty-macos-tahoe
runs-on: namespace-profile-ghostty-macos-sequoia
needs: test
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
determinate: true
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
@@ -601,9 +689,6 @@ jobs:
- name: Xcode Select
run: sudo xcode-select -s /Applications/Xcode_26.0.app
- name: Xcode Version
run: xcodebuild -version
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
@@ -621,12 +706,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -649,12 +734,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -676,12 +761,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -703,12 +788,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -730,12 +815,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -757,12 +842,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -791,12 +876,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -818,12 +903,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -853,14 +938,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -901,6 +986,33 @@ jobs:
build-args: |
DISTRO_VERSION=13
flatpak-check-zig-cache:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-xsm
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
useDaemon: false # sometimes fails on short jobs
- name: Check Flatpak Zig Dependencies
run: nix develop -c ./flatpak/build-support/check-zig-cache.sh
flatpak:
if: github.repository == 'ghostty-org/ghostty'
name: "Flatpak"
@@ -916,7 +1028,7 @@ jobs:
- arch: aarch64
runner: namespace-profile-ghostty-md-arm64
runs-on: ${{ matrix.variant.runner }}
needs: test
needs: [flatpak-check-zig-cache, test]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
@@ -926,94 +1038,3 @@ jobs:
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
timeout-minutes: 30
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: valgrind deps
run: |
sudo apt update -y
sudo apt install -y valgrind libc6-dbg
- name: valgrind
run: |
nix develop -c zig build test-valgrind
build-freebsd:
name: Build on FreeBSD
needs: test
runs-on: namespace-profile-mitchellh-sm-systemd
strategy:
matrix:
release:
- "14.3"
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
steps:
- name: Checkout Ghostty
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Start SSH
run: |
sudo systemctl start ssh
- name: Set up FreeBSD VM
uses: vmactions/freebsd-vm@05856381fab64eeee9b038a0818f6cec649ca17a # v1.2.3
with:
release: ${{ matrix.release }}
copyback: false
usesh: true
prepare: |
pkg install -y \
devel/blueprint-compiler \
devel/gettext \
devel/git \
devel/pkgconf \
graphics/wayland \
lang/zig \
security/ca_root_nss \
textproc/hs-pandoc \
x11-fonts/jetbrains-mono \
x11-toolkits/libadwaita \
x11-toolkits/gtk40 \
x11-toolkits/gtk4-layer-shell
run: |
zig env
- name: Run tests
shell: freebsd {0}
run: |
cd $GITHUB_WORKSPACE
zig build test
- name: Build GTK app runtime
shell: freebsd {0}
run: |
cd $GITHUB_WORKSPACE
zig build
./zig-out/bin/ghostty +version

View File

@@ -22,14 +22,14 @@ jobs:
fetch-depth: 0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -50,6 +50,8 @@ jobs:
if ! git diff --exit-code build.zig.zon; then
nix develop -c ./nix/build-support/check-zig-cache.sh --update
nix develop -c ./nix/build-support/check-zig-cache.sh
nix develop -c ./flatpak/build-support/check-zig-cache.sh --update
nix develop -c ./flatpak/build-support/check-zig-cache.sh
fi
# Verify the build still works. We choose an arbitrary build type

View File

@@ -1,23 +0,0 @@
# Agent Development Guide
A file for [guiding coding agents](https://agents.md/).
## Commands
- **Build:** `zig build`
- **Test (Zig):** `zig build test`
- **Test filter (Zig)**: `zig build test -Dtest-filter=<test name>`
- **Formatting (Zig)**: `zig fmt .`
- **Formatting (other)**: `prettier -w .`
## Directory Structure
- Shared Zig core: `src/`
- C API: `include/ghostty.h`
- macOS app: `macos/`
- GTK (Linux and FreeBSD) app: `src/apprt/gtk`
## macOS App
- Do not use `xcodebuild`
- Use `zig build` to build the macOS app and any shared Zig code

View File

@@ -119,6 +119,7 @@
# GTK
/src/apprt/gtk/ @ghostty-org/gtk
/src/apprt/gtk-ng/ @ghostty-org/gtk
/src/os/cgroup.zig @ghostty-org/gtk
/src/os/flatpak.zig @ghostty-org/gtk
/dist/linux/ @ghostty-org/gtk
@@ -168,7 +169,6 @@
/po/es_BO.UTF-8.po @ghostty-org/es_BO
/po/es_AR.UTF-8.po @ghostty-org/es_AR
/po/fr_FR.UTF-8.po @ghostty-org/fr_FR
/po/hu_HU.UTF-8.po @ghostty-org/hu_HU
/po/id_ID.UTF-8.po @ghostty-org/id_ID
/po/ja_JP.UTF-8.po @ghostty-org/ja_JP
/po/mk_MK.UTF-8.po @ghostty-org/mk_MK

View File

@@ -1,9 +1,9 @@
# Contributing to Ghostty
# Ghostty Development Process
This document describes the process of contributing to Ghostty. It is intended
for anyone considering opening an **issue**, **discussion** or **pull request**.
For people who are interested in developing Ghostty and technical details behind
it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
This document describes the development process for Ghostty. It is intended for
anyone considering opening an **issue** or **pull request**. If in doubt,
please open a [discussion](https://github.com/ghostty-org/ghostty/discussions);
we can always convert that to an issue later.
> [!NOTE]
>
@@ -13,57 +13,15 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
> time to fixing bugs, maintaining features, and reviewing code, I do kindly
> ask you spend a few minutes reading this document. Thank you. ❤️
## AI Assistance Notice
> [!IMPORTANT]
>
> If you are using **any kind of AI assistance** to contribute to Ghostty,
> it must be disclosed in the pull request.
If you are using any kind of AI assistance while contributing to Ghostty,
**this must be disclosed in the pull request**, along with the extent to
which AI assistance was used (e.g. docs only vs. code generation).
If PR responses are being generated by an AI, disclose that as well.
As a small exception, trivial tab-completion doesn't need to be disclosed,
so long as it is limited to single keywords or short phrases.
An example disclosure:
> This PR was written primarily by Claude Code.
Or a more detailed disclosure:
> I consulted ChatGPT to understand the codebase but the solution
> was fully authored manually by myself.
Failure to disclose this is first and foremost rude to the human operators
on the other end of the pull request, but it also makes it difficult to
determine how much scrutiny to apply to the contribution.
In a perfect world, AI assistance would produce equal or higher quality
work than any human. That isn't the world we live in today, and in most cases
it's generating slop. I say this despite being a fan of and using them
successfully myself (with heavy supervision)!
When using AI assistance, we expect contributors to understand the code
that is produced and be able to answer critical questions about it. It
isn't a maintainers job to review a PR so broken that it requires
significant rework to be acceptable.
Please be respectful to maintainers and disclose AI assistance.
## Quick Guide
### I'd like to contribute
**I'd like to contribute!**
[All issues are actionable](#issues-are-actionable). Pick one and start
working on it. Thank you. If you need help or guidance, comment on the issue.
Issues that are extra friendly to new contributors are tagged with
["contributor friendly"].
All issues are actionable. Pick one and start working on it. Thank you.
If you need help or guidance, comment on the issue. Issues that are extra
friendly to new contributors are tagged with "contributor friendly".
["contributor friendly"]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
### I'd like to translate Ghostty to my language
**I'd like to translate Ghostty to my language!**
We have written a [Translator's Guide](po/README_TRANSLATORS.md) for
everyone interested in contributing translations to Ghostty.
@@ -72,39 +30,25 @@ and you can submit pull requests directly, although please make sure that
our [Style Guide](po/README_TRANSLATORS.md#style-guide) is followed before
submission.
### I have a bug! / Something isn't working
**I have a bug!**
1. Search the issue tracker and discussions for similar issues. Tip: also
search for [closed issues] and [discussions] — your issue might have already
been fixed!
2. If your issue hasn't been reported already, open an ["Issue Triage" discussion]
and make sure to fill in the template **completely**. They are vital for
maintainers to figure out important details about your setup. Because of
this, please make sure that you _only_ use the "Issue Triage" category for
reporting bugs — thank you!
1. Search the issue tracker and discussions for similar issues.
2. If you don't have steps to reproduce, open a discussion.
3. If you have steps to reproduce, open an issue.
[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed
[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed
["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage
**I have an idea for a feature!**
### I have an idea for a feature
1. Open a discussion.
Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas).
**I've implemented a feature!**
### I've implemented a feature
1. If there is an issue for the feature, open a pull request straight away.
1. If there is an issue for the feature, open a pull request.
2. If there is no issue, open a discussion and link to your branch.
3. If you want to live dangerously, open a pull request and
[hope for the best](#pull-requests-implement-an-issue).
3. If you want to live dangerously, open a pull request and hope for the best.
### I have a question
**I have a question!**
Open an [Q&A discussion], or join our [Discord Server] and ask away in the
`#help` channel.
[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a
[Discord Server]: https://discord.gg/ghostty
1. Open a discussion or use Discord.
## General Patterns
@@ -142,3 +86,187 @@ 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.
## 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.

View File

@@ -1,355 +0,0 @@
# Developing Ghostty
This document describes the technical details behind Ghostty's development.
If you'd like to open any pull requests or would like to implement new features
into Ghostty, please make sure to read our ["Contributing to Ghostty"](CONTRIBUTING.md)
document first.
To start development on Ghostty, you need to build Ghostty from a Git checkout,
which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously
you need to clone the Git repository instead of unpacking the source tarball:
```shell
git clone https://github.com/ghostty-org/ghostty
cd ghostty
```
> [!NOTE]
>
> Ghostty may require [extra dependencies](#extra-dependencies)
> when building from a Git checkout compared to a source tarball.
> Tip versions may also require a different version of Zig or other toolchains
> (e.g. the Xcode SDK on macOS) compared to stable versions — make sure to
> follow the steps closely!
When you're developing Ghostty, it's very likely that you will want to build a
_debug_ build to diagnose issues more easily. This is already the default for
Zig builds, so simply run `zig build` **without any `-Doptimize` flags**.
There are many more build steps than just `zig build`, some of which are listed
here:
| Command | Description |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `zig build run` | Runs Ghostty |
| `zig build run-valgrind` | Runs Ghostty under Valgrind to [check for memory leaks](#checking-for-memory-leaks) |
| `zig build test` | Runs unit tests (accepts `-Dtest-filter=<filter>` to only run tests whose name matches the filter) |
| `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) |
| `zig build dist` | Builds a source tarball |
| `zig build distcheck` | Builds and validates a source tarball |
## Extra Dependencies
Building Ghostty from a Git checkout on Linux requires some additional
dependencies:
- `blueprint-compiler` (version 0.16.0 or newer)
macOS users don't require any additional dependencies.
## Xcode Version and SDKs
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
and the iOS SDK are all installed.
A common issue is that the incorrect version of Xcode is either
installed or selected. Use the `xcode-select` command to
ensure that the correct version of Xcode is selected:
```shell-session
sudo xcode-select --switch /Applications/Xcode-beta.app
```
> [!IMPORTANT]
>
> Main branch development of Ghostty is preparing for the next major
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
> **Xcode 26 and the macOS 26 SDK**.
>
> You do not need to be running on macOS 26 to build Ghostty, you can
> still use Xcode 26 beta on macOS 15 stable.
## AI and Agents
If you're using AI assistance with Ghostty, Ghostty provides an
[AGENTS.md file](https://github.com/ghostty-org/ghostty/blob/main/AGENTS.md)
read by most of the popular AI agents to help produce higher quality
results.
We also provide commands in `.agents/commands` that have some vetted
prompts for common tasks that have been shown to produce good results.
We provide these to help reduce the amount of time a contributor has to
spend prompting the AI to get good results, and hopefully to lower the slop
produced.
- `/gh-issue <number/url>` - Produces a prompt for diagnosing a GitHub
issue, explaining the problem, and suggesting a plan for resolving it.
Requires `gh` to be installed with read-only access to Ghostty.
> [!WARNING]
>
> All AI assistance usage [must be disclosed](https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md#ai-assistance-notice)
> and we expect contributors to understand the code that is produced and
> be able to answer questions about it. If you don't understand the
> code produced, feel free to disclose that, but if it has problems, we
> 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.
## Linting
### Prettier
Ghostty's docs and resources (not including Zig code) are linted using
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
check will fail builds with improper formatting. Therefore, if you are
modifying anything Prettier will lint, you may want to install it locally and
run this from the repo root before you commit:
```
prettier --write .
```
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
Nix users can use the following command to format with Prettier:
```
nix develop -c prettier --write .
```
### Alejandra
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
will fail builds with improper formatting.
Nix users can use the following command to format with Alejandra:
```
nix develop -c alejandra .
```
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
```
alejandra .
```
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
that manages the Zig package cache. This allows the package to be built in the
Nix sandbox.
Occasionally (usually when `build.zig.zon` is updated), the hash that
identifies the cache will need to be updated. There are jobs that monitor the
hash in CI, and builds will fail if it drifts.
To update it, you can run the following in the repository root:
```
./nix/build-support/check-zig-cache-hash.sh --update
```
This will write out the `nix/zigCacheHash.nix` file with the updated hash
that can then be committed and pushed to fix the builds.
## 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.

128
README.md
View File

@@ -13,9 +13,7 @@
·
<a href="https://ghostty.org/docs">Documentation</a>
·
<a href="CONTRIBUTING.md">Contributing</a>
·
<a href="HACKING.md">Developing</a>
<a href="#developing-ghostty">Developing</a>
</p>
</p>
@@ -51,14 +49,6 @@ See the [download page](https://ghostty.org/download) on the Ghostty website.
See the [documentation](https://ghostty.org/docs) on the Ghostty website.
## Contributing and Developing
If you have any ideas, issues, etc. regarding Ghostty, or would like to
contribute to Ghostty through pull requests, please check out our
["Contributing to Ghostty"](CONTRIBUTING.md) document. Those who would like
to get involved with Ghostty's development as well should also read the
["Developing Ghostty"](HACKING.md) document for more technical details.
## Roadmap and Status
The high-level ambitious plan for the project, in order:
@@ -194,3 +184,119 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
> stack memory of each thread at the time of the crash. This information
> is used to rebuild the stack trace but can also contain sensitive data
> depending when the crash occurred.
## Developing Ghostty
See the documentation on the Ghostty website for
[building Ghostty from a source tarball](http://ghostty.org/docs/install/build).
Building Ghostty from a Git checkout is very similar, except you want to
omit the `-Doptimize` flag to build a debug build, and you may require
additional dependencies since the source tarball includes some processed
files that are not in the Git repository.
Other useful commands:
- `zig build test` for running unit tests.
- `zig build test -Dtest-filter=<filter>` for running a specific subset of those unit tests
- `zig build run -Dconformance=<name>` runs a conformance test case from
the `conformance` directory. The `name` is the name of the file. This runs
in the current running terminal emulator so if you want to check the
behavior of this project, you must run this command in Ghostty.
### Extra Dependencies
Building Ghostty from a Git checkout on Linux requires some additional
dependencies:
- `blueprint-compiler`
macOS users don't require any additional dependencies.
> [!NOTE]
> This only applies to building from a _Git checkout_. This section does
> not apply if you're building from a released _source tarball_. For
> source tarballs, see the
> [website](http://ghostty.org/docs/install/build).
### Xcode Version and SDKs
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
and the iOS SDK are all installed.
A common issue is that the incorrect version of Xcode is either
installed or selected. Use the `xcode-select` command to
ensure that the correct version of Xcode is selected:
```shell-session
sudo xcode-select --switch /Applications/Xcode-beta.app
```
> [!IMPORTANT]
>
> Main branch development of Ghostty is preparing for the next major
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
> **Xcode 26 and the macOS 26 SDK**.
>
> You do not need to be running on macOS 26 to build Ghostty, you can
> still use Xcode 26 beta on macOS 15 stable.
### Linting
#### Prettier
Ghostty's docs and resources (not including Zig code) are linted using
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
check will fail builds with improper formatting. Therefore, if you are
modifying anything Prettier will lint, you may want to install it locally and
run this from the repo root before you commit:
```
prettier --write .
```
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
Nix users can use the following command to format with Prettier:
```
nix develop -c prettier --write .
```
#### Alejandra
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
will fail builds with improper formatting.
Nix users can use the following command to format with Alejandra:
```
nix develop -c alejandra .
```
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
```
alejandra .
```
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
#### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
that manages the Zig package cache. This allows the package to be built in the
Nix sandbox.
Occasionally (usually when `build.zig.zon` is updated), the hash that
identifies the cache will need to be updated. There are jobs that monitor the
hash in CI, and builds will fail if it drifts.
To update it, you can run the following in the repository root:
```
./nix/build-support/check-zig-cache-hash.sh --update
```
This will write out the `nix/zigCacheHash.nix` file with the updated hash
that can then be committed and pushed to fix the builds.

View File

@@ -19,15 +19,7 @@ pub fn build(b: *std.Build) !void {
// All our steps which we'll hook up later. The steps are shown
// up here just so that they are more self-documenting.
const run_step = b.step("run", "Run the app");
const run_valgrind_step = b.step(
"run-valgrind",
"Run the app under valgrind",
);
const test_step = b.step("test", "Run tests");
const test_valgrind_step = b.step(
"test-valgrind",
"Run tests under valgrind",
);
const test_step = b.step("test", "Run all tests");
const translations_step = b.step(
"update-translations",
"Update translation files",
@@ -85,11 +77,9 @@ pub fn build(b: *std.Build) !void {
// Runtime "none" is libghostty, anything else is an executable.
if (config.app_runtime != .none) {
if (config.emit_exe) {
exe.install();
resources.install();
if (i18n) |v| v.install();
}
exe.install();
resources.install();
if (i18n) |v| v.install();
} else {
// Libghostty
//
@@ -191,31 +181,6 @@ pub fn build(b: *std.Build) !void {
}
}
// Valgrind
if (config.app_runtime != .none) {
// We need to rebuild Ghostty with a baseline CPU target.
const valgrind_exe = exe: {
var valgrind_config = config;
valgrind_config.target = valgrind_config.baselineTarget();
break :exe try buildpkg.GhosttyExe.init(
b,
&valgrind_config,
&deps,
);
};
const run_cmd = b.addSystemCommand(&.{
"valgrind",
"--leak-check=full",
"--num-callers=50",
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
"--gen-suppressions=all",
});
run_cmd.addArtifactArg(valgrind_exe.exe);
if (b.args) |args| run_cmd.addArgs(args);
run_valgrind_step.dependOn(&run_cmd.step);
}
// Tests
{
const test_exe = b.addTest(.{
@@ -223,7 +188,7 @@ pub fn build(b: *std.Build) !void {
.filters = if (test_filter) |v| &.{v} else &.{},
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = config.baselineTarget(),
.target = config.target,
.optimize = .Debug,
.strip = false,
.omit_frame_pointer = false,
@@ -233,21 +198,8 @@ pub fn build(b: *std.Build) !void {
if (config.emit_test_exe) b.installArtifact(test_exe);
_ = try deps.add(test_exe);
// Normal test running
const test_run = b.addRunArtifact(test_exe);
test_step.dependOn(&test_run.step);
// Valgrind test running
const valgrind_run = b.addSystemCommand(&.{
"valgrind",
"--leak-check=full",
"--num-callers=50",
b.fmt("--suppressions={s}", .{b.pathFromRoot("valgrind.supp")}),
"--gen-suppressions=all",
});
valgrind_run.addArtifactArg(test_exe);
test_valgrind_step.dependOn(&valgrind_run.step);
}
// update-translations does what it sounds like and updates the "pot"

View File

@@ -20,8 +20,8 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
.hash = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l",
.url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
.hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
.lazy = true,
},
.zig_objc = .{
@@ -55,8 +55,8 @@
.gobject = .{
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
// Temporary until we generate them at build time automatically.
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
.hash = "gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z",
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
.hash = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM",
.lazy = true,
},
@@ -112,8 +112,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
.hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
.hash = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
.lazy = true,
},
},

18
build.zig.zon.json generated
View File

@@ -24,10 +24,10 @@
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
},
"gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z": {
"gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM": {
"name": "gobject",
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
"hash": "sha256-h6aKUerGlX2ATVEeoN03eWaqDqvUmKdedgpxrSoHvrY="
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
"hash": "sha256-B0ziLzKud+kdKu5T1BTE9GMh8EPM/KhhhoNJlys5QPI="
},
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
"name": "gtk4_layer_shell",
@@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": {
"N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
"hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"hash": "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
@@ -129,10 +129,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l": {
"z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
"hash": "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c="
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="
},
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf",

19
build.zig.zon.nix generated
View File

@@ -49,7 +49,6 @@
inherit name rev hash;
url = url_without_query;
deepClone = false;
fetchSubmodules = false;
};
fetchZigArtifact = {
@@ -123,11 +122,11 @@ in
};
}
{
name = "gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z";
name = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM";
path = fetchZigArtifact {
name = "gobject";
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst";
hash = "sha256-h6aKUerGlX2ATVEeoN03eWaqDqvUmKdedgpxrSoHvrY=";
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst";
hash = "sha256-B0ziLzKud+kdKu5T1BTE9GMh8EPM/KhhhoNJlys5QPI=";
};
}
{
@@ -163,11 +162,11 @@ in
};
}
{
name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME";
name = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz";
hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz";
hash = "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA=";
};
}
{
@@ -291,11 +290,11 @@ in
};
}
{
name = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l";
name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz";
hash = "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c=";
url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz";
hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=";
};
}
{

6
build.zig.zon.txt generated
View File

@@ -27,9 +27,9 @@ https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d6
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz
https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz

View File

@@ -4,7 +4,7 @@ Name=@NAME@
Type=Application
Comment=A terminal emulator
TryExec=@GHOSTTY@
Exec=@GHOSTTY@ --gtk-single-instance=true
Exec=@GHOSTTY@ --launched-from=desktop
Icon=com.mitchellh.ghostty
Categories=System;TerminalEmulator;
Keywords=terminal;tty;pty;
@@ -23,4 +23,4 @@ X-KDE-Shortcuts=Ctrl+Alt+T
[Desktop Action new-window]
Name=New Window
Exec=@GHOSTTY@ --gtk-single-instance=true
Exec=@GHOSTTY@ --launched-from=desktop

View File

@@ -1,3 +1,3 @@
[D-BUS Service]
Name=@APPID@
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
Exec=@GHOSTTY@ --launched-from=dbus

View File

@@ -1,4 +1,4 @@
[D-BUS Service]
Name=@APPID@
SystemdService=app-@APPID@.service
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
Exec=@GHOSTTY@ --launched-from=dbus

View File

@@ -8,7 +8,7 @@ Requires=dbus.socket
Type=notify-reload
ReloadSignal=SIGUSR2
BusName=@APPID@
ExecStart=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
ExecStart=@GHOSTTY@ --launched-from=systemd
[Install]
WantedBy=graphical-session.target

28
flake.lock generated
View File

@@ -47,19 +47,6 @@
"url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1755972213,
"narHash": "sha256-VYK7aDAv8H1enXn1ECRHmGbeY6RqLnNwUJkOwloIsko=",
"rev": "73e96df7cff5783f45e21342a75a1540c4eddce4",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable-small/nixos-25.11pre850642.73e96df7cff5/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable-small/nixexprs.tar.xz"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
@@ -112,20 +99,25 @@
},
"zon2nix": {
"inputs": {
"nixpkgs": "nixpkgs_2"
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1757167408,
"narHash": "sha256-4XyJ6fmKd9wgJ7vHUQuULYy5ps2gUgkkDk/PrJb2OPY=",
"lastModified": 1742104771,
"narHash": "sha256-LhidlyEA9MP8jGe1rEnyjGFCzLLgCdDpYeWggibayr0=",
"owner": "jcollie",
"repo": "zon2nix",
"rev": "dc78177e2ad28d5a407c9e783ee781bd559d7dd5",
"rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613",
"type": "github"
},
"original": {
"owner": "jcollie",
"repo": "zon2nix",
"rev": "dc78177e2ad28d5a407c9e783ee781bd559d7dd5",
"rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613",
"type": "github"
}
}

View File

@@ -24,12 +24,9 @@
};
zon2nix = {
url = "github:jcollie/zon2nix?rev=dc78177e2ad28d5a407c9e783ee781bd559d7dd5";
url = "github:jcollie/zon2nix?rev=56c159be489cc6c0e73c3930bd908ddc6fe89613";
inputs = {
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch
# we are using for "normal" builds.
#
# nixpkgs.follows = "nixpkgs";
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};

View File

@@ -0,0 +1,108 @@
#!/usr/bin/env bash
#
# This script checks if the flatpak/zig-packages.json file is up-to-date.
# If the `--update` flag is passed, it will update all necessary
# files to be up to date.
#
# The files owned by this are:
#
# - flatpak/zig-packages.json
#
# All of these are auto-generated and should not be edited manually.
# Nothing in this script should fail.
set -eu
set -o pipefail
WORK_DIR=$(mktemp -d)
if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then
echo "could not create temp dir"
exit 1
fi
function cleanup {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
help() {
echo ""
echo "To fix, please (manually) re-run the script from the repository root,"
echo "commit, and submit a PR with the update:"
echo ""
echo " ./flatpak/build-support/check-zig-cache.sh --update"
echo " git add flatpak/zig-packages.json"
echo " git commit -m \"flatpak: update zig-packages.json\""
echo ""
}
# Turn Nix's base64 hashes into regular hexadecimal form
decode_hash() {
input=$1
input=${input#sha256-}
echo "$input" | base64 -d | od -vAn -t x1 | tr -d ' \n'
}
ROOT="$(realpath "$(dirname "$0")/../../")"
ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json"
BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json"
if [ ! -f "${BUILD_ZIG_ZON_JSON}" ]; then
echo -e "\nERROR: build.zig.zon2json-lock missing."
help
exit 1
fi
if [ -f "${ZIG_PACKAGES_JSON}" ]; then
OLD_HASH=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}')
fi
while read -r url sha256 dest; do
src_type=archive
sha256=$(decode_hash "$sha256")
git_commit=
if [[ "$url" =~ ^git\+* ]]; then
src_type=git
sha256=
url=${url#git+}
git_commit=${url##*#}
url=${url%%/\?ref*}
url=${url%%#*}
fi
jq \
-nec \
--arg type "$src_type" \
--arg url "$url" \
--arg git_commit "$git_commit" \
--arg dest "$dest" \
--arg sha256 "$sha256" \
'{
type: $type,
url: $url,
commit: $git_commit,
dest: $dest,
sha256: $sha256,
} | with_entries(select(.value != ""))'
done < <(jq -rc 'to_entries[] | [.value.url, .value.hash, "vendor/p/\(.key)"] | @tsv' "$BUILD_ZIG_ZON_JSON") |
jq -s '.' >"$WORK_DIR/zig-packages.json"
NEW_HASH=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}')
if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then
echo -e "\nOK: flatpak/zig-packages.json unchanged."
exit 0
elif [ "${1:-}" != "--update" ]; then
echo -e "\nERROR: flatpak/zig-packages.json needs to be updated."
echo ""
echo " * Old hash: ${OLD_HASH}"
echo " * New hash: ${NEW_HASH}"
help
exit 1
else
mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON"
echo -e "\nOK: flatpak/zig-packages.json updated."
exit 0
fi

View File

@@ -2,6 +2,8 @@ app-id: com.mitchellh.ghostty-debug
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
rename-icon: com.mitchellh.ghostty
@@ -35,7 +37,7 @@ modules:
- name: ghostty
buildsystem: simple
build-options:
append-path: /app/zig
append-path: /usr/lib/sdk/ziglang
build-commands:
- zig build
-Doptimize=Debug

View File

@@ -2,6 +2,8 @@ app-id: com.mitchellh.ghostty
runtime: org.gnome.Platform
runtime-version: "48"
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang
default-branch: tip
command: ghostty
finish-args:
@@ -34,7 +36,7 @@ modules:
- name: ghostty
buildsystem: simple
build-options:
append-path: /app/zig
append-path: /usr/lib/sdk/ziglang
build-commands:
- zig build
-Doptimize=ReleaseFast

View File

@@ -3,24 +3,6 @@ buildsystem: simple
build-commands:
- true
modules:
- name: zig
buildsystem: simple
cleanup:
- "*"
build-commands:
- mkdir -p /app/zig
- cp -r ./* /app/zig
- chmod a+x /app/zig/zig
sources:
- type: archive
sha256: 24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c
url: https://ziglang.org/download/0.14.1/zig-x86_64-linux-0.14.1.tar.xz
only-arches: [x86_64]
- type: archive
sha256: f7a654acc967864f7a050ddacfaa778c7504a0eca8d2b678839c21eea47c992b
url: https://ziglang.org/download/0.14.1/zig-aarch64-linux-0.14.1.tar.xz
only-arches: [aarch64]
- name: bzip2-redirect
buildsystem: simple
build-commands:

View File

@@ -31,9 +31,9 @@
},
{
"type": "archive",
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z",
"sha256": "87a68a51eac6957d804d511ea0dd377966aa0eabd498a75e760a71ad2a07beb6"
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
"dest": "vendor/p/gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM",
"sha256": "074ce22f32ae77e91d2aee53d414c4f46321f043ccfca861868349972b3940f2"
},
{
"type": "archive",
@@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
"dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
"sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"dest": "vendor/p/N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
"sha256": "825e3634e679f6893eba61c21db7414215828055698f93c06435468494696e20"
},
{
"type": "archive",
@@ -157,9 +157,9 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
"dest": "vendor/p/z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l",
"sha256": "b56acb944394e3fd193a5ce018e5c1d3c7d6eeca9fc8015fdeb96fd3097d6ff7"
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
"sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e"
},
{
"type": "archive",

View File

@@ -419,7 +419,6 @@ typedef struct {
ghostty_env_var_s* env_vars;
size_t env_var_count;
const char* initial_input;
bool wait_after_command;
} ghostty_surface_config_s;
typedef struct {
@@ -451,28 +450,6 @@ typedef struct {
ghostty_config_color_s colors[256];
} ghostty_config_palette_s;
// config.QuickTerminalSize
typedef enum {
GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
} ghostty_quick_terminal_size_tag_e;
typedef union {
float percentage;
uint32_t pixels;
} ghostty_quick_terminal_size_value_u;
typedef struct {
ghostty_quick_terminal_size_tag_e tag;
ghostty_quick_terminal_size_value_u value;
} ghostty_quick_terminal_size_s;
typedef struct {
ghostty_quick_terminal_size_s primary;
ghostty_quick_terminal_size_s secondary;
} ghostty_config_quick_terminal_size_s;
// apprt.Target.Key
typedef enum {
GHOSTTY_TARGET_APP,
@@ -703,12 +680,6 @@ typedef struct {
uintptr_t len;
} ghostty_action_open_url_s;
// apprt.action.CloseTabMode
typedef enum {
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
} ghostty_action_close_tab_mode_e;
// apprt.surface.Message.ChildExited
typedef struct {
uint32_t exit_code;
@@ -722,15 +693,15 @@ typedef enum {
GHOSTTY_PROGRESS_STATE_ERROR,
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
GHOSTTY_PROGRESS_STATE_PAUSE,
} ghostty_action_progress_report_state_e;
} ghostty_terminal_osc_command_progressreport_state_e;
// terminal.osc.Command.ProgressReport.C
typedef struct {
ghostty_action_progress_report_state_e state;
ghostty_terminal_osc_command_progressreport_state_e state;
// -1 if no progress was reported, otherwise 0-100 indicating percent
// completeness.
int8_t progress;
} ghostty_action_progress_report_s;
} ghostty_terminal_osc_command_progressreport_s;
// apprt.Action.Key
typedef enum {
@@ -815,9 +786,8 @@ typedef union {
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url;
ghostty_action_close_tab_mode_e close_tab_mode;
ghostty_surface_message_childexited_s child_exited;
ghostty_action_progress_report_s progress_report;
ghostty_terminal_osc_command_progressreport_s progress_report;
} ghostty_action_u;
typedef struct {
@@ -964,7 +934,7 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t,
double,
ghostty_input_scroll_mods_t);
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*);
void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
void ghostty_surface_split_focus(ghostty_surface_t,

View File

@@ -104,7 +104,6 @@
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; };
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
@@ -127,7 +126,6 @@
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
@@ -254,7 +252,6 @@
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = "<group>"; };
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = "<group>"; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
@@ -640,7 +637,6 @@
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */,
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
);
path = QuickTerminal;
@@ -943,10 +939,6 @@
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */,
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,
@@ -988,7 +980,6 @@
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */,
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,

View File

@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "df074165274afaa39539c05d57b0832620775b11",
"version" : "2.7.1"
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
"version" : "2.6.4"
}
}
],

View File

@@ -119,9 +119,6 @@ class AppDelegate: NSObject,
@Published private(set) var appIcon: NSImage? = nil {
didSet {
NSApplication.shared.applicationIconImage = appIcon
let appPath = Bundle.main.bundlePath
NSWorkspace.shared.setIcon(appIcon, forFile: appPath, options: [])
NSWorkspace.shared.noteFileSystemChanged(appPath)
}
}
@@ -258,21 +255,13 @@ class AppDelegate: NSObject,
// Setup signal handlers
setupSignals()
switch Ghostty.launchSource {
case .app:
// Don't have to do anything.
break
case .zig_run, .cli:
// Part of launch services (clicking an app, using `open`, etc.) activates
// the application and brings it to the front. When using the CLI we don't
// get this behavior, so we have to do it manually.
// If we launched via zig run then we need to force foreground.
if Ghostty.launchSource == .zig_run {
// This never gets called until we click the dock icon. This forces it
// activate immediately.
applicationDidBecomeActive(.init(name: NSApplication.didBecomeActiveNotification))
// We run in the background, this forces us to the front.
DispatchQueue.main.async {
NSApp.setActivationPolicy(.regular)
@@ -402,69 +391,34 @@ class AppDelegate: NSObject,
// Ghostty will validate as well but we can avoid creating an entirely new
// surface by doing our own validation here. We can also show a useful error
// this way.
var isDirectory = ObjCBool(true)
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
// Set to true if confirmation is required before starting up the
// new terminal.
var requiresConfirm: Bool = false
// Initialize the surface config which will be used to create the tab or window for the opened file.
var config = Ghostty.SurfaceConfiguration()
if (isDirectory.boolValue) {
// When opening a directory, check the configuration to decide
// whether to open in a new tab or new window.
// When opening a directory, create a new tab in the main
// window with that as the working directory.
// If no windows exist, a new one will be created.
config.workingDirectory = filename
_ = TerminalController.newTab(ghostty, withBaseConfig: config)
} else {
// Unconditionally require confirmation in the file execution case.
// In the future I have ideas about making this more fine-grained if
// we can not inherit of unsandboxed state. For now, we need to confirm
// because there is a sandbox escape possible if a sandboxed application
// somehow is tricked into `open`-ing a non-sandboxed application.
requiresConfirm = true
// When opening a file, we want to execute the file. To do this, we
// don't override the command directly, because it won't load the
// profile/rc files for the shell, which is super important on macOS
// due to things like Homebrew. Instead, we set the command to
// `<filename>; exit` which is what Terminal and iTerm2 do.
config.initialInput = "\(filename); exit\n"
// For commands executed directly, we want to ensure we wait after exit
// because in most cases scripts don't block on exit and we don't want
// the window to just flash closed once complete.
config.waitAfterCommand = true
// Set the parent directory to our working directory so that relative
// paths in scripts work.
config.workingDirectory = (filename as NSString).deletingLastPathComponent
_ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
if requiresConfirm {
// Confirmation required. We use an app-wide NSAlert for now. In the future we
// may want to show this as a sheet on the focused window (especially if we're
// opening a tab). I'm not sure.
let alert = NSAlert()
alert.messageText = "Allow Ghostty to execute \"\(filename)\"?"
alert.addButton(withTitle: "Allow")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
switch (alert.runModal()) {
case .alertFirstButtonReturn:
break
default:
return false
}
}
switch ghostty.config.macosDockDropBehavior {
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
return true
}
@@ -880,13 +834,6 @@ class AppDelegate: NSObject,
case .xray:
self.appIcon = NSImage(named: "XrayImage")!
case .custom:
if let userIcon = NSImage(contentsOfFile: config.macosCustomIcon) {
self.appIcon = userIcon
} else {
self.appIcon = nil // Revert back to official icon if invalid location
}
case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
@@ -945,7 +892,7 @@ class AppDelegate: NSObject,
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? {
for c in TerminalController.all {
for view in c.surfaceTree {
if view.id == uuid {
if view.uuid == uuid {
return view
}
}
@@ -999,10 +946,18 @@ class AppDelegate: NSObject,
@IBAction func newWindow(_ sender: Any?) {
_ = TerminalController.newWindow(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func newTab(_ sender: Any?) {
_ = TerminalController.newTab(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func closeAllWindows(_ sender: Any?) {

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View File

@@ -34,7 +34,7 @@ struct TerminalEntity: AppEntity {
/// Returns the view associated with this entity. This may no longer exist.
@MainActor
var surfaceView: Ghostty.SurfaceView? {
Self.defaultQuery.all.first { $0.id == self.id }
Self.defaultQuery.all.first { $0.uuid == self.id }
}
@MainActor
@@ -46,7 +46,7 @@ struct TerminalEntity: AppEntity {
@MainActor
init(_ view: Ghostty.SurfaceView) {
self.id = view.id
self.id = view.uuid
self.title = view.title
self.workingDirectory = view.pwd
if let nsImage = ImageRenderer(content: view.screenshot()).nsImage {
@@ -80,7 +80,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
@MainActor
func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] {
return all.filter {
identifiers.contains($0.id)
identifiers.contains($0.uuid)
}.map {
TerminalEntity($0)
}

View File

@@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController {
private var previousActiveSpace: CGSSpace? = nil
/// The window frame saved when the quick terminal's surface tree becomes empty.
///
///
/// This preserves the user's window size and position when all terminal surfaces
/// are closed (e.g., via the `exit` command). When a new surface is created,
/// the window will be restored to this frame, preventing SwiftUI from resetting
@@ -34,9 +34,6 @@ class QuickTerminalController: BaseTerminalController {
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
/// Tracks if we're currently handling a manual resize to prevent recursion
private var isHandlingResize: Bool = false
init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top,
@@ -79,11 +76,6 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onNewTab),
name: Ghostty.Notification.ghosttyNewTab,
object: nil)
center.addObserver(
self,
selector: #selector(windowDidResize(_:)),
name: NSWindow.didResizeNotification,
object: nil)
}
required init?(coder: NSCoder) {
@@ -117,29 +109,14 @@ class QuickTerminalController: BaseTerminalController {
syncAppearance()
// Setup our initial size based on our configured position
position.setLoaded(window, size: derivedConfig.quickTerminalSize)
position.setLoaded(window)
// Upon first adding this Window to its host view, older SwiftUI
// seems to have a "hiccup" and corrupts the frameRect,
// sometimes setting the size to zero, sometimes corrupting it.
// We pass the actual window's frame as "initial" frame directly
// to the window, so it can use that instead of the frameworks
// "interpretation"
if let qtWindow = window as? QuickTerminalWindow {
qtWindow.initialFrame = window.frame
}
// Setup our content
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
))
// Clear out our frame at this point, the fixup from above is complete.
if let qtWindow = window as? QuickTerminalWindow {
qtWindow.initialFrame = nil
}
// Animate the window in
animateIn()
@@ -217,28 +194,11 @@ class QuickTerminalController: BaseTerminalController {
}
}
override func windowDidResize(_ notification: Notification) {
guard let window = notification.object as? NSWindow,
window == self.window,
visible,
!isHandlingResize else { return }
guard let screen = window.screen ?? NSScreen.main else { return }
// Prevent recursive loops
isHandlingResize = true
defer { isHandlingResize = false }
switch position {
case .top, .bottom, .center:
// For centered positions (top, bottom, center), we need to recenter the window
// when it's manually resized to maintain proper positioning
let newOrigin = position.centeredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
case .left, .right:
// For side positions, we may need to adjust vertical centering
let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
}
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
// We use the actual screen the window is on for this, since it should
// be on the proper screen.
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
return position.restrictFrameSize(frameSize, on: screen)
}
// MARK: Base Controller Overrides
@@ -358,17 +318,15 @@ class QuickTerminalController: BaseTerminalController {
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
// Grab our last closed frame to use, and clear our state since we're animating in.
let lastClosedFrame = self.lastClosedFrame
self.lastClosedFrame = nil
// Move our window off screen to the initial animation position.
position.setInitial(
in: window,
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame)
// Restore our previous frame if we have one
if let lastClosedFrame {
window.setFrame(lastClosedFrame, display: false)
self.lastClosedFrame = nil
}
// Move our window off screen to the top
position.setInitial(in: window, on: screen)
// We need to set our window level to a high value. In testing, only
// popUpMenu and above do what we want. This gets it above the menu bar
@@ -399,11 +357,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setFinal(
in: window.animator(),
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame)
position.setFinal(in: window.animator(), on: screen)
}, completionHandler: {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
@@ -481,19 +435,11 @@ 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 {
lastClosedFrame = window.frame
}
lastClosedFrame = window.frame
// If we hid the dock then we unhide it.
hiddenDock = nil
@@ -509,6 +455,11 @@ class QuickTerminalController: BaseTerminalController {
// We always animate out to whatever screen the window is actually on.
guard let screen = window.screen ?? NSScreen.main else { return }
// If we are in fullscreen, then we exit fullscreen.
if let fullscreenStyle, fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
}
// If we have a previously active application, restore focus to it. We
// do this BEFORE the animation below because when the animation completes
// macOS will bring forward another window.
@@ -530,11 +481,7 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setInitial(
in: window.animator(),
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: window.frame)
position.setInitial(in: window.animator(), on: screen)
}, completionHandler: {
// This causes the window to be removed from the screen list and macOS
// handles what should be focused next.
@@ -665,7 +612,6 @@ class QuickTerminalController: BaseTerminalController {
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
let quickTerminalSize: QuickTerminalSize
let backgroundOpacity: Double
init() {
@@ -673,7 +619,6 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
self.quickTerminalSpaceBehavior = .move
self.quickTerminalSize = QuickTerminalSize()
self.backgroundOpacity = 1.0
}
@@ -682,7 +627,6 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
self.quickTerminalSize = config.quickTerminalSize
self.backgroundOpacity = config.backgroundOpacity
}
}

View File

@@ -7,86 +7,95 @@ enum QuickTerminalPosition : String {
case right
case center
/// Set the loaded state for a window. This should only be called when the window is first loaded,
/// usually in `windowDidLoad` or in a similar callback. This is the initial state.
func setLoaded(_ window: NSWindow, size: QuickTerminalSize) {
/// Set the loaded state for a window.
func setLoaded(_ window: NSWindow) {
guard let screen = window.screen ?? NSScreen.main else { return }
window.setFrame(.init(
origin: window.frame.origin,
size: size.calculate(position: self, screenDimensions: screen.visibleFrame.size)
), display: false)
switch (self) {
case .top, .bottom:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width,
height: screen.frame.height / 4)
), display: false)
case .left, .right:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 4,
height: screen.frame.height)
), display: false)
case .center:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 2,
height: screen.frame.height / 3)
), display: false)
}
}
/// Set the initial state for a window NOT yet into position (either before animating in or
/// after animating out).
func setInitial(
in window: NSWindow,
on screen: NSScreen,
terminalSize: QuickTerminalSize,
closedFrame: NSRect? = nil
) {
// Invisible
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
// We always start invisible
window.alphaValue = 0
// Position depends
window.setFrame(.init(
origin: initialOrigin(for: window, on: screen),
size: closedFrame?.size ?? configuredFrameSize(
on: screen,
terminalSize: terminalSize)
size: restrictFrameSize(window.frame.size, on: screen)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(
in window: NSWindow,
on screen: NSScreen,
terminalSize: QuickTerminalSize,
closedFrame: NSRect? = nil
) {
func setFinal(in window: NSWindow, on screen: NSScreen) {
// We always end visible
window.alphaValue = 1
// Position depends
window.setFrame(.init(
origin: finalOrigin(for: window, on: screen),
size: closedFrame?.size ?? configuredFrameSize(
on: screen,
terminalSize: terminalSize)
size: restrictFrameSize(window.frame.size, on: screen)
), display: true)
}
/// Get the configured frame size for initial positioning and animations.
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.visibleFrame.size)
return NSSize(width: dimensions.width, height: dimensions.height)
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
var finalSize = size
switch (self) {
case .top, .bottom:
finalSize.width = screen.frame.width
case .left, .right:
finalSize.height = screen.visibleFrame.height
case .center:
finalSize.width = screen.frame.width / 2
finalSize.height = screen.frame.height / 3
}
return finalSize
}
/// The initial point origin for this position.
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.maxY)
return .init(x: screen.frame.minX, y: screen.frame.maxY)
case .bottom:
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: -window.frame.height)
return .init(x: screen.frame.minX, y: -window.frame.height)
case .left:
return .init(
x: screen.visibleFrame.minX-window.frame.width,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
return .init(x: screen.frame.minX-window.frame.width, y: 0)
case .right:
return .init(
x: screen.visibleFrame.maxX,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
return .init(x: screen.frame.maxX, y: 0)
case .center:
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width)
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
}
}
@@ -94,27 +103,19 @@ enum QuickTerminalPosition : String {
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.maxY - window.frame.height)
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
case .bottom:
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.minY)
return .init(x: screen.frame.minX, y: screen.frame.minY)
case .left:
return .init(
x: screen.visibleFrame.minX,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
return .init(x: screen.frame.minX, y: window.frame.origin.y)
case .right:
return .init(
x: screen.visibleFrame.maxX - window.frame.width,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
case .center:
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
}
}
@@ -135,52 +136,4 @@ enum QuickTerminalPosition : String {
case .right: self == .top || self == .bottom
}
}
/// Calculate the centered origin for a window, keeping it properly positioned after manual resizing
func centeredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .top:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: window.frame.origin.y // Keep the same Y position
)
case .bottom:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: window.frame.origin.y // Keep the same Y position
)
case .center:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .left, .right:
// For left/right positions, only adjust horizontal centering if needed
return window.frame.origin
}
}
/// Calculate the vertically centered origin for side-positioned windows
func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .left:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .right:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .top, .bottom, .center:
// These positions don't need vertical recentering during resize
return window.frame.origin
}
}
}

View File

@@ -1,84 +0,0 @@
import GhosttyKit
/// Represents the Ghostty `quick-terminal-size` configuration. See the documentation for
/// that for more details on exactly how it works. Some of those docs will be reproduced in various comments
/// in this file but that is the best source of truth for it.
///
/// The size determines the size of the quick terminal along the primary and secondary axis. The primary and
/// secondary axis is defined by the `quick-terminal-position`.
struct QuickTerminalSize {
let primary: Size?
let secondary: Size?
init(primary: Size? = nil, secondary: Size? = nil) {
self.primary = primary
self.secondary = secondary
}
init(from cStruct: ghostty_config_quick_terminal_size_s) {
self.primary = Size(from: cStruct.primary)
self.secondary = Size(from: cStruct.secondary)
}
enum Size {
case percentage(Float)
case pixels(UInt32)
init?(from cStruct: ghostty_quick_terminal_size_s) {
switch cStruct.tag {
case GHOSTTY_QUICK_TERMINAL_SIZE_NONE:
return nil
case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE:
self = .percentage(cStruct.value.percentage)
case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS:
self = .pixels(cStruct.value.pixels)
default:
return nil
}
}
func toPixels(parentDimension: CGFloat) -> CGFloat {
switch self {
case .percentage(let value):
return parentDimension * CGFloat(value) / 100.0
case .pixels(let value):
return CGFloat(value)
}
}
}
/// This is an almost direct port of th Zig function QuickTerminalSize.calculate
func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize {
let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height)
switch position {
case .left, .right:
return CGSize(
width: primary?.toPixels(parentDimension: dims.width) ?? 400,
height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height
)
case .top, .bottom:
return CGSize(
width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width,
height: primary?.toPixels(parentDimension: dims.height) ?? 400
)
case .center:
if dims.width >= dims.height {
// Landscape
return CGSize(
width: primary?.toPixels(parentDimension: dims.width) ?? 800,
height: secondary?.toPixels(parentDimension: dims.height) ?? 400
)
} else {
// Portrait
return CGSize(
width: secondary?.toPixels(parentDimension: dims.width) ?? 400,
height: primary?.toPixels(parentDimension: dims.height) ?? 800
)
}
}
}
}

View File

@@ -29,19 +29,4 @@ class QuickTerminalWindow: NSPanel {
// We don't want to activate the owning app when quick terminal is triggered.
self.styleMask.insert(.nonactivatingPanel)
}
/// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround
/// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026
var initialFrame: NSRect? = nil
override func setFrame(_ frameRect: NSRect, display flag: Bool) {
// Upon first adding this Window to its host view, older SwiftUI
// seems to have a "hiccup" and corrupts the frameRect,
// sometimes setting the size to zero, sometimes corrupting it.
// If we find we have cached the "initial" frame, use that instead
// the propagated one through the framework
//
// https://github.com/ghostty-org/ghostty/pull/8026
super.setFrame(initialFrame ?? frameRect, display: flag)
}
}

View File

@@ -1,7 +1,7 @@
import AppKit
/// SplitTree represents a tree of views that can be divided.
struct SplitTree<ViewType: NSView & Codable & Identifiable> {
struct SplitTree<ViewType: NSView & Codable>: Codable {
/// The root of the tree. This can be nil to indicate the tree is empty.
let root: Node?
@@ -29,12 +29,12 @@ struct SplitTree<ViewType: NSView & Codable & Identifiable> {
}
/// The path to a specific node in the tree.
struct Path: Codable {
struct Path {
let path: [Component]
var isEmpty: Bool { path.isEmpty }
enum Component: Codable {
enum Component {
case left
case right
}
@@ -53,7 +53,7 @@ struct SplitTree<ViewType: NSView & Codable & Identifiable> {
let node: Node
let bounds: CGRect
}
/// Direction for spatial navigation within the split tree.
enum Direction {
case left
@@ -127,51 +127,44 @@ extension SplitTree {
root: try root.insert(view: view, at: at, direction: direction),
zoomed: nil)
}
/// Find a node containing a view with the specified ID.
/// - Parameter id: The ID of the view to find
/// - Returns: The node containing the view if found, nil otherwise
func find(id: ViewType.ID) -> Node? {
guard let root else { return nil }
return root.find(id: id)
}
/// Remove a node from the tree. If the node being removed is part of a split,
/// the sibling node takes the place of the parent split.
func remove(_ target: Node) -> Self {
guard let root else { return self }
// If we're removing the root itself, return an empty tree
if root == target {
return .init(root: nil, zoomed: nil)
}
// Otherwise, try to remove from the tree
let newRoot = root.remove(target)
// Update zoomed if it was the removed node
let newZoomed = (zoomed == target) ? nil : zoomed
return .init(root: newRoot, zoomed: newZoomed)
}
/// Replace a node in the tree with a new node.
func replace(node: Node, with newNode: Node) throws -> Self {
guard let root else { throw SplitError.viewNotFound }
// Get the path to the node we want to replace
guard let path = root.path(to: node) else {
throw SplitError.viewNotFound
}
// Replace the node
let newRoot = try root.replaceNode(at: path, with: newNode)
// Update zoomed if it was the replaced node
let newZoomed = (zoomed == node) ? newNode : zoomed
return .init(root: newRoot, zoomed: newZoomed)
}
/// Find the next view to focus based on the current focused node and direction
func focusTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? {
guard let root else { return nil }
@@ -237,13 +230,13 @@ extension SplitTree {
let newRoot = root.equalize()
return .init(root: newRoot, zoomed: zoomed)
}
/// Resize a node in the tree by the given pixel amount in the specified direction.
///
///
/// This method adjusts the split ratios of the tree to accommodate the requested resize
/// operation. For up/down resizing, it finds the nearest parent vertical split and adjusts
/// its ratio. For left/right resizing, it finds the nearest parent horizontal split.
/// The bounds parameter is used to construct the spatial tree representation which is
/// The bounds parameter is used to construct the spatial tree representation which is
/// needed to calculate the current pixel dimensions.
///
/// This will always reset the zoomed state.
@@ -257,22 +250,22 @@ extension SplitTree {
/// - Throws: SplitError.viewNotFound if the node is not found in the tree or no suitable parent split exists
func resize(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self {
guard let root else { throw SplitError.viewNotFound }
// Find the path to the target node
guard let path = root.path(to: node) else {
throw SplitError.viewNotFound
}
// Determine which type of split we need to find based on resize direction
let targetSplitDirection: Direction = switch direction {
case .up, .down: .vertical
case .left, .right: .horizontal
}
// Find the nearest parent split of the correct type by walking up the path
var splitPath: Path?
var splitNode: Node?
for i in stride(from: path.path.count - 1, through: 0, by: -1) {
let parentPath = Path(path: Array(path.path.prefix(i)))
if let parent = root.node(at: parentPath), case .split(let split) = parent {
@@ -283,29 +276,29 @@ extension SplitTree {
}
}
}
guard let splitPath = splitPath,
guard let splitPath = splitPath,
let splitNode = splitNode,
case .split(let split) = splitNode else {
throw SplitError.viewNotFound
}
// Get current spatial representation to calculate pixel dimensions
let spatial = root.spatial(within: bounds.size)
guard let splitSlot = spatial.slots.first(where: { $0.node == splitNode }) else {
throw SplitError.viewNotFound
}
// Calculate the new ratio based on pixel change
let pixelOffset = Double(pixels)
let newRatio: Double
switch (split.direction, direction) {
case (.horizontal, .left):
// Moving left boundary: decrease left side
newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio - (pixelOffset / splitSlot.bounds.width)))
case (.horizontal, .right):
// Moving right boundary: increase left side
// Moving right boundary: increase left side
newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio + (pixelOffset / splitSlot.bounds.width)))
case (.vertical, .up):
// Moving top boundary: decrease top side
@@ -317,7 +310,7 @@ extension SplitTree {
// Direction doesn't match split type - shouldn't happen due to earlier logic
throw SplitError.viewNotFound
}
// Create new split with adjusted ratio
let newSplit = Node.Split(
direction: split.direction,
@@ -325,12 +318,12 @@ extension SplitTree {
left: split.left,
right: split.right
)
// Replace the split node with the new one
let newRoot = try root.replaceNode(at: splitPath, with: .split(newSplit))
return .init(root: newRoot, zoomed: nil)
}
/// Returns the total bounds of the split hierarchy using NSView bounds.
/// Ignores x/y coordinates and assumes views are laid out in a perfect grid.
/// Also ignores any possible padding between views.
@@ -341,60 +334,6 @@ extension SplitTree {
}
}
// MARK: SplitTree Codable
fileprivate enum CodingKeys: String, CodingKey {
case version
case root
case zoomed
static let currentVersion: Int = 1
}
extension SplitTree: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Check version
let version = try container.decode(Int.self, forKey: .version)
guard version == CodingKeys.currentVersion else {
throw DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Unsupported SplitTree version: \(version)"
)
)
}
// Decode root
self.root = try container.decodeIfPresent(Node.self, forKey: .root)
// Zoomed is encoded as its path. Get the path and then find it.
if let zoomedPath = try container.decodeIfPresent(Path.self, forKey: .zoomed),
let root = self.root {
self.zoomed = root.node(at: zoomedPath)
} else {
self.zoomed = nil
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// Encode version
try container.encode(CodingKeys.currentVersion, forKey: .version)
// Encode root
try container.encodeIfPresent(root, forKey: .root)
// Zoomed is encoded as its path since its a reference type. This lets us
// map it on decode back to the correct node in root.
if let zoomed, let path = root?.path(to: zoomed) {
try container.encode(path, forKey: .zoomed)
}
}
}
// MARK: SplitTree.Node
extension SplitTree.Node {
@@ -403,23 +342,6 @@ extension SplitTree.Node {
typealias SplitError = SplitTree.SplitError
typealias Path = SplitTree.Path
/// Find a node containing a view with the specified ID.
/// - Parameter id: The ID of the view to find
/// - Returns: The node containing the view if found, nil otherwise
func find(id: ViewType.ID) -> Node? {
switch self {
case .leaf(let view):
return view.id == id ? self : nil
case .split(let split):
if let found = split.left.find(id: id) {
return found
}
return split.right.find(id: id)
}
}
/// Returns the node in the tree that contains the given view.
func node(view: ViewType) -> Node? {
switch (self) {
@@ -474,20 +396,20 @@ extension SplitTree.Node {
return search(self) ? Path(path: components) : nil
}
/// Returns the node at the given path from this node as root.
func node(at path: Path) -> Node? {
if path.isEmpty {
return self
}
guard case .split(let split) = self else {
return nil
}
let component = path.path[0]
let remainingPath = Path(path: Array(path.path.dropFirst()))
switch component {
case .left:
return split.left.node(at: remainingPath)
@@ -599,12 +521,12 @@ extension SplitTree.Node {
if self == target {
return nil
}
switch self {
case .leaf:
// A leaf that isn't the target stays as is
return self
case .split(let split):
// Neither child is directly the target, so we need to recursively
// try to remove from both children
@@ -621,7 +543,7 @@ extension SplitTree.Node {
} else if newRight == nil {
return newLeft
}
// Both children still exist after removal
return .split(.init(
direction: split.direction,
@@ -640,7 +562,7 @@ extension SplitTree.Node {
case .leaf:
// Leaf nodes don't have a ratio to resize
return self
case .split(let split):
// Create a new split with the updated ratio
return .split(.init(
@@ -651,7 +573,7 @@ extension SplitTree.Node {
))
}
}
/// Get the leftmost leaf in this subtree
func leftmostLeaf() -> ViewType {
switch self {
@@ -661,7 +583,7 @@ extension SplitTree.Node {
return split.left.leftmostLeaf()
}
}
/// Get the rightmost leaf in this subtree
func rightmostLeaf() -> ViewType {
switch self {
@@ -671,7 +593,7 @@ extension SplitTree.Node {
return split.right.rightmostLeaf()
}
}
/// Equalize this node and all its children, returning a new node with splits
/// adjusted so that each split's ratio is based on the relative weight
/// (number of leaves) of its children.
@@ -679,14 +601,14 @@ extension SplitTree.Node {
let (equalizedNode, _) = equalizeWithWeight()
return equalizedNode
}
/// Internal helper that equalizes and returns both the node and its weight.
private func equalizeWithWeight() -> (node: Node, weight: Int) {
switch self {
case .leaf:
// A leaf has weight 1 and doesn't change
return (self, 1)
case .split(let split):
// Calculate weights based on split direction
let leftWeight = split.left.weightForDirection(split.direction)
@@ -707,7 +629,7 @@ extension SplitTree.Node {
left: leftNode,
right: rightNode
)
return (.split(newSplit), totalWeight)
}
}
@@ -734,12 +656,12 @@ extension SplitTree.Node {
switch self {
case .leaf(let view):
return [(view, bounds)]
case .split(let split):
// Calculate bounds for left and right based on split direction and ratio
let leftBounds: CGRect
let rightBounds: CGRect
switch split.direction {
case .horizontal:
// Split horizontally: left | right
@@ -756,7 +678,7 @@ extension SplitTree.Node {
width: bounds.width * (1 - split.ratio),
height: bounds.height
)
case .vertical:
// Split vertically: top / bottom
// Note: In our normalized coordinate system, Y increases upward
@@ -774,13 +696,13 @@ extension SplitTree.Node {
height: bounds.height * split.ratio
)
}
// Recursively calculate bounds for children
return split.left.calculateViewBounds(in: leftBounds) +
split.right.calculateViewBounds(in: rightBounds)
}
}
/// Returns the total bounds of this subtree using NSView bounds.
/// Ignores x/y coordinates and assumes views are laid out in a perfect grid.
/// - Returns: The total width and height needed to contain all views in this subtree
@@ -788,11 +710,11 @@ extension SplitTree.Node {
switch self {
case .leaf(let view):
return view.bounds.size
case .split(let split):
let leftBounds = split.left.viewBounds()
let rightBounds = split.right.viewBounds()
switch split.direction {
case .horizontal:
// Horizontal split: width is sum, height is max
@@ -800,7 +722,7 @@ extension SplitTree.Node {
width: leftBounds.width + rightBounds.width,
height: Swift.max(leftBounds.height, rightBounds.height)
)
case .vertical:
// Vertical split: height is sum, width is max
return CGSize(
@@ -838,7 +760,7 @@ extension SplitTree.Node {
/// // +--------+----+
/// // | C | D |
/// // +--------+----+
/// //
/// //
/// // The spatial representation would have:
/// // - Total dimensions: (width: 2, height: 2)
/// // - Node bounds based on actual split ratios
@@ -883,7 +805,7 @@ extension SplitTree.Node {
/// Example:
/// ```
/// // Single leaf: (1, 1)
/// // Horizontal split with 2 leaves: (2, 1)
/// // Horizontal split with 2 leaves: (2, 1)
/// // Vertical split with 2 leaves: (1, 2)
/// // Complex layout with both: (2, 2) or larger
/// ```
@@ -924,7 +846,7 @@ extension SplitTree.Node {
///
/// The calculation process:
/// 1. **Leaf nodes**: Create a single slot with the provided bounds
/// 2. **Split nodes**:
/// 2. **Split nodes**:
/// - Divide the bounds according to the split ratio and direction
/// - Create a slot for the split node itself
/// - Recursively calculate slots for both children
@@ -1004,7 +926,7 @@ extension SplitTree.Spatial {
///
/// This method finds all slots positioned in the given direction from the reference node:
/// - **Left**: Slots with bounds to the left of the reference node
/// - **Right**: Slots with bounds to the right of the reference node
/// - **Right**: Slots with bounds to the right of the reference node
/// - **Up**: Slots with bounds above the reference node (Y=0 is top)
/// - **Down**: Slots with bounds below the reference node
///
@@ -1033,41 +955,41 @@ extension SplitTree.Spatial {
let dy = rect2.minY - rect1.minY
return sqrt(dx * dx + dy * dy)
}
let result = switch direction {
case .left:
// Slots to the left: their right edge is at or left of reference's left edge
slots.filter {
$0.node != referenceNode && $0.bounds.maxX <= refSlot.bounds.minX
$0.node != referenceNode && $0.bounds.maxX <= refSlot.bounds.minX
}.sorted {
distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
}
case .right:
// Slots to the right: their left edge is at or right of reference's right edge
slots.filter {
$0.node != referenceNode && $0.bounds.minX >= refSlot.bounds.maxX
$0.node != referenceNode && $0.bounds.minX >= refSlot.bounds.maxX
}.sorted {
distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
}
case .up:
// Slots above: their bottom edge is at or above reference's top edge
slots.filter {
$0.node != referenceNode && $0.bounds.maxY <= refSlot.bounds.minY
$0.node != referenceNode && $0.bounds.maxY <= refSlot.bounds.minY
}.sorted {
distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
}
case .down:
// Slots below: their top edge is at or below reference's bottom edge
slots.filter {
$0.node != referenceNode && $0.bounds.minY >= refSlot.bounds.maxY
$0.node != referenceNode && $0.bounds.minY >= refSlot.bounds.maxY
}.sorted {
distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
}
}
return result
}
@@ -1086,14 +1008,14 @@ extension SplitTree.Spatial {
func doesBorder(side: Direction, from node: SplitTree.Node) -> Bool {
// Find the slot for this node
guard let slot = slots.first(where: { $0.node == node }) else { return false }
// Calculate the overall bounds of all slots
let overallBounds = slots.reduce(CGRect.null) { result, slot in
result.union(slot.bounds)
}
return switch side {
case .up:
case .up:
slot.bounds.minY == overallBounds.minY
case .down:
slot.bounds.maxY == overallBounds.maxY
@@ -1130,10 +1052,10 @@ extension SplitTree.Node {
case view
case split
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.view) {
let view = try container.decode(ViewType.self, forKey: .view)
self = .leaf(view: view)
@@ -1149,14 +1071,14 @@ extension SplitTree.Node {
)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .leaf(let view):
try container.encode(view, forKey: .view)
case .split(let split):
try container.encode(split, forKey: .split)
}
@@ -1171,7 +1093,7 @@ extension SplitTree.Node {
switch self {
case .leaf(let view):
return [view]
case .split(let split):
return split.left.leaves() + split.right.leaves()
}
@@ -1223,7 +1145,7 @@ extension SplitTree.Node {
var structuralIdentity: StructuralIdentity {
StructuralIdentity(self)
}
/// A hashable representation of a node that captures its structural identity.
///
/// This type provides a way to track changes to a node's structure in SwiftUI
@@ -1237,20 +1159,20 @@ extension SplitTree.Node {
/// for unchanged portions of the tree.
struct StructuralIdentity: Hashable {
private let node: SplitTree.Node
init(_ node: SplitTree.Node) {
self.node = node
}
static func == (lhs: Self, rhs: Self) -> Bool {
lhs.node.isStructurallyEqual(to: rhs.node)
}
func hash(into hasher: inout Hasher) {
node.hashStructure(into: &hasher)
}
}
/// Checks if this node is structurally equal to another node.
/// Two nodes are structurally equal if they have the same tree structure
/// and the same views (by identity) in the same positions.
@@ -1259,26 +1181,26 @@ extension SplitTree.Node {
case let (.leaf(view1), .leaf(view2)):
// Views must be the same instance
return view1 === view2
case let (.split(split1), .split(split2)):
// Splits must have same direction and structurally equal children
// Note: We intentionally don't compare ratios as they may change slightly
return split1.direction == split2.direction &&
split1.left.isStructurallyEqual(to: split2.left) &&
split1.right.isStructurallyEqual(to: split2.right)
default:
// Different node types
return false
}
}
/// Hash keys for structural identity
private enum HashKey: UInt8 {
case leaf = 0
case split = 1
}
/// Hashes the structural identity of this node.
/// Includes the tree structure and view identities in the hash.
fileprivate func hashStructure(into hasher: inout Hasher) {
@@ -1286,7 +1208,7 @@ extension SplitTree.Node {
case .leaf(let view):
hasher.combine(HashKey.leaf)
hasher.combine(ObjectIdentifier(view))
case .split(let split):
hasher.combine(HashKey.split)
hasher.combine(split.direction)
@@ -1325,17 +1247,17 @@ extension SplitTree {
struct StructuralIdentity: Hashable {
private let root: Node?
private let zoomed: Node?
init(_ tree: SplitTree) {
self.root = tree.root
self.zoomed = tree.zoomed
}
static func == (lhs: Self, rhs: Self) -> Bool {
areNodesStructurallyEqual(lhs.root, rhs.root) &&
areNodesStructurallyEqual(lhs.zoomed, rhs.zoomed)
}
func hash(into hasher: inout Hasher) {
hasher.combine(0) // Tree marker
if let root = root {
@@ -1346,7 +1268,7 @@ extension SplitTree {
zoomed.hashStructure(into: &hasher)
}
}
/// Helper to compare optional nodes for structural equality
private static func areNodesStructurallyEqual(_ lhs: Node?, _ rhs: Node?) -> Bool {
switch (lhs, rhs) {

View File

@@ -290,12 +290,8 @@ class BaseTerminalController: NSWindowController,
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
alert.beginSheetModal(for: window) { response in
let alertWindow = alert.window
self.alert = nil
if response == .alertFirstButtonReturn {
// This is important so that we avoid losing focus when Stage
// Manager is used (#8336)
alertWindow.orderOut(nil)
completion()
}
}

View File

@@ -95,11 +95,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
selector: #selector(onCloseTab),
name: .ghosttyCloseTab,
object: nil)
center.addObserver(
self,
selector: #selector(onCloseOtherTabs),
name: .ghosttyCloseOtherTabs,
object: nil)
center.addObserver(
self,
selector: #selector(onResetWindowSize),
@@ -201,12 +196,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
if let parent {
if parent.styleMask.contains(.fullScreen) {
// If our previous window was fullscreen then we want our new window to
// be fullscreen. This behavior actually doesn't match the native tabbing
// behavior of macOS apps where new windows create tabs when in native
// fullscreen but this is how we've always done it. This matches iTerm2
// behavior.
c.toggleFullscreen(mode: .native)
parent.toggleFullScreen(nil)
} else if ghostty.config.windowFullscreen {
switch (ghostty.config.windowFullscreenMode) {
case .native:
@@ -236,10 +226,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
c.showWindow(self)
// All new_window actions force our app to be active, so that the new
// window is focused and visible.
NSApp.activate(ignoringOtherApps: true)
}
// Setup our undo
@@ -346,10 +332,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
controller.showWindow(self)
window.makeKeyAndOrderFront(self)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
// It takes an event loop cycle until the macOS tabGroup state becomes
@@ -439,7 +421,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
continue
}
if let equiv = ghostty.config.keyboardShortcut(for: "goto_tab:\(tab)") {
let action = "goto_tab:\(tab)"
if let equiv = ghostty.config.keyboardShortcut(for: action) {
window.keyEquivalent = "\(equiv)"
} else {
window.keyEquivalent = ""
@@ -563,7 +546,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeWindow(nil)
}
private func closeTabImmediately(registerRedo: Bool = true) {
private func closeTabImmediately() {
guard let window = window else { return }
guard let tabGroup = window.tabGroup,
tabGroup.windows.count > 1 else {
@@ -580,69 +563,19 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
expiresAfter: undoExpiration
) { ghostty in
let newController = TerminalController(ghostty, with: undoState)
if registerRedo {
undoManager.registerUndo(
withTarget: newController,
expiresAfter: newController.undoExpiration
) { target in
target.closeTabImmediately()
}
// Register redo action
undoManager.registerUndo(
withTarget: newController,
expiresAfter: newController.undoExpiration
) { target in
target.closeTabImmediately()
}
}
}
window.close()
}
private func closeOtherTabsImmediately() {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
guard tabGroup.windows.count > 1 else { return }
// Start an undo grouping
if let undoManager {
undoManager.beginUndoGrouping()
}
defer {
undoManager?.endUndoGrouping()
}
// Iterate through all tabs except the current one.
for window in tabGroup.windows where window != self.window {
// We ignore any non-terminal tabs. They don't currently exist and we can't
// properly undo them anyways so I'd rather ignore them and get a bug report
// later if and when we introduce non-terminal tabs.
if let controller = window.windowController as? TerminalController {
// We must not register a redo, because it messes with our own redo
// that we register later.
controller.closeTabImmediately(registerRedo: false)
}
}
if let undoManager {
undoManager.setActionName("Close Other Tabs")
// We need to register an undo that refocuses this window. Otherwise, the
// undo operation above for each tab will steal focus.
undoManager.registerUndo(
withTarget: self,
expiresAfter: undoExpiration
) { target in
DispatchQueue.main.async {
target.window?.makeKeyAndOrderFront(nil)
}
// Register redo action
undoManager.registerUndo(
withTarget: target,
expiresAfter: target.undoExpiration
) { target in
target.closeOtherTabsImmediately()
}
}
}
}
/// Closes the current window (including any other tabs) immediately and without
/// confirmation. This will setup proper undo state so the action can be undone.
@@ -806,9 +739,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
alert.alertStyle = .warning
alert.beginSheetModal(for: alertWindow, completionHandler: { response in
if (response == .alertFirstButtonReturn) {
// This is important so that we avoid losing focus when Stage
// Manager is used (#8336)
alert.window.orderOut(nil)
closeAllWindowsImmediately()
}
})
@@ -860,7 +790,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
// Restore focus to the previously focused surface
if let focusedUUID = undoState.focusedSurface,
let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) {
let focusTarget = surfaceTree.first(where: { $0.uuid == focusedUUID }) {
DispatchQueue.main.async {
Ghostty.moveFocus(to: focusTarget, from: nil)
}
@@ -875,7 +805,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
return .init(
frame: window.frame,
surfaceTree: surfaceTree,
focusedSurface: focusedSurface?.id,
focusedSurface: focusedSurface?.uuid,
tabIndex: window.tabGroup?.windows.firstIndex(of: window),
tabGroup: window.tabGroup)
}
@@ -1077,38 +1007,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
}
@IBAction func closeOtherTabs(_ sender: Any?) {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
// If we only have one window then we have no other tabs to close
guard tabGroup.windows.count > 1 else { return }
// Check if we have to confirm close.
guard tabGroup.windows.contains(where: { window in
// Ignore ourself
if window == self.window { return false }
// Ignore non-terminals
guard let controller = window.windowController as? TerminalController else {
return false
}
// Check if any surfaces require confirmation
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
}) else {
self.closeOtherTabsImmediately()
return
}
confirmClose(
messageText: "Close Other Tabs?",
informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed."
) {
self.closeOtherTabsImmediately()
}
}
@IBAction func returnToDefaultSize(_ sender: Any?) {
guard let defaultSize else { return }
window?.setFrame(defaultSize, display: true)
@@ -1292,12 +1190,6 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeTab(self)
}
@objc private func onCloseOtherTabs(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return }
closeOtherTabs(self)
}
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return }

View File

@@ -4,13 +4,13 @@ import Cocoa
class TerminalRestorableState: Codable {
static let selfKey = "state"
static let versionKey = "version"
static let version: Int = 5
static let version: Int = 4
let focusedSurface: String?
let surfaceTree: SplitTree<Ghostty.SurfaceView>
init(from controller: TerminalController) {
self.focusedSurface = controller.focusedSurface?.id.uuidString
self.focusedSurface = controller.focusedSurface?.uuid.uuidString
self.surfaceTree = controller.surfaceTree
}
@@ -96,7 +96,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
if let focusedStr = state.focusedSurface {
var foundView: Ghostty.SurfaceView?
for view in c.surfaceTree {
if view.id.uuidString == focusedStr {
if view.uuid.uuidString == focusedStr {
foundView = view
break
}

View File

@@ -31,16 +31,9 @@ class HiddenTitlebarTerminalWindow: TerminalWindow {
.closable,
.miniaturizable,
]
/// Apply the hidden titlebar style.
private func reapplyHiddenStyle() {
// If our window is fullscreen then we don't reapply the hidden style because
// it can result in messing up non-native fullscreen. See:
// https://github.com/ghostty-org/ghostty/issues/8415
if terminalController?.fullscreenStyle?.isFullscreen ?? false {
return
}
// Apply our style mask while preserving the .fullScreen option
if styleMask.contains(.fullScreen) {
styleMask = Self.hiddenStyleMask.union([.fullScreen])

View File

@@ -70,39 +70,4 @@ extension Ghostty.Action {
}
}
}
struct ProgressReport {
enum State {
case remove
case set
case error
case indeterminate
case pause
init(_ c: ghostty_action_progress_report_state_e) {
switch c {
case GHOSTTY_PROGRESS_STATE_REMOVE:
self = .remove
case GHOSTTY_PROGRESS_STATE_SET:
self = .set
case GHOSTTY_PROGRESS_STATE_ERROR:
self = .error
case GHOSTTY_PROGRESS_STATE_INDETERMINATE:
self = .indeterminate
case GHOSTTY_PROGRESS_STATE_PAUSE:
self = .pause
default:
self = .remove
}
}
}
let state: State
let progress: UInt8?
init(c: ghostty_action_progress_report_s) {
self.state = State(c.state)
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
}
}
}

View File

@@ -455,7 +455,7 @@ extension Ghostty {
newSplit(app, target: target, direction: action.action.new_split)
case GHOSTTY_ACTION_CLOSE_TAB:
closeTab(app, target: target, mode: action.action.close_tab_mode)
closeTab(app, target: target)
case GHOSTTY_ACTION_CLOSE_WINDOW:
closeWindow(app, target: target)
@@ -543,9 +543,6 @@ extension Ghostty {
case GHOSTTY_ACTION_KEY_SEQUENCE:
keySequence(app, target: target, v: action.action.key_sequence)
case GHOSTTY_ACTION_PROGRESS_REPORT:
progressReport(app, target: target, v: action.action.progress_report)
case GHOSTTY_ACTION_CONFIG_CHANGE:
configChange(app, target: target, v: action.action.config_change)
@@ -781,34 +778,20 @@ extension Ghostty {
}
}
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) {
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("close tabs does nothing with an app target")
Ghostty.logger.warning("close tab 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 }
switch (mode) {
case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS:
NotificationCenter.default.post(
name: .ghosttyCloseTab,
object: surfaceView
)
return
case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER:
NotificationCenter.default.post(
name: .ghosttyCloseOtherTabs,
object: surfaceView
)
return
default:
assertionFailure()
}
NotificationCenter.default.post(
name: .ghosttyCloseTab,
object: surfaceView
)
default:
@@ -1526,33 +1509,6 @@ extension Ghostty {
assertionFailure()
}
}
private static func progressReport(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_progress_report_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("progress report 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 }
let progressReport = Ghostty.Action.ProgressReport(c: v)
DispatchQueue.main.async {
if progressReport.state == .remove {
surfaceView.progressReport = nil
} else {
surfaceView.progressReport = progressReport
}
}
default:
assertionFailure()
}
}
private static func configReload(
_ app: ghostty_app_t,

View File

@@ -164,7 +164,7 @@ extension Ghostty {
let key = "window-position-x"
return ghostty_config_get(config, &v, key, UInt(key.count)) ? v : nil
}
var windowPositionY: Int16? {
guard let config = self.config else { return nil }
var v: Int16 = 0
@@ -282,17 +282,6 @@ extension Ghostty {
return MacOSTitlebarProxyIcon(rawValue: str) ?? defaultValue
}
var macosDockDropBehavior: MacDockDropBehavior {
let defaultValue = MacDockDropBehavior.new_tab
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-dock-drop-behavior"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
let str = String(cString: ptr)
return MacDockDropBehavior(rawValue: str) ?? defaultValue
}
var macosWindowShadow: Bool {
guard let config = self.config else { return false }
var v = false;
@@ -312,24 +301,6 @@ extension Ghostty {
return MacOSIcon(rawValue: str) ?? defaultValue
}
var macosCustomIcon: String {
#if os(macOS)
let homeDirURL = FileManager.default.homeDirectoryForCurrentUser
let ghosttyConfigIconPath = homeDirURL.appendingPathComponent(
".config/ghostty/Ghostty.icns",
conformingTo: .fileURL).path()
let defaultValue = ghosttyConfigIconPath
guard let config = self.config else { return defaultValue }
var v: UnsafePointer<Int8>? = nil
let key = "macos-custom-icon"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
guard let ptr = v else { return defaultValue }
return String(cString: ptr)
#else
return ""
#endif
}
var macosIconFrame: MacOSIconFrame {
let defaultValue = MacOSIconFrame.aluminum
guard let config = self.config else { return defaultValue }
@@ -504,14 +475,6 @@ extension Ghostty {
let str = String(cString: ptr)
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
}
var quickTerminalSize: QuickTerminalSize {
guard let config = self.config else { return QuickTerminalSize() }
var v = ghostty_config_quick_terminal_size_s()
let key = "quick-terminal-size"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() }
return QuickTerminalSize(from: v)
}
#endif
var resizeOverlay: ResizeOverlay {
@@ -626,11 +589,6 @@ extension Ghostty.Config {
static let attention = BellFeatures(rawValue: 1 << 2)
static let title = BellFeatures(rawValue: 1 << 3)
}
enum MacDockDropBehavior: String {
case new_tab = "new-tab"
case new_window = "new-window"
}
enum MacHidden : String {
case never

View File

@@ -280,7 +280,6 @@ extension Ghostty {
case paper
case retro
case xray
case custom
case customStyle = "custom-style"
}
@@ -329,9 +328,6 @@ extension Notification.Name {
/// Close tab
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
/// Close other tabs
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
/// Close window
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")

View File

@@ -113,11 +113,6 @@ extension Ghostty {
}
}
.ghosttySurfaceView(surfaceView)
// Progress report overlay
if let progressReport = surfaceView.progressReport {
ProgressReportOverlay(report: progressReport)
}
#if canImport(AppKit)
// If we are in the middle of a key sequence, then we show a visual element. We only
@@ -272,49 +267,6 @@ extension Ghostty {
}
}
// Progress report overlay that shows a progress bar at the top of the terminal
struct ProgressReportOverlay: View {
let report: Action.ProgressReport
@ViewBuilder
private var progressBar: some View {
if let progress = report.progress {
// Determinate progress bar
ProgressView(value: Double(progress), total: 100)
.progressViewStyle(.linear)
.tint(report.state == .error ? .red : report.state == .pause ? .orange : nil)
.animation(.easeInOut(duration: 0.2), value: progress)
} else {
// Indeterminate states
switch report.state {
case .indeterminate:
ProgressView()
.progressViewStyle(.linear)
case .error:
ProgressView()
.progressViewStyle(.linear)
.tint(.red)
case .pause:
Rectangle().fill(Color.orange)
default:
EmptyView()
}
}
}
var body: some View {
VStack(spacing: 0) {
progressBar
.scaleEffect(x: 1, y: 0.5, anchor: .center)
.frame(height: 2)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.allowsHitTesting(false)
}
}
// This is the resize overlay that shows on top of a surface to show the current
// size during a resize operation.
struct SurfaceResizeOverlay: View {
@@ -472,9 +424,6 @@ extension Ghostty {
/// Extra input to send as stdin
var initialInput: String? = nil
/// Wait after the command
var waitAfterCommand: Bool = false
init() {}
@@ -526,9 +475,6 @@ extension Ghostty {
// Zero is our default value that means to inherit the font size.
config.font_size = fontSize ?? 0
// Set wait after command
config.wait_after_command = waitAfterCommand
// Use withCString to ensure strings remain valid for the duration of the closure
return try workingDirectory.withCString { cWorkingDir in

View File

@@ -6,11 +6,9 @@ import GhosttyKit
extension Ghostty {
/// The NSView implementation for a terminal surface.
class SurfaceView: OSView, ObservableObject, Codable, Identifiable {
typealias ID = UUID
class SurfaceView: OSView, ObservableObject, Codable {
/// Unique ID per surface
let id: UUID
let uuid: UUID
// The current title of the surface as defined by the pty. This can be
// changed with escape codes. This is public because the callbacks go
@@ -43,23 +41,6 @@ extension Ghostty {
// The hovered URL string
@Published var hoverUrl: String? = nil
// The progress report (if any)
@Published var progressReport: Action.ProgressReport? = nil {
didSet {
// Cancel any existing timer
progressReportTimer?.invalidate()
progressReportTimer = nil
// If we have a new progress report, start a timer to remove it after 15 seconds
if progressReport != nil {
progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
self?.progressReport = nil
self?.progressReportTimer = nil
}
}
}
}
// The currently active key sequence. The sequence is not active if this is empty.
@Published var keySequence: [KeyboardShortcut] = []
@@ -161,9 +142,6 @@ extension Ghostty {
// A timer to fallback to ghost emoji if no title is set within the grace period
private var titleFallbackTimer: Timer?
// Timer to remove progress report after 15 seconds
private var progressReportTimer: Timer?
// This is the title from the terminal. This is nil if we're currently using
// the terminal title as the main title property. If the title is set manually
@@ -182,7 +160,7 @@ extension Ghostty {
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
self.markedText = NSMutableAttributedString()
self.id = uuid ?? .init()
self.uuid = uuid ?? .init()
// Our initial config always is our application wide config.
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
@@ -370,9 +348,6 @@ extension Ghostty {
// Remove any notifications associated with this surface
let identifiers = Array(self.notificationIdentifiers)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
// Cancel progress report timer
progressReportTimer?.invalidate()
}
func focusDidChange(_ focused: Bool) {
@@ -1266,7 +1241,7 @@ extension Ghostty {
var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags)
key_ev.composing = composing
// For text, we only encode UTF8 if we don't have a single control
// character. Control characters are encoded by Ghostty itself.
// Without this, `ctrl+enter` does the wrong thing.
@@ -1352,7 +1327,7 @@ extension Ghostty {
var item: NSMenuItem
// If we have a selection, add copy
if let text = self.accessibilitySelectedText(), text.count > 0 {
if self.selectedRange().length > 0 {
menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "")
}
menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "")
@@ -1470,7 +1445,7 @@ extension Ghostty {
content.body = body
content.sound = UNNotificationSound.default
content.categoryIdentifier = Ghostty.userNotificationCategory
content.userInfo = ["surface": self.id.uuidString]
content.userInfo = ["surface": self.uuid.uuidString]
let uuid = UUID().uuidString
let request = UNNotificationRequest(
@@ -1578,7 +1553,7 @@ extension Ghostty {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(pwd, forKey: .pwd)
try container.encode(id.uuidString, forKey: .uuid)
try container.encode(uuid.uuidString, forKey: .uuid)
try container.encode(title, forKey: .title)
try container.encode(titleFromTerminal != nil, forKey: .isUserSetTitle)
}
@@ -1685,10 +1660,8 @@ extension Ghostty.SurfaceView: NSTextInputClient {
}
// Ghostty will tell us where it thinks an IME keyboard should render.
var x: Double = 0
var y: Double = 0
var width: Double = cellSize.width
var height: Double = cellSize.height
var x: Double = 0;
var y: Double = 0;
// QuickLook never gives us a matching range to our selection so if we detect
// this then we return the top-left selection point rather than the cursor point.
@@ -1706,19 +1679,15 @@ extension Ghostty.SurfaceView: NSTextInputClient {
// Free our text
ghostty_surface_free_text(surface, &text)
} else {
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
ghostty_surface_ime_point(surface, &x, &y)
}
} else {
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
ghostty_surface_ime_point(surface, &x, &y)
}
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
// bottom-left since that is what UIKit expects
let viewRect = NSMakeRect(
x,
frame.size.height - y,
max(width, cellSize.width),
max(height, cellSize.height))
let viewRect = NSMakeRect(x, frame.size.height - y, 0, 0)
// Convert the point to the window coordinates
let winRect = self.convert(viewRect, to: nil)

View File

@@ -30,9 +30,6 @@ extension Ghostty {
// The hovered URL
@Published var hoverUrl: String? = nil
// The progress report (if any)
@Published var progressReport: Action.ProgressReport? = nil
// The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms.

View File

@@ -407,14 +407,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
self.styleMask = window.styleMask
self.toolbar = window.toolbar
self.toolbarStyle = window.toolbarStyle
self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers
self.dock = window.screen?.hasDock ?? false
self.titlebarAccessoryViewControllers = if (window.hasTitleBar) {
// Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash.
window.titlebarAccessoryViewControllers
} else {
[]
}
if let cgWindowId = window.cgWindowId {
// We hide the menu only if this window is not on any fullscreen

View File

@@ -9,7 +9,6 @@
# - build.zig.zon.nix
# - build.zig.zon.txt
# - build.zig.zon.json
# - flatpak/zig-packages.json
#
# All of these are auto-generated and should not be edited manually.
@@ -35,8 +34,8 @@ help() {
echo "commit, and submit a PR with the update:"
echo ""
echo " ./nix/build-support/check-zig-cache-hash.sh --update"
echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json"
echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json\""
echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json"
echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json\""
echo ""
}
@@ -45,7 +44,6 @@ BUILD_ZIG_ZON="$ROOT/build.zig.zon"
BUILD_ZIG_ZON_NIX="$ROOT/build.zig.zon.nix"
BUILD_ZIG_ZON_TXT="$ROOT/build.zig.zon.txt"
BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json"
ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json"
if [ -f "${BUILD_ZIG_ZON_NIX}" ]; then
OLD_HASH_NIX=$(sha512sum "${BUILD_ZIG_ZON_NIX}" | awk '{print $1}')
@@ -71,40 +69,27 @@ elif [ "$1" != "--update" ]; then
exit 1
fi
if [ -f "${ZIG_PACKAGES_JSON}" ]; then
OLD_HASH_FLATPAK=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}')
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: flatpak/zig-packages.json missing."
help
exit 1
fi
zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json"
zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json"
alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json"
prettier --log-level warn --write "$WORK_DIR/zig-packages.json"
prettier --write "$WORK_DIR/build.zig.zon.json"
NEW_HASH_NIX=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}')
NEW_HASH_TXT=$(sha512sum "$WORK_DIR/build.zig.zon.txt" | awk '{print $1}')
NEW_HASH_JSON=$(sha512sum "$WORK_DIR/build.zig.zon.json" | awk '{print $1}')
NEW_HASH_FLATPAK=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}')
if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ] && [ "${OLD_HASH_FLATPAK}" == "${NEW_HASH_FLATPAK}" ]; then
if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ]; then
echo -e "\nOK: build.zig.zon.nix unchanged."
echo -e "OK: build.zig.zon.txt unchanged."
echo -e "OK: build.zig.zon.json unchanged."
echo -e "OK: flatpak/zig-packages.json unchanged."
exit 0
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: build.zig.zon.nix, build.zig.zon.txt, or build.zig.zon.json needs to be updated.\n"
echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}"
echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}"
echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}"
echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}"
echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}"
echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}"
echo " * Old flatpak/zig-packages.json hash: ${OLD_HASH_FLATPAK}"
echo " * New flatpak/zig-packages.json hash: ${NEW_HASH_FLATPAK}"
echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}"
echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}"
echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}"
echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}"
echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}"
echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}"
help
exit 1
else
@@ -114,8 +99,6 @@ else
echo -e "OK: build.zig.zon.txt updated."
mv "$WORK_DIR/build.zig.zon.json" "$BUILD_ZIG_ZON_JSON"
echo -e "OK: build.zig.zon.json updated."
mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON"
echo -e "OK: flatpak/zig-packages.json updated."
exit 0
fi

View File

@@ -3,7 +3,6 @@
lib,
stdenv,
bashInteractive,
nushell,
appstream,
flatpak-builder,
gdb,
@@ -61,7 +60,6 @@
pandoc,
pinact,
hyperfine,
poop,
typos,
shellcheck,
uv,
@@ -126,9 +124,6 @@ in
# CI
uv
# Scripting
nushell
# We need these GTK-related deps on all platform so we can build
# dist tarballs.
blueprint-compiler
@@ -188,9 +183,6 @@ in
# developer shell
glycin-loaders
librsvg
# for benchmarking
poop
];
# This should be set onto the rpath of the ghostty binary if you want

View File

@@ -13,7 +13,11 @@ pub fn build(b: *std.Build) !void {
const unit_tests = b.addTest(.{
.name = "test",
.root_module = module,
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
});
unit_tests.linkLibC();
@@ -30,6 +34,12 @@ pub fn build(b: *std.Build) !void {
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = flags.items,
});
unit_tests.addIncludePath(wuffs_dep.path("release/c"));
unit_tests.addCSourceFile(.{
.file = wuffs_dep.path("release/c/wuffs-v0.4.c"),
.flags = flags.items,
});
}
if (b.lazyDependency("pixels", .{})) |pixels_dep| {

View File

@@ -2,15 +2,14 @@
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Damyan Bogoev <damyan.bogoev@gmail.com>, 2025.
# reo101 <pavel.atanasov2001@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-22 14:52+0300\n"
"Last-Translator: reo101 <pavel.atanasov2001@gmail.com>\n"
"PO-Revision-Date: 2025-05-19 11:34+0300\n"
"Last-Translator: Damyan Bogoev <damyan.bogoev@gmail.com>\n"
"Language-Team: Bulgarian <dict@ludost.net>\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
@@ -209,12 +208,12 @@ msgstr "Позволи"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Запомни избора за това разделяне"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "За да покажеш това съобщение отново, презареди конфигурацията"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -279,15 +278,15 @@ msgstr "Копирано в клипборда"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Клипбордът е изчистен"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Командата завърши успешно"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Командата завърши неуспешно"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -2,15 +2,14 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Francesc Arpi <francesc.arpi@gmail.com>, 2025.
# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-24 19:22+0200\n"
"Last-Translator: Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>\n"
"PO-Revision-Date: 2025-03-20 08:07+0100\n"
"Last-Translator: Francesc Arpi <francesc.arpi@gmail.com>\n"
"Language-Team: \n"
"Language: ca\n"
"MIME-Version: 1.0\n"
@@ -88,7 +87,7 @@ msgstr "Divideix a la dreta"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Executa una ordre…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -161,7 +160,7 @@ msgstr "Obre la configuració"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Paleta de comandes"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +208,12 @@ msgstr "Permet"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Recorda lopció per a aquest panell dividit"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Recarrega la configuració per tornar a mostrar aquest missatge"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -279,15 +278,15 @@ msgstr "Copiat al porta-retalls"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Porta-retalls netejat"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Comanda completada amb èxit"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Comanda fallida"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -299,7 +298,7 @@ msgstr "Mostra les pestanyes obertes"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Nova divisió"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -9,7 +9,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-25 19:38+0100\n"
"PO-Revision-Date: 2025-03-06 14:57+0100\n"
"Last-Translator: Robin <r@rpfaeffle.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\n"
"Language: de\n"
@@ -39,7 +39,7 @@ msgstr "OK"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Konfigurationsfehler"
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
@@ -47,14 +47,11 @@ msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe "
"die untenstehenden Fehler und lade entweder deine Konfiguration erneut oder "
"ignoriere die Fehler."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore"
msgstr "Ignorieren"
msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
@@ -89,7 +86,7 @@ msgstr "Fenster nach rechts teilen"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Einen Befehl ausführen…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -162,7 +159,7 @@ msgstr "Konfiguration öffnen"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Befehlspalette"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -210,13 +207,12 @@ msgstr "Erlauben"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Auswahl für dieses geteilte Fenster beibehalten"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr ""
"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -281,15 +277,15 @@ msgstr "In die Zwischenablage kopiert"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Zwischenablage geleert"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Befehl erfolgreich"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Befehl fehlgeschlagen"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -301,7 +297,7 @@ msgstr "Offene Tabs einblenden"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Neues geteiltes Fenster"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-22 09:35-0300\n"
"PO-Revision-Date: 2025-05-19 20:17-0300\n"
"Last-Translator: Alan Moyano <alanmoyano203@gmail.com>\n"
"Language-Team: Argentinian <es@tp.org.es>\n"
"Language: es_AR\n"
@@ -208,12 +208,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Recordar elección para esta división"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Recargar la configuración para volver a mostrar este mensaje"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Portapapeles limpiado"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Comando ejecutado correctamente"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "El comando ha fallado"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 17:46+0200\n"
"PO-Revision-Date: 2025-03-28 17:46+0200\n"
"Last-Translator: Miguel Peredo <miguelp@quientienemail.com>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
"Language: es_BO\n"
@@ -87,7 +87,7 @@ msgstr "Dividir a la derecha"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Ejecutar comando..."
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -160,7 +160,7 @@ msgstr "Abrir configuración"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Paleta de comandos"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -208,12 +208,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Recordar su elección para esta división de ventana"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Recargar configuración para mostrar este aviso nuevamente"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "El portapapeles está limpio"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Comando ejecutado con éxito"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Comando fallido"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -298,7 +298,7 @@ msgstr "Ver pestañas abiertas"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Nueva ventana dividida"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 21:01+0200\n"
"PO-Revision-Date: 2025-03-22 09:31+0100\n"
"Last-Translator: Kirwiisp <swiip__@hotmail.com>\n"
"Language-Team: French <traduc@traduc.org>\n"
"Language: fr\n"
@@ -88,7 +88,7 @@ msgstr "Panneau à droite"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Exécuter une commande…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -161,7 +161,7 @@ msgstr "Ouvrir la configuration"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Palette de commandes"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +209,12 @@ msgstr "Autoriser"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Se rappeler du choix pour ce panneau"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Recharger la configuration pour afficher à nouveau ce message"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -279,15 +279,15 @@ msgstr "Copié dans le presse-papiers"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Presse-papiers vidé"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Commande réussie"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "La commande a échoué"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -299,7 +299,7 @@ msgstr "Voir les onglets ouverts"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Nouveau panneau"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-26 15:46+0100\n"
"PO-Revision-Date: 2025-06-29 21:15+0100\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\n"
"Language: ga\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
"X-Generator: Poedit 3.4.2\n"
"X-Generator: Poedit 3.4.4\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
@@ -209,12 +209,12 @@ msgstr "Ceadaigh"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Sábháil an rogha don scoilt seo"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -280,15 +280,15 @@ msgstr "Cóipeáilte chuig an ghearrthaisce"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Gearrthaisce glanta"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "D'éirigh leis an ordú"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Theip ar an ordú"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -2,15 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Sl (Shahaf Levi), Sl's Repository Ltd <ghostty@slsrepo.com>, 2025.
# CraziestOwl <craziestowl@proton.me>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 08:00+0300\n"
"Last-Translator: CraziestOwl <craziestowl@proton.me>\n"
"PO-Revision-Date: 2025-03-13 00:00+0000\n"
"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd <ghostty@slsrepo."
"com>\n"
"Language-Team: Hebrew <he_IL@lists.sourceforge.net>\n"
"Language: he\n"
"MIME-Version: 1.0\n"
@@ -276,15 +276,15 @@ msgstr "הועתק ללוח ההעתקה"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "לוח ההעתקה רוקן"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "הפקודה הצליחה"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "הפקודה נכשלה"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -1,320 +0,0 @@
# Hungarian translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Balázs Szücs <bszucs1209@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 17:14+0200\n"
"Last-Translator: Balázs Szücs <bszucs1209@gmail.com>\n"
"Language-Team: Hungarian <translation-team-hu@lists.sourceforge.net>\n"
"Language: hu\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Terminál címének módosítása"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Hagyja üresen az alapértelmezett cím visszaállításához."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Mégse"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr "Rendben"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Konfigurációs hibák"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Egy vagy több konfigurációs hiba található. Kérjük, ellenőrizze az alábbi "
"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a "
"hibákat."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore"
msgstr "Figyelmen kívül hagyás"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
msgid "Reload Configuration"
msgstr "Konfiguráció frissítése"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "Felosztás felfelé"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "Felosztás lefelé"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "Felosztás balra"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "Felosztás jobbra"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Parancs végrehajtása…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
msgid "Copy"
msgstr "Másolás"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
msgid "Paste"
msgstr "Beillesztés"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73
msgid "Clear"
msgstr "Törlés"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78
msgid "Reset"
msgstr "Visszaállítás"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "Felosztás"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
msgid "Change Title…"
msgstr "Cím módosítása…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "Fül"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
#: src/apprt/gtk/Window.zig:265
msgid "New Tab"
msgstr "Új fül"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35
msgid "Close Tab"
msgstr "Fül bezárása"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr "Ablak"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18
msgid "New Window"
msgstr "Új ablak"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23
msgid "Close Window"
msgstr "Ablak bezárása"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Konfiguráció"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Konfiguráció megnyitása"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Parancspaletta"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
msgstr "Terminálvizsgáló"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1038
msgid "About Ghostty"
msgstr "A Ghostty névjegye"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
msgid "Quit"
msgstr "Kilépés"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "Vágólap-hozzáférés engedélyezése"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma "
"lent látható."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Elutasítás"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
msgid "Allow"
msgstr "Engedélyezés"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Választás megjegyzése erre a felosztásra"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent "
"látható."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
msgstr "Figyelem: potenciálisan veszélyes beillesztés"
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel "
"néhány parancs végrehajtásra kerülhet."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close"
msgstr "Bezárás"
#: src/apprt/gtk/CloseDialog.zig:87
msgid "Quit Ghostty?"
msgstr "Kilép a Ghostty-ból?"
#: src/apprt/gtk/CloseDialog.zig:88
msgid "Close Window?"
msgstr "Ablak bezárása?"
#: src/apprt/gtk/CloseDialog.zig:89
msgid "Close Tab?"
msgstr "Fül bezárása?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Felosztás bezárása?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
msgstr "Minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated."
msgstr "Ebben az ablakban minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated."
msgstr "Ezen a fülön minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul."
#: src/apprt/gtk/Surface.zig:1266
msgid "Copied to clipboard"
msgstr "Vágólapra másolva"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Vágólap törölve"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Parancs sikeres"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Parancs sikertelen"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
msgstr "Főmenü"
#: src/apprt/gtk/Window.zig:239
msgid "View Open Tabs"
msgstr "Megnyitott fülek megtekintése"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Új felosztás"
#: src/apprt/gtk/Window.zig:329
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ A Ghostty hibakereső verzióját futtatja! A teljesítmény csökkenni fog."
#: src/apprt/gtk/Window.zig:775
msgid "Reloaded the configuration"
msgstr "Konfiguráció frissítve"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"
msgstr "Ghostty fejlesztők"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Terminálvizsgáló"

View File

@@ -2,15 +2,14 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Andrej Daskalov <andrej.daskalov@gmail.com>, 2025.
# Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-25 22:17+0200\n"
"Last-Translator: Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.com>\n"
"PO-Revision-Date: 2025-03-23 14:17+0100\n"
"Last-Translator: Andrej Daskalov <andrej.daskalov@gmail.com>\n"
"Language-Team: Macedonian\n"
"Language: mk\n"
"MIME-Version: 1.0\n"
@@ -88,7 +87,7 @@ msgstr "Подели надесно"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Изврши команда…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -161,7 +160,7 @@ msgstr "Отвори конфигурација"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Командна палета"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +208,12 @@ msgstr "Дозволи"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Запомни го изборот за оваа поделба"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Одново вчитај конфигурација за да се повторно прикаже пораката"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -279,15 +278,15 @@ msgstr "Копирано во привремена меморија"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Исчистена привремена меморија"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Командата успеа"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Командата не успеа"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -299,7 +298,7 @@ msgstr "Прегледај отворени јазичиња"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Нова поделба"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -1,7 +1,7 @@
# Norwegian Bokmal translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Hanna Rose <me@hanna.lol>, 2025.
# Hanna Rose <hanna@hanna.lol>, 2025.
# Uzair Aftab <uzaaft@outlook.com>, 2025.
# Christoffer Tønnessen <christoffer@cto.gg>, 2025.
# cryptocode <cryptocode@zolo.io>, 2025.
@@ -11,8 +11,8 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 12:52+0000\n"
"Last-Translator: Hanna Rose <me@hanna.lol>\n"
"PO-Revision-Date: 2025-04-14 16:25+0200\n"
"Last-Translator: cryptocode <cryptocode@zolo.io>\n"
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\n"
"Language: nb\n"
"MIME-Version: 1.0\n"
@@ -90,7 +90,7 @@ msgstr "Del til høyre"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Kjør en kommando..."
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -163,7 +163,7 @@ msgstr "Åpne konfigurasjon"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Kommandopalett"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -211,12 +211,12 @@ msgstr "Tillat"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Husk valget for dette delte vinduet?"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Last inn konfigurasjonen på nytt for å vise denne meldingen igjen"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -281,15 +281,15 @@ msgstr "Kopiert til utklippstavlen"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Utklippstavle tømt"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Kommando lyktes"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Kommando mislyktes"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -3,15 +3,14 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Gustavo Peres <gsodevel@gmail.com>, 2025.
# Guilherme Tiscoski <github@guilhermetiscoski.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-25 11:46-0500\n"
"Last-Translator: Guilherme Tiscoski <github@guihermetiscoski.com>\n"
"PO-Revision-Date: 2025-06-20 10:19-0300\n"
"Last-Translator: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
"net>\n"
"Language: pt_BR\n"
@@ -90,7 +89,7 @@ msgstr "Dividir à direita"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Executar um comando…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -163,7 +162,7 @@ msgstr "Abrir configuração"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Paleta de comandos"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -211,12 +210,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Lembrar escolha para esta divisão"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Recarregue a configuração para mostrar este aviso novamente"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -281,15 +280,15 @@ msgstr "Copiado para a área de transferência"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Área de transferência limpa"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Comando executado com sucesso"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Comando falhou"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -3,16 +3,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# blackzeshi <sergey_zhuzhgov@mail.ru>, 2025.
# Ivan Bastrakov <bastaynav@proton.me>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-03-24 00:01+0500\n"
"Last-Translator: blackzeshi <sergey_zhuzhgov@mail.ru>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"PO-Revision-Date: 2025-09-03 01:50+0300\n"
"Last-Translator: Ivan Bastrakov <bastaynav@proton.me>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -90,7 +89,7 @@ msgstr "Сплит вправо"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Выполнить команду…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -163,7 +162,7 @@ msgstr "Открыть конфигурационный файл"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Палитра команд"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -211,12 +210,12 @@ msgstr "Разрешить"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Запомнить выбор для этого сплита"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Перезагрузите конфигурацию, чтобы снова увидеть это сообщение"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -224,8 +223,7 @@ msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Приложение пытается записать данные в буфер обмена. Текущее содержимое "
"буфера обмена показано ниже."
"Приложение пытается записать данные в буфер обмена. Эти данные показаны ниже."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
@@ -281,15 +279,15 @@ msgstr "Скопировано в буфер обмена"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Буфер обмена очищен"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Команда выполнена успешно"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Команда завершилась с ошибкой"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -301,7 +299,7 @@ msgstr "Просмотреть открытые вкладки"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Новый сплит"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-23 17:30+0300\n"
"PO-Revision-Date: 2025-03-24 22:01+0300\n"
"Last-Translator: Emir SARI <emir_sari@icloud.com>\n"
"Language-Team: Turkish\n"
"Language: tr\n"
@@ -88,7 +88,7 @@ msgstr "Sağa Doğru Böl"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Bir komut çalıştır…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -161,7 +161,7 @@ msgstr "Yapılandırmayı Aç"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Komut Paleti"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +209,12 @@ msgstr "İzin Ver"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Bu bölme için tercihi anımsa"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -279,15 +279,15 @@ msgstr "Panoya kopyalandı"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Pano temizlendi"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Komut başarılı oldu"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Komut başarısız oldu"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -2,15 +2,14 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Danylo Zalizchuk <danilmail0110@gmail.com>, 2025.
# Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-08-25 19:59+0100\n"
"Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n"
"PO-Revision-Date: 2025-03-16 20:16+0200\n"
"Last-Translator: Danylo Zalizchuk <danilmail0110@gmail.com>\n"
"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
@@ -21,17 +20,17 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Змінити заголовок терміналу"
msgstr "Змінити назву терміналу"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Залиште порожнім, щоб відновити заголовок за замовчуванням."
msgstr "Залиште порожнім, щоб відновити назву за замовчуванням."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Скасувати"
msgstr "Відмінити"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
@@ -40,7 +39,7 @@ msgstr "ОК"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Помилки налаштування"
msgstr "Помилки конфігурації"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
@@ -48,8 +47,9 @@ msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Виявлено одну або декілька помилок налаштування. Будь ласка, перегляньте "
"помилки нижче і або перезавантажте налаштування, або проігноруйте ці помилки."
"Виявлено одну або декілька помилок у конфігурації. Будь ласка, перегляньте "
"наведені нижче помилки і або перезавантажте конфігурацію, або проігноруйте "
"ці помилки."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
@@ -61,35 +61,35 @@ msgstr "Ігнорувати"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
msgid "Reload Configuration"
msgstr "Перезавантажити налаштування"
msgstr "Перезавантажити конфігурацію"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "Нова панель зверху"
msgstr "Розділити панель вгору"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "Нова панель знизу"
msgstr "Розділити панель вниз"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "Нова панель ліворуч"
msgstr "Розділити панель ліворуч"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "Нова панель праворуч"
msgstr "Розділити панель праворуч"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Виконати команду…"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -115,7 +115,7 @@ msgstr "Скинути"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "Панель"
msgstr "Розділена панель"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
@@ -153,16 +153,16 @@ msgstr "Закрити вікно"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Налаштування"
msgstr "Конфігурація"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Відкрити налаштування"
msgstr "Відкрити конфігурацію"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Палітра команд"
msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -182,7 +182,7 @@ msgstr "Завершити"
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
msgid "Authorize Clipboard Access"
msgstr "Надати доступ до буфера обміну"
msgstr "Дозволити доступ до буфера обміну"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
@@ -190,15 +190,15 @@ msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Програма намагається прочитати дані з буфера обміну. Нижче наведено вміст "
"буфера обміну."
"Програма намагається прочитати дані з буфера обміну. Нижче показано поточний "
"вміст буфера обміну."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Заборонити"
msgstr "Відхилити"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
@@ -210,12 +210,12 @@ msgstr "Дозволити"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr "Запамʼятати для цієї панелі"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr "Перезавантажте налаштування, щоб показати це повідомлення знову"
msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -223,8 +223,8 @@ msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Програма намагається записати дані до буфера обміну. Нижче наведено вміст "
"буфера обміну."
"Програма намагається записати дані до буфера обміну. Нижче показано поточний "
"вміст буфера обміну."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
@@ -235,8 +235,8 @@ msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Вставка цього тексту в термінал може бути небезпечною, бо схоже, що деякі "
"команди можуть бути виконані."
"Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає "
"так, ніби деякі команди можуть бути виконані."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close"
@@ -256,7 +256,7 @@ msgstr "Закрити вкладку?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Закрити панель?"
msgstr "Закрити розділену панель?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
@@ -272,23 +272,24 @@ msgstr "Всі сесії терміналу в цій вкладці будут
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr "Процес, що виконується в цій панелі, буде завершено."
msgstr ""
"Поточний процес, що виконується в цій розділеній панелі, буде завершено."
#: src/apprt/gtk/Surface.zig:1266
msgid "Copied to clipboard"
msgstr "Скопійовано до буферa обміну"
msgstr "Скопійовано в буфер обміну"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Буфер обміну очищено"
msgstr ""
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Команда завершилась успішно"
msgstr ""
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Команда завершилась з помилкою"
msgstr ""
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -300,7 +301,7 @@ msgstr "Переглянути відкриті вкладки"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Нова панель"
msgstr ""
#: src/apprt/gtk/Window.zig:329
msgid ""
@@ -310,7 +311,7 @@ msgstr ""
#: src/apprt/gtk/Window.zig:775
msgid "Reloaded the configuration"
msgstr "Налаштування перезавантажено"
msgstr "Конфігурацію перезавантажено"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"

View File

@@ -247,7 +247,6 @@ const DerivedConfig = struct {
clipboard_paste_protection: bool,
clipboard_paste_bracketed_safe: bool,
copy_on_select: configpkg.CopyOnSelect,
right_click_action: configpkg.RightClickAction,
confirm_close_surface: configpkg.ConfirmCloseSurface,
cursor_click_to_move: bool,
desktop_notifications: bool,
@@ -258,7 +257,6 @@ const DerivedConfig = struct {
mouse_shift_capture: configpkg.MouseShiftCapture,
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
macos_option_as_alt: ?configpkg.OptionAsAlt,
selection_clear_on_copy: bool,
selection_clear_on_typing: bool,
vt_kam_allowed: bool,
wait_after_command: bool,
@@ -273,7 +271,6 @@ const DerivedConfig = struct {
title_report: bool,
links: []Link,
link_previews: configpkg.LinkPreviews,
scroll_to_bottom: configpkg.Config.ScrollToBottom,
const Link = struct {
regex: oni.Regex,
@@ -317,7 +314,6 @@ const DerivedConfig = struct {
.clipboard_paste_protection = config.@"clipboard-paste-protection",
.clipboard_paste_bracketed_safe = config.@"clipboard-paste-bracketed-safe",
.copy_on_select = config.@"copy-on-select",
.right_click_action = config.@"right-click-action",
.confirm_close_surface = config.@"confirm-close-surface",
.cursor_click_to_move = config.@"cursor-click-to-move",
.desktop_notifications = config.@"desktop-notifications",
@@ -328,7 +324,6 @@ const DerivedConfig = struct {
.mouse_shift_capture = config.@"mouse-shift-capture",
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
.macos_option_as_alt = config.@"macos-option-as-alt",
.selection_clear_on_copy = config.@"selection-clear-on-copy",
.selection_clear_on_typing = config.@"selection-clear-on-typing",
.vt_kam_allowed = config.@"vt-kam-allowed",
.wait_after_command = config.@"wait-after-command",
@@ -343,7 +338,6 @@ const DerivedConfig = struct {
.title_report = config.@"title-report",
.links = links,
.link_previews = config.@"link-previews",
.scroll_to_bottom = config.@"scroll-to-bottom",
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
@@ -1535,6 +1529,11 @@ pub const Text = struct {
/// The viewport information about this text, if it is visible in
/// the viewport.
///
/// NOTE(mitchellh): This will only be non-null currently if the entirety
/// of the selection is contained within the viewport. We don't have a
/// use case currently for partial bounds but we should support this
/// eventually.
viewport: ?Viewport = null,
pub const Viewport = struct {
@@ -1545,13 +1544,6 @@ pub const Text = struct {
/// The linear offset of the start of the selection and the length.
/// This is "linear" in the sense that it is the offset in the
/// flattened viewport as a single array of text.
///
/// Note: these values are currently wrong if there is a partially
/// visible selection in the viewport (i.e. the top-left or
/// bottom-right of the selection is outside the viewport). But the
/// apprt usecase we have right now doesn't require these to be
/// correct so... let's fix this later. The wrong values will always
/// be within the text bounds so we aren't risking an overflow.
offset_start: u32,
offset_len: u32,
};
@@ -1593,57 +1585,17 @@ pub fn dumpTextLocked(
// Calculate our viewport info if we can.
const vp: ?Text.Viewport = viewport: {
// If our bottom right pin is before the viewport, then we can't
// possibly have this text be within the viewport.
const vp_tl_pin = self.io.terminal.screen.pages.getTopLeft(.viewport);
const br_pin = sel.bottomRight(&self.io.terminal.screen);
if (br_pin.before(vp_tl_pin)) break :viewport null;
// If our top-left pin is after the viewport, then we can't possibly
// have this text be within the viewport.
const vp_br_pin = self.io.terminal.screen.pages.getBottomRight(.viewport) orelse {
// I don't think this is possible but I don't want to crash on
// that assertion so let's just break out...
log.warn("viewport bottom-right pin not found, bug?", .{});
break :viewport null;
};
const tl_pin = sel.topLeft(&self.io.terminal.screen);
if (vp_br_pin.before(tl_pin)) break :viewport null;
// We established that our top-left somewhere before the viewport
// bottom-right and that our bottom-right is somewhere after
// the top-left. This means that at least some portion of our
// selection is within the viewport.
// Our top-left point. If it doesn't exist in the viewport it must
// be before and we can return (0,0).
const tl_pt: terminal.Point = self.io.terminal.screen.pages.pointFromPin(
// If our tl or br is not in the viewport then we don't
// have a viewport. One day we should extend this to support
// partial selections that are in the viewport.
const tl_pt = self.io.terminal.screen.pages.pointFromPin(
.viewport,
tl_pin,
) orelse tl: {
if (comptime std.debug.runtime_safety) {
assert(tl_pin.before(vp_tl_pin));
}
break :tl .{ .viewport = .{} };
};
// Our bottom-right point. If it doesn't exist in the viewport
// it must be the bottom-right of the viewport.
sel.topLeft(&self.io.terminal.screen),
) orelse break :viewport null;
const br_pt = self.io.terminal.screen.pages.pointFromPin(
.viewport,
br_pin,
) orelse br: {
if (comptime std.debug.runtime_safety) {
assert(vp_br_pin.before(br_pin));
}
break :br self.io.terminal.screen.pages.pointFromPin(
.viewport,
vp_br_pin,
).?;
};
sel.bottomRight(&self.io.terminal.screen),
) orelse break :viewport null;
const tl_coord = tl_pt.coord();
const br_coord = br_pt.coord();
@@ -1714,6 +1666,73 @@ pub fn selectionString(self: *Surface, alloc: Allocator) !?[:0]const u8 {
});
}
/// Return the apprt selection metadata used by apprt's for implementing
/// things like contextual information on right click and so on.
///
/// This only returns non-null if the selection is fully contained within
/// the viewport. The use case for this function at the time of authoring
/// it is for apprt's to implement right-click contextual menus and
/// those only make sense for selections fully contained within the
/// viewport. We don't handle the case where you right click a word-wrapped
/// word at the end of the viewport yet.
pub fn selectionInfo(self: *const Surface) ?apprt.Selection {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
const sel = self.io.terminal.screen.selection orelse return null;
// Get the TL/BR pins for the selection and convert to viewport.
const tl = sel.topLeft(&self.io.terminal.screen);
const br = sel.bottomRight(&self.io.terminal.screen);
const tl_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, tl) orelse return null;
const br_pt = self.io.terminal.screen.pages.pointFromPin(.viewport, br) orelse return null;
const tl_coord = tl_pt.coord();
const br_coord = br_pt.coord();
// Utilize viewport sizing to convert to offsets
const start = tl_coord.y * self.io.terminal.screen.pages.cols + tl_coord.x;
const end = br_coord.y * self.io.terminal.screen.pages.cols + br_coord.x;
// Our sizes are all scaled so we need to send the unscaled values back.
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
const x: f64 = x: {
// Simple x * cell width gives the left
var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width);
// Add padding
x += @floatFromInt(self.size.padding.left);
// Scale
x /= content_scale.x;
break :x x;
};
const y: f64 = y: {
// Simple y * cell height gives the top
var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height);
// We want the text baseline
y += @floatFromInt(self.size.cell.height);
y -= @floatFromInt(self.font_metrics.cell_baseline);
// Add padding
y += @floatFromInt(self.size.padding.top);
// Scale
y /= content_scale.y;
break :y y;
};
return .{
.tl_x_px = x,
.tl_y_px = y,
.offset_start = start,
.offset_len = end - start,
};
}
/// Returns the pwd of the terminal, if any. This is always copied because
/// the pwd can change at any point from termio. If we are calling from the IO
/// thread you should just check the terminal directly.
@@ -1732,7 +1751,6 @@ pub fn pwd(
pub fn imePoint(self: *const Surface) apprt.IMEPos {
self.renderer_state.mutex.lock();
const cursor = self.renderer_state.terminal.screen.cursor;
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
self.renderer_state.mutex.unlock();
// TODO: need to handle when scrolling and the cursor is not
@@ -1767,38 +1785,7 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
break :y y;
};
// Our height for now is always just the cell height because our preedit
// rendering only renders in a single line.
const height: f64 = height: {
var height: f64 = @floatFromInt(self.size.cell.height);
height /= content_scale.y;
break :height height;
};
const width: f64 = width: {
var width: f64 = @floatFromInt(preedit_width * self.size.cell.width);
// Our max width is the remaining screen width after the cursor.
// We don't have to deal with wrapping because the preedit doesn't
// wrap right now.
const screen_width: f64 = @floatFromInt(self.size.terminal().width);
const x_offset: f64 = @floatFromInt((cursor.x + 1) * self.size.cell.width);
const max = screen_width - x_offset;
width = @min(width, max);
// Note: we don't apply content scale here because it looks like
// for some reason in macOS its already scaled. I'm not sure why
// that is so I'm going to just leave this comment here so its known
// that I left this out on purpose pending more investigation.
break :width width;
};
return .{
.x = x,
.y = y,
.width = width,
.height = height,
};
return .{ .x = x, .y = y };
}
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
@@ -1846,32 +1833,6 @@ fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard)
};
}
fn copySelectionToClipboards(
self: *Surface,
sel: terminal.Selection,
clipboards: []const apprt.Clipboard,
) void {
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return;
};
defer self.alloc.free(buf);
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
}
/// Set the selection contents.
///
/// This must be called with the renderer mutex held.
@@ -1889,12 +1850,33 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
const sel = sel_ orelse return;
if (prev_) |prev| if (sel.eql(prev)) return;
const buf = self.io.terminal.screen.selectionString(self.alloc, .{
.sel = sel,
.trim = self.config.clipboard_trim_trailing_spaces,
}) catch |err| {
log.err("error reading selection string err={}", .{err});
return;
};
defer self.alloc.free(buf);
// Set the clipboard. This is not super DRY but it is clear what
// we're doing for each setting without being clever.
switch (self.config.copy_on_select) {
.false => unreachable, // handled above with an early exit
// Both standard and selection clipboards are set.
.clipboard => {
self.copySelectionToClipboards(sel, &.{ .standard, .selection });
const clipboards: []const apprt.Clipboard = &.{ .standard, .selection };
for (clipboards) |clipboard| self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
},
// The selection clipboard is set if supported, otherwise the standard.
@@ -1903,7 +1885,17 @@ fn setSelection(self: *Surface, sel_: ?terminal.Selection) !void {
.selection
else
.standard;
self.copySelectionToClipboards(sel, &.{clipboard});
self.rt_surface.setClipboardString(
buf,
clipboard,
false,
) catch |err| {
log.err(
"error setting clipboard string clipboard={} err={}",
.{ clipboard, err },
);
};
},
}
}
@@ -2316,8 +2308,7 @@ pub fn keyCallback(
try self.setSelection(null);
}
if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom);
try self.io.terminal.scrollViewport(.{ .bottom = {} });
try self.queueRender();
}
@@ -2803,21 +2794,8 @@ pub fn scrollCallback(
// that a wheel tick of 1 results in single scroll event.
const yoff_adjusted: f64 = if (scroll_mods.precision)
yoff
else yoff_adjusted: {
// Round out the yoff to an absolute minimum of 1. macos tries to
// simulate precision scrolling with non precision events by
// ramping up the magnitude of the offsets as it detects faster
// scrolling. Single click (very slow) scrolls are reported with a
// magnitude of 0.1 which would normally require a few clicks
// before we register an actual scroll event (depending on cell
// height and the mouse_scroll_multiplier setting).
const yoff_max: f64 = if (yoff > 0)
@max(yoff, 1)
else
@min(yoff, -1);
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier;
};
else
yoff * cell_size * self.config.mouse_scroll_multiplier;
// Add our previously saved pending amount to the offset to get the
// new offset value. The signs of the pending and yoff should match
@@ -3604,63 +3582,18 @@ pub fn mouseButtonCallback(
break :pin pin;
};
switch (self.config.right_click_action) {
.ignore => {},
.@"context-menu" => {
// If we already have a selection and the selection contains
// where we clicked then we don't want to modify the selection.
if (self.io.terminal.screen.selection) |prev_sel| {
if (prev_sel.contains(screen, pin)) break :sel;
// If we already have a selection and the selection contains
// where we clicked then we don't want to modify the selection.
if (self.io.terminal.screen.selection) |prev_sel| {
if (prev_sel.contains(screen, pin)) break :sel;
// The selection doesn't contain our pin, so we create a new
// word selection where we clicked.
}
const sel = screen.selectWord(pin) orelse break :sel;
try self.setSelection(sel);
try self.queueRender();
// Don't consume so that we show the context menu in apprt.
return false;
},
.copy => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
}
try self.setSelection(null);
try self.queueRender();
},
.@"copy-or-paste" => if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
try self.setSelection(null);
try self.queueRender();
} else {
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
// We don't need to clear selection because we didn't have
// one to begin with.
},
.paste => {
// Before we yield the lock, clear our selection if we have
// one.
try self.setSelection(null);
try self.queueRender();
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
},
// The selection doesn't contain our pin, so we create a new
// word selection where we clicked.
}
// Consume the event such that the context menu is not displayed.
return true;
const sel = screen.selectWord(pin) orelse break :sel;
try self.setSelection(sel);
try self.queueRender();
}
return false;
@@ -4546,17 +4479,6 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return true;
};
// Clear the selection if configured to do so.
if (self.config.selection_clear_on_copy) {
if (self.setSelection(null)) {
self.queueRender() catch |err| {
log.warn("failed to queue render after clear selection err={}", .{err});
};
} else |err| {
log.warn("failed to clear selection after copy err={}", .{err});
}
}
return true;
}
@@ -4569,32 +4491,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
const pos = try self.rt_surface.getCursorPos();
if (try self.linkAtPos(pos)) |link_info| {
const url_text = switch (link_info[0]) {
.open => url_text: {
// For regex links, get the text from selection
break :url_text (self.io.terminal.screen.selectionString(self.alloc, .{
.sel = link_info[1],
.trim = self.config.clipboard_trim_trailing_spaces,
})) catch |err| {
log.err("error reading url string err={}", .{err});
return false;
};
},
._open_osc8 => url_text: {
// For OSC8 links, get the URI directly from hyperlink data
const uri = self.osc8URI(link_info[1].start()) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
break :url_text try self.alloc.dupeZ(u8, uri);
},
// Get the URL text from selection
const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{
.sel = link_info[1],
.trim = self.config.clipboard_trim_trailing_spaces,
})) catch |err| {
log.err("error reading url string err={}", .{err});
return false;
};
defer self.alloc.free(url_text);
self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
log.err("error copying url to clipboard err={}", .{err});
return false;
return true;
};
return true;
@@ -4762,13 +4671,10 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.close_tab => |v| return try self.rt_app.performAction(
.close_tab => return try self.rt_app.performAction(
.{ .surface = self },
.close_tab,
switch (v) {
.this => .this,
.other => .other,
},
{},
),
inline .previous_tab,

View File

@@ -17,6 +17,7 @@ const structs = @import("apprt/structs.zig");
pub const action = @import("apprt/action.zig");
pub const ipc = @import("apprt/ipc.zig");
pub const gtk = @import("apprt/gtk.zig");
pub const gtk_ng = @import("apprt/gtk-ng.zig");
pub const none = @import("apprt/none.zig");
pub const browser = @import("apprt/browser.zig");
pub const embedded = @import("apprt/embedded.zig");
@@ -43,6 +44,7 @@ pub const runtime = switch (build_config.artifact) {
.exe => switch (build_config.app_runtime) {
.none => none,
.gtk => gtk,
.@"gtk-ng" => gtk_ng,
},
.lib => embedded,
.wasm_module => browser,
@@ -60,18 +62,21 @@ pub const Runtime = enum {
/// GTK4. Rich windowed application. This uses a full GObject-based
/// approach to building the application.
@"gtk-ng",
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
/// WARNING: Deprecated. This will be removed very soon. All bug fixes
/// and features should go into the gtk-ng backend.
gtk,
pub fn default(target: std.Target) Runtime {
return switch (target.os.tag) {
// The Linux and FreeBSD default is GTK because it is a full
// featured application.
.linux, .freebsd => .gtk,
// Otherwise, we do NONE so we don't create an exe and we create
// libghostty. On macOS, Xcode is used to build the app that links
// to libghostty.
else => .none,
};
// The Linux default is GTK because it is a full featured application.
if (target.os.tag == .linux) return .@"gtk-ng";
// Otherwise, we do NONE so we don't create an exe and we
// create libghostty. On macOS, Xcode is used to build the app
// that links to libghostty.
return .none;
}
};

View File

@@ -83,9 +83,8 @@ pub const Action = union(Key) {
/// the tab should be opened in a new window.
new_tab,
/// Closes the tab belonging to the currently focused split, or all other
/// tabs, depending on the mode.
close_tab: CloseTabMode,
/// Closes the tab belonging to the currently focused split.
close_tab,
/// Create a new split. The value determines the location of the split
/// relative to the target.
@@ -542,7 +541,7 @@ pub const InitialSize = extern struct {
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk => @import("gobject").ext.defineBoxed(
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
InitialSize,
.{ .name = "GhosttyApprtInitialSize" },
),
@@ -702,11 +701,3 @@ pub const OpenUrl = struct {
};
}
};
/// sync with ghostty_action_close_tab_mode_e in ghostty.h
pub const CloseTabMode = enum(c_int) {
/// Close the current tab.
this,
/// Close all other tabs.
other,
};

View File

@@ -447,9 +447,6 @@ pub const Surface = struct {
/// Input to send to the command after it is started.
initial_input: ?[*:0]const u8 = null,
/// Wait after the command exits
wait_after_command: bool = false,
};
pub fn init(self: *Surface, app: *App, opts: Options) !void {
@@ -543,11 +540,6 @@ pub const Surface = struct {
);
}
// Wait after command
if (opts.wait_after_command) {
config.@"wait-after-command" = true;
}
// Initialize our surface right away. We're given a view that is
// ready to use.
try self.core_surface.init(
@@ -909,7 +901,10 @@ pub const Surface = struct {
// our translation settings for Ghostty. If we aren't from
// the desktop then we didn't set our LANGUAGE var so we
// don't need to remove it.
if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
switch (self.app.config.@"launched-from".?) {
.desktop => env.remove("LANGUAGE"),
.dbus, .systemd, .cli => {},
}
}
return env;
@@ -1819,18 +1814,10 @@ pub const CAPI = struct {
surface.mousePressureCallback(stage, pressure);
}
export fn ghostty_surface_ime_point(
surface: *Surface,
x: *f64,
y: *f64,
width: *f64,
height: *f64,
) void {
export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void {
const pos = surface.core_surface.imePoint();
x.* = pos.x;
y.* = pos.y;
width.* = pos.width;
height.* = pos.height;
}
/// Request that the surface become closed. This will go through the

15
src/apprt/gtk-ng.zig Normal file
View File

@@ -0,0 +1,15 @@
const internal_os = @import("../os/main.zig");
// The required comptime API for any apprt.
pub const App = @import("gtk-ng/App.zig");
pub const Surface = @import("gtk-ng/Surface.zig");
pub const resourcesDir = internal_os.resourcesDir;
// The exported API, custom for the apprt.
pub const class = @import("gtk-ng/class.zig");
pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef;
test {
@import("std").testing.refAllDecls(@This());
_ = @import("gtk-ng/ext.zig");
}

104
src/apprt/gtk-ng/App.zig Normal file
View File

@@ -0,0 +1,104 @@
/// This is the main entrypoint to the apprt for Ghostty. Ghostty will
/// initialize this in main to start the application..
const App = @This();
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const internal_os = @import("../../os/main.zig");
const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const Application = @import("class/application.zig").Application;
const Surface = @import("Surface.zig");
const gtk_version = @import("gtk_version.zig");
const adw_version = @import("adw_version.zig");
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
const log = std.log.scoped(.gtk);
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
/// message so that we can call `drawFrame` ourselves from the app thread,
/// because GTK's `GLArea` does not support drawing from a different thread.
pub const must_draw_from_app_thread = true;
/// GTK application ID
pub const application_id = switch (builtin.mode) {
.Debug, .ReleaseSafe => "com.mitchellh.ghostty-debug",
.ReleaseFast, .ReleaseSmall => "com.mitchellh.ghostty",
};
/// GTK object path
pub const object_path = switch (builtin.mode) {
.Debug, .ReleaseSafe => "/com/mitchellh/ghostty_debug",
.ReleaseFast, .ReleaseSmall => "/com/mitchellh/ghostty",
};
/// The GObject Application instance
app: *Application,
pub fn init(
self: *App,
core_app: *CoreApp,
// Required by the apprt interface but we don't use it.
opts: struct {},
) !void {
_ = opts;
const app: *Application = try .new(self, core_app);
errdefer app.unref();
self.* = .{ .app = app };
return;
}
pub fn run(self: *App) !void {
try self.app.run();
}
pub fn terminate(self: *App) void {
// We force deinitialize the app. We don't unref because other things
// tend to have a reference at this point, so this just forces the
// disposal now.
self.app.deinit();
}
/// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: *App) void {
self.app.wakeup();
}
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !bool {
return try self.app.performAction(target, action, value);
}
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
/// able to be performed, `false` otherwise.
///
/// Note that this is a static function. Since this is called from a CLI app (or
/// some other process that is not Ghostty) there is no full-featured apprt App
/// to use.
pub fn performIpc(
alloc: Allocator,
target: apprt.ipc.Target,
comptime action: apprt.ipc.Action.Key,
value: apprt.ipc.Action.Value(action),
) !bool {
switch (action) {
.new_window => return try ipcNewWindow(alloc, target, value),
}
}
/// Redraw the inspector for the given surface.
pub fn redrawInspector(_: *App, surface: *Surface) void {
surface.redrawInspector();
}

View File

@@ -0,0 +1,103 @@
const Self = @This();
const std = @import("std");
const apprt = @import("../../apprt.zig");
const CoreSurface = @import("../../Surface.zig");
const ApprtApp = @import("App.zig");
const Application = @import("class/application.zig").Application;
const Surface = @import("class/surface.zig").Surface;
/// The GObject Surface
surface: *Surface,
pub fn deinit(self: *Self) void {
_ = self;
}
/// Returns the GObject surface for this apprt surface. This is a function
/// so we can add some extra logic if we ever have to here.
pub fn gobj(self: *Self) *Surface {
return self.surface;
}
pub fn core(self: *Self) *CoreSurface {
// This asserts the non-optional because libghostty should only
// be calling this for initialized surfaces.
return self.surface.core().?;
}
pub fn rtApp(self: *Self) *ApprtApp {
_ = self;
return Application.default().rt();
}
pub fn close(self: *Self, process_active: bool) void {
_ = process_active;
self.surface.close();
}
pub fn cgroup(self: *Self) ?[]const u8 {
return self.surface.cgroupPath();
}
pub fn getTitle(self: *Self) ?[:0]const u8 {
return self.surface.getTitle();
}
pub fn getContentScale(self: *const Self) !apprt.ContentScale {
return self.surface.getContentScale();
}
pub fn getSize(self: *const Self) !apprt.SurfaceSize {
return self.surface.getSize();
}
pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
return self.surface.getCursorPos();
}
pub fn supportsClipboard(
self: *const Self,
clipboard_type: apprt.Clipboard,
) bool {
_ = self;
return switch (clipboard_type) {
.standard,
.selection,
.primary,
=> true,
};
}
pub fn clipboardRequest(
self: *Self,
clipboard_type: apprt.Clipboard,
state: apprt.ClipboardRequest,
) !void {
try self.surface.clipboardRequest(
clipboard_type,
state,
);
}
pub fn setClipboardString(
self: *Self,
val: [:0]const u8,
clipboard_type: apprt.Clipboard,
confirm: bool,
) !void {
self.surface.setClipboardString(
val,
clipboard_type,
confirm,
);
}
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
return try self.surface.defaultTermioEnv();
}
/// Redraw the inspector for our surface.
pub fn redrawInspector(self: *Self) void {
self.surface.redrawInspector();
}

View File

@@ -0,0 +1,122 @@
const std = @import("std");
// Until the gobject bindings are built at the same time we are building
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
// macros match the version of `libadwaita` that we are building/linking
// against.
const c = @cImport({
@cInclude("adwaita.h");
});
const adw = @import("adw");
const log = std.log.scoped(.gtk);
pub const comptime_version: std.SemanticVersion = .{
.major = c.ADW_MAJOR_VERSION,
.minor = c.ADW_MINOR_VERSION,
.patch = c.ADW_MICRO_VERSION,
};
pub fn getRuntimeVersion() std.SemanticVersion {
return .{
.major = adw.getMajorVersion(),
.minor = adw.getMinorVersion(),
.patch = adw.getMicroVersion(),
};
}
pub fn logVersion() void {
log.info("libadwaita version build={} runtime={}", .{
comptime_version,
getRuntimeVersion(),
});
}
/// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to not build with
/// libadwaita.
///
/// This can be run in both a comptime and runtime context. If it is run in a
/// comptime context, it will only check the version in the headers. If it is
/// run in a runtime context, it will check the actual version of the library we
/// are linked against. So generally you probably want to do both checks!
///
/// This is inlined so that the comptime checks will disable the runtime checks
/// if the comptime checks fail.
pub inline fn atLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// If our header has lower versions than the given version, we can return
// false immediately. This prevents us from compiling against unknown
// symbols and makes runtime checks very slightly faster.
if (comptime comptime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) == .lt) return false;
// If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true;
return runtimeAtLeast(major, minor, micro);
}
/// Verifies that the libadwaita version at runtime is at least the given version.
///
/// This function should be used in cases where the only the runtime behavior
/// is affected by the version check. For checks which would affect code
/// generation, use `atLeast`.
pub inline fn runtimeAtLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
) bool {
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
// because the function gets the actual runtime version.
const runtime_version = getRuntimeVersion();
return runtime_version.order(.{
.major = major,
.minor = minor,
.patch = micro,
}) != .lt;
}
test "versionAtLeast" {
const testing = std.testing;
const funs = &.{ atLeast, runtimeAtLeast };
inline for (funs) |fun| {
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
}
}
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
pub inline fn supportsDialogs() bool {
return atLeast(1, 5, 0);
}
pub inline fn supportsTabOverview() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsSwitchRow() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsToolbarView() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsBanner() bool {
return atLeast(1, 3, 0);
}

View File

@@ -14,10 +14,10 @@ pub const app_id = "com.mitchellh.ghostty";
/// The path to the Blueprint files. The folder structure is expected to be
/// `{version}/{name}.blp` where `version` is the major and minor
/// minimum adwaita version.
pub const ui_path = "src/apprt/gtk/ui";
pub const ui_path = "src/apprt/gtk-ng/ui";
/// The path to the CSS files.
pub const css_path = "src/apprt/gtk/css";
pub const css_path = "src/apprt/gtk-ng/css";
/// The possible icon sizes we'll embed into the gresource file.
/// If any size doesn't exist then it will be an error. We could

213
src/apprt/gtk-ng/cgroup.zig Normal file
View File

@@ -0,0 +1,213 @@
/// Contains all the logic for putting the Ghostty process and
/// each individual surface into its own cgroup.
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const App = @import("App.zig");
const internal_os = @import("../../os/main.zig");
const log = std.log.scoped(.gtk_systemd_cgroup);
pub const Options = struct {
memory_high: ?u64 = null,
pids_max: ?u64 = null,
};
/// Initialize the cgroup for the app. This will create our
/// transient scope, initialize the cgroups we use for the app,
/// configure them, and return the cgroup path for the app.
///
/// Returns the path of the current cgroup for the app, which is
/// allocated with the given allocator.
pub fn init(
alloc: Allocator,
dbus: *gio.DBusConnection,
opts: Options,
) ![]const u8 {
const pid = std.os.linux.getpid();
// Get our initial cgroup. We need this so we can compare
// and detect when we've switched to our transient group.
const original = try internal_os.cgroup.current(
alloc,
pid,
) orelse "";
defer alloc.free(original);
// Create our transient scope. If this succeeds then the unit
// was created, but we may not have moved into it yet, so we need
// to do a dumb busy loop to wait for the move to complete.
try createScope(dbus, pid);
const transient = transient: while (true) {
const current = try internal_os.cgroup.current(
alloc,
pid,
) orelse "";
if (!std.mem.eql(u8, original, current)) break :transient current;
alloc.free(current);
std.time.sleep(25 * std.time.ns_per_ms);
};
errdefer alloc.free(transient);
log.info("transient scope created cgroup={s}", .{transient});
// Create the app cgroup and put ourselves in it. This is
// required because controllers can't be configured while a
// process is in a cgroup.
try internal_os.cgroup.create(transient, "app", pid);
// Create a cgroup that will contain all our surfaces. We will
// enable the controllers and configure resource limits for surfaces
// only on this cgroup so that it doesn't affect our main app.
try internal_os.cgroup.create(transient, "surfaces", null);
const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient});
defer alloc.free(surfaces);
// Enable all of our cgroup controllers. If these fail then
// we just log. We can't reasonably undo what we've done above
// so we log the warning and still return the transient group.
// I don't know a scenario where this fails yet.
try enableControllers(alloc, transient);
try enableControllers(alloc, surfaces);
// Configure the "high" memory limit. This limit is used instead
// of "max" because it's a soft limit that can be exceeded and
// can be monitored by things like systemd-oomd to kill if needed,
// versus an instant hard kill.
if (opts.memory_high) |limit| {
try internal_os.cgroup.configureLimit(surfaces, .{
.memory_high = limit,
});
}
// Configure the "max" pids limit. This is a hard limit and cannot be
// exceeded.
if (opts.pids_max) |limit| {
try internal_os.cgroup.configureLimit(surfaces, .{
.pids_max = limit,
});
}
return transient;
}
/// Enable all the cgroup controllers for the given cgroup.
fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
const raw = try internal_os.cgroup.controllers(alloc, cgroup);
defer alloc.free(raw);
// Build our string builder for enabling all controllers
var builder = std.ArrayList(u8).init(alloc);
defer builder.deinit();
// Controllers are space-separated
var it = std.mem.splitScalar(u8, raw, ' ');
while (it.next()) |controller| {
try builder.append('+');
try builder.appendSlice(controller);
if (it.rest().len > 0) try builder.append(' ');
}
// Enable them all
try internal_os.cgroup.configureControllers(
cgroup,
builder.items,
);
}
/// Create a transient systemd scope unit for the current process and
/// move our process into it.
fn createScope(
dbus: *gio.DBusConnection,
pid_: std.os.linux.pid_t,
) !void {
const pid: u32 = @intCast(pid_);
// The unit name needs to be unique. We use the pid for this.
var name_buf: [256]u8 = undefined;
const name = std.fmt.bufPrintZ(
&name_buf,
"app-ghostty-transient-{}.scope",
.{pid},
) catch unreachable;
const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
defer glib.free(builder_type);
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(builder_type);
builder.add("s", name.ptr);
builder.add("s", "fail");
{
// Properties
const properties_type = glib.VariantType.new("a(sv)");
defer glib.free(properties_type);
builder.open(properties_type);
defer builder.close();
// https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
const pressure_value = glib.Variant.newString("kill");
builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
// Delegate
const delegate_value = glib.Variant.newBoolean(1);
builder.add("(sv)", "Delegate", delegate_value);
// Pid to move into the unit
const pids_value_type = glib.VariantType.new("u");
defer glib.free(pids_value_type);
const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32));
builder.add("(sv)", "PIDs", pids_value);
}
{
// Aux
const aux_type = glib.VariantType.new("a(sa(sv))");
defer glib.free(aux_type);
builder.open(aux_type);
defer builder.close();
}
var err: ?*glib.Error = null;
defer if (err) |e| e.free();
const reply_type = glib.VariantType.new("(o)");
defer glib.free(reply_type);
const value = builder.end();
const reply = dbus.callSync(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"StartTransientUnit",
value,
reply_type,
.{},
-1,
null,
&err,
) orelse {
if (err) |e| log.err(
"creating transient cgroup scope failed code={} err={s}",
.{
e.f_code,
if (e.f_message) |msg| msg else "(no message)",
},
);
return error.DbusCallFailed;
};
defer reply.unref();
}

View File

@@ -1,7 +1,6 @@
//! This files contains all the GObject classes for the GTK apprt
//! along with helpers to work with them.
const std = @import("std");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
@@ -54,111 +53,6 @@ pub fn Common(
}
}).private else {};
/// Get the class for the object.
///
/// This _seems_ ugly and unsafe but this is how GObject
/// works under the hood. From the [GObject Type System
/// Concepts](https://docs.gtk.org/gobject/concepts.html) documentation:
///
/// Every object must define two structures: its class structure
/// and its instance structure. All class structures must contain
/// as first member a GTypeClass structure. All instance structures
/// must contain as first member a GTypeInstance structure.
///
/// These constraints allow the type system to make sure that
/// every object instance (identified by a pointer to the objects
/// instance structure) contains in its first bytes a pointer to the
/// objects class structure.
///
/// The C standard mandates that the first field of a C structure is
/// stored starting in the first byte of the buffer used to hold the
/// structures fields in memory. This means that the first field of
/// an instance of an object B is As first field which in turn is
/// GTypeInstances first field which in turn is g_class, a pointer
/// to Bs class structure.
///
/// This means that to access the class structure for an object you cast it
/// to `*gobject.TypeInstance` and then access the `f_g_class` field.
///
/// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L555-571
/// https://gitlab.gnome.org/GNOME/glib/-/blob/2c08654b62d52a31c4e4d13d7d85e12b989e72be/gobject/gtype.h#L2673
///
pub fn getClass(self: *Self) ?*Self.Class {
const type_instance: *gobject.TypeInstance = @ptrCast(self);
return @ptrCast(type_instance.f_g_class orelse return null);
}
/// Define a virtual method. The `Self.Class` type must have a field
/// named `name` which is a function pointer in the following form:
///
/// ?*const fn (*Self) callconv(.c) void
///
/// The virtual method may take additional parameters and specify
/// a non-void return type. The parameters and return type must be
/// valid for the C calling convention.
pub fn defineVirtualMethod(
comptime name: [:0]const u8,
) type {
return struct {
pub fn call(
class: anytype,
object: *ClassInstance(@TypeOf(class)),
params: anytype,
) (fn_info.return_type orelse void) {
const func = @field(
gobject.ext.as(Self.Class, class),
name,
).?;
@call(.auto, func, .{
gobject.ext.as(Self, object),
} ++ params);
}
pub fn implement(
class: anytype,
implementation: *const ImplementFunc(@TypeOf(class)),
) void {
@field(gobject.ext.as(
Self.Class,
class,
), name) = @ptrCast(implementation);
}
/// The type info of the virtual method.
const fn_info = fn_info: {
// This is broken down like this so its slightly more
// readable. We expect a field named "name" on the Class
// with the rough type of `?*const fn` and we need the
// function info.
const Field = @FieldType(Self.Class, name);
const opt = @typeInfo(Field).optional;
const ptr = @typeInfo(opt.child).pointer;
break :fn_info @typeInfo(ptr.child).@"fn";
};
/// The instance type for a class.
fn ClassInstance(comptime T: type) type {
return @typeInfo(T).pointer.child.Instance;
}
/// The function type for implementations. This is the same type
/// as the virtual method but the self parameter points to the
/// target instead of the original class.
fn ImplementFunc(comptime T: type) type {
var params: [fn_info.params.len]std.builtin.Type.Fn.Param = undefined;
@memcpy(&params, fn_info.params);
params[0].type = *ClassInstance(T);
return @Type(.{ .@"fn" = .{
.calling_convention = fn_info.calling_convention,
.is_generic = fn_info.is_generic,
.is_var_args = fn_info.is_var_args,
.return_type = fn_info.return_type,
.params = &params,
} });
}
};
}
/// A helper that creates a property that reads and writes a
/// private field with only shallow copies. This is good for primitives
/// such as bools, numbers, etc.

View File

@@ -116,11 +116,6 @@ pub const Application = extern struct {
/// and initialization was successful.
transient_cgroup_base: ?[]const u8 = null,
/// This is set to true so long as we request a window exactly
/// once. This prevents quitting the app before we've shown one
/// window.
requested_window: bool = false,
/// This is set to false internally when the event loop
/// should exit and the application should quit. This must
/// only be set by the main loop thread.
@@ -223,8 +218,10 @@ pub const Application = extern struct {
const single_instance = switch (config.@"gtk-single-instance") {
.true => true,
.false => false,
// This should have been resolved to true/false during config loading.
.detect => unreachable,
.desktop => switch (config.@"launched-from".?) {
.desktop, .systemd, .dbus => true,
.cli => false,
},
};
// Setup the flags for our application.
@@ -416,7 +413,9 @@ pub const Application = extern struct {
// This just calls the `activate` signal but its part of the normal startup
// routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening
// a window).
// a window). An initial window will not be immediately created if we were
// launched by D-Bus activation or systemd. D-Bus activation will send it's
// own `activate` or `new-window` signal later.
//
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
const priv = self.private();
@@ -424,11 +423,15 @@ pub const Application = extern struct {
// We need to scope any config access because once we run our
// event loop, this can change out from underneath us.
const config = priv.config.get();
if (config.@"initial-window") self.as(gio.Application).activate();
if (config.@"initial-window") switch (config.@"launched-from".?) {
.desktop, .cli => self.as(gio.Application).activate(),
.dbus, .systemd => {},
};
}
// If we are NOT the primary instance, then we never want to run.
// This means that another instance of the GTK app is running.
// This means that another instance of the GTK app is running and
// our "activate" call above will open a window.
if (self.as(gio.Application).getIsRemote() != 0) {
log.debug(
"application is remote, exiting run loop after activation",
@@ -458,24 +461,11 @@ pub const Application = extern struct {
// If the quit timer has expired, quit.
if (priv.quit_timer == .expired) break :q true;
// If we have no windows attached to our app, also quit.
if (priv.requested_window and @as(
?*glib.List,
self.as(gtk.Application).getWindows(),
) == null) break :q true;
// No quit conditions met
// There's no quit timer running, or it hasn't expired, don't quit.
break :q false;
};
if (must_quit) {
// All must quit scenarios do not need confirmation.
// Furthermore, must quit scenarios may result in a situation
// where its unsafe to even access the app/surface memory
// since its in the process of being freed. We must simply
// begin our exit immediately.
self.quitNow();
}
if (must_quit) self.quit();
}
}
@@ -498,15 +488,7 @@ pub const Application = extern struct {
const parent: ?*gtk.Widget = parent: {
const list = gtk.Window.listToplevels();
defer list.free();
const focused = @as(?*glib.List, list.findCustom(
null,
findActiveWindow,
)) orelse {
// If we have an active surface then we should have
// a window available but in the rare case we don't we
// should exit so we don't crash.
break :parent null;
};
const focused = list.findCustom(null, findActiveWindow);
break :parent @ptrCast(@alignCast(focused.f_data));
};
@@ -560,7 +542,7 @@ pub const Application = extern struct {
value: apprt.Action.Value(action),
) !bool {
switch (action) {
.close_tab => return Action.closeTab(target, value),
.close_tab => return Action.closeTab(target),
.close_window => return Action.closeWindow(target),
.config_change => try Action.configChange(
@@ -731,24 +713,27 @@ pub const Application = extern struct {
}
}
fn loadRuntimeCss(self: *Self) Allocator.Error!void {
fn loadRuntimeCss(
self: *Self,
) Allocator.Error!void {
const alloc = self.allocator();
const config = self.private().config.get();
var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048);
var buf: std.ArrayListUnmanaged(u8) = .empty;
defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config = self.private().config.get();
const window_theme = config.@"window-theme";
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.print(
\\widget.unfocused-split {{
\\ opacity: {d:.2};
\\ background-color: rgb({d},{d},{d});
\\}}
\\
, .{
1.0 - config.@"unfocused-split-opacity",
unfocused_fill.r,
@@ -762,7 +747,6 @@ pub const Application = extern struct {
\\ color: rgb({[r]d},{[g]d},{[b]d});
\\ background: rgb({[r]d},{[g]d},{[b]d});
\\}}
\\
, .{
.r = color.r,
.g = color.g,
@@ -775,129 +759,9 @@ pub const Application = extern struct {
\\.window headerbar {{
\\ font-family: "{[font_family]s}";
\\}}
\\
, .{ .font_family = font_family });
}
try loadRuntimeCss414(config, &writer);
try loadRuntimeCss416(config, &writer);
// ensure that we have a sentinel
try writer.writeByte(0);
const data = buf.items[0 .. buf.items.len - 1 :0];
log.debug("runtime CSS is {d} bytes", .{data.len + 1});
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(
self.private().css_provider,
data,
);
}
/// Load runtime CSS for older than GTK 4.16
fn loadRuntimeCss414(
config: *const CoreConfig,
writer: *const std.ArrayListUnmanaged(u8).Writer,
) Allocator.Error!void {
if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
const window_theme = config.@"window-theme";
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
switch (window_theme) {
.ghostty => try writer.print(
\\windowhandle {{
\\ background-color: rgb({d},{d},{d});
\\ color: rgb({d},{d},{d});
\\}}
\\windowhandle:backdrop {{
\\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha);
\\}}
\\
, .{
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
headerbar_foreground.r,
headerbar_foreground.g,
headerbar_foreground.b,
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
}),
else => {},
}
}
/// Load runtime for GTK 4.16 and newer
fn loadRuntimeCss416(
config: *const CoreConfig,
writer: *const std.ArrayListUnmanaged(u8).Writer,
) Allocator.Error!void {
if (gtk_version.runtimeUntil(4, 16, 0)) return;
const window_theme = config.@"window-theme";
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.writeAll(
\\/*
\\ * Child Exited Overlay
\\ */
\\
\\.child-exited.normal revealer widget {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--success-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\.child-exited.abnormal revealer widget {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--error-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\/*
\\ * Surface
\\ */
\\
\\.surface progressbar.error trough progress {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--error-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\.surface .bell-overlay {
\\ border-color: color-mix(
\\ in srgb,
\\ var(--accent-color),
\\ transparent 50%
\\ );
\\}
\\
\\/*
\\ * Splits
\\ */
\\
\\.window .split paned > separator {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--window-bg-color),
\\ transparent 0%
\\ );
\\}
\\
);
switch (window_theme) {
.ghostty => try writer.print(
\\:root {{
@@ -930,6 +794,15 @@ pub const Application = extern struct {
}),
else => {},
}
const data = try alloc.dupeZ(u8, buf.items);
defer alloc.free(data);
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(
self.private().css_provider,
data,
);
}
fn loadCustomCss(self: *Self) !void {
@@ -999,8 +872,7 @@ pub const Application = extern struct {
self.syncActionAccelerator("win.close", .{ .close_window = {} });
self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this });
self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this });
self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
@@ -1704,16 +1576,12 @@ pub const Application = extern struct {
/// All apprt action handlers
const Action = struct {
pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool {
pub fn closeTab(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction(
"tab.close",
glib.ext.VariantType.stringFor([:0]const u8),
@as([*:0]const u8, @tagName(value)),
) != 0;
return surface.as(gtk.Widget).activateAction("tab.close", null) != 0;
},
}
}
@@ -1985,13 +1853,6 @@ const Action = struct {
self: *Application,
parent: ?*CoreSurface,
) !void {
// Note that we've requested a window at least once. This is used
// to trigger quit on no windows. Note I'm not sure if this is REALLY
// necessary, but I don't want to risk a bug where on a slow machine
// or something we quit immediately after starting up because there
// was a delay in the event loop before we created a Window.
self.private().requested_window = true;
const win = Window.new(self);
initAndShowWindow(self, win, parent);
}
@@ -2355,7 +2216,7 @@ const Action = struct {
Window,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a window, ignoring toggle_window_decorations", .{});
log.warn("surface is not in a window, ignoring new_tab", .{});
return false;
};

View File

@@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Dialog = @import("dialog.zig").Dialog;
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
pub const CloseConfirmationDialog = extern struct {
const Self = @This();

View File

@@ -35,18 +35,36 @@ pub const ImguiWidget = extern struct {
pub const properties = struct {};
pub const signals = struct {};
pub const signals = struct {
/// Emitted when the child widget should render. During the callback,
/// the Imgui context is valid.
pub const render = struct {
pub const name = "render";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
pub const virtual_methods = struct {
/// This virtual method will be called to allow the Dear ImGui
/// application to do one-time setup of the context. The correct context
/// will be current when the virtual method is called.
pub const setup = C.defineVirtualMethod("setup");
/// This virtual method will be called at each frame to allow the Dear
/// ImGui application to draw the application. The correct context will
/// be current when the virtual method is called.
pub const render = C.defineVirtualMethod("render");
/// Emitted when first realized to allow the embedded ImGui application
/// to initialize itself. When this is called, the ImGui context
/// is properly set.
///
/// This might be called multiple times, but each time it is
/// called a new Imgui context will be created.
pub const setup = struct {
pub const name = "setup";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
@@ -95,25 +113,6 @@ pub const ImguiWidget = extern struct {
priv.gl_area.queueRender();
}
//---------------------------------------------------------------
// Public wrappers for virtual methods
/// This virtual method will be called to allow the Dear ImGui application
/// to do one-time setup of the context. The correct context will be current
/// when the virtual method is called.
pub fn setup(self: *Self) callconv(.c) void {
const class = self.getClass() orelse return;
virtual_methods.setup.call(class, self, .{});
}
/// This virtual method will be called at each frame to allow the Dear ImGui
/// application to draw the application. The correct context will be current
/// when the virtual method is called.
pub fn render(self: *Self) callconv(.c) void {
const class = self.getClass() orelse return;
virtual_methods.render.call(class, self, .{});
}
//---------------------------------------------------------------
// Private Methods
@@ -233,8 +232,13 @@ pub const ImguiWidget = extern struct {
// initialize the ImgUI OpenGL backend for our context.
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
// Call the virtual method to setup the UI.
self.setup();
// Setup our app
signals.setup.impl.emit(
self,
null,
.{},
null,
);
}
/// Handle a request to unrealize the GLArea
@@ -275,8 +279,13 @@ pub const ImguiWidget = extern struct {
self.newFrame();
cimgui.c.igNewFrame();
// Call the virtual method to draw the UI.
self.render();
// Use the callback to draw the UI.
signals.render.impl.emit(
self,
null,
.{},
null,
);
// Render
cimgui.c.igRender();
@@ -413,34 +422,15 @@ pub const ImguiWidget = extern struct {
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
}
//---------------------------------------------------------------
// Default virtual method handlers
/// Default setup function. Does nothing but log a warning.
fn defaultSetup(_: *Self) callconv(.c) void {
log.warn("default Dear ImGui setup called, this is a bug.", .{});
}
/// Default render function. Does nothing but log a warning.
fn defaultRender(_: *Self) callconv(.c) void {
log.warn("default Dear ImGui render called, this is a bug.", .{});
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
pub const getClass = C.getClass;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
/// Function pointers for virtual methods.
setup: ?*const fn (*Self) callconv(.c) void,
render: ?*const fn (*Self) callconv(.c) void,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
@@ -454,10 +444,6 @@ pub const ImguiWidget = extern struct {
}),
);
// Initialize our virtual methods with default functions.
class.setup = defaultSetup;
class.render = defaultRender;
// Bindings
class.bindTemplateChildPrivate("gl_area", .{});
class.bindTemplateChildPrivate("im_context", .{});
@@ -478,6 +464,8 @@ pub const ImguiWidget = extern struct {
class.bindTemplateCallback("im_commit", &imCommit);
// Signals
signals.render.impl.register(.{});
signals.setup.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);

View File

@@ -17,7 +17,7 @@ const log = std.log.scoped(.gtk_ghostty_inspector_widget);
pub const InspectorWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = ImguiWidget;
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWidget",
.instanceInit = &init,
@@ -50,6 +50,9 @@ pub const InspectorWidget = extern struct {
/// We attach a weak notify to the object.
surface: ?*Surface = null,
/// The embedded Dear ImGui widget.
imgui_widget: *ImguiWidget,
pub var offset: c_int = 0;
};
@@ -75,30 +78,13 @@ pub const InspectorWidget = extern struct {
);
}
/// Called to do initial setup of the UI.
fn imguiSetup(
_: *Self,
) callconv(.c) void {
Inspector.setup();
}
/// Called for every frame to draw the UI.
fn imguiRender(
self: *Self,
) callconv(.c) void {
const priv = self.private();
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
//---------------------------------------------------------------
// Public methods
/// Queue a render of the Dear ImGui widget.
pub fn queueRender(self: *Self) void {
self.as(ImguiWidget).queueRender();
const priv = self.private();
priv.imgui_widget.queueRender();
}
//---------------------------------------------------------------
@@ -203,6 +189,24 @@ pub const InspectorWidget = extern struct {
// for completeness sake we should clean this up.
}
fn imguiRender(
_: *ImguiWidget,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
fn imguiSetup(
_: *ImguiWidget,
_: *Self,
) callconv(.c) void {
Inspector.setup();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@@ -226,6 +230,13 @@ pub const InspectorWidget = extern struct {
}),
);
// Bindings
class.bindTemplateChildPrivate("imgui_widget", .{});
// Template callbacks
class.bindTemplateCallback("imgui_render", &imguiRender);
class.bindTemplateCallback("imgui_setup", &imguiSetup);
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
@@ -234,8 +245,6 @@ pub const InspectorWidget = extern struct {
// Signals
// Virtual methods
ImguiWidget.virtual_methods.setup.implement(class, imguiSetup);
ImguiWidget.virtual_methods.render.implement(class, imguiRender);
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}

View File

@@ -105,24 +105,6 @@ pub const Surface = extern struct {
);
};
pub const @"error" = struct {
pub const name = "error";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"error",
),
},
);
};
pub const @"font-size-request" = struct {
pub const name = "font-size-request";
const impl = gobject.ext.defineProperty(
@@ -490,15 +472,6 @@ pub const Surface = extern struct {
// false by a parent widget.
bell_ringing: bool = false,
/// True if this surface is in an error state. This is currently
/// a simple boolean with no additional information on WHAT the
/// error state is, because we don't yet need it or use it. For now,
/// if this is true, then it means the terminal is non-functional.
@"error": bool = false,
/// The source that handles setting our child property.
idle_rechild: ?c_uint = null,
/// A weak reference to an inspector window.
inspector: ?*InspectorWindow = null,
@@ -507,8 +480,6 @@ pub const Surface = extern struct {
context_menu: *gtk.PopoverMenu,
drop_target: *gtk.DropTarget,
progress_bar_overlay: *gtk.ProgressBar,
error_page: *adw.StatusPage,
terminal_page: *gtk.Overlay,
pub var offset: c_int = 0;
};
@@ -583,21 +554,13 @@ pub const Surface = extern struct {
config_: ?*Config,
bell_ringing_: c_int,
) callconv(.c) c_int {
const bell_ringing = bell_ringing_ != 0;
// If the bell isn't ringing exit early because when the surface is
// first created there's a race between this code being run and the
// config being set on the surface. That way we don't overwhelm people
// with the warning that we issue if the config isn't set and overwhelm
// ourselves with large numbers of bug reports.
if (!bell_ringing) return @intFromBool(false);
const config = if (config_) |v| v.get() else {
log.warn("config unavailable for computing whether border should be shown, likely bug", .{});
log.warn("config unavailable for computing whether border should be shown , likely bug", .{});
return @intFromBool(false);
};
return @intFromBool(config.@"bell-features".border);
const bell_ringing = bell_ringing_ != 0;
return @intFromBool(config.@"bell-features".border and bell_ringing);
}
pub fn toggleFullscreen(self: *Self) void {
@@ -867,7 +830,7 @@ pub const Surface = extern struct {
// such as single quote on a US international keyboard layout.
if (priv.im_composing) return true;
// If we were composing and now we're not, it means that we committed
// If we were composing and now we're not it means that we committed
// the text. We also don't want to encode a key event for this.
// Example: enable Japanese input method, press "konn" and then
// press enter. The final enter should not be encoded and "konn"
@@ -907,24 +870,9 @@ pub const Surface = extern struct {
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: {
const w3c_key: input.Key = w3c: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// If the key should be remappable, then consult the pre-remapped
// XKB keyval/keysym to get the (possibly) remapped key.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (w3c_key.shouldBeRemappable()) {
if (gtk_key.keyFromKeyval(keyval)) |remapped|
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
};
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :keycode entry.key;
} else .unidentified;
// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(
@@ -1364,19 +1312,6 @@ pub const Surface = extern struct {
priv.progress_bar_timer = null;
}
if (priv.idle_rechild) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove idle source", .{});
}
priv.idle_rechild = null;
}
// This works around a GTK double-free bug where if you bind
// to a top-level template child, it frees twice if the widget is
// also the root child of the template. By unsetting the child here,
// we avoid the double-free.
self.as(adw.Bin).setChild(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
@@ -1582,12 +1517,6 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
}
pub fn setError(self: *Self, v: bool) void {
const priv = self.private();
priv.@"error" = v;
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
}
fn propConfig(
self: *Self,
_: *gobject.ParamSpec,
@@ -1640,46 +1569,6 @@ pub const Surface = extern struct {
}
}
fn propError(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (priv.@"error") {
// Ensure we have an opaque background. The window will NOT set
// this if we have transparency set and we need an opaque
// background for the error message to be readable.
self.as(gtk.Widget).addCssClass("background");
} else {
// Regardless of transparency setting, we remove the background
// CSS class from this widget. Parent widgets will set it
// appropriately (see window.zig for example).
self.as(gtk.Widget).removeCssClass("background");
}
// We need to set our child property on an idle tick, because the
// error property can be triggered by signals that are in the middle
// of widget mapping and changing our child during that time
// results in a hard gtk crash.
if (priv.idle_rechild == null) priv.idle_rechild = glib.idleAdd(
onIdleRechild,
self,
);
}
fn onIdleRechild(ud: ?*anyopaque) callconv(.c) c_int {
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
const priv = self.private();
priv.idle_rechild = null;
if (priv.@"error") {
self.as(adw.Bin).setChild(priv.error_page.as(gtk.Widget));
} else {
self.as(adw.Bin).setChild(priv.terminal_page.as(gtk.Widget));
}
return 0;
}
fn propMouseHoverUrl(
self: *Self,
_: *gobject.ParamSpec,
@@ -2030,11 +1919,8 @@ pub const Surface = extern struct {
// Bell stops ringing if any mouse button is pressed.
self.setBellRinging(false);
// Get our surface. If we don't have one, ignore this.
const priv = self.private();
const core_surface = priv.core_surface orelse return;
// If we don't have focus, grab it.
const priv = self.private();
const gl_area_widget = priv.gl_area.as(gtk.Widget);
if (gl_area_widget.hasFocus() == 0) {
_ = gl_area_widget.grabFocus();
@@ -2042,10 +1928,10 @@ pub const Surface = extern struct {
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const consumed = consumed: {
const consumed = if (priv.core_surface) |surface| consumed: {
const gtk_mods = event.getModifierState();
const mods = gtk_key.translateMods(gtk_mods);
break :consumed core_surface.mouseButtonCallback(
break :consumed surface.mouseButtonCallback(
.press,
button,
mods,
@@ -2053,7 +1939,7 @@ pub const Surface = extern struct {
log.warn("error in key callback err={}", .{err});
break :err false;
};
};
} else false;
// If a right click isn't consumed, mouseButtonCallback selects the hovered
// word and returns false. We can use this to handle the context menu
@@ -2394,23 +2280,21 @@ pub const Surface = extern struct {
) callconv(.c) void {
log.debug("realize", .{});
// Make the GL area current so we can detect any OpenGL errors. If
// we have errors here we can't render and we switch to the error
// state.
const priv = self.private();
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
self.setError(true);
return;
}
// If we already have an initialized surface then we notify it.
// If we don't, we'll initialize it on the first resize so we have
// our proper initial dimensions.
const priv = self.private();
if (priv.core_surface) |v| realize: {
// We need to make the context current so we can call GL functions.
// This is required for all surface operations.
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is usually due to a driver or gtk bug", .{});
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
break :realize;
}
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
break :realize;
@@ -2724,10 +2608,8 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("url_right", .{});
class.bindTemplateChildPrivate("child_exited_overlay", .{});
class.bindTemplateChildPrivate("context_menu", .{});
class.bindTemplateChildPrivate("error_page", .{});
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("terminal_page", .{});
class.bindTemplateChildPrivate("drop_target", .{});
class.bindTemplateChildPrivate("im_context", .{});
@@ -2757,7 +2639,6 @@ pub const Surface = extern struct {
class.bindTemplateCallback("child_exited_close", &childExitedClose);
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_error", &propError);
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
@@ -2770,7 +2651,6 @@ pub const Surface = extern struct {
properties.config.impl,
properties.@"child-exited".impl,
properties.@"default-size".impl,
properties.@"error".impl,
properties.@"font-size-request".impl,
properties.focused.impl,
properties.@"min-size".impl,

View File

@@ -18,6 +18,7 @@ const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const SplitTree = @import("split_tree.zig").SplitTree;
const Surface = @import("surface.zig").Surface;
@@ -198,11 +199,8 @@ pub const Tab = extern struct {
}
fn initActionMap(self: *Self) void {
const s_param_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_param_type.free();
const actions = [_]ext.actions.Action(Self){
.init("close", actionClose, s_param_type),
.init("close", actionClose, null),
.init("ring-bell", actionRingBell, null),
};
@@ -316,44 +314,18 @@ pub const Tab = extern struct {
fn actionClose(
_: *gio.SimpleAction,
param_: ?*glib.Variant,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const param = param_ orelse {
log.warn("tab.close-tab called without a parameter", .{});
return;
};
var str: ?[*:0]const u8 = null;
param.get("&s", &str);
const tab_view = ext.getAncestor(
adw.TabView,
self.as(gtk.Widget),
) orelse return;
const page = tab_view.getPage(self.as(gtk.Widget));
const mode = std.meta.stringToEnum(
apprt.action.CloseTabMode,
std.mem.span(
str orelse {
log.warn("invalid mode provided to tab.close-tab", .{});
return;
},
),
) orelse {
// Need to be defensive here since actions can be triggered externally.
log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?});
return;
};
// Delegate to our parent to handle this, since this will emit
// a close-page signal that the parent can intercept.
switch (mode) {
.this => tab_view.closePage(page),
.other => tab_view.closeOtherPages(page),
}
tab_view.closePage(page);
}
fn actionRingBell(

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