Merge branch 'ghostty-org:main' into main

This commit is contained in:
nabawy ibrahim
2025-02-18 05:16:30 +02:00
committed by GitHub
367 changed files with 14366 additions and 1841 deletions

3
.gitattributes vendored
View File

@ -1,3 +1,6 @@
build.zig.zon.nix linguist-generated=true
build.zig.zon.txt linguist-generated=true
build.zig.zon2json-lock linguist-generated=true
vendor/** linguist-vendored
website/** linguist-documentation
pkg/breakpad/vendor/** linguist-vendored

View File

@ -50,5 +50,5 @@ jobs:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
useDaemon: false # sometimes fails on short jobs
- name: Check Zig cache hash
run: nix develop -c ./nix/build-support/check-zig-cache-hash.sh
- name: Check Zig cache
run: nix develop -c ./nix/build-support/check-zig-cache.sh

View File

@ -68,7 +68,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -136,7 +136,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@ -298,7 +298,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -164,7 +164,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.3
SPARKLE_VERSION: 2.6.4
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@ -14,6 +14,7 @@ jobs:
- build-bench
- build-linux-libghostty
- build-nix
- build-snap
- build-macos
- build-macos-matrix
- build-windows
@ -25,6 +26,7 @@ jobs:
- alejandra
- typos
- test-pkg-linux
- test-debian-12
steps:
- id: status
name: Determine status
@ -202,10 +204,14 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- 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
run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }}
# The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
@ -238,35 +244,65 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
- name: Test All
run: |
# OpenGL
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
# Metal
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build test -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build test --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
- name: Build All
run: |
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_harfbuzz
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=opengl -Dfont-backend=coretext_noshape
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_freetype
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_harfbuzz
nix develop -c zig build --system ${{ steps.deps.outputs.deps }} -Dapp-runtime=glfw -Drenderer=metal -Dfont-backend=coretext_noshape
build-snap:
strategy:
fail-fast: false
matrix:
os:
[namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64]
runs-on: ${{ matrix.os }}
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.0
with:
path: |
/nix
/zig
- run: sudo apt install -y udev
- run: sudo systemctl start systemd-udevd
- uses: snapcore/action-build@v1
build-windows:
runs-on: windows-2022
@ -366,7 +402,7 @@ jobs:
run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs
- name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw
@ -379,10 +415,9 @@ jobs:
strategy:
fail-fast: false
matrix:
adwaita: ["true", "false"]
x11: ["true", "false"]
wayland: ["true", "false"]
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
@ -413,7 +448,6 @@ jobs:
nix develop -c \
zig build \
-Dapp-runtime=gtk \
-Dgtk-adwaita=${{ matrix.adwaita }} \
-Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }}
@ -471,8 +505,12 @@ jobs:
- name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app
- name: get the Zig deps
id: deps
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
- name: test
run: nix develop -c zig build test
run: nix develop -c zig build test --system ${{ steps.deps.outputs.deps }}
prettier:
if: github.repository == 'ghostty-org/ghostty'
@ -589,3 +627,26 @@ jobs:
- name: Test ${{ matrix.pkg }} Build
run: |
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
test-debian-12:
name: Test build on Debian 12
runs-on: namespace-profile-ghostty-sm
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install and configure Namespace CLI
uses: namespacelabs/nscloud-setup@v0
- name: Configure Namespace powered Buildx
uses: namespacelabs/nscloud-setup-buildx-action@v0
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: src/build/docker/debian/Dockerfile
build-args: |
DISTRO_VERSION=12
ZIG_VERSION=0.13.0

View File

@ -48,14 +48,14 @@ jobs:
run: |
# Only proceed if build.zig.zon has changed
if ! git diff --exit-code build.zig.zon; then
nix develop -c ./nix/build-support/check-zig-cache-hash.sh --update
nix develop -c ./nix/build-support/check-zig-cache-hash.sh
nix develop -c ./nix/build-support/check-zig-cache.sh --update
nix develop -c ./nix/build-support/check-zig-cache.sh
fi
# Verify the build still works. We choose an arbitrary build type
# as a canary instead of testing all build types.
- name: Test Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true
run: nix build .#ghostty
- name: Create pull request
uses: peter-evans/create-pull-request@v7
@ -66,7 +66,9 @@ jobs:
commit-message: "deps: Update iTerm2 color schemes"
add-paths: |
build.zig.zon
nix/zigCacheHash.nix
build.zig.zon2json-lock
build.zig.zon.nix
build.zig.zon.txt
body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
labels: dependencies

View File

@ -23,13 +23,6 @@ https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz.minisig
```
> [!NOTE]
>
> **Version 1.0.0 the filename is `ghostty-source.tar.gz`.** Future
> versions will use the `ghostty-VERSION.tar.gz` format since it is more
> typical for source tarballs. But for version 1.0.0, the filename is
> `ghostty-source.tar.gz`.
Signature files are signed with
[minisign](https://jedisct1.github.io/minisign/)
using the following public key:
@ -88,6 +81,13 @@ for system packages which separate a build and install step, since the
install step can then be done with a `mv` or `cp` command (from `/tmp/ghostty`
to wherever the package manager expects it).
> [!NOTE]
>
> **Version 1.1.1 and 1.1.2 are missing `fetch-zig-cache.sh`.** This was
> an oversight on the release process. You can use the script from version
> 1.1.0 to fetch the Zig cache for these versions. Future versions will
> restore the script.
### Build Options
Ghostty uses the Zig build system. You can see all available build options by

View File

@ -1,32 +1,39 @@
.{
.name = "ghostty",
.version = "1.1.1",
.version = "1.1.3",
.paths = .{""},
.dependencies = .{
// Zig libs
.libxev = .{
.url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz",
// mitchellh/libxev
.url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
.hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
},
.mach_glfw = .{
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
// mitchellh/mach-glfw
.url = "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz",
.hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
.lazy = true,
},
.vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
// rockorager/libvaxis
.url = "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93",
.hash = "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb",
},
.z2d = .{
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
// vancluever/z2d
.url = "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz",
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
},
.zig_objc = .{
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
// mitchellh/zig-objc
.url = "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz",
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
},
.zig_js = .{
.url = "https://github.com/mitchellh/zig-js/archive/d0b8b0a57c52fbc89f9d9fecba75ca29da7dd7d1.tar.gz",
// mitchellh/zig-js
.url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
.hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc",
},
.ziglyph = .{
@ -34,13 +41,20 @@
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
},
.zig_wayland = .{
.url = "https://codeberg.org/ifreund/zig-wayland/archive/fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
// codeberg ifreund/zig-wayland
.url = "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
.hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
},
.zf = .{
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
// natecraddock/zf
.url = "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz",
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
},
.gobject = .{
// ianprime0509/zig-gobject
.url = "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst",
.hash = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d",
},
// C libs
.cimgui = .{ .path = "./pkg/cimgui" },
@ -72,15 +86,15 @@
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
},
.plasma_wayland_protocols = .{
.url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
.url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
},
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz",
.hash = "12203d2647e5daf36a9c85b969e03f422540786ce9ea624eb4c26d204fe1f46218f3",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz",
.hash = "1220a1dbe41bc69aacf75026a7158812198ea265fb9cac64dcb91cd31f3b1b8c1f92",
},
},
}

390
build.zig.zon.nix generated Normal file
View File

@ -0,0 +1,390 @@
# generated by zon2nix (https://github.com/Cloudef/zig2nix)
{
lib,
linkFarm,
fetchurl,
fetchgit,
runCommandLocal,
zig,
name ? "zig-packages",
}:
with builtins;
with lib; let
unpackZigArtifact = {
name,
artifact,
}:
runCommandLocal name
{
nativeBuildInputs = [zig];
}
''
hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})"
mv "$TMPDIR/p/$hash" "$out"
chmod 755 "$out"
'';
fetchZig = {
name,
url,
hash,
}: let
artifact = fetchurl {inherit url hash;};
in
unpackZigArtifact {inherit name artifact;};
fetchGitZig = {
name,
url,
hash,
}: let
parts = splitString "#" url;
url_base = elemAt parts 0;
url_without_query = elemAt (splitString "?" url_base) 0;
rev_base = elemAt parts 1;
rev =
if match "^[a-fA-F0-9]{40}$" rev_base != null
then rev_base
else "refs/heads/${rev_base}";
in
fetchgit {
inherit name rev hash;
url = url_without_query;
deepClone = false;
};
fetchZigArtifact = {
name,
url,
hash,
}: let
parts = splitString "://" url;
proto = elemAt parts 0;
path = elemAt parts 1;
fetcher = {
"git+http" = fetchGitZig {
inherit name hash;
url = "http://${path}";
};
"git+https" = fetchGitZig {
inherit name hash;
url = "https://${path}";
};
http = fetchZig {
inherit name hash;
url = "http://${path}";
};
https = fetchZig {
inherit name hash;
url = "https://${path}";
};
};
in
fetcher.${proto};
in
linkFarm name [
{
name = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c";
path = fetchZigArtifact {
name = "libxev";
url = "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz";
hash = "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8=";
};
}
{
name = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62";
path = fetchZigArtifact {
name = "mach_glfw";
url = "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz";
hash = "sha256-HhXIvWUS8/CHWY4VXPG2ZEo+we8XOn3o5rYJCQ1n8Nk=";
};
}
{
name = "1220736fa4ba211162c7a0e46cc8fe04d95921927688bff64ab5da7420d098a7272d";
path = fetchZigArtifact {
name = "glfw";
url = "https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz";
hash = "sha256-IeBVAOQmtyFqVxzuXPek1onuPwIamcOyYtxqKpPEQjU=";
};
}
{
name = "12202adbfecdad671d585c9a5bfcbd5cdf821726779430047742ce1bf94ad67d19cb";
path = fetchZigArtifact {
name = "xcode_frameworks";
url = "https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz";
hash = "sha256-mP/I2coL57UJm/3+4Q8sPAgQwk8V4zM+S4VBBTrX2To=";
};
}
{
name = "122004bfd4c519dadfb8e6281a42fc34fd1aa15aea654ea8a492839046f9894fa2cf";
path = fetchZigArtifact {
name = "vulkan_headers";
url = "https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz";
hash = "sha256-K+zrRudgHFukOM6En1StRYRMNYkeRk+qHTXvrXaG+FU=";
};
}
{
name = "1220b3164434d2ec9db146a40bf3a30f490590d68fa8529776a3138074f0da2c11ca";
path = fetchZigArtifact {
name = "wayland_headers";
url = "https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz";
hash = "sha256-uFilLZinKkZt6RdVTV3lUmJpzpswDdFva22FvwU/XQI=";
};
}
{
name = "122089c326186c84aa2fd034b16abc38f3ebf4862d9ae106dc1847ac44f557b36465";
path = fetchZigArtifact {
name = "x11_headers";
url = "https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz";
hash = "sha256-EhV2bmTY/OMYN1wEul35gD0hQgS/Al262jO3pVr0O+c=";
};
}
{
name = "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb";
path = fetchZigArtifact {
name = "vaxis";
url = "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93";
hash = "sha256-ZzLNJOsXzyBhkdUhbET30RoU2T9xKYsBUQz2NAjK/G8=";
};
}
{
name = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5";
path = fetchZigArtifact {
name = "zigimg";
url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e";
hash = "sha256-oLf3YH3yeg4ikVO/GahMCDRMTU31AHkfSnF4rt7xTKo=";
};
}
{
name = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40";
path = fetchZigArtifact {
name = "zg";
url = "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz";
hash = "sha256-2x9hT7bYq9KJYWLVOf21a+QvTG/F7HWT+YK15IMRzNY=";
};
}
{
name = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a";
path = fetchZigArtifact {
name = "z2d";
url = "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz";
hash = "sha256-P0UJ54RO/vVyDa+UkBl+QEOjzoMMEFSOTexQP/uBXfc=";
};
}
{
name = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634";
path = fetchZigArtifact {
name = "zig_objc";
url = "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz";
hash = "sha256-H+HIbh2T23uzrsg9/1/vl9Ir1HCAa2pzeTx6zktJH9Q=";
};
}
{
name = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc";
path = fetchZigArtifact {
name = "zig_js";
url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz";
hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=";
};
}
{
name = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25";
path = fetchZigArtifact {
name = "ziglyph";
url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz";
hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k=";
};
}
{
name = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38";
path = fetchZigArtifact {
name = "zig_wayland";
url = "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz";
hash = "sha256-RtAystqK/GRYIquTK1KfD7rRSCrfuzAvCD1Z9DE1ldc=";
};
}
{
name = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8";
path = fetchZigArtifact {
name = "zf";
url = "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz";
hash = "sha256-/oLryY3VQfjbtQi+UP+n6FJTVA/YxIetjO+6Ovrh6/E=";
};
}
{
name = "1220c72c1697dd9008461ead702997a15d8a1c5810247f02e7983b9f74c6c6e4c087";
path = fetchZigArtifact {
name = "vaxis";
url = "git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423";
hash = "sha256-QWN4jOrA91KlbqmeEHHJ4HTnCC9nmfxt8DHUXJpAzLI=";
};
}
{
name = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d";
path = fetchZigArtifact {
name = "gobject";
url = "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst";
hash = "sha256-UU97kNv/bZzQPKz1djhEDLapLguvfBpFfWVb6FthtcI=";
};
}
{
name = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f";
path = fetchZigArtifact {
name = "wayland";
url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz";
hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0=";
};
}
{
name = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef";
path = fetchZigArtifact {
name = "wayland_protocols";
url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz";
hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg=";
};
}
{
name = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566";
path = fetchZigArtifact {
name = "plasma_wayland_protocols";
url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz";
hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE=";
};
}
{
name = "1220a1dbe41bc69aacf75026a7158812198ea265fb9cac64dcb91cd31f3b1b8c1f92";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz";
hash = "sha256-ZdVc1mmLwF45PZiqL/j/l7MO2O6hZ11lqIToGFdHiEU=";
};
}
{
name = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402";
path = fetchZigArtifact {
name = "imgui";
url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz";
hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=";
};
}
{
name = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d";
path = fetchZigArtifact {
name = "freetype";
url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz";
hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw=";
};
}
{
name = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66";
path = fetchZigArtifact {
name = "libpng";
url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz";
hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=";
};
}
{
name = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb";
path = fetchZigArtifact {
name = "zlib";
url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz";
hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw=";
};
}
{
name = "12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7";
path = fetchZigArtifact {
name = "fontconfig";
url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz";
hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU=";
};
}
{
name = "122032442d95c3b428ae8e526017fad881e7dc78eab4d558e9a58a80bfbd65a64f7d";
path = fetchZigArtifact {
name = "libxml2";
url = "https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz";
hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU=";
};
}
{
name = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122";
path = fetchZigArtifact {
name = "harfbuzz";
url = "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz";
hash = "sha256-nxygiYE7BZRK0c6MfgGCEwJtNdybq0gKIeuHaDg5ZVY=";
};
}
{
name = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b";
path = fetchZigArtifact {
name = "highway";
url = "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz";
hash = "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k=";
};
}
{
name = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb";
path = fetchZigArtifact {
name = "oniguruma";
url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz";
hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA=";
};
}
{
name = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e";
path = fetchZigArtifact {
name = "sentry";
url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz";
hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8=";
};
}
{
name = "12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea";
path = fetchZigArtifact {
name = "breakpad";
url = "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz";
hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk=";
};
}
{
name = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641";
path = fetchZigArtifact {
name = "utfcpp";
url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz";
hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8=";
};
}
{
name = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd";
path = fetchZigArtifact {
name = "wuffs";
url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz";
hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=";
};
}
{
name = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806";
path = fetchZigArtifact {
name = "pixels";
url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz";
hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro=";
};
}
{
name = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1";
path = fetchZigArtifact {
name = "glslang";
url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz";
hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U=";
};
}
{
name = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da";
path = fetchZigArtifact {
name = "spirv_cross";
url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz";
hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M=";
};
}
]

38
build.zig.zon.txt generated Normal file
View File

@ -0,0 +1,38 @@
git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93
git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423
git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz
https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz
https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz
https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz
https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst
https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz
https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz
https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz
https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz
https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz
https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz
https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz
https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz
https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz
https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz
https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz
https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz
https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz
https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz
https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz
https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz
https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz
https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz
https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz
https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz
https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz
https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz
https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz
https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz

192
build.zig.zon2json-lock generated Normal file
View File

@ -0,0 +1,192 @@
{
"1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c": {
"name": "libxev",
"url": "https://deps.files.ghostty.org/libxev-1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c.tar.gz",
"hash": "sha256-VHP90NTytIZ8UZsYRKOOxN490/I6yv6ec40sP8y5MJ8="
},
"12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62": {
"name": "mach_glfw",
"url": "https://deps.files.ghostty.org/mach_glfw-12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62.tar.gz",
"hash": "sha256-HhXIvWUS8/CHWY4VXPG2ZEo+we8XOn3o5rYJCQ1n8Nk="
},
"1220736fa4ba211162c7a0e46cc8fe04d95921927688bff64ab5da7420d098a7272d": {
"name": "glfw",
"url": "https://github.com/mitchellh/glfw/archive/b552c6ec47326b94015feddb36058ea567b87159.tar.gz",
"hash": "sha256-IeBVAOQmtyFqVxzuXPek1onuPwIamcOyYtxqKpPEQjU="
},
"12202adbfecdad671d585c9a5bfcbd5cdf821726779430047742ce1bf94ad67d19cb": {
"name": "xcode_frameworks",
"url": "https://github.com/mitchellh/xcode-frameworks/archive/69801c154c39d7ae6129ea1ba8fe1afe00585fc8.tar.gz",
"hash": "sha256-mP/I2coL57UJm/3+4Q8sPAgQwk8V4zM+S4VBBTrX2To="
},
"122004bfd4c519dadfb8e6281a42fc34fd1aa15aea654ea8a492839046f9894fa2cf": {
"name": "vulkan_headers",
"url": "https://github.com/mitchellh/vulkan-headers/archive/04c8a0389d5a0236a96312988017cd4ce27d8041.tar.gz",
"hash": "sha256-K+zrRudgHFukOM6En1StRYRMNYkeRk+qHTXvrXaG+FU="
},
"1220b3164434d2ec9db146a40bf3a30f490590d68fa8529776a3138074f0da2c11ca": {
"name": "wayland_headers",
"url": "https://github.com/mitchellh/wayland-headers/archive/5f991515a29f994d87b908115a2ab0b899474bd1.tar.gz",
"hash": "sha256-uFilLZinKkZt6RdVTV3lUmJpzpswDdFva22FvwU/XQI="
},
"122089c326186c84aa2fd034b16abc38f3ebf4862d9ae106dc1847ac44f557b36465": {
"name": "x11_headers",
"url": "https://github.com/mitchellh/x11-headers/archive/2ffbd62d82ff73ec929dd8de802bc95effa0ef88.tar.gz",
"hash": "sha256-EhV2bmTY/OMYN1wEul35gD0hQgS/Al262jO3pVr0O+c="
},
"1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb": {
"name": "vaxis",
"url": "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93",
"hash": "sha256-ZzLNJOsXzyBhkdUhbET30RoU2T9xKYsBUQz2NAjK/G8="
},
"1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5": {
"name": "zigimg",
"url": "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
"hash": "sha256-oLf3YH3yeg4ikVO/GahMCDRMTU31AHkfSnF4rt7xTKo="
},
"122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40": {
"name": "zg",
"url": "https://codeberg.org/atman/zg/archive/v0.13.2.tar.gz",
"hash": "sha256-2x9hT7bYq9KJYWLVOf21a+QvTG/F7HWT+YK15IMRzNY="
},
"12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a": {
"name": "z2d",
"url": "https://deps.files.ghostty.org/z2d-12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a.tar.gz",
"hash": "sha256-P0UJ54RO/vVyDa+UkBl+QEOjzoMMEFSOTexQP/uBXfc="
},
"1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634": {
"name": "zig_objc",
"url": "https://deps.files.ghostty.org/zig_objc-1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634.tar.gz",
"hash": "sha256-H+HIbh2T23uzrsg9/1/vl9Ir1HCAa2pzeTx6zktJH9Q="
},
"12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc": {
"name": "zig_js",
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
},
"12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25": {
"name": "ziglyph",
"url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
"hash": "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="
},
"12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38": {
"name": "zig_wayland",
"url": "https://deps.files.ghostty.org/zig-wayland-fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
"hash": "sha256-RtAystqK/GRYIquTK1KfD7rRSCrfuzAvCD1Z9DE1ldc="
},
"1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8": {
"name": "zf",
"url": "https://deps.files.ghostty.org/zf-1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8.tar.gz",
"hash": "sha256-/oLryY3VQfjbtQi+UP+n6FJTVA/YxIetjO+6Ovrh6/E="
},
"1220c72c1697dd9008461ead702997a15d8a1c5810247f02e7983b9f74c6c6e4c087": {
"name": "vaxis",
"url": "git+https://github.com/rockorager/libvaxis/?ref=main#dc0a228a5544988d4a920cfb40be9cd28db41423",
"hash": "sha256-QWN4jOrA91KlbqmeEHHJ4HTnCC9nmfxt8DHUXJpAzLI="
},
"12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d": {
"name": "gobject",
"url": "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst",
"hash": "sha256-UU97kNv/bZzQPKz1djhEDLapLguvfBpFfWVb6FthtcI="
},
"12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f": {
"name": "wayland",
"url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
"hash": "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="
},
"12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef": {
"name": "wayland_protocols",
"url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
"hash": "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="
},
"12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566": {
"name": "plasma_wayland_protocols",
"url": "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz",
"hash": "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="
},
"1220a1dbe41bc69aacf75026a7158812198ea265fb9cac64dcb91cd31f3b1b8c1f92": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz",
"hash": "sha256-ZdVc1mmLwF45PZiqL/j/l7MO2O6hZ11lqIToGFdHiEU="
},
"1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402": {
"name": "imgui",
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d": {
"name": "freetype",
"url": "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz",
"hash": "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="
},
"1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66": {
"name": "libpng",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
},
"1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb": {
"name": "zlib",
"url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz",
"hash": "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="
},
"12201149afb3326c56c05bb0a577f54f76ac20deece63aa2f5cd6ff31a4fa4fcb3b7": {
"name": "fontconfig",
"url": "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz",
"hash": "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="
},
"122032442d95c3b428ae8e526017fad881e7dc78eab4d558e9a58a80bfbd65a64f7d": {
"name": "libxml2",
"url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.11.5.tar.gz",
"hash": "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="
},
"1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122": {
"name": "harfbuzz",
"url": "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz",
"hash": "sha256-nxygiYE7BZRK0c6MfgGCEwJtNdybq0gKIeuHaDg5ZVY="
},
"12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b": {
"name": "highway",
"url": "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz",
"hash": "sha256-NUqLRTm1iOcLmOxwhEJz4/J0EwLEw3e8xOgbPRhm98k="
},
"1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb": {
"name": "oniguruma",
"url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",
"hash": "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="
},
"1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e": {
"name": "sentry",
"url": "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz",
"hash": "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="
},
"12207fd37bb8251919c112dcdd8f616a491857b34a451f7e4486490077206dc2a1ea": {
"name": "breakpad",
"url": "https://github.com/getsentry/breakpad/archive/b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz",
"hash": "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="
},
"1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641": {
"name": "utfcpp",
"url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
"hash": "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="
},
"122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd": {
"name": "wuffs",
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806": {
"name": "pixels",
"url": "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz",
"hash": "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="
},
"12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1": {
"name": "glslang",
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
},
"1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da": {
"name": "spirv_cross",
"url": "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz",
"hash": "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="
}
}

62
flake.lock generated
View File

@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
@ -21,11 +21,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
@ -36,11 +36,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1733423277,
"narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=",
"lastModified": 1738255539,
"narHash": "sha256-hP2eOqhIO/OILW+3moNWO4GtdJFYCqAe9yJZgvlCoDQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e36963a147267afc055f7cf65225958633e536bf",
"rev": "c3511a3b53b482aa7547c9d1626fd7310c1de1c5",
"type": "github"
},
"original": {
@ -52,11 +52,11 @@
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1733229606,
"narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=",
"lastModified": 1738136902,
"narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550",
"rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c",
"type": "github"
},
"original": {
@ -69,9 +69,11 @@
"root": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable",
"zig": "zig"
"zig": "zig",
"zig2nix": "zig2nix"
}
},
"systems": {
@ -92,17 +94,19 @@
"zig": {
"inputs": {
"flake-compat": [],
"flake-utils": "flake-utils",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs-stable"
]
},
"locked": {
"lastModified": 1717848532,
"narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=",
"lastModified": 1738239110,
"narHash": "sha256-Y5i9mQ++dyIQr+zEPNy+KIbc5wjPmfllBrag3cHZgcE=",
"owner": "mitchellh",
"repo": "zig-overlay",
"rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43",
"rev": "1a8fb6f3a04724519436355564b95fce5e272504",
"type": "github"
},
"original": {
@ -110,6 +114,30 @@
"repo": "zig-overlay",
"type": "github"
}
},
"zig2nix": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs-stable"
]
},
"locked": {
"lastModified": 1738263917,
"narHash": "sha256-j/3fwe2pEOquHabP/puljOKwAZFjIE9gXZqA91sC48M=",
"owner": "jcollie",
"repo": "zig2nix",
"rev": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
"type": "github"
},
"original": {
"owner": "jcollie",
"ref": "c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a",
"repo": "zig2nix",
"type": "github"
}
}
},
"root": "root",

View File

@ -8,6 +8,7 @@
# glibc versions used by our dependencies from Nix are compatible with the
# system glibc that the user is building for.
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
flake-utils.url = "github:numtide/flake-utils";
# Used for shell.nix
flake-compat = {
@ -19,9 +20,18 @@
url = "github:mitchellh/zig-overlay";
inputs = {
nixpkgs.follows = "nixpkgs-stable";
flake-utils.follows = "flake-utils";
flake-compat.follows = "";
};
};
zig2nix = {
url = "github:jcollie/zig2nix?ref=c311d8e77a6ee0d995f40a6e10a89a3a4ab04f9a";
inputs = {
nixpkgs.follows = "nixpkgs-stable";
flake-utils.follows = "flake-utils";
};
};
};
outputs = {
@ -29,6 +39,7 @@
nixpkgs-unstable,
nixpkgs-stable,
zig,
zig2nix,
...
}:
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
@ -40,6 +51,7 @@
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
zig = zig.packages.${system}."0.13.0";
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
zig2nix = zig2nix;
};
packages.${system} = let
@ -49,6 +61,7 @@
revision = self.shortRev or self.dirtyShortRev or "dirty";
};
in rec {
deps = pkgs-stable.callPackage ./build.zig.zon.nix {};
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");

View File

@ -412,6 +412,7 @@ typedef enum {
GHOSTTY_FULLSCREEN_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e;
// apprt.action.SecureInput
@ -585,6 +586,7 @@ typedef enum {
GHOSTTY_ACTION_RENDER_INSPECTOR,
GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
GHOSTTY_ACTION_SET_TITLE,
GHOSTTY_ACTION_PROMPT_TITLE,
GHOSTTY_ACTION_PWD,
GHOSTTY_ACTION_MOUSE_SHAPE,
GHOSTTY_ACTION_MOUSE_VISIBILITY,
@ -644,7 +646,7 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
ghostty_clipboard_e,
bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, bool);
typedef void (*ghostty_runtime_action_cb)(ghostty_app_t,
typedef bool (*ghostty_runtime_action_cb)(ghostty_app_t,
ghostty_target_s,
ghostty_action_s);

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

View File

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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "macOS-AppIcon-1024px.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View File

@ -72,6 +72,7 @@
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
@ -168,6 +169,7 @@
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = "<group>"; };
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
@ -270,6 +272,7 @@
A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
isa = PBXGroup;
children = (
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
A5CEAFFE29C2410700646FDA /* Backport.swift */,
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
@ -623,6 +626,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,

