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 vendor/** linguist-vendored
website/** linguist-documentation website/** linguist-documentation
pkg/breakpad/vendor/** linguist-vendored pkg/breakpad/vendor/** linguist-vendored

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ jobs:
- build-bench - build-bench
- build-linux-libghostty - build-linux-libghostty
- build-nix - build-nix
- build-snap
- build-macos - build-macos
- build-macos-matrix - build-macos-matrix
- build-windows - build-windows
@ -25,6 +26,7 @@ jobs:
- alejandra - alejandra
- typos - typos
- test-pkg-linux - test-pkg-linux
- test-debian-12
steps: steps:
- id: status - id: status
name: Determine status name: Determine status
@ -202,10 +204,14 @@ jobs:
- name: XCode Select - name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app 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 # GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. # Mac app to access.
- name: Build GhosttyKit - 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 # The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment. # codesigning. IMPORTANT: this must NOT run in a Nix environment.
@ -238,35 +244,65 @@ jobs:
- name: XCode Select - name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app 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 - name: Test All
run: | run: |
# OpenGL # OpenGL
nix develop -c zig build test -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=freetype
nix develop -c zig build test -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
nix develop -c zig build test -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_freetype
nix develop -c zig build test -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_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=coretext_noshape
# Metal # Metal
nix develop -c zig build test -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=freetype
nix develop -c zig build test -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
nix develop -c zig build test -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_freetype
nix develop -c zig build test -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_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=coretext_noshape
- name: Build All - name: Build All
run: | run: |
nix develop -c zig build -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=freetype
nix develop -c zig build -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
nix develop -c zig build -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_freetype
nix develop -c zig build -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_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=coretext_noshape
nix develop -c zig build -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=freetype
nix develop -c zig build -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
nix develop -c zig build -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_freetype
nix develop -c zig build -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_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=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: build-windows:
runs-on: windows-2022 runs-on: windows-2022
@ -366,7 +402,7 @@ jobs:
run: nix develop -c zig build -Dapp-runtime=none test run: nix develop -c zig build -Dapp-runtime=none test
- name: Test GTK Build - 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 - name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw run: nix develop -c zig build -Dapp-runtime=glfw
@ -379,10 +415,9 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
adwaita: ["true", "false"]
x11: ["true", "false"] x11: ["true", "false"]
wayland: ["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 runs-on: namespace-profile-ghostty-sm
needs: test needs: test
env: env:
@ -413,7 +448,6 @@ jobs:
nix develop -c \ nix develop -c \
zig build \ zig build \
-Dapp-runtime=gtk \ -Dapp-runtime=gtk \
-Dgtk-adwaita=${{ matrix.adwaita }} \
-Dgtk-x11=${{ matrix.x11 }} \ -Dgtk-x11=${{ matrix.x11 }} \
-Dgtk-wayland=${{ matrix.wayland }} -Dgtk-wayland=${{ matrix.wayland }}
@ -471,8 +505,12 @@ jobs:
- name: XCode Select - name: XCode Select
run: sudo xcode-select -s /Applications/Xcode_16.0.app 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 - name: test
run: nix develop -c zig build test run: nix develop -c zig build test --system ${{ steps.deps.outputs.deps }}
prettier: prettier:
if: github.repository == 'ghostty-org/ghostty' if: github.repository == 'ghostty-org/ghostty'
@ -589,3 +627,26 @@ jobs:
- name: Test ${{ matrix.pkg }} Build - name: Test ${{ matrix.pkg }} Build
run: | run: |
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test" 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: | run: |
# Only proceed if build.zig.zon has changed # Only proceed if build.zig.zon has changed
if ! git diff --exit-code build.zig.zon; then 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.sh --update
nix develop -c ./nix/build-support/check-zig-cache-hash.sh nix develop -c ./nix/build-support/check-zig-cache.sh
fi fi
# Verify the build still works. We choose an arbitrary build type # Verify the build still works. We choose an arbitrary build type
# as a canary instead of testing all build types. # as a canary instead of testing all build types.
- name: Test Build - name: Test Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true run: nix build .#ghostty
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7
@ -66,7 +66,9 @@ jobs:
commit-message: "deps: Update iTerm2 color schemes" commit-message: "deps: Update iTerm2 color schemes"
add-paths: | add-paths: |
build.zig.zon build.zig.zon
nix/zigCacheHash.nix build.zig.zon2json-lock
build.zig.zon.nix
build.zig.zon.txt
body: | body: |
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }} Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
labels: dependencies 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 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 Signature files are signed with
[minisign](https://jedisct1.github.io/minisign/) [minisign](https://jedisct1.github.io/minisign/)
using the following public key: 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` install step can then be done with a `mv` or `cp` command (from `/tmp/ghostty`
to wherever the package manager expects it). 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 ### Build Options
Ghostty uses the Zig build system. You can see all available build options by Ghostty uses the Zig build system. You can see all available build options by

View File

@ -1,32 +1,39 @@
.{ .{
.name = "ghostty", .name = "ghostty",
.version = "1.1.1", .version = "1.1.3",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// Zig libs // Zig libs
.libxev = .{ .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", .hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
}, },
.mach_glfw = .{ .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", .hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
.lazy = true, .lazy = true,
}, },
.vaxis = .{ .vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b", // rockorager/libvaxis
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f", .url = "git+https://github.com/rockorager/libvaxis#2237a7059eae99e9f132dd5acd1555e49d6c7d93",
.hash = "1220f5aec880d4f430cc1597ede88f1530da69e39a4986080e976b0c7b919c2ebfeb",
}, },
.z2d = .{ .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", .hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
}, },
.zig_objc = .{ .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", .hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
}, },
.zig_js = .{ .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", .hash = "12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc",
}, },
.ziglyph = .{ .ziglyph = .{
@ -34,13 +41,20 @@
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25", .hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
}, },
.zig_wayland = .{ .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", .hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
}, },
.zf = .{ .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", .hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
}, },
.gobject = .{
// ianprime0509/zig-gobject
.url = "https://deps.files.ghostty.org/gobject-12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d.tar.zst",
.hash = "12208d70ee791d7ef7e16e1c3c9c1127b57f1ed066a24f87d57fc9f730c5dc394b9d",
},
// C libs // C libs
.cimgui = .{ .path = "./pkg/cimgui" }, .cimgui = .{ .path = "./pkg/cimgui" },
@ -72,15 +86,15 @@
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef", .hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
}, },
.plasma_wayland_protocols = .{ .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", .hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
}, },
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/db227d159adc265818f2e898da0f70ef8d7b580e.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/efb1bb1843500a751eb30afa58fe48a6bec8952c.tar.gz",
.hash = "12203d2647e5daf36a9c85b969e03f422540786ce9ea624eb4c26d204fe1f46218f3", .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-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1696426674, "lastModified": 1733328505,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -21,11 +21,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1705309234, "lastModified": 1731533236,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -36,11 +36,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1733423277, "lastModified": 1738255539,
"narHash": "sha256-TxabjxEgkNbCGFRHgM/b9yZWlBj60gUOUnRT/wbVQR8=", "narHash": "sha256-hP2eOqhIO/OILW+3moNWO4GtdJFYCqAe9yJZgvlCoDQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e36963a147267afc055f7cf65225958633e536bf", "rev": "c3511a3b53b482aa7547c9d1626fd7310c1de1c5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -52,11 +52,11 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1733229606, "lastModified": 1738136902,
"narHash": "sha256-FLYY5M0rpa5C2QAE3CKLYAM6TwbKicdRK6qNrSHlNrE=", "narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "566e53c2ad750c84f6d31f9ccb9d00f823165550", "rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -69,9 +69,11 @@
"root": { "root": {
"inputs": { "inputs": {
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs-stable": "nixpkgs-stable", "nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable", "nixpkgs-unstable": "nixpkgs-unstable",
"zig": "zig" "zig": "zig",
"zig2nix": "zig2nix"
} }
}, },
"systems": { "systems": {
@ -92,17 +94,19 @@
"zig": { "zig": {
"inputs": { "inputs": {
"flake-compat": [], "flake-compat": [],
"flake-utils": "flake-utils", "flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs-stable" "nixpkgs-stable"
] ]
}, },
"locked": { "locked": {
"lastModified": 1717848532, "lastModified": 1738239110,
"narHash": "sha256-d+xIUvSTreHl8pAmU1fnmkfDTGQYCn2Rb/zOwByxS2M=", "narHash": "sha256-Y5i9mQ++dyIQr+zEPNy+KIbc5wjPmfllBrag3cHZgcE=",
"owner": "mitchellh", "owner": "mitchellh",
"repo": "zig-overlay", "repo": "zig-overlay",
"rev": "02fc5cc555fc14fda40c42d7c3250efa43812b43", "rev": "1a8fb6f3a04724519436355564b95fce5e272504",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -110,6 +114,30 @@
"repo": "zig-overlay", "repo": "zig-overlay",
"type": "github" "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", "root": "root",

View File

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

View File

@ -412,6 +412,7 @@ typedef enum {
GHOSTTY_FULLSCREEN_NATIVE, GHOSTTY_FULLSCREEN_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE, GHOSTTY_FULLSCREEN_NON_NATIVE,
GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU, GHOSTTY_FULLSCREEN_NON_NATIVE_VISIBLE_MENU,
GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH,
} ghostty_action_fullscreen_e; } ghostty_action_fullscreen_e;
// apprt.action.SecureInput // apprt.action.SecureInput
@ -585,6 +586,7 @@ typedef enum {
GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_RENDER_INSPECTOR,
GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_DESKTOP_NOTIFICATION,
GHOSTTY_ACTION_SET_TITLE, GHOSTTY_ACTION_SET_TITLE,
GHOSTTY_ACTION_PROMPT_TITLE,
GHOSTTY_ACTION_PWD, GHOSTTY_ACTION_PWD,
GHOSTTY_ACTION_MOUSE_SHAPE, GHOSTTY_ACTION_MOUSE_SHAPE,
GHOSTTY_ACTION_MOUSE_VISIBILITY, GHOSTTY_ACTION_MOUSE_VISIBILITY,
@ -644,7 +646,7 @@ typedef void (*ghostty_runtime_write_clipboard_cb)(void*,
ghostty_clipboard_e, ghostty_clipboard_e,
bool); bool);
typedef void (*ghostty_runtime_close_surface_cb)(void*, 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_target_s,
ghostty_action_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 */; }; A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.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 */; }; 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 */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
@ -270,6 +272,7 @@
A534263D2A7DCBB000EBB7A2 /* Helpers */ = { A534263D2A7DCBB000EBB7A2 /* Helpers */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */,
A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5A6F7292CC41B8700B232A5 /* Xcode.swift */,
A5CEAFFE29C2410700646FDA /* Backport.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */,
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
@ -623,6 +626,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */,
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,

View File

@ -28,7 +28,9 @@ class AppDelegate: NSObject,
@IBOutlet private var menuNewWindow: NSMenuItem? @IBOutlet private var menuNewWindow: NSMenuItem?
@IBOutlet private var menuNewTab: NSMenuItem? @IBOutlet private var menuNewTab: NSMenuItem?
@IBOutlet private var menuSplitRight: NSMenuItem? @IBOutlet private var menuSplitRight: NSMenuItem?
@IBOutlet private var menuSplitLeft: NSMenuItem?
@IBOutlet private var menuSplitDown: NSMenuItem? @IBOutlet private var menuSplitDown: NSMenuItem?
@IBOutlet private var menuSplitUp: NSMenuItem?
@IBOutlet private var menuClose: NSMenuItem? @IBOutlet private var menuClose: NSMenuItem?
@IBOutlet private var menuCloseTab: NSMenuItem? @IBOutlet private var menuCloseTab: NSMenuItem?
@IBOutlet private var menuCloseWindow: NSMenuItem? @IBOutlet private var menuCloseWindow: NSMenuItem?
@ -41,6 +43,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuToggleVisibility: NSMenuItem? @IBOutlet private var menuToggleVisibility: NSMenuItem?
@IBOutlet private var menuToggleFullScreen: NSMenuItem? @IBOutlet private var menuToggleFullScreen: NSMenuItem?
@IBOutlet private var menuBringAllToFront: NSMenuItem?
@IBOutlet private var menuZoomSplit: NSMenuItem? @IBOutlet private var menuZoomSplit: NSMenuItem?
@IBOutlet private var menuPreviousSplit: NSMenuItem? @IBOutlet private var menuPreviousSplit: NSMenuItem?
@IBOutlet private var menuNextSplit: NSMenuItem? @IBOutlet private var menuNextSplit: NSMenuItem?
@ -52,6 +55,7 @@ class AppDelegate: NSObject,
@IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuIncreaseFontSize: NSMenuItem?
@IBOutlet private var menuDecreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem?
@IBOutlet private var menuResetFontSize: NSMenuItem? @IBOutlet private var menuResetFontSize: NSMenuItem?
@IBOutlet private var menuChangeTitle: NSMenuItem?
@IBOutlet private var menuQuickTerminal: NSMenuItem? @IBOutlet private var menuQuickTerminal: NSMenuItem?
@IBOutlet private var menuTerminalInspector: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem?
@ -93,7 +97,7 @@ class AppDelegate: NSObject,
} }
/// Tracks the windows that we hid for toggleVisibility. /// Tracks the windows that we hid for toggleVisibility.
private var hiddenWindows: [Weak<NSWindow>] = [] private var hiddenState: ToggleVisibilityState? = nil
/// The observer for the app appearance. /// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
@ -217,8 +221,8 @@ class AppDelegate: NSObject,
} }
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
// If we're back then clear the hidden windows // If we're back manually then clear the hidden state because macOS handles it.
self.hiddenWindows = [] self.hiddenState = nil
// First launch stuff // First launch stuff
if (!applicationHasBecomeActive) { if (!applicationHasBecomeActive) {
@ -245,7 +249,13 @@ class AppDelegate: NSObject,
// This probably isn't fully safe. The isEmpty check above is aspirational, it doesn't // 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 // 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 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. // If the user is shutting down, restarting, or logging out, we don't confirm quit.
why: if let event = NSAppleEventManager.shared().currentAppleEvent { 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_window", menuItem: self.menuCloseWindow)
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows) syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight) 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: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: "copy_to_clipboard", menuItem: self.menuCopy)
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) 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: "increase_font_size:1", menuItem: self.menuIncreaseFontSize)
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) 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_quick_terminal", menuItem: self.menuQuickTerminal)
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector)
@ -524,6 +537,15 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance. // AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance(config: config) } 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. // If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance let c = ConfigurationErrorsController.sharedInstance
c.errors = config.errors c.errors = config.errors
@ -557,6 +579,30 @@ class AppDelegate: NSObject,
self.appIcon = nil self.appIcon = nil
break 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: case .customStyle:
guard let ghostColor = config.macosIconGhostColor else { break } guard let ghostColor = config.macosIconGhostColor else { break }
guard let screenColors = config.macosIconScreenColor 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 /// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
@IBAction func toggleVisibility(_ sender: Any) { @IBAction func toggleVisibility(_ sender: Any) {
// If we have focus, then we hide all windows.
if NSApp.isActive {
// Toggle visibility doesn't do anything if the focused window is native // Toggle visibility doesn't do anything if the focused window is native
// fullscreen. // fullscreen. This is only relevant if Ghostty is active.
guard let keyWindow = NSApp.keyWindow, guard let keyWindow = NSApp.keyWindow,
!keyWindow.styleMask.contains(.fullScreen) else { return } !keyWindow.styleMask.contains(.fullScreen) else { return }
// If we have focus, then we hide all windows. // Keep track of our hidden state to restore properly
if NSApp.isActive { self.hiddenState = .init()
// 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) }
NSApp.hide(nil) NSApp.hide(nil)
return return
} }
@ -734,8 +774,16 @@ class AppDelegate: NSObject,
// Bring all windows to the front. Note: we don't use NSApp.unhide because // 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 // that will unhide ALL hidden windows. We want to only bring forward the
// ones that we hid. // ones that we hid.
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() } hiddenState?.restore()
self.hiddenWindows = [] hiddenState = nil
}
@IBAction func bringAllToFront(_ sender: Any) {
if !NSApp.isActive {
NSApp.activate(ignoringOtherApps: true)
}
NSApplication.shared.arrangeInFront(sender)
} }
private struct DerivedConfig { private struct DerivedConfig {
@ -755,4 +803,33 @@ class AppDelegate: NSObject,
self.quickTerminalPosition = config.quickTerminalPosition 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="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target"> <customObject id="bbz-4X-AYv" userLabel="AppDelegate" customClass="AppDelegate" customModule="Ghostty" customModuleProvider="target">
<connections> <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="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/> <outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/> <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="menuSelectSplitLeft" destination="cTK-oy-KuV" id="Jpr-5q-dqz"/>
<outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/> <outlet property="menuSelectSplitRight" destination="upj-mc-L7X" id="nLY-o1-lky"/>
<outlet property="menuServices" destination="aQe-vS-j8Q" id="uWQ-Wo-T1L"/> <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="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="menuTerminalInspector" destination="QwP-M5-fvh" id="wJi-Dh-S9f"/>
<outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/> <outlet property="menuToggleFullScreen" destination="8kY-Pi-KaY" id="yQg-6V-OO6"/>
<outlet property="menuToggleVisibility" destination="DOX-wA-ilh" id="iBj-Bc-2bq"/> <outlet property="menuToggleVisibility" destination="DOX-wA-ilh" id="iBj-Bc-2bq"/>
@ -143,10 +147,22 @@
<action selector="splitRight:" target="-1" id="cv2-Xg-FR4"/> <action selector="splitRight:" target="-1" id="cv2-Xg-FR4"/>
</connections> </connections>
</menuItem> </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"> <menuItem title="Split Down" id="UDZ-4y-6xL">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <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> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="sjq-M1-UGS"/> <menuItem isSeparatorItem="YES" id="sjq-M1-UGS"/>
@ -232,6 +248,13 @@
</connections> </connections>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="L3L-I8-sqk"/> <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"> <menuItem title="Quick Terminal" id="1pv-LF-NBJ">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
@ -270,12 +293,6 @@
<action selector="toggleGhosttyFullScreen:" target="-1" id="QB9-7R-xyc"/> <action selector="toggleGhosttyFullScreen:" target="-1" id="QB9-7R-xyc"/>
</connections> </connections>
</menuItem> </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"> <menuItem title="Show/Hide All Terminals" id="DOX-wA-ilh">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<connections> <connections>
@ -370,6 +387,13 @@
</items> </items>
</menu> </menu>
</menuItem> </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> </items>
</menu> </menu>
</menuItem> </menuItem>

View File

@ -521,11 +521,21 @@ class BaseTerminalController: NSWindowController,
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) 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) { @IBAction func splitDown(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_DOWN) 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) { @IBAction func splitZoom(_ sender: Any) {
guard let surface = focusedSurface?.surface else { return } guard let surface = focusedSurface?.surface else { return }
ghostty.splitToggleZoom(surface: surface) ghostty.splitToggleZoom(surface: surface)

View File

@ -283,9 +283,12 @@ class TerminalController: BaseTerminalController {
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
guard let window else { return } 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 { guard let x, let y else {
if (!LastWindowPosition.shared.restore(window)) {
window.center() window.center()
}
return return
} }
@ -490,6 +493,20 @@ class TerminalController: BaseTerminalController {
override func windowDidMove(_ notification: Notification) { override func windowDidMove(_ notification: Notification) {
super.windowDidMove(notification) super.windowDidMove(notification)
self.fixTabBar() 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 // 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 // If our index is the same we do nothing
guard finalIndex != selectedIndex else { return } guard finalIndex != selectedIndex else { return }
// Get our parent // Get our target window
let parent = tabbedWindows[finalIndex] 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) tabGroup.removeWindow(selectedWindow)
parent.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above) targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above)
selectedWindow.makeKeyAndOrderFront(nil)
// Ensure our window remains selected
selectedWindow.makeKey()
NSAnimationContext.endGrouping()
} }
@objc private func onGotoTab(notification: SwiftUI.Notification) { @objc private func onGotoTab(notification: SwiftUI.Notification) {

View File

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

View File

@ -95,6 +95,23 @@ fileprivate class CenteredDynamicLabel: NSTextField {
setContentHuggingPriority(.defaultLow, for: .horizontal) setContentHuggingPriority(.defaultLow, for: .horizontal)
setContentCompressionResistancePriority(.defaultHigh, 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 { extension NSToolbarItem.Identifier {

View File

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

View File

@ -257,7 +257,7 @@ extension Ghostty {
// MARK: Ghostty Callbacks (iOS) // MARK: Ghostty Callbacks (iOS)
static func wakeup(_ userdata: UnsafeMutableRawPointer?) {} 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( static func readClipboard(
_ userdata: UnsafeMutableRawPointer?, _ userdata: UnsafeMutableRawPointer?,
location: ghostty_clipboard_e, location: ghostty_clipboard_e,
@ -423,7 +423,7 @@ extension Ghostty {
// MARK: Actions (macOS) // 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 // Make sure it a target we understand so all our action handlers can assert
switch (target.tag) { switch (target.tag) {
case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE: case GHOSTTY_TARGET_APP, GHOSTTY_TARGET_SURFACE:
@ -431,7 +431,7 @@ extension Ghostty {
default: default:
Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)") Ghostty.logger.warning("unknown action target=\(target.tag.rawValue)")
return return false
} }
// Action dispatch // Action dispatch
@ -455,13 +455,13 @@ extension Ghostty {
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen) toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
case GHOSTTY_ACTION_MOVE_TAB: 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: 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: 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: case GHOSTTY_ACTION_RESIZE_SPLIT:
resizeSplit(app, target: target, resize: action.action.resize_split) resizeSplit(app, target: target, resize: action.action.resize_split)
@ -484,6 +484,9 @@ extension Ghostty {
case GHOSTTY_ACTION_SET_TITLE: case GHOSTTY_ACTION_SET_TITLE:
setTitle(app, target: target, v: action.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: case GHOSTTY_ACTION_PWD:
pwdChanged(app, target: target, v: action.action.pwd) pwdChanged(app, target: target, v: action.action.pwd)
@ -541,10 +544,15 @@ extension Ghostty {
fallthrough fallthrough
case GHOSTTY_ACTION_QUIT_TIMER: case GHOSTTY_ACTION_QUIT_TIMER:
Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)") Ghostty.logger.info("known but unimplemented action action=\(action.tag.rawValue)")
return false
default: default:
Ghostty.logger.warning("unknown action action=\(action.tag.rawValue)") 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) { private static func quit(_ app: ghostty_app_t) {
@ -716,15 +724,19 @@ extension Ghostty {
private static func moveTab( private static func moveTab(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,
move: ghostty_action_move_tab_s) { move: ghostty_action_move_tab_s) -> Bool {
switch (target.tag) { switch (target.tag) {
case GHOSTTY_TARGET_APP: case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("move tab does nothing with an app target") Ghostty.logger.warning("move tab does nothing with an app target")
return return false
case GHOSTTY_TARGET_SURFACE: case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return } guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return } 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( NotificationCenter.default.post(
name: .ghosttyMoveTab, name: .ghosttyMoveTab,
object: surfaceView, object: surfaceView,
@ -736,20 +748,27 @@ extension Ghostty {
default: default:
assertionFailure() assertionFailure()
} }
return true
} }
private static func gotoTab( private static func gotoTab(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,
tab: ghostty_action_goto_tab_e) { tab: ghostty_action_goto_tab_e) -> Bool {
switch (target.tag) { switch (target.tag) {
case GHOSTTY_TARGET_APP: case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto tab does nothing with an app target") Ghostty.logger.warning("goto tab does nothing with an app target")
return return false
case GHOSTTY_TARGET_SURFACE: case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return } guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return } 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( NotificationCenter.default.post(
name: Notification.ghosttyGotoTab, name: Notification.ghosttyGotoTab,
object: surfaceView, object: surfaceView,
@ -761,20 +780,31 @@ extension Ghostty {
default: default:
assertionFailure() assertionFailure()
} }
return true
} }
private static func gotoSplit( private static func gotoSplit(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,
direction: ghostty_action_goto_split_e) { direction: ghostty_action_goto_split_e) -> Bool {
switch (target.tag) { switch (target.tag) {
case GHOSTTY_TARGET_APP: case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("goto split does nothing with an app target") Ghostty.logger.warning("goto split does nothing with an app target")
return return false
case GHOSTTY_TARGET_SURFACE: case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return } guard let surface = target.target.surface else { return false }
guard let surfaceView = self.surfaceView(from: surface) else { return } 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( NotificationCenter.default.post(
name: Notification.ghosttyFocusSplit, name: Notification.ghosttyFocusSplit,
object: surfaceView, object: surfaceView,
@ -786,6 +816,8 @@ extension Ghostty {
default: default:
assertionFailure() assertionFailure()
} }
return true
} }
private static func resizeSplit( 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( private static func pwdChanged(
_ app: ghostty_app_t, _ app: ghostty_app_t,
target: ghostty_target_s, target: ghostty_target_s,

View File

@ -216,6 +216,8 @@ extension Ghostty {
.nonNative .nonNative
case "visible-menu": case "visible-menu":
.nonNativeVisibleMenu .nonNativeVisibleMenu
case "padded-notch":
.nonNativePaddedNotch
default: default:
defaultValue defaultValue
} }
@ -300,6 +302,16 @@ extension Ghostty {
return buffer.map { .init(ghostty: $0) } 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 { var focusFollowsMouse : Bool {
guard let config = self.config else { return false } guard let config = self.config else { return false }
var v = false; var v = false;
@ -514,6 +526,11 @@ extension Ghostty.Config {
case download case download
} }
enum MacHidden : String {
case never
case always
}
enum ResizeOverlay : String { enum ResizeOverlay : String {
case always case always
case never 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 { func topLeft() -> SurfaceView {
switch (self) { switch (self) {
case .leaf(let leaf): case .leaf(let leaf):
@ -120,14 +129,7 @@ extension Ghostty {
/// Returns true if the split tree contains the given view. /// Returns true if the split tree contains the given view.
func contains(view: SurfaceView) -> Bool { func contains(view: SurfaceView) -> Bool {
switch (self) { return leaf(for: view) != nil
case .leaf(let leaf):
return leaf.surface == view
case .split(let container):
return container.topLeft.contains(view: view) ||
container.bottomRight.contains(view: view)
}
} }
/// Find a surface view by UUID. /// 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 // MARK: - Sequence
func makeIterator() -> IndexingIterator<[Leaf]> { func makeIterator() -> IndexingIterator<[Leaf]> {

View File

@ -198,6 +198,14 @@ extension Ghostty {
/// macos-icon /// macos-icon
enum MacOSIcon: String { enum MacOSIcon: String {
case official case official
case blueprint
case chalkboard
case glass
case holographic
case microchip
case paper
case retro
case xray
case customStyle = "custom-style" 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 // A timer to fallback to ghost emoji if no title is set within the grace period
private var titleFallbackTimer: Timer? 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) /// Event monitor (see individual events for why)
private var eventMonitor: Any? = nil private var eventMonitor: Any? = nil
@ -380,6 +385,45 @@ extension Ghostty {
NSCursor.setHiddenUntilMouseMoves(!visible) 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) { func setTitle(_ title: String) {
// This fixes an issue where very quick changes to the title could // This fixes an issue where very quick changes to the title could
// cause an unpleasant flickering. We set a timer so that we can // cause an unpleasant flickering. We set a timer so that we can
@ -390,6 +434,11 @@ extension Ghostty {
withTimeInterval: 0.075, withTimeInterval: 0.075,
repeats: false repeats: false
) { [weak self] _ in ) { [weak self] _ in
// Set the title if it wasn't manually set.
guard self?.titleFromTerminal == nil else {
self?.titleFromTerminal = title
return
}
self?.title = title self?.title = title
} }
} }
@ -849,28 +898,8 @@ extension Ghostty {
var handled: Bool = false var handled: Bool = false
if let list = keyTextAccumulator, list.count > 0 { if let list = keyTextAccumulator, list.count > 0 {
handled = true 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 { for text in list {
_ = keyAction(action, event: keyTextEvent, text: text) _ = keyAction(action, event: event, text: text)
}
} }
} }
@ -1132,11 +1161,15 @@ extension Ghostty {
menu.addItem(.separator()) menu.addItem(.separator())
menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "") 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 Down", action: #selector(splitDown(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "")
menu.addItem(.separator()) menu.addItem(.separator())
menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "")
menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), 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 return menu
} }
@ -1189,11 +1222,21 @@ extension Ghostty {
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT) 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) { @IBAction func splitDown(_ sender: Any) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_DOWN) 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) { @objc func resetTerminal(_ sender: Any) {
guard let surface = self.surface else { return } guard let surface = self.surface else { return }
let action = "reset" 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 /// Show a user notification and associate it with this surface
func showUserNotification(title: String, body: String) { func showUserNotification(title: String, body: String) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()

View File

@ -6,6 +6,7 @@ enum FullscreenMode {
case native case native
case nonNative case nonNative
case nonNativeVisibleMenu case nonNativeVisibleMenu
case nonNativePaddedNotch
/// Initializes the fullscreen style implementation for the mode. This will not toggle any /// 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 /// fullscreen properties. This may fail if the window isn't configured properly for a given
@ -20,6 +21,9 @@ enum FullscreenMode {
case .nonNativeVisibleMenu: case .nonNativeVisibleMenu:
return NonNativeFullscreenVisibleMenu(window) return NonNativeFullscreenVisibleMenu(window)
case .nonNativePaddedNotch:
return NonNativeFullscreenPaddedNotch(window)
} }
} }
} }
@ -141,6 +145,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
struct Properties { struct Properties {
var hideMenu: Bool = true var hideMenu: Bool = true
var paddedNotch: Bool = false
} }
private var savedState: SavedState? 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 // 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 // if a bug is reported to Ghostty we can just advise the user to
// update. // 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 return frame
@ -349,3 +357,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
class NonNativeFullscreenVisibleMenu: NonNativeFullscreen { class NonNativeFullscreenVisibleMenu: NonNativeFullscreen {
override var properties: Properties { Properties(hideMenu: false) } 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 #!/bin/sh
set -e # NOTE THIS IS A TEMPORARY SCRIPT TO SUPPORT PACKAGE MAINTAINERS.
# 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).
# #
# 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; # [1]: https://github.com/ziglang/zig/issues/20976
# 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`.
if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ] if [ -z ${ZIG_GLOBAL_CACHE_DIR+x} ]
then then
@ -34,6 +15,13 @@ then
exit 1 exit 1
fi fi
zig build --fetch # Go through each line of our build.zig.zon.txt and fetch it.
zig fetch git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e SCRIPT_PATH="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
zig fetch git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b 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, python3,
qemu, qemu,
scdoc, scdoc,
snapcraft,
valgrind, valgrind,
#, vulkan-loader # unused #, vulkan-loader # unused
vttest, vttest,
@ -30,7 +31,9 @@
glib, glib,
glslang, glslang,
gtk4, gtk4,
gobject-introspection,
libadwaita, libadwaita,
blueprint-compiler,
adwaita-icon-theme, adwaita-icon-theme,
hicolor-icon-theme, hicolor-icon-theme,
harfbuzz, harfbuzz,
@ -47,6 +50,7 @@
simdutf, simdutf,
zlib, zlib,
alejandra, alejandra,
jq,
minisign, minisign,
pandoc, pandoc,
hyperfine, hyperfine,
@ -54,6 +58,8 @@
wayland, wayland,
wayland-scanner, wayland-scanner,
wayland-protocols, wayland-protocols,
zig2nix,
system,
}: let }: let
# See package.nix. Keep in sync. # See package.nix. Keep in sync.
rpathLibs = rpathLibs =
@ -83,6 +89,7 @@
libadwaita libadwaita
gtk4 gtk4
glib glib
gobject-introspection
wayland wayland
]; ];
in in
@ -92,6 +99,7 @@ in
packages = packages =
[ [
# For builds # For builds
jq
llvmPackages_latest.llvm llvmPackages_latest.llvm
minisign minisign
ncurses ncurses
@ -100,6 +108,7 @@ in
scdoc scdoc
zig zig
zip zip
zig2nix.packages.${system}.zon2nix
# For web and wasm stuff # For web and wasm stuff
nodejs nodejs
@ -129,6 +138,7 @@ in
qemu qemu
gdb gdb
snapcraft
valgrind valgrind
wraptest wraptest
@ -154,9 +164,11 @@ in
libXrandr libXrandr
# Only needed for GTK builds # Only needed for GTK builds
blueprint-compiler
libadwaita libadwaita
gtk4 gtk4
glib glib
gobject-introspection
wayland wayland
wayland-scanner wayland-scanner
wayland-protocols wayland-protocols

View File

@ -2,6 +2,7 @@
lib, lib,
stdenv, stdenv,
bzip2, bzip2,
callPackage,
expat, expat,
fontconfig, fontconfig,
freetype, freetype,
@ -12,7 +13,9 @@
libGL, libGL,
glib, glib,
gtk4, gtk4,
gobject-introspection,
libadwaita, libadwaita,
blueprint-compiler,
wrapGAppsHook4, wrapGAppsHook4,
gsettings-desktop-schemas, gsettings-desktop-schemas,
git, git,
@ -42,6 +45,10 @@
zig_hook = zig_0_13.hook.overrideAttrs { zig_hook = zig_0_13.hook.overrideAttrs {
zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off"; zig_default_flags = "-Dcpu=baseline -Doptimize=${optimize} --color off";
}; };
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.1.3";
# We limit source like this to try and reduce the amount of rebuilds as possible # 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 # thus we only provide the source that is needed for the build
@ -60,62 +67,12 @@
../vendor ../vendor
../build.zig ../build.zig
../build.zig.zon ../build.zig.zon
./build-support/fetch-zig-cache.sh ../build.zig.zon.nix
] ]
); );
}; };
# This hash is the computation of the zigCache fixed-output derivation. This deps = callPackage ../build.zig.zon.nix {name = "ghostty-cache-${finalAttrs.version}";};
# 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;
nativeBuildInputs = nativeBuildInputs =
[ [
@ -124,7 +81,9 @@ in
pandoc pandoc
pkg-config pkg-config
zig_hook zig_hook
gobject-introspection
wrapGAppsHook4 wrapGAppsHook4
blueprint-compiler
] ]
++ lib.optionals enableWayland [ ++ lib.optionals enableWayland [
wayland-scanner wayland-scanner
@ -164,7 +123,7 @@ in
zigBuildFlags = [ zigBuildFlags = [
"--system" "--system"
"${zigCache}/p" "${finalAttrs.deps}"
"-Dversion-string=${finalAttrs.version}-${revision}-nix" "-Dversion-string=${finalAttrs.version}-${revision}-nix"
"-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-x11=${lib.boolToString enableX11}"
"-Dgtk-wayland=${lib.boolToString enableWayland}" "-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 # This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details. # 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 // This should be kept in sync with the submodule in the cimgui source
// code in ./vendor/ to be safe that they're compatible. // code in ./vendor/ to be safe that they're compatible.
.imgui = .{ .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", .hash = "1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402",
}, },

View File

@ -3,8 +3,9 @@
.version = "2.13.2", .version = "2.13.2",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// freetype/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", .hash = "1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d",
}, },

View File

@ -3,8 +3,9 @@
.version = "14.2.0", .version = "14.2.0",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// KhronosGroup/glslang
.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", .hash = "12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1",
}, },

View File

@ -3,8 +3,9 @@
.version = "8.4.0", .version = "8.4.0",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// harfbuzz/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", .hash = "1220b8588f106c996af10249bfa092c6fb2f35fbacb1505ef477a0b04a7dd1063122",
}, },

View File

@ -3,8 +3,9 @@
.version = "1.1.0", .version = "1.1.0",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// google/highway
.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", .hash = "12205c83b8311a24b1d5ae6d21640df04f4b0726e314337c043cde1432758cbe165b",
}, },

View File

@ -3,8 +3,9 @@
.version = "1.6.43", .version = "1.6.43",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// glennrp/libpng
.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", .hash = "1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66",
}, },

View File

@ -3,8 +3,9 @@
.version = "6.9.9", .version = "6.9.9",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// kkos/oniguruma
.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", .hash = "1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb",
}, },

View File

@ -3,8 +3,9 @@
.version = "0.7.8", .version = "0.7.8",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// getsentry/sentry-native
.sentry = .{ .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", .hash = "1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e",
}, },

View File

@ -3,8 +3,9 @@
.version = "13.1.1", .version = "13.1.1",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// KhronosGroup/SPIRV-Cross
.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", .hash = "1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da",
}, },

View File

@ -3,8 +3,9 @@
.version = "4.0.5", .version = "4.0.5",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// nemtrif/utfcpp
.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", .hash = "1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641",
}, },

View File

@ -2,13 +2,15 @@
.name = "wuffs", .name = "wuffs",
.version = "0.0.0", .version = "0.0.0",
.dependencies = .{ .dependencies = .{
// google/wuffs
.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", .hash = "122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd",
}, },
// make-github-pseudonymous-again/pixels
.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", .hash = "12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806",
}, },

View File

@ -3,8 +3,9 @@
.version = "1.3.1", .version = "1.3.1",
.paths = .{""}, .paths = .{""},
.dependencies = .{ .dependencies = .{
// madler/zlib
.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", .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; const applied: *const configpkg.Config = if (applied_) |*c| c else config;
// Notify the apprt that the app has changed configuration. // Notify the apprt that the app has changed configuration.
try rt_app.performAction( _ = try rt_app.performAction(
.app, .app,
.config_change, .config_change,
.{ .config = applied }, .{ .config = applied },
@ -180,7 +180,7 @@ pub fn addSurface(
// Since we have non-zero surfaces, we can cancel the quit timer. // 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 // It is up to the apprt if there is a quit timer at all and if it
// should be canceled. // should be canceled.
rt_surface.app.performAction( _ = rt_surface.app.performAction(
.app, .app,
.quit_timer, .quit_timer,
.stop, .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 // If we have no surfaces, we can start the quit timer. It is up to the
// apprt to determine if this is necessary. // 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, .app,
.quit_timer, .quit_timer,
.start, .start,
@ -294,7 +294,7 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
break :target .app; break :target .app;
}; };
try rt_app.performAction( _ = try rt_app.performAction(
target, target,
.new_window, .new_window,
{}, {},
@ -419,7 +419,7 @@ pub fn colorSchemeEvent(
// Request our configuration be reloaded because the new scheme may // Request our configuration be reloaded because the new scheme may
// impact the colors of the app. // impact the colors of the app.
try rt_app.performAction( _ = try rt_app.performAction(
.app, .app,
.reload_config, .reload_config,
.{ .soft = true }, .{ .soft = true },
@ -437,13 +437,13 @@ pub fn performAction(
switch (action) { switch (action) {
.unbind => unreachable, .unbind => unreachable,
.ignore => {}, .ignore => {},
.quit => try rt_app.performAction(.app, .quit, {}), .quit => _ = try rt_app.performAction(.app, .quit, {}),
.new_window => try self.newWindow(rt_app, .{ .parent = null }), .new_window => _ = try self.newWindow(rt_app, .{ .parent = null }),
.open_config => try rt_app.performAction(.app, .open_config, {}), .open_config => _ = try rt_app.performAction(.app, .open_config, {}),
.reload_config => try rt_app.performAction(.app, .reload_config, .{}), .reload_config => _ = try rt_app.performAction(.app, .reload_config, .{}),
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}), .close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}),
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}),
.toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}), .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 // This separate block ({}) is important because our errdefers must
// be scoped here to be valid. // 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 // Initialize our IO backend
var io_exec = try termio.Exec.init(alloc, .{ var io_exec = try termio.Exec.init(alloc, .{
.command = command, .command = command,
.env = env,
.env_override = config.env,
.shell_integration = config.@"shell-integration", .shell_integration = config.@"shell-integration",
.shell_integration_features = config.@"shell-integration-features", .shell_integration_features = config.@"shell-integration-features",
.working_directory = config.@"working-directory", .working_directory = config.@"working-directory",
@ -561,7 +571,7 @@ pub fn init(
errdefer self.io.deinit(); errdefer self.io.deinit();
// Report initial cell size on surface creation // Report initial cell size on surface creation
try rt_app.performAction( _ = try rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.cell_size, .cell_size,
.{ .width = size.cell.width, .height = size.cell.height }, .{ .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_width_cells: u32 = 10;
const min_window_height_cells: u32 = 4; const min_window_height_cells: u32 = 4;
try rt_app.performAction( _ = try rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.size_limit, .size_limit,
.{ .{
@ -637,7 +647,7 @@ pub fn init(
size.padding.top + size.padding.top +
size.padding.bottom; size.padding.bottom;
rt_app.performAction( _ = rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.initial_size, .initial_size,
.{ .width = final_width, .height = final_height }, .{ .width = final_width, .height = final_height },
@ -649,7 +659,7 @@ pub fn init(
} }
if (config.title) |title| { if (config.title) |title| {
try rt_app.performAction( _ = try rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.set_title, .set_title,
.{ .title = title }, .{ .title = title },
@ -670,7 +680,7 @@ pub fn init(
break :xdg; break :xdg;
}; };
defer alloc.free(title); defer alloc.free(title);
try rt_app.performAction( _ = try rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.set_title, .set_title,
.{ .title = title }, .{ .title = title },
@ -823,7 +833,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
// We know that our title should end in 0. // We know that our title should end in 0.
const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0);
log.debug("changing title \"{s}\"", .{slice}); log.debug("changing title \"{s}\"", .{slice});
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.set_title, .set_title,
.{ .title = slice }, .{ .title = slice },
@ -859,7 +869,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.color_change => |change| { .color_change => |change| {
// Notify our apprt, but don't send a mode 2031 DSR report // Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color. // because VT sequences were used to change the color.
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.color_change, .color_change,
.{ .{
@ -878,7 +888,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.set_mouse_shape => |shape| { .set_mouse_shape => |shape| {
log.debug("changing mouse shape: {}", .{shape}); log.debug("changing mouse shape: {}", .{shape});
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_shape, .mouse_shape,
shape, shape,
@ -910,7 +920,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
const str = try self.alloc.dupeZ(u8, w.slice()); const str = try self.alloc.dupeZ(u8, w.slice());
defer self.alloc.free(str); defer self.alloc.free(str);
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.pwd, .pwd,
.{ .pwd = str }, .{ .pwd = str },
@ -961,7 +971,7 @@ fn passwordInput(self: *Surface, v: bool) !void {
} }
// Notify our apprt so it can do whatever it wants. // Notify our apprt so it can do whatever it wants.
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.secure_input, .secure_input,
if (v) .on else .off, if (v) .on else .off,
@ -1050,7 +1060,7 @@ fn mouseRefreshLinks(
self.renderer_state.mouse.point = pos_vp; self.renderer_state.mouse.point = pos_vp;
self.mouse.over_link = true; self.mouse.over_link = true;
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_shape, .mouse_shape,
.pointer, .pointer,
@ -1063,7 +1073,7 @@ fn mouseRefreshLinks(
.trim = false, .trim = false,
}); });
defer self.alloc.free(str); defer self.alloc.free(str);
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_over_link, .mouse_over_link,
.{ .url = str }, .{ .url = str },
@ -1077,7 +1087,7 @@ fn mouseRefreshLinks(
log.warn("failed to get URI for OSC8 hyperlink", .{}); log.warn("failed to get URI for OSC8 hyperlink", .{});
break :link; break :link;
}; };
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_over_link, .mouse_over_link,
.{ .url = uri }, .{ .url = uri },
@ -1087,12 +1097,12 @@ fn mouseRefreshLinks(
try self.queueRender(); try self.queueRender();
} else if (over_link) { } else if (over_link) {
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_shape, .mouse_shape,
self.io.terminal.mouse_shape, self.io.terminal.mouse_shape,
); );
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_over_link, .mouse_over_link,
.{ .url = "" }, .{ .url = "" },
@ -1104,7 +1114,7 @@ fn mouseRefreshLinks(
/// Called when our renderer health state changes. /// Called when our renderer health state changes.
fn updateRendererHealth(self: *Surface, health: renderer.Health) void { fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
log.warn("renderer health status change status={}", .{health}); log.warn("renderer health status change status={}", .{health});
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.renderer_health, .renderer_health,
health, health,
@ -1116,7 +1126,7 @@ fn updateRendererHealth(self: *Surface, health: renderer.Health) void {
/// This should be called anytime `config_conditional_state` changes /// This should be called anytime `config_conditional_state` changes
/// so that the apprt can reload the configuration. /// so that the apprt can reload the configuration.
fn notifyConfigConditionalState(self: *Surface) void { fn notifyConfigConditionalState(self: *Surface) void {
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.reload_config, .reload_config,
.{ .soft = true }, .{ .soft = true },
@ -1196,14 +1206,14 @@ pub fn updateConfig(
// If we have a title set then we update our window to have the // If we have a title set then we update our window to have the
// newly configured title. // newly configured title.
if (config.title) |title| try self.rt_app.performAction( if (config.title) |title| _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.set_title, .set_title,
.{ .title = title }, .{ .title = title },
); );
// Notify the window // Notify the window
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.config_change, .config_change,
.{ .config = config }, .{ .config = config },
@ -1470,7 +1480,7 @@ fn setCellSize(self: *Surface, size: renderer.CellSize) !void {
self.io.queueMessage(.{ .resize = self.size }, .unlocked); self.io.queueMessage(.{ .resize = self.size }, .unlocked);
// Notify the window // Notify the window
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.cell_size, .cell_size,
.{ .width = size.width, .height = size.height }, .{ .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) { } 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 // 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 }, .{ .surface = self },
.mouse_shape, .mouse_shape,
self.io.terminal.mouse_shape, self.io.terminal.mouse_shape,
); );
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_over_link, .mouse_over_link,
.{ .url = "" }, .{ .url = "" },
@ -1789,7 +1799,7 @@ pub fn keyCallback(
.mods = self.mouse.mods, .mods = self.mouse.mods,
.over_link = self.mouse.over_link, .over_link = self.mouse.over_link,
.hidden = self.mouse.hidden, .hidden = self.mouse.hidden,
}).keyToMouseShape()) |shape| try self.rt_app.performAction( }).keyToMouseShape()) |shape| _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_shape, .mouse_shape,
shape, shape,
@ -1914,7 +1924,7 @@ fn maybeHandleBinding(
} }
// Start or continue our key sequence // Start or continue our key sequence
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.key_sequence, .key_sequence,
.{ .trigger = entry.key_ptr.* }, .{ .trigger = entry.key_ptr.* },
@ -2023,7 +2033,7 @@ fn endKeySequence(
mem: KeySequenceMemory, mem: KeySequenceMemory,
) void { ) void {
// Notify apprt key sequence ended // Notify apprt key sequence ended
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.key_sequence, .key_sequence,
.end, .end,
@ -3359,12 +3369,12 @@ pub fn cursorPosCallback(
self.mouse.link_point = null; self.mouse.link_point = null;
if (self.mouse.over_link) { if (self.mouse.over_link) {
self.mouse.over_link = false; self.mouse.over_link = false;
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_shape, .mouse_shape,
self.io.terminal.mouse_shape, self.io.terminal.mouse_shape,
); );
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_over_link, .mouse_over_link,
.{ .url = "" }, .{ .url = "" },
@ -3790,7 +3800,7 @@ fn scrollToBottom(self: *Surface) !void {
fn hideMouse(self: *Surface) void { fn hideMouse(self: *Surface) void {
if (self.mouse.hidden) return; if (self.mouse.hidden) return;
self.mouse.hidden = true; self.mouse.hidden = true;
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_visibility, .mouse_visibility,
.hidden, .hidden,
@ -3802,7 +3812,7 @@ fn hideMouse(self: *Surface) void {
fn showMouse(self: *Surface) void { fn showMouse(self: *Surface) void {
if (!self.mouse.hidden) return; if (!self.mouse.hidden) return;
self.mouse.hidden = false; self.mouse.hidden = false;
self.rt_app.performAction( _ = self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.mouse_visibility, .mouse_visibility,
.visible, .visible,
@ -4015,6 +4025,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
try self.setFontSize(size); try self.setFontSize(size);
}, },
.prompt_surface_title => return try self.rt_app.performAction(
.{ .surface = self },
.prompt_title,
{},
),
.clear_screen => { .clear_screen => {
// This is a duplicate of some of the logic in termio.clearScreen // 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 // 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, v,
), ),
.new_tab => try self.rt_app.performAction( .new_tab => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.new_tab, .new_tab,
{}, {},
), ),
.close_tab => try self.rt_app.performAction( .close_tab => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.close_tab, .close_tab,
{}, {},
@ -4109,7 +4125,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
.next_tab, .next_tab,
.last_tab, .last_tab,
.goto_tab, .goto_tab,
=> |v, tag| try self.rt_app.performAction( => |v, tag| return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.goto_tab, .goto_tab,
switch (tag) { 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 }, .{ .surface = self },
.move_tab, .move_tab,
.{ .amount = position }, .{ .amount = position },
), ),
.new_split => |direction| try self.rt_app.performAction( .new_split => |direction| return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.new_split, .new_split,
switch (direction) { 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 }, .{ .surface = self },
.goto_split, .goto_split,
switch (direction) { 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 }, .{ .surface = self },
.resize_split, .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 }, .{ .surface = self },
.equalize_splits, .equalize_splits,
{}, {},
), ),
.toggle_split_zoom => try self.rt_app.performAction( .toggle_split_zoom => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.toggle_split_zoom, .toggle_split_zoom,
{}, {},
), ),
.toggle_maximize => try self.rt_app.performAction( .toggle_maximize => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.toggle_maximize, .toggle_maximize,
{}, {},
), ),
.toggle_fullscreen => try self.rt_app.performAction( .toggle_fullscreen => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.toggle_fullscreen, .toggle_fullscreen,
switch (self.config.macos_non_native_fullscreen) { switch (self.config.macos_non_native_fullscreen) {
.false => .native, .false => .native,
.true => .macos_non_native, .true => .macos_non_native,
.@"visible-menu" => .macos_non_native_visible_menu, .@"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 }, .{ .surface = self },
.toggle_window_decorations, .toggle_window_decorations,
{}, {},
), ),
.toggle_tab_overview => try self.rt_app.performAction( .toggle_tab_overview => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.toggle_tab_overview, .toggle_tab_overview,
{}, {},
), ),
.toggle_secure_input => try self.rt_app.performAction( .toggle_secure_input => return try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.secure_input, .secure_input,
.toggle, .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 }, .{ .surface = self },
.inspector, .inspector,
switch (mode) { 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_time = now;
self.app.last_notification_digest = new_digest; self.app.last_notification_digest = new_digest;
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.desktop_notification, .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 /// Tell the surface to present itself to the user. This may involve raising the
/// window and switching tabs. /// window and switching tabs.
fn presentSurface(self: *Surface) !void { fn presentSurface(self: *Surface) !void {
try self.rt_app.performAction( _ = try self.rt_app.performAction(
.{ .surface = self }, .{ .surface = self },
.present_terminal, .present_terminal,
{}, {},

View File

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

View File

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

View File

@ -147,13 +147,14 @@ pub const App = struct {
glfw.postEmptyEvent(); 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( pub fn performAction(
self: *App, self: *App,
target: apprt.Target, target: apprt.Target,
comptime action: apprt.Action.Key, comptime action: apprt.Action.Key,
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !void { ) !bool {
switch (action) { switch (action) {
.quit => self.quit = true, .quit => self.quit = true,
@ -238,8 +239,14 @@ pub const App = struct {
.pwd, .pwd,
.config_change, .config_change,
.toggle_maximize, .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. /// 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 { fn sizeCallback(window: glfw.Window, width: i32, height: i32) void {
_ = width; _ = width;
_ = height; _ = height;

View File

@ -25,7 +25,6 @@ const Config = configpkg.Config;
const CoreApp = @import("../../App.zig"); const CoreApp = @import("../../App.zig");
const CoreSurface = @import("../../Surface.zig"); const CoreSurface = @import("../../Surface.zig");
const adwaita = @import("adwaita.zig");
const cgroup = @import("cgroup.zig"); const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig"); const Surface = @import("Surface.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
@ -109,6 +108,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
c.gtk_get_micro_version(), 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 // Load our configuration
var config = try Config.load(core_app.alloc); var config = try Config.load(core_app.alloc);
errdefer config.deinit(); 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 { const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
// I'm unsure of any scenario where this happens. Because we don't // I'm unsure of any scenario where this happens. Because we don't
// want to litter null checks everywhere, we just exit here. // 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); 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 // The "none" cursor is used for hiding the cursor
const cursor_none = c.gdk_cursor_new_from_name("none", null); const cursor_none = c.gdk_cursor_new_from_name("none", null);
errdefer if (cursor_none) |cursor| c.g_object_unref(cursor); errdefer if (cursor_none) |cursor| c.g_object_unref(cursor);
@ -288,80 +286,18 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}; };
// Create our GTK Application which encapsulates our process. // Create our GTK Application which encapsulates our process.
const app: *c.GtkApplication = app: { log.debug("creating GTK application id={s} single-instance={}", .{
log.debug("creating GTK application id={s} single-instance={} adwaita={}", .{
app_id, app_id,
single_instance, single_instance,
adwaita,
}); });
// If not libadwaita, create a standard GTK application. // Using an AdwApplication lets us use Adwaita widgets and access things
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or // such as the color scheme.
!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));
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( const adw_app = @as(?*c.AdwApplication, @ptrCast(c.adw_application_new(
app_id.ptr, app_id.ptr,
app_flags, app_flags,
))) orelse return error.GtkInitFailed; ))) orelse return error.GtkInitFailed;
errdefer c.g_object_unref(adw_app);
const style_manager = c.adw_application_get_style_manager(adw_app); const style_manager = c.adw_application_get_style_manager(adw_app);
c.adw_style_manager_set_color_scheme( c.adw_style_manager_set_color_scheme(
@ -374,17 +310,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
else else
c.ADW_COLOR_SCHEME_PREFER_DARK; c.ADW_COLOR_SCHEME_PREFER_DARK;
}, },
.system => c.ADW_COLOR_SCHEME_PREFER_LIGHT, .system => c.ADW_COLOR_SCHEME_PREFER_LIGHT,
.dark => c.ADW_COLOR_SCHEME_FORCE_DARK, .dark => c.ADW_COLOR_SCHEME_FORCE_DARK,
.light => c.ADW_COLOR_SCHEME_FORCE_LIGHT, .light => c.ADW_COLOR_SCHEME_FORCE_LIGHT,
}, },
); );
break :app @ptrCast(adw_app); const app: *c.GtkApplication = @ptrCast(adw_app);
}; const gapp: *c.GApplication = @ptrCast(app);
errdefer c.g_object_unref(app);
const gapp = @as(*c.GApplication, @ptrCast(app));
// force the resource path to a known value so that it doesn't depend on // force the resource path to a known value so that it doesn't depend on
// the app id and load in compiled resources // the app id and load in compiled resources
@ -507,13 +440,14 @@ pub fn terminate(self: *App) void {
self.config.deinit(); 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( pub fn performAction(
self: *App, self: *App,
target: apprt.Target, target: apprt.Target,
comptime action: apprt.Action.Key, comptime action: apprt.Action.Key,
value: apprt.Action.Value(action), value: apprt.Action.Value(action),
) !void { ) !bool {
switch (action) { switch (action) {
.quit => self.quit(), .quit => self.quit(),
.new_window => _ = try self.newWindow(switch (target) { .new_window => _ = try self.newWindow(switch (target) {
@ -525,12 +459,12 @@ pub fn performAction(
.new_tab => try self.newTab(target), .new_tab => try self.newTab(target),
.close_tab => try self.closeTab(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), .move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value), .new_split => try self.newSplit(target, value),
.resize_split => self.resizeSplit(target, value), .resize_split => self.resizeSplit(target, value),
.equalize_splits => self.equalizeSplits(target), .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), .open_config => try configpkg.edit.open(self.core_app.alloc),
.config_change => self.configChange(target, value.config), .config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value), .reload_config => try self.reloadConfig(target, value),
@ -559,8 +493,16 @@ pub fn performAction(
.render_inspector, .render_inspector,
.renderer_health, .renderer_health,
.color_change, .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 { 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) { switch (target) {
.app => {}, .app => return false,
.surface => |v| { .surface => |v| {
const window = v.rt_surface.container.window() orelse { const window = v.rt_surface.container.window() orelse {
log.info( log.info(
"gotoTab invalid for container={s}", "gotoTab invalid for container={s}",
.{@tagName(v.rt_surface.container)}, .{@tagName(v.rt_surface.container)},
); );
return; return false;
}; };
switch (tab) { return switch (tab) {
.previous => window.gotoPreviousTab(v.rt_surface), .previous => window.gotoPreviousTab(v.rt_surface),
.next => window.gotoNextTab(v.rt_surface), .next => window.gotoNextTab(v.rt_surface),
.last => window.gotoLastTab(), .last => window.gotoLastTab(),
else => window.gotoTab(@intCast(@intFromEnum(tab))), else => window.gotoTab(@intCast(@intFromEnum(tab))),
} };
}, },
} }
} }
@ -668,18 +610,22 @@ fn gotoSplit(
_: *const App, _: *const App,
target: apprt.Target, target: apprt.Target,
direction: apprt.action.GotoSplit, direction: apprt.action.GotoSplit,
) void { ) bool {
switch (target) { switch (target) {
.app => {}, .app => return false,
.surface => |v| { .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) { const map = s.directionMap(switch (v.rt_surface.container) {
.split_tl => .top_left, .split_tl => .top_left,
.split_br => .bottom_right, .split_br => .bottom_right,
.none, .tab_ => unreachable, .none, .tab_ => unreachable,
}); });
const surface_ = map.get(direction) orelse return; const surface_ = map.get(direction) orelse return false;
if (surface_) |surface| surface.grabFocus(); if (surface_) |surface| {
surface.grabFocus();
return true;
}
return false;
}, },
} }
} }
@ -967,12 +913,10 @@ fn configChange(
// App changes needs to show a toast that our configuration // App changes needs to show a toast that our configuration
// has reloaded. // has reloaded.
if (adwaita.enabled(&self.config)) {
if (self.core_app.focusedSurface()) |core_surface| { if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface; const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded(); 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| { if (self.directionPrevious(from)) |prev| {
result.put(.previous, prev.surface); result.put(.previous, prev.surface);
if (!prev.wrapped) { 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(.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); result.put(.next, next.surface);
if (!next.wrapped) { if (!next.wrapped) {
result.put(.down, next.surface); 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; 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 { fn directionPrevious(self: *const Split, from: Side) ?struct {
surface: *Surface, surface: *Surface,
wrapped: bool, wrapped: bool,

View File

@ -1270,10 +1270,12 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
return; return;
}; };
// Convert surface coordinate into coordinate space of the
// context menu's parent
var point: c.graphene_point_t = .{ .x = x, .y = y }; var point: c.graphene_point_t = .{ .x = x, .y = y };
if (c.gtk_widget_compute_point( if (c.gtk_widget_compute_point(
self.primaryWidget(), self.primaryWidget(),
@ptrCast(window.window), c.gtk_widget_get_parent(@ptrCast(window.context_menu)),
&c.GRAPHENE_POINT_INIT(point.x, point.y), &c.GRAPHENE_POINT_INIT(point.x, point.y),
@ptrCast(&point), @ptrCast(&point),
) == 0) { ) == 0) {
@ -2124,7 +2126,7 @@ pub fn present(self: *Surface) void {
if (self.container.window()) |window| { if (self.container.window()) |window| {
if (self.container.tab()) |tab| { if (self.container.tab()) |tab| {
if (window.notebook.getTabPosition(tab)) |position| if (window.notebook.getTabPosition(tab)) |position|
window.notebook.gotoNthTab(position); _ = window.notebook.gotoNthTab(position);
} }
c.gtk_window_present(window.window); 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 /// 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. /// `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 { 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 c = @import("c.zig").c;
const adwaita = @import("adwaita.zig"); const adwaita = @import("adwaita.zig");
const gtk_key = @import("key.zig"); const gtk_key = @import("key.zig");
const Notebook = @import("notebook.zig").Notebook; const TabView = @import("TabView.zig");
const HeaderBar = @import("headerbar.zig").HeaderBar; const HeaderBar = @import("headerbar.zig");
const version = @import("version.zig"); const version = @import("version.zig");
const winproto = @import("winproto.zig"); const winproto = @import("winproto.zig");
@ -34,9 +34,7 @@ app: *App,
/// Our window /// Our window
window: *c.GtkWindow, window: *c.GtkWindow,
/// The header bar for the window. This is possibly null since it can be /// The header bar for the window.
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
/// GtkHeaderBar depending on if adw is enabled and linked.
headerbar: HeaderBar, headerbar: HeaderBar,
/// The tab overview for the window. This is possibly null since there is no /// The tab overview for the window. This is possibly null since there is no
@ -44,14 +42,12 @@ headerbar: HeaderBar,
tab_overview: ?*c.GtkWidget, tab_overview: ?*c.GtkWidget,
/// The notebook (tab grouping) for this window. /// The notebook (tab grouping) for this window.
/// can be either c.GtkNotebook or c.AdwTabView. notebook: TabView,
notebook: Notebook,
context_menu: *c.GtkWidget, context_menu: *c.GtkWidget,
/// The libadwaita widget for receiving toast send requests. If libadwaita is /// The libadwaita widget for receiving toast send requests.
/// not used, this is null and unused. toast_overlay: *c.GtkWidget,
toast_overlay: ?*c.GtkWidget,
/// See adwTabOverviewOpen for why we have this. /// See adwTabOverviewOpen for why we have this.
adw_tab_overview_focus_timer: ?c.guint = null, adw_tab_overview_focus_timer: ?c.guint = null,
@ -87,49 +83,39 @@ pub fn init(self: *Window, app: *App) !void {
}; };
// Create the window // Create the window
const window: *c.GtkWidget = window: { const gtk_widget = c.adw_application_window_new(app.app);
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) { errdefer c.gtk_window_destroy(@ptrCast(gtk_widget));
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_window: *c.GtkWindow = @ptrCast(window); self.window = @ptrCast(@alignCast(gtk_widget));
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty"); c.gtk_window_set_title(self.window, "Ghostty");
c.gtk_window_set_default_size(gtk_window, 1000, 600); c.gtk_window_set_default_size(self.window, 1000, 600);
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window"); c.gtk_widget_add_css_class(gtk_widget, "window");
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window"); c.gtk_widget_add_css_class(gtk_widget, "terminal-window");
// GTK4 grabs F10 input by default to focus the menubar icon. We want // 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) // 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 // 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 // GTK version is before 4.16. The conditional is because above 4.16
// we use GTK CSS color variables. // we use GTK CSS color variables.
if (!version.atLeast(4, 16, 0) and app.config.@"window-theme" == .ghostty) { 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. // Create our box which will hold our widgets in the main content area.
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
// Setup our notebook // Setup our notebook
self.notebook.init(); self.notebook.init(self);
// If we are using Adwaita, then we can support the tab overview. // 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(); 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.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
tab_overview, tab_overview,
@ -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 we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| { if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable; if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
const btn = switch (app.config.@"gtk-tabs-location") { const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: { .top, .bottom => btn: {
const btn = c.gtk_toggle_button_new(); const btn = c.gtk_toggle_button_new();
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs"); c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic"); c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
@ -186,7 +171,7 @@ pub fn init(self: *Window, app: *App) !void {
.hidden => btn: { .hidden => btn: {
const btn = c.adw_tab_button_new(); 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"); c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
break :btn btn; break :btn btn;
}, },
@ -203,13 +188,13 @@ pub fn init(self: *Window, app: *App) !void {
self.headerbar.packStart(btn); 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(self.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(self.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::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 // 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. // 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()); 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) { if (comptime std.debug.runtime_safety) {
const warning_box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0); 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."; const warning_text = "⚠️ You're running a debug build of Ghostty! Performance will be degraded.";
if ((comptime adwaita.versionAtLeast(1, 3, 0)) and if (adwaita.versionAtLeast(1, 3, 0)) {
adwaita.enabled(&app.config) and
adwaita.versionAtLeast(1, 3, 0))
{
const banner = c.adw_banner_new(warning_text); const banner = c.adw_banner_new(warning_text);
c.adw_banner_set_revealed(@ptrCast(banner), 1); c.adw_banner_set_revealed(@ptrCast(banner), 1);
c.gtk_box_append(@ptrCast(warning_box), @ptrCast(banner)); 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_widget_set_margin_bottom(warning, 10);
c.gtk_box_append(@ptrCast(warning_box), warning); 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_widget_add_css_class(@ptrCast(warning_box), "background");
c.gtk_box_append(@ptrCast(box), warning_box); c.gtk_box_append(@ptrCast(box), warning_box);
} }
// Setup our toast overlay if we have one // Setup our toast overlay if we have one
self.toast_overlay = if (adwaita.enabled(&self.app.config)) toast: { self.toast_overlay = c.adw_toast_overlay_new();
const toast_overlay = c.adw_toast_overlay_new();
c.adw_toast_overlay_set_child( c.adw_toast_overlay_set_child(
@ptrCast(toast_overlay), @ptrCast(self.toast_overlay),
@ptrCast(@alignCast(self.notebook.asWidget())), @ptrCast(@alignCast(self.notebook.asWidget())),
); );
c.gtk_box_append(@ptrCast(box), toast_overlay); c.gtk_box_append(@ptrCast(box), self.toast_overlay);
break :toast toast_overlay;
} else toast: {
c.gtk_box_append(@ptrCast(box), self.notebook.asWidget());
break :toast null;
};
// If we have a tab overview then we can set it on our notebook. // If we have a tab overview then we can set it on our notebook.
if (self.tab_overview) |tab_overview| { if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable; if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.notebook == .adw); c.adw_tab_overview_set_view(@ptrCast(tab_overview), @ptrCast(@alignCast(self.notebook.tab_view)));
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw.tab_view);
} }
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu))); 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). // focused (i.e. when the libadw tab overview is shown).
const ec_key_press = c.gtk_event_controller_key_new(); const ec_key_press = c.gtk_event_controller_key_new();
errdefer c.g_object_unref(ec_key_press); 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 // 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(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(self.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(self.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, "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); _ = 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 // Our actions for the menu
initActions(self); 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()); const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget()); c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
if (self.app.config.@"gtk-tabs-location" != .hidden) { if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new(); 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); 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)); const tab_bar_widget: *c.GtkWidget = @ptrCast(@alignCast(tab_bar));
switch (self.app.config.@"gtk-tabs-location") { switch (self.app.config.@"gtk-tabs-location") {
// left and right are not supported in libadwaita. .top => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.top, .left, .right => c.adw_toolbar_view_add_top_bar(toolbar_view, tab_bar_widget),
.bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget), .bottom => c.adw_toolbar_view_add_bottom_bar(toolbar_view, tab_bar_widget),
.hidden => unreachable, .hidden => unreachable,
} }
} }
c.adw_toolbar_view_set_content(toolbar_view, box); 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, .flat => c.ADW_TOOLBAR_FLAT,
.raised => c.ADW_TOOLBAR_RAISED, .raised => c.ADW_TOOLBAR_RAISED,
.@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER, .@"raised-border" => c.ADW_TOOLBAR_RAISED_BORDER,
@ -320,51 +294,34 @@ pub fn init(self: *Window, app: *App) !void {
@ptrCast(@alignCast(toolbar_view)), @ptrCast(@alignCast(toolbar_view)),
); );
c.adw_application_window_set_content( c.adw_application_window_set_content(
@ptrCast(gtk_window), @ptrCast(gtk_widget),
@ptrCast(@alignCast(self.tab_overview)), @ptrCast(@alignCast(self.tab_overview)),
); );
} else tab_bar: { } 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; if (app.config.@"gtk-tabs-location" == .hidden) break :tab_bar;
// In earlier adwaita versions, we need to add the tabbar manually since we do not use // In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView. // an AdwToolbarView.
const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?; const tab_bar: *c.AdwTabBar = c.adw_tab_bar_new().?;
c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline"); c.gtk_widget_add_css_class(@ptrCast(@alignCast(tab_bar)), "inline");
switch (app.config.@"gtk-tabs-location") { switch (app.config.@"gtk-tabs-location") {
.top, .top => c.gtk_box_insert_child_after(
.left, @ptrCast(box),
.right, @ptrCast(@alignCast(tab_bar)),
=> c.gtk_box_insert_child_after(@ptrCast(box), @ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(self.headerbar.asWidget()))), @ptrCast(@alignCast(self.headerbar.asWidget())),
),
.bottom => c.gtk_box_append( .bottom => c.gtk_box_append(
@ptrCast(box), @ptrCast(box),
@ptrCast(@alignCast(tab_bar)), @ptrCast(@alignCast(tab_bar)),
), ),
.hidden => unreachable, .hidden => unreachable,
} }
c.adw_tab_bar_set_view(tab_bar, 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); if (!app.config.@"gtk-wide-tabs") c.adw_tab_bar_set_expand_tabs(tab_bar, 0);
},
.gtk => {},
}
// 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);
}
} }
// Show the window // Show the window
c.gtk_widget_show(window); c.gtk_widget_show(gtk_widget);
} }
pub fn updateConfig( pub fn updateConfig(
@ -407,10 +364,8 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
// Disable the title buttons (close, maximize, minimize, ...) // Disable the title buttons (close, maximize, minimize, ...)
// *inside* the tab overview if CSDs are disabled. // *inside* the tab overview if CSDs are disabled.
// We do spare the search button, though. // 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| { if (self.tab_overview) |tab_overview| {
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
c.adw_tab_overview_set_show_start_title_buttons( c.adw_tab_overview_set_show_start_title_buttons(
@ptrCast(tab_overview), @ptrCast(tab_overview),
@intFromBool(csd_enabled), @intFromBool(csd_enabled),
@ -421,7 +376,6 @@ pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
); );
} }
} }
}
fn toggleCssClass( fn toggleCssClass(
widget: *c.GtkWidget, widget: *c.GtkWidget,
@ -506,23 +460,25 @@ pub fn closeTab(self: *Window, tab: *Tab) void {
} }
/// Go to the previous tab for a surface. /// 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 { const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{}); 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(); self.focusCurrentTab();
return true;
} }
/// Go to the next tab for a surface. /// 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 { const tab = surface.container.tab() orelse {
log.info("surface is not attached to a tab bar, cannot navigate", .{}); 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(); self.focusCurrentTab();
return true;
} }
/// Move the current tab for a surface. /// 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. /// 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(); const max = self.notebook.nPages();
self.gotoTab(@intCast(max)); return self.gotoTab(@intCast(max));
} }
/// Go to the specific tab index. /// Go to the specific tab index.
pub fn gotoTab(self: *Window, n: usize) void { pub fn gotoTab(self: *Window, n: usize) bool {
if (n == 0) return; if (n == 0) return false;
const max = self.notebook.nPages(); const max = self.notebook.nPages();
if (max == 0) return; if (max == 0) return false;
const page_idx = std.math.cast(c_int, n - 1) orelse return; const page_idx = std.math.cast(c_int, n - 1) orelse return false;
self.notebook.gotoNthTab(@min(page_idx, max - 1)); if (!self.notebook.gotoNthTab(@min(page_idx, max - 1))) return false;
self.focusCurrentTab(); self.focusCurrentTab();
return true;
} }
/// Toggle tab overview (if present) /// Toggle tab overview (if present)
pub fn toggleTabOverview(self: *Window) void { pub fn toggleTabOverview(self: *Window) void {
if (self.tab_overview) |tab_overview_widget| { 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)); 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)); 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 { 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); const toast = c.adw_toast_new(title);
c.adw_toast_set_timeout(toast, 3); 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 { 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 /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick
/// because we need to return an AdwTabPage from this function. /// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage { fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
if (!adwaita.versionAtLeast(1, 4, 0)) unreachable;
const self: *Window = userdataSelf(ud.?); 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 alloc = self.app.core_app.alloc;
const surface = self.actionSurface(); const surface = self.actionSurface();
const tab = Tab.create(alloc, self, surface) catch return null; 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( fn adwTabOverviewOpen(
@ -860,7 +815,7 @@ fn gtkKeyPressed(
// //
// If someone can confidently show or explain that this is not // If someone can confidently show or explain that this is not
// necessary, please remove this check. // 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| { if (self.tab_overview) |tab_overview_widget| {
const tab_overview: *c.AdwTabOverview = @ptrCast(@alignCast(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; if (c.adw_tab_overview_get_open(tab_overview) == 0) return 0;
@ -888,10 +843,7 @@ fn gtkActionAbout(
const icon = "com.mitchellh.ghostty"; const icon = "com.mitchellh.ghostty";
const website = "https://ghostty.org"; const website = "https://ghostty.org";
if ((comptime adwaita.versionAtLeast(1, 5, 0)) and if (adwaita.versionAtLeast(1, 5, 0)) {
adwaita.versionAtLeast(1, 5, 0) and
adwaita.enabled(&self.app.config))
{
c.adw_show_about_dialog( c.adw_show_about_dialog(
@ptrCast(self.window), @ptrCast(self.window),
"application-name", "application-name",

View File

@ -1,20 +1,5 @@
const std = @import("std"); const std = @import("std");
const c = @import("c.zig").c; 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 /// Verifies that the running libadwaita version is at least the given
/// version. This will return false if Ghostty is configured to /// version. This will return false if Ghostty is configured to
@ -33,8 +18,6 @@ pub inline fn versionAtLeast(
comptime minor: u16, comptime minor: u16,
comptime micro: u16, comptime micro: u16,
) bool { ) bool {
if (comptime !build_options.adwaita) return false;
// If our header has lower versions than the given version, // If our header has lower versions than the given version,
// we can return false immediately. This prevents us from // we can return false immediately. This prevents us from
// compiling against unknown symbols and makes runtime checks // 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 /// Imported C API directly from header files
pub const c = @cImport({ pub const c = @cImport({
@cInclude("gtk/gtk.h"); @cInclude("gtk/gtk.h");
if (build_options.adwaita) { @cInclude("adwaita.h");
@cInclude("libadwaita-1/adwaita.h");
}
if (build_options.x11) { if (build_options.x11) {
// Add in X11-specific GDK backend which we use for specific things // 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 { pub fn main() !void {
comptime { var gpa = std.heap.GeneralPurposeAllocator(.{}){};
@setEvalBranchQuota(13000); const alloc = gpa.allocator();
var counter = std.io.countingWriter(std.io.null_writer);
try writeGResourceXML(&counter.writer());
var buf: [counter.bytes_written]u8 = undefined; var extra_ui_files = std.ArrayList([]const u8).init(alloc);
var stream = std.io.fixedBufferStream(&buf); defer {
try writeGResourceXML(stream.writer()); for (extra_ui_files.items) |item| alloc.free(item);
const final = buf; extra_ui_files.deinit();
return final[0..stream.getWritten().len]; }
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));
} }
} }
fn writeGResourceXML(writer: anytype) !void { const writer = std.io.getStdOut().writer();
try writer.writeAll( try writer.writeAll(
\\<?xml version="1.0" encoding="UTF-8"?> \\<?xml version="1.0" encoding="UTF-8"?>
\\<gresources> \\<gresources>
\\
);
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty"> \\ <gresource prefix="/com/mitchellh/ghostty">
\\ \\
); );
@ -87,9 +91,6 @@ fn writeGResourceXML(writer: anytype) !void {
} }
try writer.writeAll( try writer.writeAll(
\\ </gresource> \\ </gresource>
\\
);
try writer.writeAll(
\\ <gresource prefix="/com/mitchellh/ghostty/icons"> \\ <gresource prefix="/com/mitchellh/ghostty/icons">
\\ \\
); );
@ -99,6 +100,23 @@ fn writeGResourceXML(writer: anytype) !void {
.{ icon.alias, icon.source }, .{ 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( try writer.writeAll(
\\ </gresource> \\ </gresource>
\\</gresources> \\</gresources>
@ -107,12 +125,24 @@ fn writeGResourceXML(writer: anytype) !void {
} }
pub const dependencies = deps: { pub const dependencies = deps: {
var deps: [css_files.len + icons.len][]const u8 = undefined; const total = css_files.len + icons.len + ui_files.len + blueprint_files.len;
for (css_files, 0..) |css_file, i| { var deps: [total][]const u8 = undefined;
deps[i] = std.fmt.comptimePrint("src/apprt/gtk/{s}", .{css_file}); 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| { for (icons) |icon| {
deps[i] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); 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; break :deps deps;
}; };

View File

@ -1,58 +1,59 @@
const HeaderBar = @This();
const std = @import("std"); const std = @import("std");
const c = @import("c.zig").c; const c = @import("c.zig").c;
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const adwaita = @import("adwaita.zig");
const HeaderBarAdw = @import("headerbar_adw.zig"); /// the Adwaita headerbar widget
const HeaderBarGtk = @import("headerbar_gtk.zig"); headerbar: *c.AdwHeaderBar,
pub const HeaderBar = union(enum) { /// the Adwaita window title widget
adw: HeaderBarAdw, title: *c.AdwWindowTitle,
gtk: HeaderBarGtk,
pub fn init(self: *HeaderBar) void { pub fn init(self: *HeaderBar) void {
const window: *Window = @fieldParentPtr("headerbar", self); const window: *Window = @fieldParentPtr("headerbar", self);
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) { self.* = .{
HeaderBarAdw.init(self); .headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
} else { .title = @ptrCast(@alignCast(c.adw_window_title_new(
HeaderBarGtk.init(self); c.gtk_window_get_title(window.window) orelse "Ghostty",
} null,
} ))),
pub fn setVisible(self: HeaderBar, visible: bool) void {
switch (self) {
inline else => |v| v.setVisible(visible),
}
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
inline else => |v| v.asWidget(),
}; };
c.adw_header_bar_set_title_widget(
self.headerbar,
@ptrCast(@alignCast(self.title)),
);
} }
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void { pub fn setVisible(self: *const HeaderBar, visible: bool) void {
switch (self) { c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
inline else => |v| v.packEnd(widget),
}
} }
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void { pub fn asWidget(self: *const HeaderBar) *c.GtkWidget {
switch (self) { return @ptrCast(@alignCast(self.headerbar));
inline else => |v| v.packStart(widget),
}
} }
pub fn setTitle(self: HeaderBar, title: [:0]const u8) void { pub fn packEnd(self: *const HeaderBar, widget: *c.GtkWidget) void {
switch (self) { c.adw_header_bar_pack_end(
inline else => |v| v.setTitle(title), @ptrCast(@alignCast(self.headerbar)),
} widget,
);
} }
pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void { pub fn packStart(self: *const HeaderBar, widget: *c.GtkWidget) void {
switch (self) { c.adw_header_bar_pack_start(
inline else => |v| v.setSubtitle(subtitle), @ptrCast(@alignCast(self.headerbar)),
widget,
);
} }
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: *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(), 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; _ = self;
return true; 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. /// Update the blur state of the window.
fn syncBlur(self: *Window) !void { fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return; 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( fn getWindowProperty(
self: *Window, self: *Window,
comptime T: type, 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. /// 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 /// Until then this MUST match build.zig.zon and should always be the
/// _next_ version to release. /// _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. /// Standard build configuration options.
optimize: std.builtin.OptimizeMode, optimize: std.builtin.OptimizeMode,
@ -32,7 +32,6 @@ renderer: renderer.Impl = .opengl,
font_backend: font.Backend = .freetype, font_backend: font.Backend = .freetype,
/// Feature flags /// Feature flags
adwaita: bool = false,
x11: bool = false, x11: bool = false,
wayland: bool = false, wayland: bool = false,
sentry: bool = true, sentry: bool = true,
@ -132,12 +131,6 @@ pub fn init(b: *std.Build) !Config {
//--------------------------------------------------------------- //---------------------------------------------------------------
// Feature Flags // 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( config.flatpak = b.option(
bool, bool,
"flatpak", "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 // We need to break these down individual because addOption doesn't
// support all types. // support all types.
step.addOption(bool, "flatpak", self.flatpak); step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11); step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland); step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry); step.addOption(bool, "sentry", self.sentry);
@ -442,7 +434,6 @@ pub fn fromOptions() Config {
.version = options.app_version, .version = options.app_version,
.flatpak = options.flatpak, .flatpak = options.flatpak,
.adwaita = options.adwaita,
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?, .app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?, .font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?, .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 HelpStrings = @import("HelpStrings.zig");
const MetallibStep = @import("MetallibStep.zig"); const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig"); const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig");
config: *const Config, config: *const Config,
@ -13,6 +14,7 @@ options: *std.Build.Step.Options,
help_strings: HelpStrings, help_strings: HelpStrings,
metallib: ?*MetallibStep, metallib: ?*MetallibStep,
unicode_tables: UnicodeTables, unicode_tables: UnicodeTables,
framedata: GhosttyFrameData,
/// Used to keep track of a list of file sources. /// Used to keep track of a list of file sources.
pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub const LazyPathList = std.ArrayList(std.Build.LazyPath);
@ -22,6 +24,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps {
.config = cfg, .config = cfg,
.help_strings = try HelpStrings.init(b, cfg), .help_strings = try HelpStrings.init(b, cfg),
.unicode_tables = try UnicodeTables.init(b), .unicode_tables = try UnicodeTables.init(b),
.framedata = try GhosttyFrameData.init(b),
// Setup by retarget // Setup by retarget
.options = undefined, .options = undefined,
@ -430,9 +433,30 @@ pub fn add(
}, },
.gtk => { .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); step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (self.config.adwaita) step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts); step.linkSystemLibrary2("libadwaita-1", dynamic_link_opts);
if (self.config.x11) step.linkSystemLibrary2("X11", 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) { if (self.config.wayland) {
const scanner = Scanner.create(b.dependency("zig_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); scanner.generate("org_kde_kwin_server_decoration_manager", 1);
step.root_module.addImport("wayland", wayland); step.root_module.addImport("wayland", wayland);
step.root_module.addImport("gdk_wayland", gobject.module("gdkwayland4"));
step.linkSystemLibrary2("wayland-client", dynamic_link_opts); step.linkSystemLibrary2("wayland-client", dynamic_link_opts);
} }
{ {
const gresource = @import("../apprt/gtk/gresource.zig"); const gresource = @import("../apprt/gtk/gresource.zig");
const wf = b.addWriteFiles(); const gresource_xml = gresource_xml: {
const gresource_xml = wf.add("gresource.xml", gresource.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(&.{ const generate_resources_c = b.addSystemCommand(&.{
"glib-compile-resources", "glib-compile-resources",
@ -499,6 +563,7 @@ pub fn add(
self.help_strings.addImport(step); self.help_strings.addImport(step);
self.unicode_tables.addImport(step); self.unicode_tables.addImport(step);
self.framedata.addImport(step);
return static_libs; 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