View File

@ -28,7 +28,9 @@ class AppDelegate: NSObject,
@IBOutlet private var menuNewWindow: NSMenuItem?
@IBOutlet private var menuNewTab: NSMenuItem?
@IBOutlet private var menuSplitRight: NSMenuItem?
@IBOutlet private var menuSplitLeft: NSMenuItem?
@IBOutlet private var menuSplitDown: NSMenuItem?
@IBOutlet private var menuSplitUp: NSMenuItem?
@IBOutlet private var menuClose: NSMenuItem?
@IBOutlet private var menuCloseTab: NSMenuItem?
@IBOutlet private var menuCloseWindow: NSMenuItem?
@ -41,6 +43,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuToggleVisibility: NSMenuItem?
@IBOutlet private var menuToggleFullScreen: NSMenuItem?
@IBOutlet private var menuBringAllToFront: NSMenuItem?
@IBOutlet private var menuZoomSplit: NSMenuItem?
@IBOutlet private var menuPreviousSplit: NSMenuItem?
@IBOutlet private var menuNextSplit: NSMenuItem?
@ -52,6 +55,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuIncreaseFontSize: NSMenuItem?
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@IBOutlet private var menuResetFontSize: NSMenuItem?
@IBOutlet private var menuChangeTitle: NSMenuItem?
@IBOutlet private var menuQuickTerminal: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem?
@ -93,7 +97,7 @@ class AppDelegate: NSObject,
}
/// Tracks the windows that we hid for toggleVisibility.
private var hiddenWindows: [Weak<NSWindow>] = []
private var hiddenState: ToggleVisibilityState? = nil
/// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil
@ -217,8 +221,8 @@ class AppDelegate: NSObject,
}
func applicationDidBecomeActive(_ notification: Notification) {
// If we're back then clear the hidden windows
self.hiddenWindows = []
// If we're back manually then clear the hidden state because macOS handles it.
self.hiddenState = nil
// First launch stuff
if (!applicationHasBecomeActive) {
@ -245,7 +249,13 @@ class AppDelegate: NSObject,
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't
// quite work with SwiftUI because windows are retained on close. So instead we check
// if there are any that are visible. I'm guessing this breaks under certain scenarios.
if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow }
//
// NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it
// here because I don't want to remove it in a patch release cycle but we should
// target removing it soon.
if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) {
return .terminateNow
}
// If the user is shutting down, restarting, or logging out, we don't confirm quit.
why: if let event = NSAppleEventManager.shared().currentAppleEvent {
@ -355,7 +365,9 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
syncMenuShortcut(config, action: "new_split:left", menuItem: self.menuSplitLeft)
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp)
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
@ -378,6 +390,7 @@ class AppDelegate: NSObject,
syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
syncMenuShortcut(config, action: "change_title_prompt", menuItem: self.menuChangeTitle)
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
@ -524,6 +537,15 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance(config: config) }
// Decide whether to hide/unhide app from dock and app switcher
switch (config.macosHidden) {
case .never:
NSApp.setActivationPolicy(.regular)
case .always:
NSApp.setActivationPolicy(.accessory)
}
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = config.errors
@ -557,6 +579,30 @@ class AppDelegate: NSObject,
self.appIcon = nil
break
case .blueprint:
self.appIcon = NSImage(named: "BlueprintImage")!
case .chalkboard:
self.appIcon = NSImage(named: "ChalkboardImage")!
case .glass:
self.appIcon = NSImage(named: "GlassImage")!
case .holographic:
self.appIcon = NSImage(named: "HolographicImage")!
case .microchip:
self.appIcon = NSImage(named: "MicrochipImage")!
case .paper:
self.appIcon = NSImage(named: "PaperImage")!
case .retro:
self.appIcon = NSImage(named: "RetroImage")!
case .xray:
self.appIcon = NSImage(named: "XrayImage")!
case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor else { break }
@ -709,21 +755,15 @@ class AppDelegate: NSObject,
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) {
// Toggle visibility doesn't do anything if the focused window is native
// fullscreen.
guard let keyWindow = NSApp.keyWindow,
!keyWindow.styleMask.contains(.fullScreen) else { return }
// If we have focus, then we hide all windows.
if NSApp.isActive {
// We need to keep track of the windows that were visible because we only
// want to bring back these windows if we remove the toggle.
//
// We also ignore fullscreen windows because they don't hide anyways.
self.hiddenWindows = NSApp.windows.filter {
$0.isVisible &&
!$0.styleMask.contains(.fullScreen)
}.map { Weak($0) }
// Toggle visibility doesn't do anything if the focused window is native
// fullscreen. This is only relevant if Ghostty is active.
guard let keyWindow = NSApp.keyWindow,
!keyWindow.styleMask.contains(.fullScreen) else { return }
// Keep track of our hidden state to restore properly
self.hiddenState = .init()
NSApp.hide(nil)
return
}
@ -734,8 +774,16 @@ class AppDelegate: NSObject,
// Bring all windows to the front. Note: we don't use NSApp.unhide because
// that will unhide ALL hidden windows. We want to only bring forward the
// ones that we hid.
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
self.hiddenWindows = []
hiddenState?.restore()
hiddenState = nil
}
@IBAction func bringAllToFront(_ sender: Any) {
if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
}
NSApplication.shared.arrangeInFront(sender)
}
private struct DerivedConfig {
@ -755,4 +803,33 @@ class AppDelegate: NSObject,
self.quickTerminalPosition = config.quickTerminalPosition
}
}
private struct ToggleVisibilityState {
let hiddenWindows: [Weak<NSWindow>]
let keyWindow: Weak<NSWindow>?
init() {
// We need to know the key window so that we can bring focus back to the
// right window if it was hidden.
self.keyWindow = if let keyWindow = NSApp.keyWindow {
.init(keyWindow)
} else {
nil
}
// We need to keep track of the windows that were visible because we only
// want to bring back these windows if we remove the toggle.
//
// We also ignore fullscreen windows because they don't hide anyways.
self.hiddenWindows = NSApp.windows.filter {
$0.isVisible &&
!$0.styleMask.contains(.fullScreen)
}.map { Weak($0) }
}
func restore() {
hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
keyWindow?.value?.makeKey()
}
}
}

View File

@ -14,6 +14,8 @@
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
<connections>
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
@ -45,8 +47,10 @@
<outlet property="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/>
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="fgZ-Wb-8OR"/>
<outlet property="menuSplitDown" destination="UDZ-4y-6xL" id="ptr-mj-Azh"/>
<outlet property="menuSplitLeft" destination="Ppv-GP-lQU" id="Xd5-Cd-Jut"/>
<outlet property="menuSplitRight" destination="VUR-Ld-nLx" id="RxO-Zw-ovb"/>
<outlet property="menuSplitUp" destination="Ggp-7N-GbX" id="YJF-uq-S4Y"/>
<outlet property="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
<outlet property="menuToggleVisibility" destination="DOX-wA-ilh" id="iBj-Bc-2bq"/>
@ -143,10 +147,22 @@
<action selector="splitRight:" target="-1" id="cv2-Xg-FR4"/>
</connections>
</menuItem>
<menuItem title="Split Left" id="Ppv-GP-lQU">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="splitLeft:" target="-1" id="Cey-Mf-bD2"/>
</connections>
</menuItem>
<menuItem title="Split Down" id="UDZ-4y-6xL">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="splitDown:" target="-1" id="c6x-CF-u52"/>
<action selector="splitDown:" target="-1" id="Zej-CF-6nO"/>
</connections>
</menuItem>
<menuItem title="Split Up" id="Ggp-7N-GbX">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="splitUp:" target="-1" id="bbi-dK-pOS"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="sjq-M1-UGS"/>
@ -232,6 +248,13 @@
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/>
<menuItem title="Change Title..." id="24I-xg-qIq">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Vkj-tP-dMZ"/>
<menuItem title="Quick Terminal" id="1pv-LF-NBJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
@ -270,12 +293,6 @@
<action selector="toggleGhosttyFullScreen:" target="-1" id="QB9-7R-xyc"/>
</connections>
</menuItem>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
<menuItem title="Show/Hide All Terminals" id="DOX-wA-ilh">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
@ -370,6 +387,13 @@
</items>
</menu>
</menuItem>
<menuItem isSeparatorItem="YES" id="dgt-Tx-d4e"/>
<menuItem title="Bring All to Front" id="LE2-aR-0XJ">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="arrangeInFront:" target="-1" id="DRN-fu-gQh"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>

View File

@ -521,11 +521,21 @@ class BaseTerminalController: NSWindowController,
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
}
@IBAction func splitLeft(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_LEFT)
}
@IBAction func splitDown(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN)
}
@IBAction func splitUp(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_UP)
}
@IBAction func splitZoom(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return }
ghostty.splitToggleZoom(surface: surface)

View File

@ -283,9 +283,12 @@ class TerminalController: BaseTerminalController {
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
guard let window else { return }
// If we don't have both an X and Y we center.
// If we don't have an X/Y then we try to use the previously saved window pos.
guard let x, let y else {
window.center()
if (!LastWindowPosition.shared.restore(window)) {
window.center()
}
return
}
@ -490,6 +493,20 @@ class TerminalController: BaseTerminalController {
override func windowDidMove(_ notification: Notification) {
super.windowDidMove(notification)
self.fixTabBar()
// Whenever we move save our last position for the next start.
if let window {
LastWindowPosition.shared.save(window)
}
}
func windowDidBecomeMain(_ notification: Notification) {
// Whenever we get focused, use that as our last window position for
// restart. This differs from Terminal.app but matches iTerm2 behavior
// and I think its sensible.
if let window {
LastWindowPosition.shared.save(window)
}
}
// Called when the window will be encoded. We handle the data encoding here in the
@ -692,13 +709,21 @@ class TerminalController: BaseTerminalController {
// If our index is the same we do nothing
guard finalIndex != selectedIndex else { return }
// Get our parent
let parent = tabbedWindows[finalIndex]
// Get our target window
let targetWindow = tabbedWindows[finalIndex]
// Move our current selected window to the proper index
// Begin a group of window operations to minimize visual updates
NSAnimationContext.beginGrouping()
NSAnimationContext.current.duration = 0
// Remove and re-add the window in the correct position
tabGroup.removeWindow(selectedWindow)
parent.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
selectedWindow.makeKeyAndOrderFront(nil)
targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
// Ensure our window remains selected
selectedWindow.makeKey()
NSAnimationContext.endGrouping()
}
@objc private func onGotoTab(notification: SwiftUI.Notification) {

View File

@ -86,7 +86,7 @@ class TerminalManager {
// fullscreen for the logic later in this method.
c.toggleFullscreen(mode: .native)
case .nonNative, .nonNativeVisibleMenu:
case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch:
// If we're non-native then we have to do it on a later loop
// so that the content view is setup.
DispatchQueue.main.async {

View File

@ -95,6 +95,23 @@ fileprivate class CenteredDynamicLabel: NSTextField {
setContentHuggingPriority(.defaultLow, for: .horizontal)
setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
}
// Vertically center the text
override func draw(_ dirtyRect: NSRect) {
guard let attributedString = self.attributedStringValue.mutableCopy() as? NSMutableAttributedString else {
super.draw(dirtyRect)
return
}
let textSize = attributedString.size()
let yOffset = (self.bounds.height - textSize.height) / 2 - 1 // -1 to center it better
let centeredRect = NSRect(x: self.bounds.origin.x, y: self.bounds.origin.y + yOffset,
width: self.bounds.width, height: textSize.height)
attributedString.draw(in: centeredRect)
}
}
extension NSToolbarItem.Identifier {

View File

@ -13,6 +13,9 @@ extension FullscreenMode {
case GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU:
.nonNativeVisibleMenu
case GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH:
.nonNativePaddedNotch
default:
nil
}

View File

@ -257,7 +257,7 @@ extension Ghostty {
// MARK: Ghostty Callbacks (iOS)
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {}
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {}
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool { return false }
static func readClipboard(
_ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e,
@ -423,7 +423,7 @@ extension Ghostty {
// MARK: Actions (macOS)
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) {
static func action(_ app: ghostty_app_t, target: ghostty_target_s, action: ghostty_action_s) -> Bool {
// Make sure it a target we understand so all our action handlers can assert
switch (target.tag) {
case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
@ -431,7 +431,7 @@ extension Ghostty {
default:
Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
return
return false
}
// Action dispatch
@ -455,13 +455,13 @@ extension Ghostty {
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
case GHOSTTY_ACTION_MOVE_TAB:
moveTab(app, target: target, move: action.action.move_tab)
return moveTab(app, target: target, move: action.action.move_tab)
case GHOSTTY_ACTION_GOTO_TAB:
gotoTab(app, target: target, tab: action.action.goto_tab)
return gotoTab(app, target: target, tab: action.action.goto_tab)
case GHOSTTY_ACTION_GOTO_SPLIT:
gotoSplit(app, target: target, direction: action.action.goto_split)
return gotoSplit(app, target: target, direction: action.action.goto_split)
case GHOSTTY_ACTION_RESIZE_SPLIT:
resizeSplit(app, target: target, resize: action.action.resize_split)
@ -484,6 +484,9 @@ extension Ghostty {
case GHOSTTY_ACTION_SET_TITLE:
setTitle(app, target: target, v: action.action.set_title)
case GHOSTTY_ACTION_PROMPT_TITLE:
return promptTitle(app, target: target)
case GHOSTTY_ACTION_PWD:
pwdChanged(app, target: target, v: action.action.pwd)
@ -541,10 +544,15 @@ extension Ghostty {
fallthrough
case GHOSTTY_ACTION_QUIT_TIMER:
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
return false
default:
Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)")
return false
}
// If we reached here then we assume performed since all unknown actions
// are captured in the switch and return false.
return true
}
private static func quit(_ app: ghostty_app_t) {
@ -716,15 +724,19 @@ extension Ghostty {
private static func moveTab(
_ app: ghostty_app_t,
target: ghostty_target_s,
move: ghostty_action_move_tab_s) {
move: ghostty_action_move_tab_s) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("move tab does nothing with an app target")
return
return false
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return false }
// See gotoTab for notes on this check.
guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
NotificationCenter.default.post(
name: .ghosttyMoveTab,
object: surfaceView,
@ -736,20 +748,27 @@ extension Ghostty {
default:
assertionFailure()
}
return true
}
private static func gotoTab(
_ app: ghostty_app_t,
target: ghostty_target_s,
tab: ghostty_action_goto_tab_e) {
tab: ghostty_action_goto_tab_e) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto tab does nothing with an app target")
return
return false
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return false }
// Similar to goto_split (see comment there) about our performability,
// we should make this more accurate later.
guard (surfaceView.window?.tabGroup?.windows.count ?? 0) > 1 else { return false }
NotificationCenter.default.post(
name: Notification.ghosttyGotoTab,
object: surfaceView,
@ -761,20 +780,31 @@ extension Ghostty {
default:
assertionFailure()
}
return true
}
private static func gotoSplit(
_ app: ghostty_app_t,
target: ghostty_target_s,
direction: ghostty_action_goto_split_e) {
direction: ghostty_action_goto_split_e) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto split does nothing with an app target")
return
return false
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return false }
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { return false }
// For now, we return false if the window has no splits and we return
// true if the window has ANY splits. This isn't strictly correct because
// we should only be returning true if we actually performed the action,
// but this handles the most common case of caring about goto_split performability
// which is the no-split case.
guard controller.surfaceTree?.isSplit ?? false else { return false }
NotificationCenter.default.post(
name: Notification.ghosttyFocusSplit,
object: surfaceView,
@ -786,6 +816,8 @@ extension Ghostty {
default:
assertionFailure()
}
return true
}
private static func resizeSplit(
@ -978,6 +1010,26 @@ extension Ghostty {
}
}
private static func promptTitle(
_ app: ghostty_app_t,
target: ghostty_target_s) -> Bool {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("set title prompt does nothing with an app target")
return false
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return false }
surfaceView.promptTitle()
default:
assertionFailure()
}
return true
}
private static func pwdChanged(
_ app: ghostty_app_t,
target: ghostty_target_s,

View File

@ -216,6 +216,8 @@ extension Ghostty {
.nonNative
case "visible-menu":
.nonNativeVisibleMenu
case "padded-notch":
.nonNativePaddedNotch
default:
defaultValue
}
@ -300,6 +302,16 @@ extension Ghostty {
return buffer.map { .init(ghostty: $0) }
}
var macosHidden: MacHidden {
guard let config = self.config else { return .never }
var v: UnsafePointer<Int8>? = nil
let key = "macos-hidden"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .never }
guard let ptr = v else { return .never }
let str = String(cString: ptr)
return MacHidden(rawValue: str) ?? .never
}
var focusFollowsMouse : Bool {
guard let config = self.config else { return false }
var v = false;
@ -514,6 +526,11 @@ extension Ghostty.Config {
case download
}
enum MacHidden : String {
case never
case always
}
enum ResizeOverlay : String {
case always
case never

View File

@ -38,6 +38,15 @@ extension Ghostty {
}
}
/// Returns true if the tree is split.
var isSplit: Bool {
return if case .leaf = self {
false
} else {
true
}
}
func topLeft() -> SurfaceView {
switch (self) {
case .leaf(let leaf):
@ -120,14 +129,7 @@ extension Ghostty {
/// Returns true if the split tree contains the given view.
func contains(view: SurfaceView) -> Bool {
switch (self) {
case .leaf(let leaf):
return leaf.surface == view
case .split(let container):
return container.topLeft.contains(view: view) ||
container.bottomRight.contains(view: view)
}
return leaf(for: view) != nil
}
/// Find a surface view by UUID.
@ -164,6 +166,22 @@ extension Ghostty {
}
}
/// Return the node for the given view if its in the tree.
func leaf(for view: SurfaceView) -> Leaf? {
switch (self) {
case .leaf(let leaf):
if leaf.surface == view {
return leaf
} else {
return nil
}
case .split(let container):
return container.topLeft.leaf(for: view) ??
container.bottomRight.leaf(for: view)
}
}
// MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> {

View File

@ -198,6 +198,14 @@ extension Ghostty {
/// macos-icon
enum MacOSIcon: String {
case official
case blueprint
case chalkboard
case glass
case holographic
case microchip
case paper
case retro
case xray
case customStyle = "custom-style"
}

View File

@ -124,6 +124,11 @@ extension Ghostty {
// A timer to fallback to ghost emoji if no title is set within the grace period
private var titleFallbackTimer: 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
// by the user, this is set to the prior value (which may be empty, but non-nil).
private var titleFromTerminal: String?
/// Event monitor (see individual events for why)
private var eventMonitor: Any? = nil
@ -380,6 +385,45 @@ extension Ghostty {
NSCursor.setHiddenUntilMouseMoves(!visible)
}
/// Set the title by prompting the user.
func promptTitle() {
// Create an alert dialog
let alert = NSAlert()
alert.messageText = "Change Terminal Title"
alert.informativeText = "Leave blank to restore the default."
alert.alertStyle = .informational
// Add a text field to the alert
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24))
textField.stringValue = title
alert.accessoryView = textField
// Add buttons
alert.addButton(withTitle: "OK")
alert.addButton(withTitle: "Cancel")
let response = alert.runModal()
// Check if the user clicked "OK"
if response == .alertFirstButtonReturn {
// Get the input text
let newTitle = textField.stringValue
if newTitle.isEmpty {
// Empty means that user wants the title to be set automatically
// We also need to reload the config for the "title" property to be
// used again by this tab.
let prevTitle = titleFromTerminal ?? "👻"
titleFromTerminal = nil
setTitle(prevTitle)
} else {
// Set the title and prevent it from being changed automatically
titleFromTerminal = title
title = newTitle
}
}
}
func setTitle(_ title: String) {
// This fixes an issue where very quick changes to the title could
// cause an unpleasant flickering. We set a timer so that we can
@ -390,6 +434,11 @@ extension Ghostty {
withTimeInterval: 0.075,
repeats: false
) { [weak self] _ in
// Set the title if it wasn't manually set.
guard self?.titleFromTerminal == nil else {
self?.titleFromTerminal = title
return
}
self?.title = title
}
}
@ -849,28 +898,8 @@ extension Ghostty {
var handled: Bool = false
if let list = keyTextAccumulator, list.count > 0 {
handled = true
// This is a hack. libghostty on macOS treats ctrl input as not having
// text because some keyboard layouts generate bogus characters for
// ctrl+key. libghostty can't tell this is from an IM keyboard giving
// us direct values. So, we just remove control.
var modifierFlags = event.modifierFlags
modifierFlags.remove(.control)
if let keyTextEvent = NSEvent.keyEvent(
with: .keyDown,
location: event.locationInWindow,
modifierFlags: modifierFlags,
timestamp: event.timestamp,
windowNumber: event.windowNumber,
context: nil,
characters: event.characters ?? "",
charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
isARepeat: event.isARepeat,
keyCode: event.keyCode
) {
for text in list {
_ = keyAction(action, event: keyTextEvent, text: text)
}
for text in list {
_ = keyAction(action, event: event, text: text)
}
}
@ -1132,11 +1161,15 @@ extension Ghostty {
menu.addItem(.separator())
menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Split Left", action: #selector(splitLeft(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Split Down", action: #selector(splitDown(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "")
return menu
}
@ -1189,11 +1222,21 @@ extension Ghostty {
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)
}
@IBAction func splitLeft(_ sender: Any) {
guard let surface = self.surface else { return }
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_LEFT)
}
@IBAction func splitDown(_ sender: Any) {
guard let surface = self.surface else { return }
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_DOWN)
}
@IBAction func splitUp(_ sender: Any) {
guard let surface = self.surface else { return }
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_UP)
}
@objc func resetTerminal(_ sender: Any) {
guard let surface = self.surface else { return }
let action = "reset"
@ -1210,6 +1253,10 @@ extension Ghostty {
}
}
@IBAction func changeTitle(_ sender: Any) {
promptTitle()
}
/// Show a user notification and associate it with this surface
func showUserNotification(title: String, body: String) {
let content = UNMutableNotificationContent()

View File

@ -6,6 +6,7 @@ enum FullscreenMode {
case native
case nonNative
case nonNativeVisibleMenu
case nonNativePaddedNotch
/// Initializes the fullscreen style implementation for the mode. This will not toggle any
/// fullscreen properties. This may fail if the window isn't configured properly for a given
@ -20,6 +21,9 @@ enum FullscreenMode {
case .nonNativeVisibleMenu:
return NonNativeFullscreenVisibleMenu(window)
case .nonNativePaddedNotch:
return NonNativeFullscreenPaddedNotch(window)
}
}
}
@ -141,6 +145,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
struct Properties {
var hideMenu: Bool = true
var paddedNotch: Bool = false
}
private var savedState: SavedState?
@ -278,6 +283,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// put an #available check, but it was in a bug fix release so I think
// if a bug is reported to Ghostty we can just advise the user to
// update.
} else if (properties.paddedNotch) {
// We are hiding the menu, we may need to avoid the notch.
frame.size.height -= screen.safeAreaInsets.top
}
return frame
@ -349,3 +357,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
class NonNativeFullscreenVisibleMenu: NonNativeFullscreen {
override var properties: Properties { Properties(hideMenu: false) }
}
class NonNativeFullscreenPaddedNotch: NonNativeFullscreen {
override var properties: Properties { Properties(paddedNotch: true) }
}

View File

@ -0,0 +1,34 @@
import Cocoa
/// Manages the persistence and restoration of window positions across app launches.
class LastWindowPosition {
static let shared = LastWindowPosition()
private let positionKey = "NSWindowLastPosition"
func save(_ window: NSWindow) {
let origin = window.frame.origin
let point = [origin.x, origin.y]
UserDefaults.standard.set(point, forKey: positionKey)
}
func restore(_ window: NSWindow) -> Bool {
guard let points = UserDefaults.standard.array(forKey: positionKey) as? [Double],
points.count == 2 else { return false }
let lastPosition = CGPoint(x: points[0], y: points[1])
guard let screen = window.screen ?? NSScreen.main else { return false }
let visibleFrame = screen.visibleFrame
var newFrame = window.frame
newFrame.origin = lastPosition
if !visibleFrame.contains(newFrame.origin) {
newFrame.origin.x = max(visibleFrame.minX, min(visibleFrame.maxX - newFrame.width, newFrame.origin.x))
newFrame.origin.y = max(visibleFrame.minY, min(visibleFrame.maxY - newFrame.height, newFrame.origin.y))
}
window.setFrame(newFrame, display: true)
return true
}
}

View File

@ -1,63 +0,0 @@
#!/usr/bin/env bash
# Nothing in this script should fail.
set -e
CACHE_HASH_FILE="$(realpath "$(dirname "$0")/../zigCacheHash.nix")"
help() {
echo ""
echo "To fix, please (manually) re-run the script from the repository root,"
echo "commit, and push the update:"
echo ""
echo " ./nix/build-support/check-zig-cache-hash.sh --update"
echo " git add nix/zigCacheHash.nix"
echo " git commit -m \"nix: update Zig cache hash\""
echo " git push"
echo ""
}
if [ -f "${CACHE_HASH_FILE}" ]; then
OLD_CACHE_HASH="$(nix eval --raw --file "${CACHE_HASH_FILE}")"
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: Zig cache hash file missing."
help
exit 1
fi
ZIG_GLOBAL_CACHE_DIR="$(mktemp --directory --suffix nix-zig-cache)"
export ZIG_GLOBAL_CACHE_DIR
# This is not 100% necessary in CI but is helpful when running locally to keep
# a local workstation clean.
trap 'rm -rf "${ZIG_GLOBAL_CACHE_DIR}"' EXIT
# Run Zig and download the cache to the temporary directory.
sh ./nix/build-support/fetch-zig-cache.sh
# Now, calculate the hash.
ZIG_CACHE_HASH="sha256-$(nix-hash --type sha256 --to-base64 "$(nix-hash --type sha256 "${ZIG_GLOBAL_CACHE_DIR}")")"
if [ "${OLD_CACHE_HASH}" == "${ZIG_CACHE_HASH}" ]; then
echo -e "\nOK: Zig cache store hash unchanged."
exit 0
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: The Zig cache store hash has updated."
echo ""
echo " * Old hash: ${OLD_CACHE_HASH}"
echo " * New hash: ${ZIG_CACHE_HASH}"
help
exit 1
else
echo -e "\nNew Zig cache store hash: ${ZIG_CACHE_HASH}"
fi
# Write out the cache file
cat > "${CACHE_HASH_FILE}" <<EOS
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"${ZIG_CACHE_HASH}"
EOS
echo -e "\nOK: Wrote new hash to file: ${CACHE_HASH_FILE}"

View File

@ -0,0 +1,78 @@
#!/usr/bin/env bash
#
# This script checks if the build.zig.zon.nix 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:
#
# - build.zig.zon.nix
# - build.zig.zon.txt
# - build.zig.zon2json-lock
#
# All of these are auto-generated and should not be edited manually.
# Nothing in this script should fail.
set -e
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 " ./nix/build-support/check-zig-cache-hash.sh --update"
echo " git add build.zig.zon.nix"
echo " git commit -m \"nix: update build.zig.zon.nix\""
echo ""
}
ROOT="$(realpath "$(dirname "$0")/../../")"
BUILD_ZIG_ZON="$ROOT/build.zig.zon"
BUILD_ZIG_ZON_LOCK="$ROOT/build.zig.zon2json-lock"
BUILD_ZIG_ZON_NIX="$ROOT/build.zig.zon.nix"
BUILD_ZIG_ZON_TXT="$ROOT/build.zig.zon.txt"
if [ -f "${BUILD_ZIG_ZON_NIX}" ]; then
OLD_HASH=$(sha512sum "${BUILD_ZIG_ZON_NIX}" | awk '{print $1}')
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: build.zig.zon.nix missing."
help
exit 1
fi
rm -f "$BUILD_ZIG_ZON_LOCK"
zon2nix "$BUILD_ZIG_ZON" > "$WORK_DIR/build.zig.zon.nix"
alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
NEW_HASH=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}')
if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then
echo -e "\nOK: build.zig.zon.nix unchanged."
exit 0
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: build.zig.zon.nix needs to be updated."
echo ""
echo " * Old hash: ${OLD_HASH}"
echo " * New hash: ${NEW_HASH}"
help
exit 1
else
jq -r '.[] .url' "$BUILD_ZIG_ZON_LOCK" | sort > "$BUILD_ZIG_ZON_TXT"
mv "$WORK_DIR/build.zig.zon.nix" "$BUILD_ZIG_ZON_NIX"
echo -e "\nOK: build.zig.zon.nix updated."
exit 0
fi

View File

@ -1,32 +1,13 @@
#!/bin/sh
set -e
# Because Zig does not fetch recursive dependencies when you run `zig build
# --fetch` (see https://github.com/ziglang/zig/issues/20976) we need to do some
# extra work to fetch everything that we actually need to build without Internet
# access (such as when building a Nix package).
# NOTE THIS IS A TEMPORARY SCRIPT TO SUPPORT PACKAGE MAINTAINERS.
#
# An example of this happening:
# A future Zig version will hopefully fix the issue where
# `zig build --fetch` doesn't fetch transitive dependencies[1]. When that
# is resolved, we won't need any special machinery for the general use case
# at all and packagers can just use `zig build --fetch`.
#
# error: builder for '/nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv' failed with exit code 1;
# la/build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:7:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
# > .url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# > /build/tmp.xgHOheUF7V/p/12208cfdda4d5fdbc81b0c44b82e4d6dba2d4a86bff644a153e026fdfc80f8469133/build.zig.zon:16:20: error: unable to discover remote git server capabilities: TemporaryNameServerFailure
# > .url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b",
# > ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# >
# For full logs, run 'nix log /nix/store/cx8qcwrhjmjxik2547fw99v5j6np5san-ghostty-0.1.0.drv'.
#
# To update this script, add any failing URLs with a line like this:
#
# zig fetch <url>
#
# Periodically old URLs may need to be cleaned out.
#
# Hopefully when the Zig issue is fixed this script can be eliminated in favor
# of a plain `zig build --fetch`.
# [1]: https://github.com/ziglang/zig/issues/20976
if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ]
then
@ -34,6 +15,13 @@ then
exit 1
fi
zig build --fetch
zig fetch git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e
zig fetch git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b
# Go through each line of our build.zig.zon.txt and fetch it.
SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
ZON_TXT_FILE="$SCRIPT_PATH/../../build.zig.zon.txt"
while IFS= read -r url; do
echo "Fetching: $url"
zig fetch "$url" >/dev/null 2>&1 || {
echo "Failed to fetch: $url" >&2
exit 1
}
done < "$ZON_TXT_FILE"

View File

@ -0,0 +1,30 @@
#!/bin/sh
#
# This script generates a directory that can be uploaded to blob
# storage to mirror our dependencies. The dependencies are unmodified
# so their checksum and content hashes will match.
set -e # Exit immediately if a command exits with a non-zero status
SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
INPUT_FILE="$SCRIPT_PATH/../../build.zig.zon2json-lock"
OUTPUT_DIR="blob"
# Ensure the output directory exists
mkdir -p "$OUTPUT_DIR"
# Use jq to iterate over the JSON and download files
jq -r 'to_entries[] | "\(.key) \(.value.name) \(.value.url)"' "$INPUT_FILE" | while read -r key name url; do
# Skip URLs that don't start with http(s). They aren't necessary for
# our mirror.
if ! echo "$url" | grep -Eq "^https?://"; then
continue
fi
# Extract the file extension from the URL
extension=$(echo "$url" | grep -oE '\.[a-z0-9]+(\.[a-z0-9]+)?$')
filename="${name}-${key}${extension}"
echo "$url -> $filename"
curl -L -o "$OUTPUT_DIR/$filename" "$url"
done

View File

@ -14,6 +14,7 @@
python3,
qemu,
scdoc,
snapcraft,
valgrind,
#, vulkan-loader # unused
vttest,
@ -30,7 +31,9 @@
glib,
glslang,
gtk4,
gobject-introspection,
libadwaita,
blueprint-compiler,
adwaita-icon-theme,
hicolor-icon-theme,
harfbuzz,
@ -47,6 +50,7 @@
simdutf,
zlib,
alejandra,
jq,
minisign,
pandoc,
hyperfine,
@ -54,6 +58,8 @@
wayland,
wayland-scanner,
wayland-protocols,
zig2nix,
system,
}: let
# See package.nix. Keep in sync.
rpathLibs =
@ -83,6 +89,7 @@
libadwaita
gtk4
glib
gobject-introspection
wayland
];
in
@ -92,6 +99,7 @@ in
packages =
[
# For builds
jq
llvmPackages_latest.llvm
minisign
ncurses
@ -100,6 +108,7 @@ in
scdoc
zig
zip
zig2nix.packages.${system}.zon2nix
# For web and wasm stuff
nodejs
@ -129,6 +138,7 @@ in
qemu
gdb
snapcraft
valgrind
wraptest
@ -154,9 +164,11 @@ in
libXrandr
# Only needed for GTK builds
blueprint-compiler
libadwaita
gtk4
glib
gobject-introspection
wayland
wayland-scanner
wayland-protocols

View File

@ -2,6 +2,7 @@
lib,
stdenv,
bzip2,
callPackage,
expat,
fontconfig,
freetype,
@ -12,7 +13,9 @@
libGL,
glib,
gtk4,
gobject-introspection,
libadwaita,
blueprint-compiler,
wrapGAppsHook4,
gsettings-desktop-schemas,
git,
@ -42,80 +45,34 @@
zig_hook = zig_0_13.hook.overrideAttrs {
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
};
# We limit source like this to try and reduce the amount of rebuilds as possible
# thus we only provide the source that is needed for the build
#
# NOTE: as of the current moment only linux files are provided,
# since darwin support is not finished
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
lib.fileset.unions [
../dist/linux
../images
../include
../pkg
../src
../vendor
../build.zig
../build.zig.zon
./build-support/fetch-zig-cache.sh
]
);
};
# This hash is the computation of the zigCache fixed-output derivation. This
# allows us to use remote package dependencies without breaking the sandbox.
#
# This will need updating whenever dependencies get updated (e.g. changes are
# made to zig.build.zon). If you see that the main build is trying to reach
# out to the internet and failing, this is likely the cause. Change this
# value back to lib.fakeHash, and re-run. The build failure should emit the
# updated hash, which of course, should be validated before updating here.
#
# (It's also possible that you might see a hash mismatch - without the
# network errors - if you don't have a previous instance of the cache
# derivation in your store already. If so, just update the value as above.)
zigCacheHash = import ./zigCacheHash.nix;
zigCache = stdenv.mkDerivation {
inherit src;
name = "ghostty-cache";
nativeBuildInputs = [
git
zig_hook
];
dontConfigure = true;
dontUseZigBuild = true;
dontUseZigInstall = true;
dontFixup = true;
buildPhase = ''
runHook preBuild
sh ./nix/build-support/fetch-zig-cache.sh
runHook postBuild
'';
installPhase = ''
runHook preInstall
cp -r --reflink=auto $ZIG_GLOBAL_CACHE_DIR $out
runHook postInstall
'';
outputHashMode = "recursive";
outputHash = zigCacheHash;
};
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.1.1";
inherit src;
version = "1.1.3";
# We limit source like this to try and reduce the amount of rebuilds as possible
# thus we only provide the source that is needed for the build
#
# NOTE: as of the current moment only linux files are provided,
# since darwin support is not finished
src = lib.fileset.toSource {
root = ../.;
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
lib.fileset.unions [
../dist/linux
../images
../include
../pkg
../src
../vendor
../build.zig
../build.zig.zon
../build.zig.zon.nix
]
);
};
deps = callPackage ../build.zig.zon.nix {name = "ghostty-cache-${finalAttrs.version}";};
nativeBuildInputs =
[
@ -124,7 +81,9 @@ in
pandoc
pkg-config
zig_hook
gobject-introspection
wrapGAppsHook4
blueprint-compiler
]
++ lib.optionals enableWayland [
wayland-scanner
@ -164,7 +123,7 @@ in
zigBuildFlags = [
"--system"
"${zigCache}/p"
"${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dgtk-x11=${lib.boolToString enableX11}"
"-Dgtk-wayland=${lib.boolToString enableWayland}"

View File

@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-I7uuv0MkaW3gWAw6NHci+II42OfM7NdtKh2Npw2pTis="
"sha256-S8kS+gO17dl9LJGKL1+kgDUre+vPTmdTvXzgc585Fl8="

View File

@ -6,7 +6,8 @@
// This should be kept in sync with the submodule in the cimgui source
// code in ./vendor/ to be safe that they're compatible.
.imgui = .{
.url = "https://github.com/ocornut/imgui/archive/e391fe2e66eb1c96b1624ae8444dc64c23146ef4.tar.gz",
// ocornut/imgui
.url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
.hash = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402",
},

View File

@ -3,8 +3,9 @@
.version = "2.13.2",
.paths = .{""},
.dependencies = .{
// freetype/freetype
.freetype = .{
.url = "https://github.com/freetype/freetype/archive/refs/tags/VER-2-13-2.tar.gz",
.url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz",
.hash = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d",
},

View File

@ -3,8 +3,9 @@
.version = "14.2.0",
.paths = .{""},
.dependencies = .{
// KhronosGroup/glslang
.glslang = .{
.url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/14.2.0.tar.gz",
.url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
.hash = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1",
},

View File

@ -3,8 +3,9 @@
.version = "8.4.0",
.paths = .{""},
.dependencies = .{
// harfbuzz/harfbuzz
.harfbuzz = .{
.url = "https://github.com/harfbuzz/harfbuzz/archive/refs/tags/8.4.0.tar.gz",
.url = "https://deps.files.ghostty.org/harfbuzz-1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122.tar.gz",
.hash = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122",
},

View File

@ -3,8 +3,9 @@
.version = "1.1.0",
.paths = .{""},
.dependencies = .{
// google/highway
.highway = .{
.url = "https://github.com/google/highway/archive/refs/tags/1.1.0.tar.gz",
.url = "https://deps.files.ghostty.org/highway-12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b.tar.gz",
.hash = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b",
},

View File

@ -3,8 +3,9 @@
.version = "1.6.43",
.paths = .{""},
.dependencies = .{
// glennrp/libpng
.libpng = .{
.url = "https://github.com/glennrp/libpng/archive/refs/tags/v1.6.43.tar.gz",
.url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
.hash = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66",
},

View File

@ -3,8 +3,9 @@
.version = "6.9.9",
.paths = .{""},
.dependencies = .{
// kkos/oniguruma
.oniguruma = .{
.url = "https://github.com/kkos/oniguruma/archive/refs/tags/v6.9.9.tar.gz",
.url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz",
.hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb",
},

View File

@ -3,8 +3,9 @@
.version = "0.7.8",
.paths = .{""},
.dependencies = .{
// getsentry/sentry-native
.sentry = .{
.url = "https://github.com/getsentry/sentry-native/archive/refs/tags/0.7.8.tar.gz",
.url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz",
.hash = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e",
},

View File

@ -3,8 +3,9 @@
.version = "13.1.1",
.paths = .{""},
.dependencies = .{
// KhronosGroup/SPIRV-Cross
.spirv_cross = .{
.url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/476f384eb7d9e48613c45179e502a15ab95b6b49.tar.gz",
.url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz",
.hash = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da",
},

View File

@ -3,8 +3,9 @@
.version = "4.0.5",
.paths = .{""},
.dependencies = .{
// nemtrif/utfcpp
.utfcpp = .{
.url = "https://github.com/nemtrif/utfcpp/archive/refs/tags/v4.0.5.tar.gz",
.url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz",
.hash = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641",
},

View File

@ -2,13 +2,15 @@
.name = "wuffs",
.version = "0.0.0",
.dependencies = .{
// google/wuffs
.wuffs = .{
.url = "https://github.com/google/wuffs/archive/refs/tags/v0.4.0-alpha.9.tar.gz",
.url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
.hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd",
},
// make-github-pseudonymous-again/pixels
.pixels = .{
.url = "git+https://github.com/make-github-pseudonymous-again/pixels?ref=main#d843c2714d32e15b48b8d7eeb480295af537f877",
.url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz",
.hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
},

View File

@ -3,8 +3,9 @@
.version = "1.3.1",
.paths = .{""},
.dependencies = .{
// madler/zlib
.zlib = .{
.url = "https://github.com/madler/zlib/archive/refs/tags/v1.3.1.tar.gz",
.url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz",
.hash = "1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb",
},

51
snap/local/launcher Executable file
View File

@ -0,0 +1,51 @@
#!/bin/sh
set -e
# Set these to reasonable defaults if not already set
if [ -z "$XDG_CONFIG_HOME" ]; then
export XDG_CONFIG_HOME="$SNAP_REAL_HOME/.config"
fi
if [ -z "$XDG_CACHE_HOME" ]; then
export XDG_CACHE_HOME="$SNAP_REAL_HOME/.cache"
fi
if [ -z "$XDG_DATA_HOME" ]; then
export XDG_DATA_HOME="$SNAP_REAL_HOME/.local/share"
fi
export HOME="$SNAP_REAL_HOME"
if [ "$SNAP_ARCH" = "amd64" ]; then
ARCH="x86_64-linux-gnu"
elif [ "$SNAP_ARCH" = "arm64" ]; then
ARCH="aarch64-linux-gnu"
else
ARCH="$SNAP_ARCH-linux-gnu"
fi
export LD_LIBRARY_PATH=${SNAP}/usr/lib/${ARCH}:${SNAP}/usr/lib/${ARCH}/vdpau:${LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}
export LIBGL_DRIVERS_PATH=${LIBGL_DRIVERS_PATH:+$LIBGL_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/
export LIBVA_DRIVERS_PATH=${LIBVA_DRIVERS_PATH:+$LIBVA_DRIVERS_PATH:}${SNAP}/usr/lib/${ARCH}/dri/
export __EGL_VENDOR_LIBRARY_DIRS=${__EGL_VENDOR_LIBRARY_DIRS:+$__EGL_VENDOR_LIBRARY_DIRS:}${SNAP}/usr/share/glvnd/egl_vendor.d
export __EGL_EXTERNAL_PLATFORM_CONFIG_DIRS=${__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:+$__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS:}${SNAP}/usr/share/egl/egl_external_platform.d
export DRIRC_CONFIGDIR=${SNAP}/usr/share/drirc.d
export VK_LAYER_PATH=${VK_LAYER_PATH:+$VK_LAYER_PATH:}${SNAP}/usr/share/vulkan/implicit_layer.d/:${SNAP}/usr/share/vulkan/explicit_layer.d/
export XDG_DATA_DIRS=${XDG_DATA_DIRS:+$XDG_DATA_DIRS:}${SNAP}/usr/share
export XLOCALEDIR="${SNAP}/usr/share/X11/locale"
export GDK_PIXBUF_MODULEDIR="$SNAP/usr/lib/$ARCH/gdk-pixbuf-2.0/2.10.0/loaders"
export GTK_PATH="$SNAP/usr/lib/$ARCH/gtk-4.0"
if [ "${__NV_PRIME_RENDER_OFFLOAD:-}" != 1 ]; then
# Prevent picking VA-API (Intel/AMD) over NVIDIA VDPAU
# https://download.nvidia.com/XFree86/Linux-x86_64/510.54/README/primerenderoffload.html#configureapplications
export LIBVA_DRIVERS_PATH
fi
# Unset all SNAP specific environment variables to keep them from leaking
# into other snaps that might get executed from within the shell
for var in $(printenv | grep SNAP_ | cut -d= -f1); do
unset $var
done
exec "$@"

139
snap/snapcraft.yaml Normal file
View File

@ -0,0 +1,139 @@
name: ghostty
base: core24
summary: A terminal emulator
description: |
Ghostty is a fast, feature-rich, and cross-platform terminal emulator that
uses platform-native UI and GPU acceleration.
grade: stable
confinement: classic
contact: https://github.com/ghostty-org/ghostty/discussions
issues: https://github.com/ghostty-org/ghostty/issues
website: https://ghostty.org
license: MIT
icon: images/icons/icon_512.png
adopt-info: ghostty
platforms:
amd64:
arm64:
apps:
ghostty:
command: bin/ghostty
command-chain: [bin/launcher]
completer: share/bash-completion/completions/ghostty.bash
desktop: share/applications/com.mitchellh.ghostty.desktop
#refresh-mode: ignore-running # Store rejects this, needs fix in review-tools
environment:
PATH: /snap/ghostty/current/bin:/snap/ghostty/current/usr/bin:$PATH
LC_ALL: C.UTF-8
GHOSTTY_RESOURCES_DIR: /snap/ghostty/current/share/ghostty
parts:
launcher:
plugin: dump
source: snap/local
source-type: local
organize:
launcher: bin/
zig:
plugin: nil
build-packages:
- curl
override-pull: |
set -ex
case "$CRAFT_ARCH_BUILD_FOR" in
amd64) arch=x86_64 ;;
arm64) arch=aarch64 ;;
*) arch="" ;;
esac
rm -rf $CRAFT_PART_SRC/*
if [[ -n $arch ]]; then
curl -LO --retry-connrefused --retry 10 https://ziglang.org/download/0.13.0/zig-linux-$arch-0.13.0.tar.xz
else
echo "Unsupported arch"
exit 1
fi
tar xf zig-lin*xz
rm -f *xz
mv zig-linux*/* .
prime:
- -*
ghostty:
source: .
after: [zig]
plugin: nil
build-attributes: [enable-patchelf]
build-packages:
- libgtk-4-dev
- libadwaita-1-dev
- git
- patchelf
override-build: |
craftctl set version=$(git describe --abbrev=8)
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast
cp -rp zig-out/* $CRAFT_PART_INSTALL/
sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
libs:
plugin: nil
build-attributes: [enable-patchelf]
stage-packages:
- libadwaita-1-0
- libglib2.0-0t64
- libgtk-4-1
- libgtk-4-media-gstreamer
- ibus-gtk4
- libpciaccess0
- libtinfo6
- libedit2
- libelf1t64
- libsensors5
- libllvm17
- libunistring5
- librsvg2-2
- on amd64:
[
i965-va-driver,
libdrm-intel1,
libdrm-nouveau2,
libdrm-amdgpu1,
libdrm-radeon1,
]
stage:
# The libraries in dri need no-patchelf, so they come from the mesa-unpatched part
- -usr/lib/*/dri
mesa:
plugin: nil
build-attributes: [enable-patchelf]
stage-packages:
- libglu1-mesa
- libgl1-mesa-dri
- libegl-mesa0
- libegl1
- libglx-mesa0
- mesa-libgallium
stage:
# The libraries in dri need no-patchelf, so they come from the mesa-unpatched part
- usr/lib/*/*.so*
- usr/lib/*/dri/libdril_dri.so
- -usr/lib/*/libgallium*so
- -usr/lib/*/dri
mesa-gl1-dri:
plugin: nil
stage-packages:
- libgl1-mesa-dri
build-attributes: [no-patchelf]
stage:
# Only the libraries in dri need to not be patched, the rest come from the mesa part
# Otherwise snapcraft may strip the build ID and cause the driver to crash
- usr/lib/*/libgallium*so
- -usr/lib/*/dri/libdril_dri.so
- usr/lib/*/dri

View File

@ -161,7 +161,7 @@ pub fn updateConfig(self: *App, rt_app: *apprt.App, config: *const Config) !void
const applied: *const configpkg.Config = if (applied_) |*c| c else config;
// Notify the apprt that the app has changed configuration.
try rt_app.performAction(
_ = try rt_app.performAction(
.app,
.config_change,
.{ .config = applied },
@ -180,7 +180,7 @@ pub fn addSurface(
// Since we have non-zero surfaces, we can cancel the quit timer.
// It is up to the apprt if there is a quit timer at all and if it
// should be canceled.
rt_surface.app.performAction(
_ = rt_surface.app.performAction(
.app,
.quit_timer,
.stop,
@ -214,7 +214,7 @@ pub fn deleteSurface(self: *App, rt_surface: *apprt.Surface) void {
// If we have no surfaces, we can start the quit timer. It is up to the
// apprt to determine if this is necessary.
if (self.surfaces.items.len == 0) rt_surface.app.performAction(
if (self.surfaces.items.len == 0) _ = rt_surface.app.performAction(
.app,
.quit_timer,
.start,
@ -294,7 +294,7 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
break :target .app;
};
try rt_app.performAction(
_ = try rt_app.performAction(
target,
.new_window,
{},
@ -419,7 +419,7 @@ pub fn colorSchemeEvent(
// Request our configuration be reloaded because the new scheme may
// impact the colors of the app.
try rt_app.performAction(
_ = try rt_app.performAction(
.app,
.reload_config,
.{ .soft = true },
@ -437,13 +437,13 @@ pub fn performAction(
switch (action) {
.unbind => unreachable,
.ignore => {},
.quit => try rt_app.performAction(.app, .quit, {}),
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
.open_config => try rt_app.performAction(.app, .open_config, {}),
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
.toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}),
.quit => _ = try rt_app.performAction(.app, .quit, {}),
.new_window => _ = try self.newWindow(rt_app, .{ .parent = null }),
.open_config => _ = try rt_app.performAction(.app, .open_config, {}),
.reload_config => _ = try rt_app.performAction(.app, .reload_config, .{}),
.close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}),
.toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}),
}
}

View File

@ -519,9 +519,19 @@ pub fn init(
// This separate block ({}) is important because our errdefers must
// be scoped here to be valid.
{
var env = rt_surface.defaultTermioEnv() catch |err| env: {
// If an error occurs, we don't want to block surface startup.
log.warn("error getting env map for surface err={}", .{err});
break :env internal_os.getEnvMap(alloc) catch
std.process.EnvMap.init(alloc);
};
errdefer env.deinit();
// Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{
.command = command,
.env = env,
.env_override = config.env,
.shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory",
@ -561,7 +571,7 @@ pub fn init(
errdefer self.io.deinit();
// Report initial cell size on surface creation
try rt_app.performAction(
_ = try rt_app.performAction(
.{ .surface = self },
.cell_size,
.{ .width = size.cell.width, .height = size.cell.height },
@ -573,7 +583,7 @@ pub fn init(
const min_window_width_cells: u32 = 10;
const min_window_height_cells: u32 = 4;
try rt_app.performAction(
_ = try rt_app.performAction(
.{ .surface = self },
.size_limit,
.{
@ -637,7 +647,7 @@ pub fn init(
size.padding.top +
size.padding.bottom;
rt_app.performAction(
_ = rt_app.performAction(
.{ .surface = self },
.initial_size,
.{ .width = final_width, .height = final_height },
@ -649,7 +659,7 @@ pub fn init(
}
if (config.title) |title| {
try rt_app.performAction(
_ = try rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
@ -670,7 +680,7 @@ pub fn init(
break :xdg;
};
defer alloc.free(title);
try rt_app.performAction(
_ = try rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
@ -823,7 +833,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
// We know that our title should end in 0.
const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0);
log.debug("changing title \"{s}\"", .{slice});
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = slice },
@ -859,7 +869,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.color_change => |change| {
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
@ -878,7 +888,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.set_mouse_shape => |shape| {
log.debug("changing mouse shape: {}", .{shape});
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
shape,
@ -910,7 +920,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
const str = try self.alloc.dupeZ(u8, w.slice());
defer self.alloc.free(str);
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.pwd,
.{ .pwd = str },
@ -961,7 +971,7 @@ fn passwordInput(self: *Surface, v: bool) !void {
}
// Notify our apprt so it can do whatever it wants.
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.secure_input,
if (v) .on else .off,
@ -1050,7 +1060,7 @@ fn mouseRefreshLinks(
self.renderer_state.mouse.point = pos_vp;
self.mouse.over_link = true;
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
.pointer,
@ -1063,7 +1073,7 @@ fn mouseRefreshLinks(
.trim = false,
});
defer self.alloc.free(str);
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = str },
@ -1077,7 +1087,7 @@ fn mouseRefreshLinks(
log.warn("failed to get URI for OSC8 hyperlink", .{});
break :link;
};
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = uri },
@ -1087,12 +1097,12 @@ fn mouseRefreshLinks(
try self.queueRender();
} else if (over_link) {
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@ -1104,7 +1114,7 @@ fn mouseRefreshLinks(
/// Called when our renderer health state changes.
fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
log.warn("renderer health status change status={}", .{health});
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.renderer_health,
health,
@ -1116,7 +1126,7 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
/// This should be called anytime `config_conditional_state` changes
/// so that the apprt can reload the configuration.
fn notifyConfigConditionalState(self: *Surface) void {
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.reload_config,
.{ .soft = true },
@ -1196,14 +1206,14 @@ pub fn updateConfig(
// If we have a title set then we update our window to have the
// newly configured title.
if (config.title) |title| try self.rt_app.performAction(
if (config.title) |title| _ = try self.rt_app.performAction(
.{ .surface = self },
.set_title,
.{ .title = title },
);
// Notify the window
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.config_change,
.{ .config = config },
@ -1470,7 +1480,7 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
// Notify the window
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.cell_size,
.{ .width = size.width, .height = size.height },
@ -1766,12 +1776,12 @@ pub fn keyCallback(
};
} else if (self.io.terminal.flags.mouse_event != .none and !self.mouse.mods.shift) {
// If we have mouse reports on and we don't have shift pressed, we reset state
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@ -1789,7 +1799,7 @@ pub fn keyCallback(
.mods = self.mouse.mods,
.over_link = self.mouse.over_link,
.hidden = self.mouse.hidden,
}).keyToMouseShape()) |shape| try self.rt_app.performAction(
}).keyToMouseShape()) |shape| _ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
shape,
@ -1914,7 +1924,7 @@ fn maybeHandleBinding(
}
// Start or continue our key sequence
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.{ .trigger = entry.key_ptr.* },
@ -2023,7 +2033,7 @@ fn endKeySequence(
mem: KeySequenceMemory,
) void {
// Notify apprt key sequence ended
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.key_sequence,
.end,
@ -3359,12 +3369,12 @@ pub fn cursorPosCallback(
self.mouse.link_point = null;
if (self.mouse.over_link) {
self.mouse.over_link = false;
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
@ -3790,7 +3800,7 @@ fn scrollToBottom(self: *Surface) !void {
fn hideMouse(self: *Surface) void {
if (self.mouse.hidden) return;
self.mouse.hidden = true;
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.mouse_visibility,
.hidden,
@ -3802,7 +3812,7 @@ fn hideMouse(self: *Surface) void {
fn showMouse(self: *Surface) void {
if (!self.mouse.hidden) return;
self.mouse.hidden = false;
self.rt_app.performAction(
_ = self.rt_app.performAction(
.{ .surface = self },
.mouse_visibility,
.visible,
@ -4015,6 +4025,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size);
},
.prompt_surface_title => return try self.rt_app.performAction(
.{ .surface = self },
.prompt_title,
{},
),
.clear_screen => {
// This is a duplicate of some of the logic in termio.clearScreen
// but we need to do this here so we can know the answer before
@ -4093,13 +4109,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
v,
),
.new_tab => try self.rt_app.performAction(
.new_tab => return try self.rt_app.performAction(
.{ .surface = self },
.new_tab,
{},
),
.close_tab => try self.rt_app.performAction(
.close_tab => return try self.rt_app.performAction(
.{ .surface = self },
.close_tab,
{},
@ -4109,7 +4125,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.next_tab,
.last_tab,
.goto_tab,
=> |v, tag| try self.rt_app.performAction(
=> |v, tag| return try self.rt_app.performAction(
.{ .surface = self },
.goto_tab,
switch (tag) {
@ -4121,13 +4137,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
.move_tab => |position| try self.rt_app.performAction(
.move_tab => |position| return try self.rt_app.performAction(
.{ .surface = self },
.move_tab,
.{ .amount = position },
),
.new_split => |direction| try self.rt_app.performAction(
.new_split => |direction| return try self.rt_app.performAction(
.{ .surface = self },
.new_split,
switch (direction) {
@ -4142,7 +4158,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
.goto_split => |direction| try self.rt_app.performAction(
.goto_split => |direction| return try self.rt_app.performAction(
.{ .surface = self },
.goto_split,
switch (direction) {
@ -4153,7 +4169,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
.resize_split => |value| try self.rt_app.performAction(
.resize_split => |value| return try self.rt_app.performAction(
.{ .surface = self },
.resize_split,
.{
@ -4167,47 +4183,48 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
},
),
.equalize_splits => try self.rt_app.performAction(
.equalize_splits => return try self.rt_app.performAction(
.{ .surface = self },
.equalize_splits,
{},
),
.toggle_split_zoom => try self.rt_app.performAction(
.toggle_split_zoom => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_split_zoom,
{},
),
.toggle_maximize => try self.rt_app.performAction(
.toggle_maximize => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_maximize,
{},
),
.toggle_fullscreen => try self.rt_app.performAction(
.toggle_fullscreen => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_fullscreen,
switch (self.config.macos_non_native_fullscreen) {
.false => .native,
.true => .macos_non_native,
.@"visible-menu" => .macos_non_native_visible_menu,
.@"padded-notch" => .macos_non_native_padded_notch,
},
),
.toggle_window_decorations => try self.rt_app.performAction(
.toggle_window_decorations => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_window_decorations,
{},
),
.toggle_tab_overview => try self.rt_app.performAction(
.toggle_tab_overview => return try self.rt_app.performAction(
.{ .surface = self },
.toggle_tab_overview,
{},
),
.toggle_secure_input => try self.rt_app.performAction(
.toggle_secure_input => return try self.rt_app.performAction(
.{ .surface = self },
.secure_input,
.toggle,
@ -4221,7 +4238,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
}
},
.inspector => |mode| try self.rt_app.performAction(
.inspector => |mode| return try self.rt_app.performAction(
.{ .surface = self },
.inspector,
switch (mode) {
@ -4668,7 +4685,7 @@ fn showDesktopNotification(self: *Surface, title: [:0]const u8, body: [:0]const
self.app.last_notification_time = now;
self.app.last_notification_digest = new_digest;
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.desktop_notification,
.{
@ -4688,7 +4705,7 @@ fn crashThreadState(self: *Surface) crash.sentry.ThreadState {
/// Tell the surface to present itself to the user. This may involve raising the
/// window and switching tabs.
fn presentSurface(self: *Surface) !void {
try self.rt_app.performAction(
_ = try self.rt_app.performAction(
.{ .surface = self },
.present_terminal,
{},

View File

@ -158,9 +158,13 @@ pub const Action = union(Key) {
/// Show a desktop notification.
desktop_notification: DesktopNotification,
/// Set the title of the target.
/// Set the title of the target to the requested value.
set_title: SetTitle,
/// Set the title of the target to a prompted value. It is up to
/// the apprt to prompt.
prompt_title,
/// The current working directory has changed for the target terminal.
pwd: Pwd,
@ -254,6 +258,7 @@ pub const Action = union(Key) {
render_inspector,
desktop_notification,
set_title,
prompt_title,
pwd,
mouse_shape,
mouse_visibility,
@ -385,6 +390,7 @@ pub const Fullscreen = enum(c_int) {
/// window. This is much faster to enter and exit than the native mode.
macos_non_native,
macos_non_native_visible_menu,
macos_non_native_padded_notch,
};
pub const SecureInput = enum(c_int) {

View File

@ -12,6 +12,7 @@ const objc = @import("objc");
const apprt = @import("../apprt.zig");
const font = @import("../font/main.zig");
const input = @import("../input.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreApp = @import("../App.zig");
@ -45,7 +46,7 @@ pub const App = struct {
wakeup: *const fn (AppUD) callconv(.C) void,
/// Callback called to handle an action.
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) void,
action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) bool,
/// Read the clipboard value. The return value must be preserved
/// by the host until the next call. If there is no valid clipboard
@ -181,14 +182,9 @@ pub const App = struct {
if (strip) translate_mods.alt = false;
}
// On macOS we strip ctrl because UCKeyTranslate
// converts to the masked values (i.e. ctrl+c becomes 3)
// and we don't want that behavior.
//
// We also strip super because its not used for translation
// on macos and it results in a bad translation.
// We strip super on macOS because its not used for translation
// it results in a bad translation.
if (comptime builtin.target.isDarwin()) {
translate_mods.ctrl = false;
translate_mods.super = false;
}
@ -228,6 +224,7 @@ pub const App = struct {
const result: input.Keymap.Translation = if (event_text) |text| .{
.text = text,
.composing = event.composing,
.mods = translate_mods,
} else try self.keymap.translate(
&buf,
switch (target) {
@ -272,16 +269,12 @@ pub const App = struct {
// then we clear the text. We handle non-printables in the
// key encoder manual (such as tab, ctrl+c, etc.)
if (result.text.len == 1 and result.text[0] < 0x20) {
break :translate .{ .composing = false, .text = "" };
break :translate .{};
}
}
break :translate result;
} else .{ .composing = false, .text = "" };
// UCKeyTranslate always consumes all mods, so if we have any output
// then we've consumed our translate mods.
const consumed_mods: input.Mods = if (result.text.len > 0) translate_mods else .{};
} else .{};
// We need to always do a translation with no modifiers at all in
// order to get the "unshifted_codepoint" for the key event.
@ -353,7 +346,7 @@ pub const App = struct {
.key = key,
.physical_key = physical_key,
.mods = mods,
.consumed_mods = consumed_mods,
.consumed_mods = result.mods,
.composing = result.composing,
.utf8 = result.text,
.unshifted_codepoint = unshifted_codepoint,
@ -477,13 +470,14 @@ pub const App = struct {
surface.queueInspectorRender();
}
/// Perform a given action.
/// Perform a given action. Returns `true` if the action was able to be
/// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !void {
) !bool {
// Special case certain actions before they are sent to the
// embedded apprt.
self.performPreAction(target, action, value);
@ -493,7 +487,7 @@ pub const App = struct {
action,
value,
});
self.opts.action(
return self.opts.action(
self,
target.cval(),
@unionInit(apprt.Action, @tagName(action), value).cval(),
@ -1005,7 +999,7 @@ pub const Surface = struct {
}
fn queueInspectorRender(self: *Surface) void {
self.app.performAction(
_ = self.app.performAction(
.{ .surface = &self.core_surface },
.render_inspector,
{},
@ -1026,6 +1020,30 @@ pub const Surface = struct {
};
}
pub fn defaultTermioEnv(self: *const Surface) !std.process.EnvMap {
const alloc = self.app.core_app.alloc;
var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit();
if (comptime builtin.target.isDarwin()) {
if (env.get("__XCODE_BUILT_PRODUCTS_DIR_PATHS") != null) {
env.remove("__XCODE_BUILT_PRODUCTS_DIR_PATHS");
env.remove("__XPC_DYLD_LIBRARY_PATH");
env.remove("DYLD_FRAMEWORK_PATH");
env.remove("DYLD_INSERT_LIBRARIES");
env.remove("DYLD_LIBRARY_PATH");
env.remove("LD_LIBRARY_PATH");
env.remove("SECURITYSESSIONID");
env.remove("XPC_SERVICE_NAME");
}
// Remove this so that running `ghostty` within Ghostty works.
env.remove("GHOSTTY_MAC_APP");
}
return env;
}
/// The cursor position from the host directly is in screen coordinates but
/// all our interface works in pixels.
fn cursorPosToPixels(self: *const Surface, pos: apprt.CursorPos) !apprt.CursorPos {
@ -1432,7 +1450,7 @@ pub const CAPI = struct {
/// Open the configuration.
export fn ghostty_app_open_config(v: *App) void {
v.performAction(.app, .open_config, {}) catch |err| {
_ = v.performAction(.app, .open_config, {}) catch |err| {
log.err("error reloading config err={}", .{err});
return;
};
@ -1775,7 +1793,7 @@ pub const CAPI = struct {
/// Request that the surface split in the given direction.
export fn ghostty_surface_split(ptr: *Surface, direction: apprt.action.SplitDirection) void {
ptr.app.performAction(
_ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.new_split,
direction,
@ -1790,7 +1808,7 @@ pub const CAPI = struct {
ptr: *Surface,
direction: apprt.action.GotoSplit,
) void {
ptr.app.performAction(
_ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.goto_split,
direction,
@ -1809,7 +1827,7 @@ pub const CAPI = struct {
direction: apprt.action.ResizeSplit.Direction,
amount: u16,
) void {
ptr.app.performAction(
_ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.resize_split,
.{ .direction = direction, .amount = amount },
@ -1821,7 +1839,7 @@ pub const CAPI = struct {
/// Equalize the size of all splits in the current window.
export fn ghostty_surface_split_equalize(ptr: *Surface) void {
ptr.app.performAction(
_ = ptr.app.performAction(
.{ .surface = &ptr.core_surface },
.equalize_splits,
{},

View File

@ -147,13 +147,14 @@ pub const App = struct {
glfw.postEmptyEvent();
}
/// Perform a given action.
/// Perform a given action. Returns `true` if the action was able to be
/// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !void {
) !bool {
switch (action) {
.quit => self.quit = true,
@ -238,8 +239,14 @@ pub const App = struct {
.pwd,
.config_change,
.toggle_maximize,
=> log.info("unimplemented action={}", .{action}),
.prompt_title,
=> {
log.info("unimplemented action={}", .{action});
return false;
},
}
return true;
}
/// Reload the configuration. This should return the new configuration.
@ -874,6 +881,10 @@ pub const Surface = struct {
};
}
pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
return try internal_os.getEnvMap(self.app.app.alloc);
}
fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
_ = width;
_ = height;

View File

@ -25,7 +25,6 @@ const Config = configpkg.Config;
const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig");
const adwaita = @import("adwaita.zig");
const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
@ -109,6 +108,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
c.gtk_get_micro_version(),
});
// log the adwaita version
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
// Load our configuration
var config = try Config.load(core_app.alloc);
errdefer config.deinit();
@ -236,7 +243,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}
}
c.gtk_init();
c.adw_init();
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
// I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here.
@ -244,16 +252,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
std.posix.exit(1);
};
// If we're using libadwaita, log the version
if (adwaita.enabled(&config)) {
log.info("libadwaita version build={s} runtime={}.{}.{}", .{
c.ADW_VERSION_S,
c.adw_get_major_version(),
c.adw_get_minor_version(),
c.adw_get_micro_version(),
});
}
// The "none" cursor is used for hiding the cursor
const cursor_none = c.gdk_cursor_new_from_name("none", null);
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
@ -288,103 +286,38 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
};
// Create our GTK Application which encapsulates our process.
const app: *c.GtkApplication = app: {
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
app_id,
single_instance,
adwaita,
});
log.debug("creating GTK application id={s} single-instance={}", .{
app_id,
single_instance,
});
// If not libadwaita, create a standard GTK application.
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
!adwaita.enabled(&config))
{
{
const provider = c.gtk_css_provider_new();
defer c.g_object_unref(provider);
switch (config.@"window-theme") {
.system, .light => {},
.dark => {
const settings = c.gtk_settings_get_default();
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
// Using an AdwApplication lets us use Adwaita widgets and access things
// such as the color scheme.
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
errdefer c.g_object_unref(adw_app);
c.gtk_css_provider_load_from_resource(
provider,
"/com/mitchellh/ghostty/style-dark.css",
);
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
);
},
.auto, .ghostty => {
const lum = config.background.toTerminalRGB().perceivedLuminance();
if (lum <= 0.5) {
const settings = c.gtk_settings_get_default();
c.g_object_set(@ptrCast(@alignCast(settings)), "gtk-application-prefer-dark-theme", true, @as([*c]const u8, null));
c.gtk_css_provider_load_from_resource(
provider,
"/com/mitchellh/ghostty/style-dark.css",
);
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 2,
);
}
},
}
}
{
const provider = c.gtk_css_provider_new();
defer c.g_object_unref(provider);
c.gtk_css_provider_load_from_resource(provider, "/com/mitchellh/ghostty/style.css");
c.gtk_style_context_add_provider_for_display(
display,
@ptrCast(provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1,
);
}
break :app @as(?*c.GtkApplication, @ptrCast(c.gtk_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
}
// Use libadwaita if requested. Using an AdwApplication lets us use
// Adwaita widgets and access things such as the color scheme.
const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
app_id.ptr,
app_flags,
))) orelse return error.GtkInitFailed;
const style_manager = c.adw_application_get_style_manager(adw_app);
c.adw_style_manager_set_color_scheme(
style_manager,
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT
else
c.ADW_COLOR_SCHEME_PREFER_DARK;
},
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
const style_manager = c.adw_application_get_style_manager(adw_app);
c.adw_style_manager_set_color_scheme(
style_manager,
switch (config.@"window-theme") {
.auto, .ghostty => auto: {
const lum = config.background.toTerminalRGB().perceivedLuminance();
break :auto if (lum > 0.5)
c.ADW_COLOR_SCHEME_PREFER_LIGHT
else
c.ADW_COLOR_SCHEME_PREFER_DARK;
},
);
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
},
);
break :app @ptrCast(adw_app);
};
errdefer c.g_object_unref(app);
const gapp = @as(*c.GApplication, @ptrCast(app));
const app: *c.GtkApplication = @ptrCast(adw_app);
const gapp: *c.GApplication = @ptrCast(app);
// force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources
@ -507,13 +440,14 @@ pub fn terminate(self: *App) void {
self.config.deinit();
}
/// Perform a given action.
/// Perform a given action. Returns `true` if the action was able to be
/// performed, `false` otherwise.
pub fn performAction(
self: *App,
target: apprt.Target,
comptime action: apprt.Action.Key,
value: apprt.Action.Value(action),
) !void {
) !bool {
switch (action) {
.quit => self.quit(),
.new_window => _ = try self.newWindow(switch (target) {
@ -525,12 +459,12 @@ pub fn performAction(
.new_tab => try self.newTab(target),
.close_tab => try self.closeTab(target),
.goto_tab => self.gotoTab(target, value),
.goto_tab => return self.gotoTab(target, value),
.move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value),
.resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target),
.goto_split => self.gotoSplit(target, value),
.goto_split => return self.gotoSplit(target, value),
.open_config => try configpkg.edit.open(self.core_app.alloc),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
@ -559,8 +493,16 @@ pub fn performAction(
.render_inspector,
.renderer_health,
.color_change,
=> log.warn("unimplemented action={}", .{action}),
.prompt_title,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
}
// We can assume it was handled because all unknown/unimplemented actions
// are caught above.
return true;
}
fn newTab(_: *App, target: apprt.Target) !void {
@ -597,24 +539,24 @@ fn closeTab(_: *App, target: apprt.Target) !void {
}
}
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) bool {
switch (target) {
.app => {},
.app => return false,
.surface => |v| {
const window = v.rt_surface.container.window() orelse {
log.info(
"gotoTab invalid for container={s}",
.{@tagName(v.rt_surface.container)},
);
return;
return false;
};
switch (tab) {
return switch (tab) {
.previous => window.gotoPreviousTab(v.rt_surface),
.next => window.gotoNextTab(v.rt_surface),
.last => window.gotoLastTab(),
else => window.gotoTab(@intCast(@intFromEnum(tab))),
}
};
},
}
}
@ -668,18 +610,22 @@ fn gotoSplit(
_: *const App,
target: apprt.Target,
direction: apprt.action.GotoSplit,
) void {
) bool {
switch (target) {
.app => {},
.app => return false,
.surface => |v| {
const s = v.rt_surface.container.split() orelse return;
const s = v.rt_surface.container.split() orelse return false;
const map = s.directionMap(switch (v.rt_surface.container) {
.split_tl => .top_left,
.split_br => .bottom_right,
.none, .tab_ => unreachable,
});
const surface_ = map.get(direction) orelse return;
if (surface_) |surface| surface.grabFocus();
const surface_ = map.get(direction) orelse return false;
if (surface_) |surface| {
surface.grabFocus();
return true;
}
return false;
},
}
}
@ -967,11 +913,9 @@ fn configChange(
// App changes needs to show a toast that our configuration
// has reloaded.
if (adwaita.enabled(&self.config)) {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
},
}

72
src/apprt/gtk/Builder.zig Normal file
View File

@ -0,0 +1,72 @@
/// Wrapper around GTK's builder APIs that perform some comptime checks.
const Builder = @This();
const std = @import("std");
const gtk = @import("gtk");
const gobject = @import("gobject");
resource_name: [:0]const u8,
builder: ?*gtk.Builder,
pub fn init(comptime name: []const u8, comptime kind: enum { blp, ui }) Builder {
comptime {
switch (kind) {
.blp => {
// Use @embedFile to make sure that the file exists at compile
// time. Zig _should_ discard the data so that it doesn't end
// up in the final executable. At runtime we will load the data
// from a GResource.
_ = @embedFile("ui/" ++ name ++ ".blp");
// Check to make sure that our file is listed as a
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
// could crash at runtime when we try and load a nonexistent
// GResource.
const gresource = @import("gresource.zig");
for (gresource.blueprint_files) |blueprint_file| {
if (std.mem.eql(u8, blueprint_file, name)) break;
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
},
.ui => {
// Use @embedFile to make sure that the file exists at compile
// time. Zig _should_ discard the data so that it doesn't end
// up in the final executable. At runtime we will load the data
// from a GResource.
_ = @embedFile("ui/" ++ name ++ ".ui");
// Check to make sure that our file is listed as a `ui_file` in
// `gresource.zig`. If it isn't Ghostty could crash at runtime
// when we try and load a nonexistent GResource.
const gresource = @import("gresource.zig");
for (gresource.ui_files) |ui_file| {
if (std.mem.eql(u8, ui_file, name)) break;
} else @compileError("missing ui file '" ++ name ++ "' in gresource.zig");
},
}
}
return .{
.resource_name = "/com/mitchellh/ghostty/ui/" ++ name ++ ".ui",
.builder = null,
};
}
pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
class.setTemplateFromResource(self.resource_name);
}
pub fn getObject(self: *Builder, name: [:0]const u8) ?*gobject.Object {
const builder = builder: {
if (self.builder) |builder| break :builder builder;
const builder = gtk.Builder.newFromResource(self.resource_name);
self.builder = builder;
break :builder builder;
};
return builder.getObject(name);
}
pub fn deinit(self: *const Builder) void {
if (self.builder) |builder| builder.unref();
}

View File

@ -313,11 +313,7 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
if (self.directionPrevious(from)) |prev| {
result.put(.previous, prev.surface);
if (!prev.wrapped) {
// This behavior matches the behavior of macOS at the time of writing
// this. There is an open issue (#524) to make this depend on the
// actual physical location of the current split.
result.put(.up, prev.surface);
result.put(.left, prev.surface);
}
}
@ -325,13 +321,57 @@ pub fn directionMap(self: *const Split, from: Side) DirectionMap {
result.put(.next, next.surface);
if (!next.wrapped) {
result.put(.down, next.surface);
result.put(.right, next.surface);
}
}
if (self.directionLeft(from)) |left| {
result.put(.left, left);
}
if (self.directionRight(from)) |right| {
result.put(.right, right);
}
return result;
}
fn directionLeft(self: *const Split, from: Side) ?*Surface {
switch (from) {
.bottom_right => {
switch (self.orientation) {
.horizontal => return self.top_left.deepestSurface(.bottom_right),
.vertical => return directionLeft(
self.container.split() orelse return null,
.bottom_right,
),
}
},
.top_left => return directionLeft(
self.container.split() orelse return null,
.bottom_right,
),
}
}
fn directionRight(self: *const Split, from: Side) ?*Surface {
switch (from) {
.top_left => {
switch (self.orientation) {
.horizontal => return self.bottom_right.deepestSurface(.top_left),
.vertical => return directionRight(
self.container.split() orelse return null,
.top_left,
),
}
},
.bottom_right => return directionRight(
self.container.split() orelse return null,
.top_left,
),
}
}
fn directionPrevious(self: *const Split, from: Side) ?struct {
surface: *Surface,
wrapped: bool,

View File

@ -1270,10 +1270,12 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
return;
};
// Convert surface coordinate into coordinate space of the
// context menu's parent
var point: c.graphene_point_t = .{ .x = x, .y = y };
if (c.gtk_widget_compute_point(
self.primaryWidget(),
@ptrCast(window.window),
c.gtk_widget_get_parent(@ptrCast(window.context_menu)),
&c.GRAPHENE_POINT_INIT(point.x, point.y),
@ptrCast(&point),
) == 0) {
@ -2124,7 +2126,7 @@ pub fn present(self: *Surface) void {
if (self.container.window()) |window| {
if (self.container.tab()) |tab| {
if (window.notebook.getTabPosition(tab)) |position|
window.notebook.gotoNthTab(position);
_ = window.notebook.gotoNthTab(position);
}
c.gtk_window_present(window.window);
}
@ -2252,6 +2254,41 @@ fn doPaste(self: *Surface, data: [:0]const u8) void {
};
}
pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
const alloc = self.app.core_app.alloc;
var env = try internal_os.getEnvMap(alloc);
errdefer env.deinit();
// Don't leak these GTK environment variables to child processes.
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
// Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps.
if (env.get("SNAP")) |_| {
env.remove("SNAP");
env.remove("DRIRC_CONFIGDIR");
env.remove("__EGL_EXTERNAL_PLATFORM_CONFIG_DIRS");
env.remove("__EGL_VENDOR_LIBRARY_DIRS");
env.remove("LD_LIBRARY_PATH");
env.remove("LIBGL_DRIVERS_PATH");
env.remove("LIBVA_DRIVERS_PATH");
env.remove("VK_LAYER_PATH");
env.remove("XLOCALEDIR");
env.remove("GDK_PIXBUF_MODULEDIR");
env.remove("GTK_PATH");
}
if (self.container.window()) |window| {
// On some window protocols we might want to add specific
// environment variables to subprocesses, such as WINDOWID on X11.
try window.winproto.addSubprocessEnv(&env);
}
return env;
}
/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {

275
src/apprt/gtk/TabView.zig Normal file
View File

@ -0,0 +1,275 @@
/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
/// a window.
const TabView = @This();
const std = @import("std");
const gtk = @import("gtk");
const adw = @import("adw");
const gobject = @import("gobject");
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
/// our window
window: *Window,
/// the tab view
tab_view: *adw.TabView,
/// Set to true so that the adw close-page handler knows we're forcing
/// and to allow a close to happen with no confirm. This is a bit of a hack
/// because we currently use GTK alerts to confirm tab close and they
/// don't carry with them the ADW state that we are confirming or not.
/// Long term we should move to ADW alerts so we can know if we are
/// confirming or not.
forcing_close: bool = false,
pub fn init(self: *TabView, window: *Window) void {
self.* = .{
.window = window,
.tab_view = adw.TabView.new(),
};
self.tab_view.as(gtk.Widget).addCssClass("notebook");
if (adwaita.versionAtLeast(1, 2, 0)) {
// Adwaita enables all of the shortcuts by default.
// We want to manage keybindings ourselves.
self.tab_view.removeShortcuts(.{
.alt_digits = true,
.alt_zero = true,
.control_end = true,
.control_home = true,
.control_page_down = true,
.control_page_up = true,
.control_shift_end = true,
.control_shift_home = true,
.control_shift_page_down = true,
.control_shift_page_up = true,
.control_shift_tab = true,
.control_tab = true,
});
}
_ = adw.TabView.signals.page_attached.connect(
self.tab_view,
*TabView,
adwPageAttached,
self,
.{},
);
_ = adw.TabView.signals.close_page.connect(
self.tab_view,
*TabView,
adwClosePage,
self,
.{},
);
_ = adw.TabView.signals.create_window.connect(
self.tab_view,
*TabView,
adwTabViewCreateWindow,
self,
.{},
);
_ = gobject.Object.signals.notify.connect(
self.tab_view,
*TabView,
adwSelectPage,
self,
.{
.detail = "selected-page",
},
);
}
pub fn asWidget(self: *TabView) *gtk.Widget {
return self.tab_view.as(gtk.Widget);
}
pub fn nPages(self: *TabView) c_int {
return self.tab_view.getNPages();
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
fn currentPage(self: *TabView) ?c_int {
const page = self.tab_view.getSelectedPage() orelse return null;
return self.tab_view.getPagePosition(page);
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *TabView) ?*Tab {
const page = self.tab_view.getSelectedPage() orelse return null;
const child = page.getChild().as(gobject.Object);
return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
}
pub fn gotoNthTab(self: *TabView, position: c_int) bool {
const page_to_select = self.tab_view.getNthPage(position);
self.tab_view.setSelectedPage(page_to_select);
return true;
}
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
const page = self.tab_view.getPage(@ptrCast(tab.box));
return self.tab_view.getPagePosition(page);
}
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = self.nPages();
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
const page_idx = self.getTabPosition(tab) orelse return false;
const max = self.nPages() -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
if (next_idx == page_idx) return false;
return self.gotoNthTab(next_idx);
}
pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
var new_position: c_int = page_idx + position;
if (new_position < 0) {
new_position = max + new_position + 1;
} else if (new_position > max) {
new_position = new_position - max - 1;
}
if (new_position == page_idx) return;
self.reorderPage(tab, new_position);
}
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
const page = self.tab_view.getPage(@ptrCast(tab.box));
_ = self.tab_view.reorderPage(page, position);
}
pub fn setTabLabel(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const page = self.tab_view.getPage(@ptrCast(tab.box));
page.setTitle(title.ptr);
}
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
const page = self.tab_view.getPage(@ptrCast(tab.box));
page.setTooltip(tooltip.ptr);
}
fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") {
.current => if (self.currentPage()) |page| page + 1 else numPages,
.end => numPages,
};
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
const box_widget: *gtk.Widget = @ptrCast(tab.box);
const page = self.tab_view.insert(box_widget, position);
self.setTabLabel(tab, title);
self.tab_view.setSelectedPage(page);
}
pub fn closeTab(self: *TabView, tab: *Tab) void {
// Save a pointer to the GTK window in case we need it later. It may be
// impossible to access later due to how resources are cleaned up.
const window: *gtk.Window = @ptrCast(@alignCast(self.window.window));
// closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request
// confirmation.
self.forcing_close = true;
const n = self.nPages();
defer {
// self becomes invalid if we close the last page because we close
// the whole window
if (n > 1) self.forcing_close = false;
}
const page = self.tab_view.getPage(@ptrCast(tab.box));
self.tab_view.closePage(page);
// If we have no more tabs we close the window
if (self.nPages() == 0) {
// libadw versions <= 1.3.x leak the final page view
// which causes our surface to not properly cleanup. We
// unref to force the cleanup. This will trigger a critical
// warning from GTK, but I don't know any other workaround.
// Note: I'm not actually sure if 1.4.0 contains the fix,
// I just know that 1.3.x is broken and 1.5.1 is fixed.
// If we know that 1.4.0 is fixed, we can change this.
if (!adwaita.versionAtLeast(1, 4, 0)) {
const box: *gtk.Box = @ptrCast(@alignCast(tab.box));
box.as(gobject.Object).unref();
}
window.destroy();
}
}
pub fn createWindow(currentWindow: *Window) !*Window {
const alloc = currentWindow.app.core_app.alloc;
const app = currentWindow.app;
// Create a new window
return Window.create(alloc, app);
}
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
tab.window = self.window;
self.window.focusCurrentTab();
}
fn adwClosePage(
_: *adw.TabView,
page: *adw.TabPage,
self: *TabView,
) callconv(.C) c_int {
const child = page.getChild().as(gobject.Object);
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
if (!self.forcing_close) tab.closeWithConfirmation();
return 1;
}
fn adwTabViewCreateWindow(
_: *adw.TabView,
self: *TabView,
) callconv(.C) ?*adw.TabView {
const window = createWindow(self.window) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
return window.notebook.tab_view;
}
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void {
const page = self.tab_view.getSelectedPage() orelse return;
const title = page.getTitle();
self.window.setTitle(std.mem.span(title));
}

View File

@ -22,8 +22,8 @@ const Tab = @import("Tab.zig");
const c = @import("c.zig").c;
const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook;
const HeaderBar = @import("headerbar.zig").HeaderBar;
const TabView = @import("TabView.zig");
const HeaderBar = @import("headerbar.zig");
const version = @import("version.zig");
const winproto = @import("winproto.zig");
@ -34,9 +34,7 @@ app: *App,
/// Our window
window: *c.GtkWindow,
/// The header bar for the window. This is possibly null since it can be
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
/// GtkHeaderBar depending on if adw is enabled and linked.
/// The header bar for the window.
headerbar: HeaderBar,
/// The tab overview for the window. This is possibly null since there is no
@ -44,14 +42,12 @@ headerbar: HeaderBar,
tab_overview: ?*c.GtkWidget,
/// The notebook (tab grouping) for this window.
/// can be either c.GtkNotebook or c.AdwTabView.
notebook: Notebook,
notebook: TabView,
context_menu: *c.GtkWidget,
/// The libadwaita widget for receiving toast send requests. If libadwaita is
/// not used, this is null and unused.
toast_overlay: ?*c.GtkWidget,
/// The libadwaita widget for receiving toast send requests.
toast_overlay: *c.GtkWidget,
/// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null,
@ -87,49 +83,39 @@ pub fn init(self: *Window, app: *App) !void {
};
// Create the window
const window: *c.GtkWidget = window: {
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) {
const window = c.adw_application_window_new(app.app);
c.gtk_widget_add_css_class(@ptrCast(window), "adw");
break :window window;
} else {
const window = c.gtk_application_window_new(app.app);
c.gtk_widget_add_css_class(@ptrCast(window), "gtk");
break :window window;
}
};
errdefer c.gtk_window_destroy(@ptrCast(window));
const gtk_widget = c.adw_application_window_new(app.app);
errdefer c.gtk_window_destroy(@ptrCast(gtk_widget));
const gtk_window: *c.GtkWindow = @ptrCast(window);
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window");
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window");
self.window = @ptrCast(@alignCast(gtk_widget));
c.gtk_window_set_title(self.window, "Ghostty");
c.gtk_window_set_default_size(self.window, 1000, 600);
c.gtk_widget_add_css_class(gtk_widget, "window");
c.gtk_widget_add_css_class(gtk_widget, "terminal-window");
// GTK4 grabs F10 input by default to focus the menubar icon. We want
// to disable this so that terminal programs can capture F10 (such as htop)
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
c.gtk_window_set_handle_menubar_accel(self.window, 0);
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
c.gtk_window_set_icon_name(self.window, build_config.bundle_id);
// Apply class to color headerbar if window-theme is set to `ghostty` and
// GTK version is before 4.16. The conditional is because above 4.16
// we use GTK CSS color variables.
if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) {
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
c.gtk_widget_add_css_class(gtk_widget, "window-theme-ghostty");
}
// Create our box which will hold our widgets in the main content area.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
// Setup our notebook
self.notebook.init();
self.notebook.init(self);
// If we are using Adwaita, then we can support the tab overview.
self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: {
self.tab_overview = if (adwaita.versionAtLeast(1, 4, 0)) overview: {
const tab_overview = c.adw_tab_overview_new();
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
_ = c.g_signal_connect_data(
tab_overview,
@ -166,10 +152,9 @@ pub fn init(self: *Window, app: *App) !void {
// If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
.top, .bottom => btn: {
const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
@ -186,7 +171,7 @@ pub fn init(self: *Window, app: *App) !void {
.hidden => btn: {
const btn = c.adw_tab_button_new();
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
c.adw_tab_button_set_view(@ptrCast(btn), @ptrCast(@alignCast(self.notebook.tab_view)));
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn;
},
@ -203,13 +188,13 @@ pub fn init(self: *Window, app: *App) !void {
self.headerbar.packStart(btn);
}
_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::decorated", c.G_CALLBACK(&gtkWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::maximized", c.G_CALLBACK(&gtkWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "notify::fullscreened", c.G_CALLBACK(&gtkWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (!adwaita.versionAtLeast(1, 4, 0)) {
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
}
@ -218,10 +203,7 @@ pub fn init(self: *Window, app: *App) !void {
if (comptime std.debug.runtime_safety) {
const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded.";
if ((comptime adwaita.versionAtLeast(1, 3, 0)) and
adwaita.enabled(&app.config) and
adwaita.versionAtLeast(1, 3, 0))
{
if (adwaita.versionAtLeast(1, 3, 0)) {
const banner = c.adw_banner_new(warning_text);
c.adw_banner_set_revealed(@ptrCast(banner), 1);
c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner));
@ -231,30 +213,23 @@ pub fn init(self: *Window, app: *App) !void {
c.gtk_widget_set_margin_bottom(warning, 10);
c.gtk_box_append(@ptrCast(warning_box), warning);
}
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "devel");
c.gtk_widget_add_css_class(gtk_widget, "devel");
c.gtk_widget_add_css_class(@ptrCast(warning_box), "background");
c.gtk_box_append(@ptrCast(box), warning_box);
}
// Setup our toast overlay if we have one
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: {
const toast_overlay = c.adw_toast_overlay_new();
c.adw_toast_overlay_set_child(
@ptrCast(toast_overlay),
@ptrCast(@alignCast(self.notebook.asWidget())),
);
c.gtk_box_append(@ptrCast(box), toast_overlay);
break :toast toast_overlay;
} else toast: {
c.gtk_box_append(@ptrCast(box), self.notebook.asWidget());
break :toast null;
};
self.toast_overlay = c.adw_toast_overlay_new();
c.adw_toast_overlay_set_child(
@ptrCast(self.toast_overlay),
@ptrCast(@alignCast(self.notebook.asWidget())),
);
c.gtk_box_append(@ptrCast(box), self.toast_overlay);
// If we have a tab overview then we can set it on our notebook.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable;
assert(self.notebook == .adw);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
}
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
@ -273,40 +248,39 @@ pub fn init(self: *Window, app: *App) !void {
// focused (i.e. when the libadw tab overview is shown).
const ec_key_press = c.gtk_event_controller_key_new();
errdefer c.g_object_unref(ec_key_press);
c.gtk_widget_add_controller(window, ec_key_press);
c.gtk_widget_add_controller(gtk_widget, ec_key_press);
// All of our events
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(&gtkRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "realize", c.G_CALLBACK(&gtkRealize), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "close-request", c.G_CALLBACK(&gtkCloseRequest), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(self.window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(&gtkKeyPressed), self, null, c.G_CONNECT_DEFAULT);
// Our actions for the menu
initActions(self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (adwaita.versionAtLeast(1, 4, 0)) {
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new();
c.adw_tab_bar_set_view(tab_bar, self.notebook.adw.tab_view);
c.adw_tab_bar_set_view(tab_bar, @ptrCast(@alignCast(self.notebook.tab_view)));
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
switch (self.app.config.@"gtk-tabs-location") {
// left and right are not supported in libadwaita.
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.top => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
.hidden => unreachable,
}
}
c.adw_toolbar_view_set_content(toolbar_view, box);
const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"adw-toolbar-style") {
const toolbar_style: c.AdwToolbarStyle = switch (self.app.config.@"gtk-toolbar-style") {
.flat => c.ADW_TOOLBAR_FLAT,
.raised => c.ADW_TOOLBAR_RAISED,
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
@ -320,51 +294,34 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(toolbar_view)),
);
c.adw_application_window_set_content(
@ptrCast(gtk_window),
@ptrCast(gtk_widget),
@ptrCast(@alignCast(self.tab_overview)),
);
} else tab_bar: {
switch (self.notebook) {
.adw => |*adw| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView.
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
switch (app.config.@"gtk-tabs-location") {
.top,
.left,
.right,
=> c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))),
.bottom => c.gtk_box_append(
@ptrCast(box),
@ptrCast(@alignCast(tab_bar)),
),
.hidden => unreachable,
}
c.adw_tab_bar_set_view(tab_bar, adw.tab_view);
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
},
.gtk => {},
if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView.
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
switch (app.config.@"gtk-tabs-location") {
.top => c.gtk_box_insert_child_after(
@ptrCast(box),
@ptrCast(@alignCast(tab_bar)),
@ptrCast(@alignCast(self.headerbar.asWidget())),
),
.bottom => c.gtk_box_append(
@ptrCast(box),
@ptrCast(@alignCast(tab_bar)),
),
.hidden => unreachable,
}
c.adw_tab_bar_set_view(tab_bar, @ptrCast(@alignCast(self.notebook.tab_view)));
// The box is our main child
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
c.adw_application_window_set_content(
@ptrCast(gtk_window),
box,
);
} else {
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
c.gtk_window_set_child(gtk_window, box);
}
if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
}
// Show the window
c.gtk_widget_show(window);
c.gtk_widget_show(gtk_widget);
}
pub fn updateConfig(
@ -407,19 +364,16 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
// Disable the title buttons (close, maximize, minimize, ...)
// *inside* the tab overview if CSDs are disabled.
// We do spare the search button, though.
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&self.app.config))
{
if (self.tab_overview) |tab_overview| {
c.adw_tab_overview_set_show_start_title_buttons(
@ptrCast(tab_overview),
@intFromBool(csd_enabled),
);
c.adw_tab_overview_set_show_end_title_buttons(
@ptrCast(tab_overview),
@intFromBool(csd_enabled),
);
}
if (self.tab_overview) |tab_overview| {
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
c.adw_tab_overview_set_show_start_title_buttons(
@ptrCast(tab_overview),
@intFromBool(csd_enabled),
);
c.adw_tab_overview_set_show_end_title_buttons(
@ptrCast(tab_overview),
@intFromBool(csd_enabled),
);
}
}
@ -506,23 +460,25 @@ pub fn closeTab(self: *Window, tab: *Tab) void {
}
/// Go to the previous tab for a surface.
pub fn gotoPreviousTab(self: *Window, surface: *Surface) void {
pub fn gotoPreviousTab(self: *Window, surface: *Surface) bool {
const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{});
return;
return false;
};
self.notebook.gotoPreviousTab(tab);
if (!self.notebook.gotoPreviousTab(tab)) return false;
self.focusCurrentTab();
return true;
}
/// Go to the next tab for a surface.
pub fn gotoNextTab(self: *Window, surface: *Surface) void {
pub fn gotoNextTab(self: *Window, surface: *Surface) bool {
const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{});
return;
return false;
};
self.notebook.gotoNextTab(tab);
if (!self.notebook.gotoNextTab(tab)) return false;
self.focusCurrentTab();
return true;
}
/// Move the current tab for a surface.
@ -535,25 +491,26 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
}
/// Go to the last tab for a surface.
pub fn gotoLastTab(self: *Window) void {
pub fn gotoLastTab(self: *Window) bool {
const max = self.notebook.nPages();
self.gotoTab(@intCast(max));
return self.gotoTab(@intCast(max));
}
/// Go to the specific tab index.
pub fn gotoTab(self: *Window, n: usize) void {
if (n == 0) return;
pub fn gotoTab(self: *Window, n: usize) bool {
if (n == 0) return false;
const max = self.notebook.nPages();
if (max == 0) return;
const page_idx = std.math.cast(c_int, n - 1) orelse return;
self.notebook.gotoNthTab(@min(page_idx, max - 1));
if (max == 0) return false;
const page_idx = std.math.cast(c_int, n - 1) orelse return false;
if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
self.focusCurrentTab();
return true;
}
/// Toggle tab overview (if present)
pub fn toggleTabOverview(self: *Window) void {
if (self.tab_overview) |tab_overview_widget| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
c.adw_tab_overview_set_open(tab_overview, 1 - c.adw_tab_overview_get_open(tab_overview));
}
@ -600,11 +557,9 @@ pub fn onConfigReloaded(self: *Window) void {
}
pub fn sendToast(self: *Window, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) return;
const toast_overlay = self.toast_overlay orelse return;
const toast = c.adw_toast_new(title);
c.adw_toast_set_timeout(toast, 3);
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
c.adw_toast_overlay_add_toast(@ptrCast(self.toast_overlay), toast);
}
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
@ -707,13 +662,13 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
/// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
const self: *Window = userdataSelf(ud.?);
assert((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config));
const alloc = self.app.core_app.alloc;
const surface = self.actionSurface();
const tab = Tab.create(alloc, self, surface) catch return null;
return c.adw_tab_view_get_page(self.notebook.adw.tab_view, @ptrCast(@alignCast(tab.box)));
return c.adw_tab_view_get_page(@ptrCast(@alignCast(self.notebook.tab_view)), @ptrCast(@alignCast(tab.box)));
}
fn adwTabOverviewOpen(
@ -860,7 +815,7 @@ fn gtkKeyPressed(
//
// If someone can confidently show or explain that this is not
// necessary, please remove this check.
if (comptime adwaita.versionAtLeast(1, 4, 0)) {
if (adwaita.versionAtLeast(1, 4, 0)) {
if (self.tab_overview) |tab_overview_widget| {
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(tab_overview_widget));
if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0;
@ -888,10 +843,7 @@ fn gtkActionAbout(
const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org";
if ((comptime adwaita.versionAtLeast(1, 5, 0)) and
adwaita.versionAtLeast(1, 5, 0) and
adwaita.enabled(&self.app.config))
{
if (adwaita.versionAtLeast(1, 5, 0)) {
c.adw_show_about_dialog(
@ptrCast(self.window),
"application-name",

View File

@ -1,20 +1,5 @@
const std = @import("std");
const c = @import("c.zig").c;
const build_options = @import("build_options");
const Config = @import("../../config.zig").Config;
/// Returns true if Ghostty is configured to build with libadwaita and
/// the configuration has enabled adwaita.
///
/// For a comptime version of this function, use `versionAtLeast` in
/// a comptime context with all the version numbers set to 0.
///
/// This must be `inline` so that the comptime check noops conditional
/// paths that are not enabled.
pub inline fn enabled(config: *const Config) bool {
return build_options.adwaita and
config.@"gtk-adwaita";
}
/// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to
@ -33,8 +18,6 @@ pub inline fn versionAtLeast(
comptime minor: u16,
comptime micro: u16,
) bool {
if (comptime !build_options.adwaita) return false;
// 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

View File

@ -0,0 +1,32 @@
const std = @import("std");
const build_options = @import("build_options");
const gtk = @import("gtk");
const adw = @import("adw");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
const filename = filename: {
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
_ = it.next() orelse return error.NoFilename;
break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename);
};
defer alloc.free(filename);
const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0);
defer alloc.free(data);
if (gtk.initCheck() == 0) {
std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename});
return;
}
adw.init();
const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len));
defer builder.unref();
}

View File

@ -3,9 +3,7 @@ const build_options = @import("build_options");
/// Imported C API directly from header files
pub const c = @cImport({
@cInclude("gtk/gtk.h");
if (build_options.adwaita) {
@cInclude("libadwaita-1/adwaita.h");
}
@cInclude("adwaita.h");
if (build_options.x11) {
// Add in X11-specific GDK backend which we use for specific things

View File

@ -53,29 +53,33 @@ const icons = [_]struct {
},
};
pub const gresource_xml = comptimeGenerateGResourceXML();
pub const ui_files = [_][]const u8{};
pub const blueprint_files = [_][]const u8{};
fn comptimeGenerateGResourceXML() []const u8 {
comptime {
@setEvalBranchQuota(13000);
var counter = std.io.countingWriter(std.io.null_writer);
try writeGResourceXML(&counter.writer());
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var buf: [counter.bytes_written]u8 = undefined;
var stream = std.io.fixedBufferStream(&buf);
try writeGResourceXML(stream.writer());
const final = buf;
return final[0..stream.getWritten().len];
var extra_ui_files = std.ArrayList([]const u8).init(alloc);
defer {
for (extra_ui_files.items) |item| alloc.free(item);
extra_ui_files.deinit();
}
}
fn writeGResourceXML(writer: anytype) !void {
var it = try std.process.argsWithAllocator(alloc);
defer it.deinit();
while (it.next()) |filename| {
if (std.mem.eql(u8, std.fs.path.extension(filename), ".ui")) {
try extra_ui_files.append(try alloc.dupe(u8, filename));
}
}
const writer = std.io.getStdOut().writer();
try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?>
\\<gresources>
\\
);
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty">
\\
);
@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void {
}
try writer.writeAll(
\\ </gresource>
\\
);
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty/icons">
\\
);
@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void {
.{ icon.alias, icon.source },
);
}
try writer.writeAll(
\\ </gresource>
\\ <gresource prefix="/com/mitchellh/ghostty/ui">
\\
);
for (ui_files) |ui_file| {
try writer.print(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{0s}.ui\">src/apprt/gtk/ui/{0s}.ui</file>\n",
.{ui_file},
);
}
for (extra_ui_files.items) |ui_file| {
try writer.print(
" <file compressed=\"true\" preprocess=\"xml-stripblanks\" alias=\"{s}\">{s}</file>\n",
.{ std.fs.path.basename(ui_file), ui_file },
);
}
try writer.writeAll(
\\ </gresource>
\\</gresources>
@ -107,12 +125,24 @@ fn writeGResourceXML(writer: anytype) !void {
}
pub const dependencies = deps: {
var deps: [css_files.len + icons.len][]const u8 = undefined;
for (css_files, 0..) |css_file, i| {
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
var deps: [total][]const u8 = undefined;
var index: usize = 0;
for (css_files) |css_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file});
index += 1;
}
for (icons, css_files.len..) |icon, i| {
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
for (icons) |icon| {
deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source});
index += 1;
}
for (ui_files) |ui_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.ui", .{ui_file});
index += 1;
}
for (blueprint_files) |blueprint_file| {
deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{s}.blp", .{blueprint_file});
index += 1;
}
break :deps deps;
};

View File

@ -1,58 +1,59 @@
const HeaderBar = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBarAdw = @import("headerbar_adw.zig");
const HeaderBarGtk = @import("headerbar_gtk.zig");
/// the Adwaita headerbar widget
headerbar: *c.AdwHeaderBar,
pub const HeaderBar = union(enum) {
adw: HeaderBarAdw,
gtk: HeaderBarGtk,
/// the Adwaita window title widget
title: *c.AdwWindowTitle,
pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) {
HeaderBarAdw.init(self);
} else {
HeaderBarGtk.init(self);
}
}
pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self);
self.* = .{
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
.title = @ptrCast(@alignCast(c.adw_window_title_new(
c.gtk_window_get_title(window.window) orelse "Ghostty",
null,
))),
};
c.adw_header_bar_set_title_widget(
self.headerbar,
@ptrCast(@alignCast(self.title)),
);
}
pub fn setVisible(self: HeaderBar, visible: bool) void {
switch (self) {
inline else => |v| v.setVisible(visible),
}
}
pub fn setVisible(self: *const HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
inline else => |v| v.asWidget(),
};
}
pub fn asWidget(self: *const HeaderBar) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
inline else => |v| v.packEnd(widget),
}
}
pub fn packEnd(self: *const HeaderBar, widget: *c.GtkWidget) void {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
switch (self) {
inline else => |v| v.packStart(widget),
}
}
pub fn packStart(self: *const HeaderBar, widget: *c.GtkWidget) void {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
switch (self) {
inline else => |v| v.setTitle(title),
}
}
pub fn setTitle(self: *const HeaderBar, title: [:0]const u8) void {
const window: *const Window = @fieldParentPtr("headerbar", self);
c.gtk_window_set_title(window.window, title);
c.adw_window_title_set_title(self.title, title);
}
pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
switch (self) {
inline else => |v| v.setSubtitle(subtitle),
}
}
};
pub fn setSubtitle(self: *const HeaderBar, subtitle: [:0]const u8) void {
c.adw_window_title_set_subtitle(self.title, subtitle);
}

View File

@ -1,78 +0,0 @@
const HeaderBarAdw = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBar = @import("headerbar.zig").HeaderBar;
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else anyopaque;
const AdwWindowTitle = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwWindowTitle else anyopaque;
/// the window that this headerbar is attached to
window: *Window,
/// the Adwaita headerbar widget
headerbar: *AdwHeaderBar,
/// the Adwaita window title widget
title: *AdwWindowTitle,
pub fn init(headerbar: *HeaderBar) void {
if (!adwaita.versionAtLeast(0, 0, 0)) return;
const window: *Window = @fieldParentPtr("headerbar", headerbar);
headerbar.* = .{
.adw = .{
.window = window,
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
.title = @ptrCast(@alignCast(c.adw_window_title_new(
c.gtk_window_get_title(window.window) orelse "Ghostty",
null,
))),
},
};
c.adw_header_bar_set_title_widget(
headerbar.adw.headerbar,
@ptrCast(@alignCast(headerbar.adw.title)),
);
}
pub fn setVisible(self: HeaderBarAdw, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBarAdw) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}
pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
}
pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
c.gtk_window_set_title(self.window.window, title);
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_title(self.title, title);
}
}
pub fn setSubtitle(self: HeaderBarAdw, subtitle: [:0]const u8) void {
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
c.adw_window_title_set_subtitle(self.title, subtitle);
}
}

View File

@ -1,52 +0,0 @@
const HeaderBarGtk = @This();
const std = @import("std");
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBar = @import("headerbar.zig").HeaderBar;
/// the window that this headarbar is attached to
window: *Window,
/// the GTK headerbar widget
headerbar: *c.GtkHeaderBar,
pub fn init(headerbar: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", headerbar);
headerbar.* = .{
.gtk = .{
.window = window,
.headerbar = @ptrCast(c.gtk_header_bar_new()),
},
};
}
pub fn setVisible(self: HeaderBarGtk, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBarGtk) *c.GtkWidget {
return @ptrCast(@alignCast(self.headerbar));
}
pub fn packEnd(self: HeaderBarGtk, widget: *c.GtkWidget) void {
c.gtk_header_bar_pack_end(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn packStart(self: HeaderBarGtk, widget: *c.GtkWidget) void {
c.gtk_header_bar_pack_start(
@ptrCast(@alignCast(self.headerbar)),
widget,
);
}
pub fn setTitle(self: HeaderBarGtk, title: [:0]const u8) void {
c.gtk_window_set_title(self.window.window, title);
}
pub fn setSubtitle(_: HeaderBarGtk, _: [:0]const u8) void {}

View File

@ -1,169 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const NotebookAdw = @import("notebook_adw.zig").NotebookAdw;
const NotebookGtk = @import("notebook_gtk.zig").NotebookGtk;
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
pub const Notebook = union(enum) {
adw: NotebookAdw,
gtk: NotebookGtk,
pub fn init(self: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", self);
const app = window.app;
if (adwaita.enabled(&app.config)) return NotebookAdw.init(self);
return NotebookGtk.init(self);
}
pub fn asWidget(self: *Notebook) *c.GtkWidget {
return switch (self.*) {
.adw => |*adw| adw.asWidget(),
.gtk => |*gtk| gtk.asWidget(),
};
}
pub fn nPages(self: *Notebook) c_int {
return switch (self.*) {
.adw => |*adw| adw.nPages(),
.gtk => |*gtk| gtk.nPages(),
};
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
fn currentPage(self: *Notebook) ?c_int {
return switch (self.*) {
.adw => |*adw| adw.currentPage(),
.gtk => |*gtk| gtk.currentPage(),
};
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *Notebook) ?*Tab {
return switch (self.*) {
.adw => |*adw| adw.currentTab(),
.gtk => |*gtk| gtk.currentTab(),
};
}
pub fn gotoNthTab(self: *Notebook, position: c_int) void {
switch (self.*) {
.adw => |*adw| adw.gotoNthTab(position),
.gtk => |*gtk| gtk.gotoNthTab(position),
}
}
pub fn getTabPosition(self: *Notebook, tab: *Tab) ?c_int {
return switch (self.*) {
.adw => |*adw| adw.getTabPosition(tab),
.gtk => |*gtk| gtk.getTabPosition(tab),
};
}
pub fn gotoPreviousTab(self: *Notebook, tab: *Tab) void {
const page_idx = self.getTabPosition(tab) orelse return;
// The next index is the previous or we wrap around.
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
const max = self.nPages();
break :next_idx max -| 1;
};
// Do nothing if we have one tab
if (next_idx == page_idx) return;
self.gotoNthTab(next_idx);
}
pub fn gotoNextTab(self: *Notebook, tab: *Tab) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
const next_idx = if (page_idx < max) page_idx + 1 else 0;
if (next_idx == page_idx) return;
self.gotoNthTab(next_idx);
}
pub fn moveTab(self: *Notebook, tab: *Tab, position: c_int) void {
const page_idx = self.getTabPosition(tab) orelse return;
const max = self.nPages() -| 1;
var new_position: c_int = page_idx + position;
if (new_position < 0) {
new_position = max + new_position + 1;
} else if (new_position > max) {
new_position = new_position - max - 1;
}
if (new_position == page_idx) return;
self.reorderPage(tab, new_position);
}
pub fn reorderPage(self: *Notebook, tab: *Tab, position: c_int) void {
switch (self.*) {
.adw => |*adw| adw.reorderPage(tab, position),
.gtk => |*gtk| gtk.reorderPage(tab, position),
}
}
pub fn setTabLabel(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
switch (self.*) {
.adw => |*adw| adw.setTabLabel(tab, title),
.gtk => |*gtk| gtk.setTabLabel(tab, title),
}
}
pub fn setTabTooltip(self: *Notebook, tab: *Tab, tooltip: [:0]const u8) void {
switch (self.*) {
.adw => |*adw| adw.setTabTooltip(tab, tooltip),
.gtk => |*gtk| gtk.setTabTooltip(tab, tooltip),
}
}
fn newTabInsertPosition(self: *Notebook, tab: *Tab) c_int {
const numPages = self.nPages();
return switch (tab.window.app.config.@"window-new-tab-position") {
.current => if (self.currentPage()) |page| page + 1 else numPages,
.end => numPages,
};
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *Notebook, tab: *Tab, title: [:0]const u8) void {
const position = self.newTabInsertPosition(tab);
switch (self.*) {
.adw => |*adw| adw.addTab(tab, position, title),
.gtk => |*gtk| gtk.addTab(tab, position, title),
}
}
pub fn closeTab(self: *Notebook, tab: *Tab) void {
switch (self.*) {
.adw => |*adw| adw.closeTab(tab),
.gtk => |*gtk| gtk.closeTab(tab),
}
}
};
pub fn createWindow(currentWindow: *Window) !*Window {
const alloc = currentWindow.app.core_app.alloc;
const app = currentWindow.app;
// Create a new window
return Window.create(alloc, app);
}

View File

@ -1,209 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const Notebook = @import("notebook.zig").Notebook;
const createWindow = @import("notebook.zig").createWindow;
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
const AdwTabView = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabView else anyopaque;
const AdwTabPage = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwTabPage else anyopaque;
pub const NotebookAdw = struct {
/// the tab view
tab_view: *AdwTabView,
/// Set to true so that the adw close-page handler knows we're forcing
/// and to allow a close to happen with no confirm. This is a bit of a hack
/// because we currently use GTK alerts to confirm tab close and they
/// don't carry with them the ADW state that we are confirming or not.
/// Long term we should move to ADW alerts so we can know if we are
/// confirming or not.
forcing_close: bool = false,
pub fn init(notebook: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", notebook);
const app = window.app;
assert(adwaita.enabled(&app.config));
const tab_view: *c.AdwTabView = c.adw_tab_view_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_view)), "notebook");
if (comptime adwaita.versionAtLeast(1, 2, 0) and adwaita.versionAtLeast(1, 2, 0)) {
// Adwaita enables all of the shortcuts by default.
// We want to manage keybindings ourselves.
c.adw_tab_view_remove_shortcuts(tab_view, c.ADW_TAB_VIEW_SHORTCUT_ALL_SHORTCUTS);
}
notebook.* = .{
.adw = .{
.tab_view = tab_view,
},
};
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
}
pub fn asWidget(self: *NotebookAdw) *c.GtkWidget {
return @ptrCast(@alignCast(self.tab_view));
}
pub fn nPages(self: *NotebookAdw) c_int {
if (comptime adwaita.versionAtLeast(0, 0, 0))
return c.adw_tab_view_get_n_pages(self.tab_view)
else
unreachable;
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
pub fn currentPage(self: *NotebookAdw) ?c_int {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *NotebookAdw) ?*Tab {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_selected_page(self.tab_view) orelse return null;
const child = c.adw_tab_page_get_child(page);
return @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
));
}
pub fn gotoNthTab(self: *NotebookAdw, position: c_int) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page_to_select = c.adw_tab_view_get_nth_page(self.tab_view, position);
c.adw_tab_view_set_selected_page(self.tab_view, page_to_select);
}
pub fn getTabPosition(self: *NotebookAdw, tab: *Tab) ?c_int {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return null;
return c.adw_tab_view_get_page_position(self.tab_view, page);
}
pub fn reorderPage(self: *NotebookAdw, tab: *Tab, position: c_int) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
_ = c.adw_tab_view_reorder_page(self.tab_view, page, position);
}
pub fn setTabLabel(self: *NotebookAdw, tab: *Tab, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_title(page, title.ptr);
}
pub fn setTabTooltip(self: *NotebookAdw, tab: *Tab, tooltip: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box));
c.adw_tab_page_set_tooltip(page, tooltip.ptr);
}
pub fn addTab(self: *NotebookAdw, tab: *Tab, position: c_int, title: [:0]const u8) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
const page = c.adw_tab_view_insert(self.tab_view, box_widget, position);
c.adw_tab_page_set_title(page, title.ptr);
c.adw_tab_view_set_selected_page(self.tab_view, page);
}
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
// closeTab always expects to close unconditionally so we mark this
// as true so that the close_page call below doesn't request
// confirmation.
self.forcing_close = true;
const n = self.nPages();
defer {
// self becomes invalid if we close the last page because we close
// the whole window
if (n > 1) self.forcing_close = false;
}
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
c.adw_tab_view_close_page(self.tab_view, page);
// If we have no more tabs we close the window
if (self.nPages() == 0) {
const window = tab.window.window;
// libadw versions <= 1.3.x leak the final page view
// which causes our surface to not properly cleanup. We
// unref to force the cleanup. This will trigger a critical
// warning from GTK, but I don't know any other workaround.
// Note: I'm not actually sure if 1.4.0 contains the fix,
// I just know that 1.3.x is broken and 1.5.1 is fixed.
// If we know that 1.4.0 is fixed, we can change this.
if (!adwaita.versionAtLeast(1, 4, 0)) {
c.g_object_unref(tab.box);
}
// `self` will become invalid after this call because it will have
// been freed up as part of the process of closing the window.
c.gtk_window_destroy(window);
}
}
};
fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return));
tab.window = window;
window.focusCurrentTab();
}
fn adwClosePage(
_: *AdwTabView,
page: *c.AdwTabPage,
ud: ?*anyopaque,
) callconv(.C) c.gboolean {
const child = c.adw_tab_page_get_child(page);
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
@ptrCast(child),
Tab.GHOSTTY_TAB,
) orelse return 0));
const window: *Window = @ptrCast(@alignCast(ud.?));
const notebook = window.notebook.adw;
c.adw_tab_view_close_page_finish(
notebook.tab_view,
page,
@intFromBool(notebook.forcing_close),
);
if (!notebook.forcing_close) tab.closeWithConfirmation();
return 1;
}
fn adwTabViewCreateWindow(
_: *AdwTabView,
ud: ?*anyopaque,
) callconv(.C) ?*AdwTabView {
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const window = createWindow(currentWindow) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
return window.notebook.adw.tab_view;
}
fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
const title = c.adw_tab_page_get_title(page);
window.setTitle(std.mem.span(title));
}

View File

@ -1,304 +0,0 @@
const std = @import("std");
const assert = std.debug.assert;
const c = @import("c.zig").c;
const Window = @import("Window.zig");
const Tab = @import("Tab.zig");
const Notebook = @import("notebook.zig").Notebook;
const createWindow = @import("notebook.zig").createWindow;
const log = std.log.scoped(.gtk);
/// An abstraction over the GTK notebook and Adwaita tab view to manage
/// all the terminal tabs in a window.
pub const NotebookGtk = struct {
notebook: *c.GtkNotebook,
pub fn init(notebook: *Notebook) void {
const window: *Window = @fieldParentPtr("notebook", notebook);
const app = window.app;
// Create a notebook to hold our tabs.
const notebook_widget: *c.GtkWidget = c.gtk_notebook_new();
c.gtk_widget_add_css_class(notebook_widget, "notebook");
const gtk_notebook: *c.GtkNotebook = @ptrCast(notebook_widget);
const notebook_tab_pos: c_uint = switch (app.config.@"gtk-tabs-location") {
.top, .hidden => c.GTK_POS_TOP,
.bottom => c.GTK_POS_BOTTOM,
.left => c.GTK_POS_LEFT,
.right => c.GTK_POS_RIGHT,
};
c.gtk_notebook_set_tab_pos(gtk_notebook, notebook_tab_pos);
c.gtk_notebook_set_scrollable(gtk_notebook, 1);
c.gtk_notebook_set_show_tabs(gtk_notebook, 0);
c.gtk_notebook_set_show_border(gtk_notebook, 0);
// This enables all Ghostty terminal tabs to be exchanged across windows.
c.gtk_notebook_set_group_name(gtk_notebook, "ghostty-terminal-tabs");
// This is important so the notebook expands to fit available space.
// Otherwise, it will be zero/zero in the box below.
c.gtk_widget_set_vexpand(notebook_widget, 1);
c.gtk_widget_set_hexpand(notebook_widget, 1);
// Remove the background from the stack widget
const stack = c.gtk_widget_get_last_child(notebook_widget);
c.gtk_widget_add_css_class(stack, "transparent");
notebook.* = .{
.gtk = .{
.notebook = gtk_notebook,
},
};
// All of our events
_ = c.g_signal_connect_data(gtk_notebook, "page-added", c.G_CALLBACK(&gtkPageAdded), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "page-removed", c.G_CALLBACK(&gtkPageRemoved), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "switch-page", c.G_CALLBACK(&gtkSwitchPage), window, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gtk_notebook, "create-window", c.G_CALLBACK(&gtkNotebookCreateWindow), window, null, c.G_CONNECT_DEFAULT);
}
/// return the underlying widget as a generic GtkWidget
pub fn asWidget(self: *NotebookGtk) *c.GtkWidget {
return @ptrCast(@alignCast(self.notebook));
}
/// returns the number of pages in the notebook
pub fn nPages(self: *NotebookGtk) c_int {
return c.gtk_notebook_get_n_pages(self.notebook);
}
/// Returns the index of the currently selected page.
/// Returns null if the notebook has no pages.
pub fn currentPage(self: *NotebookGtk) ?c_int {
const current = c.gtk_notebook_get_current_page(self.notebook);
return if (current == -1) null else current;
}
/// Returns the currently selected tab or null if there are none.
pub fn currentTab(self: *NotebookGtk) ?*Tab {
log.warn("currentTab", .{});
const page = self.currentPage() orelse return null;
const child = c.gtk_notebook_get_nth_page(self.notebook, page);
return @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(child), Tab.GHOSTTY_TAB) orelse return null,
));
}
/// focus the nth tab
pub fn gotoNthTab(self: *NotebookGtk, position: c_int) void {
c.gtk_notebook_set_current_page(self.notebook, position);
}
/// get the position of the current tab
pub fn getTabPosition(self: *NotebookGtk, tab: *Tab) ?c_int {
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return null;
return getNotebookPageIndex(page);
}
pub fn reorderPage(self: *NotebookGtk, tab: *Tab, position: c_int) void {
c.gtk_notebook_reorder_child(self.notebook, @ptrCast(tab.box), position);
}
pub fn setTabLabel(_: *NotebookGtk, tab: *Tab, title: [:0]const u8) void {
c.gtk_label_set_text(tab.label_text, title.ptr);
}
pub fn setTabTooltip(_: *NotebookGtk, tab: *Tab, tooltip: [:0]const u8) void {
c.gtk_widget_set_tooltip_text(@ptrCast(@alignCast(tab.label_text)), tooltip.ptr);
}
/// Adds a new tab with the given title to the notebook.
pub fn addTab(self: *NotebookGtk, tab: *Tab, position: c_int, title: [:0]const u8) void {
const box_widget: *c.GtkWidget = @ptrCast(tab.box);
// Build the tab label
const label_box_widget = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 0);
const label_box = @as(*c.GtkBox, @ptrCast(label_box_widget));
const label_text_widget = c.gtk_label_new(title.ptr);
const label_text: *c.GtkLabel = @ptrCast(label_text_widget);
c.gtk_box_append(label_box, label_text_widget);
tab.label_text = label_text;
const window = tab.window;
if (window.app.config.@"gtk-wide-tabs") {
c.gtk_widget_set_hexpand(label_box_widget, 1);
c.gtk_widget_set_halign(label_box_widget, c.GTK_ALIGN_FILL);
c.gtk_widget_set_hexpand(label_text_widget, 1);
c.gtk_widget_set_halign(label_text_widget, c.GTK_ALIGN_FILL);
// This ensures that tabs are always equal width. If they're too
// long, they'll be truncated with an ellipsis.
c.gtk_label_set_max_width_chars(label_text, 1);
c.gtk_label_set_ellipsize(label_text, c.PANGO_ELLIPSIZE_END);
// We need to set a minimum width so that at a certain point
// the notebook will have an arrow button rather than shrinking tabs
// to an unreadably small size.
c.gtk_widget_set_size_request(label_text_widget, 100, 1);
}
// Build the close button for the tab
const label_close_widget = c.gtk_button_new_from_icon_name("window-close-symbolic");
const label_close: *c.GtkButton = @ptrCast(label_close_widget);
c.gtk_button_set_has_frame(label_close, 0);
c.gtk_box_append(label_box, label_close_widget);
const page_idx = c.gtk_notebook_insert_page(
self.notebook,
box_widget,
label_box_widget,
position,
);
// Clicks
const gesture_tab_click = c.gtk_gesture_click_new();
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
// Tab settings
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
c.gtk_notebook_set_tab_detachable(self.notebook, box_widget, 1);
if (self.nPages() > 1) {
c.gtk_notebook_set_show_tabs(self.notebook, 1);
}
// Switch to the new tab
c.gtk_notebook_set_current_page(self.notebook, page_idx);
}
pub fn closeTab(self: *NotebookGtk, tab: *Tab) void {
const page = c.gtk_notebook_get_page(self.notebook, @ptrCast(tab.box)) orelse return;
// Find page and tab which we're closing
const page_idx = getNotebookPageIndex(page);
// Remove the page. This will destroy the GTK widgets in the page which
// will trigger Tab cleanup. The `tab` variable is therefore unusable past that point.
c.gtk_notebook_remove_page(self.notebook, page_idx);
const remaining = self.nPages();
switch (remaining) {
// If we have no more tabs we close the window
0 => c.gtk_window_destroy(tab.window.window),
// If we have one more tab we hide the tab bar
1 => c.gtk_notebook_set_show_tabs(self.notebook, 0),
else => {},
}
// If we have remaining tabs, we need to make sure we grab focus.
if (remaining > 0)
(self.currentTab() orelse return).window.focusCurrentTab();
}
};
fn getNotebookPageIndex(page: *c.GtkNotebookPage) c_int {
var value: c.GValue = std.mem.zeroes(c.GValue);
defer c.g_value_unset(&value);
_ = c.g_value_init(&value, c.G_TYPE_INT);
c.g_object_get_property(
@ptrCast(@alignCast(page)),
"position",
&value,
);
return c.g_value_get_int(&value);
}
fn gtkPageAdded(
notebook: *c.GtkNotebook,
_: *c.GtkWidget,
page_idx: c.guint,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Window = @ptrCast(@alignCast(ud.?));
// The added page can come from another window with drag and drop, thus we migrate the tab
// window to be self.
const page = c.gtk_notebook_get_nth_page(notebook, @intCast(page_idx));
const tab: *Tab = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return,
));
tab.window = self;
// Whenever a new page is added, we always grab focus of the
// currently selected page. This was added specifically so that when
// we drag a tab out to create a new window ("create-window" event)
// we grab focus in the new window. Without this, the terminal didn't
// have focus.
self.focusCurrentTab();
}
fn gtkPageRemoved(
_: *c.GtkNotebook,
_: *c.GtkWidget,
_: c.guint,
ud: ?*anyopaque,
) callconv(.C) void {
log.warn("gtkPageRemoved", .{});
const window: *Window = @ptrCast(@alignCast(ud.?));
// Hide the tab bar if we only have one tab after removal
const remaining = c.gtk_notebook_get_n_pages(window.notebook.gtk.notebook);
if (remaining == 1) {
c.gtk_notebook_set_show_tabs(window.notebook.gtk.notebook, 0);
}
}
fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaque) callconv(.C) void {
const window: *Window = @ptrCast(@alignCast(ud.?));
const self = &window.notebook.gtk;
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
const label_text = c.gtk_label_get_text(gtk_label);
window.setTitle(std.mem.span(label_text));
}
fn gtkNotebookCreateWindow(
_: *c.GtkNotebook,
page: *c.GtkWidget,
ud: ?*anyopaque,
) callconv(.C) ?*c.GtkNotebook {
// The tab for the page is stored in the widget data.
const tab: *Tab = @ptrCast(@alignCast(
c.g_object_get_data(@ptrCast(page), Tab.GHOSTTY_TAB) orelse return null,
));
const currentWindow: *Window = @ptrCast(@alignCast(ud.?));
const newWindow = createWindow(currentWindow) catch |err| {
log.warn("error creating new window error={}", .{err});
return null;
};
// And add it to the new window.
tab.window = newWindow;
return newWindow.notebook.gtk.notebook;
}
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
const tab: *Tab = @ptrCast(@alignCast(ud));
tab.closeWithConfirmation();
}
fn gtkTabClick(
gesture: *c.GtkGestureClick,
_: c.gint,
_: c.gdouble,
_: c.gdouble,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *Tab = @ptrCast(@alignCast(ud));
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
self.closeWithConfirmation();
}
}

View File

@ -131,4 +131,10 @@ pub const Window = union(Protocol) {
inline else => |v| v.clientSideDecorationEnabled(),
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
switch (self.*) {
inline else => |*v| try v.addSubprocessEnv(env),
}
}
};

View File

@ -61,4 +61,6 @@ pub const Window = struct {
_ = self;
return true;
}
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
};

View File

@ -262,6 +262,11 @@ pub const Window = struct {
};
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
_ = self;
_ = env;
}
/// Update the blur state of the window.
fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return;

View File

@ -314,6 +314,13 @@ pub const Window = struct {
);
}
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
var buf: [64]u8 = undefined;
const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window});
try env.put("WINDOWID", window_id);
}
fn getWindowProperty(
self: *Window,
comptime T: type,

View File

@ -19,7 +19,7 @@ const GitVersion = @import("GitVersion.zig");
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
/// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release.
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 1 };
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 3 };
/// Standard build configuration options.
optimize: std.builtin.OptimizeMode,
@ -32,7 +32,6 @@ renderer: renderer.Impl = .opengl,
font_backend: font.Backend = .freetype,
/// Feature flags
adwaita: bool = false,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
@ -132,12 +131,6 @@ pub fn init(b: *std.Build) !Config {
//---------------------------------------------------------------
// Feature Flags
config.adwaita = b.option(
bool,
"gtk-adwaita",
"Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true;
config.flatpak = b.option(
bool,
"flatpak",
@ -397,7 +390,6 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
// We need to break these down individual because addOption doesn't
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
@ -442,7 +434,6 @@ pub fn fromOptions() Config {
.version = options.app_version,
.flatpak = options.flatpak,
.adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?,

View File

@ -0,0 +1,37 @@
//! GhosttyFrameData generates a compressed file and zig module which contains (and exposes) the
//! Ghostty animation frames for use in `ghostty +boo`
const GhosttyFrameData = @This();
const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
/// The exe.
exe: *std.Build.Step.Compile,
/// The output path for the compressed framedata zig file
output: std.Build.LazyPath,
pub fn init(b: *std.Build) !GhosttyFrameData {
const exe = b.addExecutable(.{
.name = "framegen",
.root_source_file = b.path("src/build/framegen/main.zig"),
.target = b.host,
});
const run = b.addRunArtifact(exe);
_ = run.addOutputFileArg("framedata.compressed");
return .{
.exe = exe,
.output = run.captureStdOut(),
};
}
/// Add the "framedata" import.
pub fn addImport(self: *const GhosttyFrameData, step: *std.Build.Step.Compile) void {
self.output.addStepDependencies(&step.step);
step.root_module.addAnonymousImport("framedata", .{
.root_source_file = self.output,
});
}

View File

@ -6,6 +6,7 @@ const Config = @import("Config.zig");
const HelpStrings = @import("HelpStrings.zig");
const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig");
config: *const Config,
@ -13,6 +14,7 @@ options: *std.Build.Step.Options,
help_strings: HelpStrings,
metallib: ?*MetallibStep,
unicode_tables: UnicodeTables,
framedata: GhosttyFrameData,
/// Used to keep track of a list of file sources.
pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
@ -22,6 +24,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
.config = cfg,
.help_strings = try HelpStrings.init(b, cfg),
.unicode_tables = try UnicodeTables.init(b),
.framedata = try GhosttyFrameData.init(b),
// Setup by retarget
.options = undefined,
@ -430,9 +433,30 @@ pub fn add(
},
.gtk => {
const gobject = b.dependency("gobject", .{
.target = target,
.optimize = optimize,
});
const gobject_imports = .{
.{ "gobject", "gobject2" },
.{ "gio", "gio2" },
.{ "glib", "glib2" },
.{ "gtk", "gtk4" },
.{ "gdk", "gdk4" },
};
inline for (gobject_imports) |import| {
const name, const module = import;
step.root_module.addImport(name, gobject.module(module));
}
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (self.config.adwaita) step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
if (self.config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
step.root_module.addImport("adw", gobject.module("adw1"));
if (self.config.x11) {
step.linkSystemLibrary2("X11", dynamic_link_opts);
step.root_module.addImport("gdk_x11", gobject.module("gdkx114"));
}
if (self.config.wayland) {
const scanner = Scanner.create(b.dependency("zig_wayland", .{}), .{
@ -460,14 +484,54 @@ pub fn add(
scanner.generate("org_kde_kwin_server_decoration_manager", 1);
step.root_module.addImport("wayland", wayland);
step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4"));
step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
}
{
const gresource = @import("../apprt/gtk/gresource.zig");
const wf = b.addWriteFiles();
const gresource_xml = wf.add("gresource.xml", gresource.gresource_xml);
const gresource_xml = gresource_xml: {
const generate_gresource_xml = b.addExecutable(.{
.name = "generate_gresource_xml",
.root_source_file = b.path("src/apprt/gtk/gresource.zig"),
.target = b.host,
});
const generate = b.addRunArtifact(generate_gresource_xml);
for (gresource.blueprint_files) |blueprint_file| {
const blueprint_compiler = b.addSystemCommand(&.{
"blueprint-compiler",
"compile",
"--output",
});
const ui_file = blueprint_compiler.addOutputFileArg(b.fmt("{s}.ui", .{blueprint_file}));
blueprint_compiler.addFileArg(b.path(b.fmt("src/apprt/gtk/ui/{s}.blp", .{blueprint_file})));
generate.addFileArg(ui_file);
}
break :gresource_xml generate.captureStdOut();
};
{
const gtk_builder_check = b.addExecutable(.{
.name = "gtk_builder_check",
.root_source_file = b.path("src/apprt/gtk/builder_check.zig"),
.target = b.host,
});
gtk_builder_check.root_module.addOptions("build_options", self.options);
gtk_builder_check.root_module.addImport("gtk", gobject.module("gtk4"));
gtk_builder_check.root_module.addImport("adw", gobject.module("adw1"));
for (gresource.dependencies) |pathname| {
const extension = std.fs.path.extension(pathname);
if (!std.mem.eql(u8, extension, ".ui")) continue;
const check = b.addRunArtifact(gtk_builder_check);
check.addFileArg(b.path(pathname));
step.step.dependOn(&check.step);
}
}
const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources",
@ -499,6 +563,7 @@ pub fn add(
self.help_strings.addImport(step);
self.unicode_tables.addImport(step);
self.framedata.addImport(step);
return static_libs;
}

View File

@ -0,0 +1,49 @@
ARG DISTRO_VERSION="12"
FROM docker.io/library/debian:${DISTRO_VERSION}
# Install Dependencies
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
apt-get -qq -y --no-install-recommends install \
# Build Tools
build-essential \
libbz2-dev \
libonig-dev \
lintian \
lsb-release \
pandoc \
wget \
# Ghostty Dependencies
libadwaita-1-dev \
libgtk-4-dev && \
# Clean up for better caching
rm -rf /var/lib/apt/lists/*
# work around the fact that Debian 12 doesn't ship a pkg-config file for bzip2
RUN . /etc/os-release; if [ $VERSION_ID -le 12 ]; then ln -s libbz2.so /usr/lib/$(gcc -dumpmachine)/libbzip2.so; fi
# Install zig
# https://ziglang.org/download/
ARG ZIG_VERSION="0.13.0"
RUN wget -q "https://ziglang.org/download/$ZIG_VERSION/zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" && \
tar -xf "zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" -C /opt && \
rm zig-linux-* && \
ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig
WORKDIR /src
COPY ./dist/linux /src/dist/linux
COPY ./images /src/images
COPY ./include /src/include
COPY ./pkg /src/pkg
COPY ./nix /src/nix
COPY ./vendor /src/vendor
COPY ./build.zig /src/build.zig
COPY ./build.zig.zon /src/build.zig.zon
COPY ./build.zig.zon.txt /src/build.zig.zon.txt
RUN ZIG_GLOBAL_CACHE_DIR=/zig/global-cache ./nix/build-support/fetch-zig-cache.sh
COPY ./src /src/src
RUN zig build -Doptimize=Debug -Dcpu=baseline -Dapp-runtime=gtk --system /zig/global-cache/p

View File

@ -0,0 +1,41 @@
<span class="b">+++==*%%%%%%%%%%%%*==+++</span>
<span class="b">++****++</span> <span class="b">++****++</span>
<span class="b">++**++</span> <span class="b">++**++</span>
<span class="b">xx**+=</span> o+*%$@@@@@@$%*+o <span class="b">=+**xx</span>
<span class="b">xx**oo</span> ·=$@@@@@@@$$$$$$$$@@@@@@@$=· <span class="b">oo**xx</span>
<span class="b">xx**</span> x$@@@$$$$$$$$$$$$$$$$$$$$$$$$@@@$x <span class="b">**xx</span>
<span class="b">ox**</span> ·$@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$· <span class="b">**xo</span>
<span class="b">==+~</span> ~@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@~ <span class="b">~+==</span>
<span class="b">x+++</span> $@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@$ <span class="b">+++x</span>
<span class="b">==</span> ·@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@· <span class="b">==</span>
<span class="b">ox++</span> ~@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@~ <span class="b">++xo</span>
<span class="b">+++~</span> @$$$$$@@@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ <span class="b">~+++</span>
<span class="b">==</span> $$$$$@@%%%%%%$$$$$$$@@@@@$$$@@@@@@@@@@@@@@@@@@@@$$$$$$ <span class="b">==</span>
<span class="b">==</span> @$$$$* $$$$% =$$$$$@ <span class="b">==</span>
<span class="b">==</span> ·$$$$@ x@$@ @$$$$$· <span class="b">==</span>
<span class="b">==</span> ·@$$$$% ·$$$$% *$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$@@$%%$$$$$$@@@@@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ <span class="b">==</span>
<span class="b">==</span> @@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ <span class="b">==</span>
<span class="b">==x·</span> $@@$$$$$$$$$@@@@@@@@@@$$$$$$$$@@@@@@@@@@$$$$$$$$$@@$ <span class="b">·+==</span>
<span class="b">++++</span> =@@@@@@@@@* x$@@@@@@@@$x *@@@@@@@@@= <span class="b">++++</span>
<span class="b">xx==++</span> <span class="b">++==oo</span>
<span class="b">++===+</span> <span class="b">++%%+o</span> <span class="b">o+%%++</span> <span class="b">+===++</span>
<span class="b">++=====%+=++++*=*========***++++***========*=*++++=+%=====++</span>
<span class="b">xx++==******====++</span> <span class="b">++==********==++</span> <span class="b">++====******==++xx</span>
<span class="b">++++</span> <span class="b">++++</span> <span class="b">++++</span>

View File

@ -0,0 +1,41 @@
<span class="b">++++++++++++</span>
<span class="b">++==*%%%**=++++++=**%%%*==++</span>
<span class="b">++**=*</span> <span class="b">*=**++</span>
<span class="b">x+**+=</span> <span class="b">=+**+x</span>
<span class="b">++==</span> o=%@@@@@@@@@@@@@@@@%=o <span class="b">==++</span>
<span class="b">===+</span> +$@@@@@$$$$$$$$$$$$$$$$@@@@@$+ <span class="b">+===</span>
<span class="b">++=+</span> =@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$@@@@= <span class="b">+=++</span>
<span class="b">++==</span> $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ <span class="b">==++</span>
<span class="b">**</span> $@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@$ <span class="b">**</span>
<span class="b">+++o</span> +@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@+ <span class="b">o+++</span>
<span class="b">==</span> %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% <span class="b">==</span>
<span class="b">xx++</span> %@$$$$@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% <span class="b">++xx</span>
<span class="b">==+·</span> x@$$$@@%=*%$$@@@@@@@@@@@$$$$$@@@@@@@@@@@@@@@@@@$$$$$@x <span class="b">·+==</span>
<span class="b">==</span> @$$$$% ~$@$$$@= x@$$$$$@ <span class="b">==</span>
<span class="b">==</span> $$$$@* %$$@· @$$$$$ <span class="b">==</span>
<span class="b">==</span> ·@$$$@* $$$$+ $$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$~ ~ox+=%@@$$$@@*==============*$@$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$@@@@@@@@@@@@@@@@@$$$$$$$@@@@@@@@@@@@@@@@$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ <span class="b">==</span>
<span class="b">==</span> $@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@@@@$$$$$$$$$$$$$$@$ <span class="b">==</span>
<span class="b">==+o</span> +@@@$$$$$$$@@@$***%@@@@$$$$$$@@@@%***$@@@$$$$$$$@@@+ <span class="b">o+==</span>
<span class="b">++==</span> ·=$@@@@@$*~ +%@@@@@@%+ ~*$@@@@@$=· <span class="b">==++</span>
<span class="b">===+</span> <span class="b">+===</span>
<span class="b">++====x+</span> <span class="b">*=**==</span> <span class="b">==**=*</span> <span class="b">+x====++</span>
<span class="b">++=====**%******=========**%****%**=========******%**=====++</span>
<span class="b">++++========++xx</span> <span class="b">++++========++++</span> <span class="b">xx++========++++</span>

View File

@ -0,0 +1,41 @@
<span class="b">++====****====++</span>
<span class="b">==***%==xo</span> <span class="b">ox==%***==</span>
<span class="b">===*++</span> <span class="b">++*===</span>
<span class="b">++**x+</span> ·oxx++xxo· <span class="b">+x**++</span>
<span class="b">===+</span> o=$@@@@@@@@@@@@@@@@@@$=o <span class="b">+===</span>
<span class="b">**+x</span> o%@@@@$$$$$$$$$$$$$$$$$$$$@@@@%o <span class="b">x+**</span>
<span class="b">==+o</span> ~$@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@$~ <span class="b">o+==</span>
<span class="b">++++</span> +@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@@+ <span class="b">++++</span>
<span class="b">ox==</span> o@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@o <span class="b">==xo</span>
<span class="b">==+~</span> %@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@% <span class="b">~+==</span>
<span class="b">ox==</span> @@$$$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@@ <span class="b">==xo</span>
<span class="b">+++x</span> @$$$$@+ ·+*$@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ <span class="b">x+++</span>
<span class="b">==+</span> *@$$$@ o=%@@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$@* <span class="b">+==</span>
<span class="b">==</span> @$$$$$o %$$$$$ *$$$$$@ <span class="b">==</span>
<span class="b">==</span> $$$$$$@@=· @$$@ @$$$$$ <span class="b">==</span>
<span class="b">==</span> ·@$$$$$x %$$$$% =$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$@ ·x*$@@@$$$$@@@@@@@@@@@@@@@@@@@@$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$@x o=%@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$@@@@@@@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ·@$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@· <span class="b">==</span>
<span class="b">==</span> ~$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$~ <span class="b">==</span>
<span class="b">==</span> @$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$@ <span class="b">==</span>
<span class="b">==</span> %@$$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$@@@@@@$$$$$$$$$$$$$@% <span class="b">==</span>
<span class="b">==+x</span> ~$@@@@$$$$@@@@%+xx=$@@@@$$$$@@@@$=xx+*@@@@$$$$@@@@$~ <span class="b">x+==</span>
<span class="b">++==</span> x*$@@@$*x ·=%$@@$%=· x*$@@@$*x <span class="b">==++</span>
<span class="b">====</span> <span class="b">=+==</span>
<span class="b">x+====+=</span> <span class="b">x+*===+=</span> <span class="b">======+x</span> <span class="b">=+====xx</span>
<span class="b">++====*%%%%***====++====*%*%%*%*====++=====**%%%%*====++</span>
<span class="b">xx++++====++++</span> <span class="b">++++====++++</span> <span class="b">++++====++++xx</span>

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