Merge remote-tracking branch 'upstream/main' into jacob/uucode

pull/8757/head
Jacob Sandlund 2025-08-17 18:41:21 -04:00
commit 0979ea3371
55 changed files with 3304 additions and 549 deletions

View File

@ -34,7 +34,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:

View File

@ -8,7 +8,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install sentry-cli
run: |
@ -29,7 +29,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install sentry-cli
run: |
@ -51,7 +51,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0
@ -205,7 +205,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0

View File

@ -56,7 +56,7 @@ jobs:
fi
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0
@ -80,7 +80,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -128,7 +128,7 @@ jobs:
GHOSTTY_COMMIT: ${{ needs.setup.outputs.commit }}
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
@ -299,7 +299,7 @@ jobs:
GHOSTTY_COMMIT_LONG: ${{ needs.setup.outputs.commit_long }}
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download macOS Artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0

View File

@ -19,7 +19,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Tip Tag
run: |
git config user.name "github-actions[bot]"
@ -31,7 +31,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-slow]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install sentry-cli
run: |
@ -52,7 +52,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos-debug-fast]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install sentry-cli
run: |
@ -73,7 +73,7 @@ jobs:
runs-on: namespace-profile-ghostty-sm
needs: [build-macos]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install sentry-cli
run: |
@ -105,7 +105,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -158,7 +158,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0
@ -378,7 +378,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0
@ -558,7 +558,7 @@ jobs:
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Important so that build number generation works
fetch-depth: 0

View File

@ -36,7 +36,7 @@ jobs:
- translations
- blueprint-compiler
- test-pkg-linux
- test-debian-12
- test-debian-13
- zig-fmt
steps:
- id: status
@ -67,7 +67,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -98,7 +98,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -134,7 +134,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -163,7 +163,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -196,7 +196,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -240,7 +240,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -276,7 +276,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
@ -319,7 +319,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# TODO(tahoe): https://github.com/NixOS/nix/issues/13342
- uses: DeterminateSystems/nix-installer-action@main
@ -359,7 +359,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
@ -437,7 +437,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# This could be from a script if we wanted to but inlining here for now
# in one place.
@ -506,7 +506,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -551,7 +551,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -600,7 +600,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -648,7 +648,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -675,7 +675,7 @@ jobs:
needs: test
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
@ -704,7 +704,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -732,7 +732,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -759,7 +759,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -786,7 +786,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -813,7 +813,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -840,7 +840,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -874,7 +874,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -901,7 +901,7 @@ jobs:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -935,7 +935,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
@ -957,8 +957,8 @@ jobs:
run: |
nix develop -c sh -c "cd pkg/${{ matrix.pkg }} ; zig build test"
test-debian-12:
name: Test build on Debian 12
test-debian-13:
name: Test build on Debian 13
runs-on: namespace-profile-ghostty-sm
needs: [test, build-dist]
steps:
@ -984,7 +984,7 @@ jobs:
context: dist
file: dist/src/build/docker/debian/Dockerfile
build-args: |
DISTRO_VERSION=12
DISTRO_VERSION=13
flatpak-check-zig-cache:
if: github.repository == 'ghostty-org/ghostty'
@ -994,7 +994,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
@ -1030,7 +1030,7 @@ jobs:
runs-on: ${{ matrix.variant.runner }}
needs: [flatpak-check-zig-cache, test]
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
with:
bundle: com.mitchellh.ghostty

View File

@ -17,7 +17,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0

View File

@ -20,8 +20,8 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
.hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
.hash = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5",
.lazy = true,
},
.zig_objc = .{

6
build.zig.zon.json generated
View File

@ -129,10 +129,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": {
"z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
"hash": "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs="
},
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf",

6
build.zig.zon.nix generated
View File

@ -290,11 +290,11 @@ in
};
}
{
name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg";
name = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz";
hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz";
hash = "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -32,4 +32,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0e
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz

View File

@ -157,9 +157,9 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
"sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e"
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
"dest": "vendor/p/z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5",
"sha256": "e99aa0acefdb723827b9071fabcf55629b5452876c12b33c173ea7c016614c9b"
},
{
"type": "archive",

View File

@ -757,6 +757,7 @@ typedef enum {
GHOSTTY_ACTION_OPEN_URL,
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
GHOSTTY_ACTION_PROGRESS_REPORT,
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
} ghostty_action_tag_e;
typedef union {

View File

@ -4804,6 +4804,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.show_on_screen_keyboard => return try self.rt_app.performAction(
.{ .surface = self },
.show_on_screen_keyboard,
{},
),
.select_all => {
const sel = self.io.terminal.screen.selectAll();
if (sel) |s| {

View File

@ -60,20 +60,22 @@ pub const Runtime = enum {
/// This is only useful if you're only interested in the lib only (macOS).
none,
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
gtk,
/// GTK4. The "-ng" variant is a rewrite of the GTK backend using
/// GTK-native technologies such as full GObject classes, Blueprint
/// files, etc.
/// GTK4. Rich windowed application. This uses a full GObject-based
/// approach to building the application.
@"gtk-ng",
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
/// WARNING: Deprecated. This will be removed very soon. All bug fixes
/// and features should go into the gtk-ng backend.
gtk,
pub fn default(target: std.Target) Runtime {
// The Linux default is GTK because it is full featured.
if (target.os.tag == .linux) return .gtk;
// The Linux default is GTK because it is a full featured application.
if (target.os.tag == .linux) return .@"gtk-ng";
// Otherwise, we do NONE so we don't create an exe and we
// create libghostty.
// create libghostty. On macOS, Xcode is used to build the app
// that links to libghostty.
return .none;
}
};

View File

@ -291,6 +291,9 @@ pub const Action = union(Key) {
/// Show a native GUI notification about the progress of some TUI operation.
progress_report: terminal.osc.Command.ProgressReport,
/// Show the on-screen keyboard.
show_on_screen_keyboard,
/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
quit,
@ -345,6 +348,7 @@ pub const Action = union(Key) {
open_url,
show_child_exited,
progress_report,
show_on_screen_keyboard,
};
/// Sync with: ghostty_action_u

View File

@ -11,4 +11,5 @@ pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef;
test {
@import("std").testing.refAllDecls(@This());
_ = @import("gtk-ng/ext.zig");
}

View File

@ -99,7 +99,6 @@ pub fn performIpc(
}
/// Redraw the inspector for the given surface.
pub fn redrawInspector(self: *App, surface: *Surface) void {
_ = self;
_ = surface;
pub fn redrawInspector(_: *App, surface: *Surface) void {
surface.redrawInspector();
}

View File

@ -32,7 +32,8 @@ pub fn rtApp(self: *Self) *ApprtApp {
}
pub fn close(self: *Self, process_active: bool) void {
self.surface.close(.{ .surface = process_active });
_ = process_active;
self.surface.close();
}
pub fn cgroup(self: *Self) ?[]const u8 {
@ -95,3 +96,8 @@ pub fn setClipboardString(
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
return try self.surface.defaultTermioEnv();
}
/// Redraw the inspector for our surface.
pub fn redrawInspector(self: *Self) void {
self.surface.redrawInspector();
}

View File

@ -39,10 +39,14 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "debug-warning" },
.{ .major = 1, .minor = 3, .name = "debug-warning" },
.{ .major = 1, .minor = 5, .name = "imgui-widget" },
.{ .major = 1, .minor = 5, .name = "inspector-widget" },
.{ .major = 1, .minor = 5, .name = "inspector-window" },
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
.{ .major = 1, .minor = 5, .name = "split-tree" },
.{ .major = 1, .minor = 5, .name = "split-tree-split" },
.{ .major = 1, .minor = 2, .name = "surface" },
.{ .major = 1, .minor = 5, .name = "surface-title-dialog" },
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
.{ .major = 1, .minor = 5, .name = "tab" },
.{ .major = 1, .minor = 5, .name = "window" },

View File

@ -53,6 +53,23 @@ pub fn Common(
}
}).private else {};
/// A helper that creates a property that reads and writes a
/// private field with only shallow copies. This is good for primitives
/// such as bools, numbers, etc.
pub fn privateShallowFieldAccessor(
comptime name: []const u8,
) gobject.ext.Accessor(
Self,
@FieldType(Private.?, name),
) {
return gobject.ext.privateFieldAccessor(
Self,
Private.?,
&Private.?.offset,
name,
);
}
/// A helper that can be used to create a property that reads and
/// writes a private boxed gobject field type.
///

View File

@ -542,8 +542,8 @@ pub const Application = extern struct {
value: apprt.Action.Value(action),
) !bool {
switch (action) {
.close_tab => Action.close(target, .tab),
.close_window => Action.close(target, .window),
.close_tab => return Action.closeTab(target),
.close_window => return Action.closeWindow(target),
.config_change => try Action.configChange(
self,
@ -561,6 +561,8 @@ pub const Application = extern struct {
.initial_size => return Action.initialSize(target, value),
.inspector => return Action.controlInspector(target, value),
.mouse_over_link => Action.mouseOverLink(target, value),
.mouse_shape => Action.mouseShape(target, value),
.mouse_visibility => Action.mouseVisibility(target, value),
@ -589,6 +591,8 @@ pub const Application = extern struct {
.progress_report => return Action.progressReport(target, value),
.prompt_title => return Action.promptTitle(target),
.quit => self.quit(),
.quit_timer => try Action.quitTimer(self, value),
@ -597,6 +601,8 @@ pub const Application = extern struct {
.render => Action.render(target),
.resize_split => return Action.resizeSplit(target, value),
.ring_bell => Action.ringBell(target),
.set_title => Action.setTitle(target, value),
@ -613,17 +619,8 @@ pub const Application = extern struct {
.toggle_tab_overview => return Action.toggleTabOverview(target),
.toggle_window_decorations => return Action.toggleWindowDecorations(target),
.toggle_command_palette => return Action.toggleCommandPalette(target),
// Unimplemented but todo on gtk-ng branch
.prompt_title,
.inspector,
// TODO: splits
.resize_split,
.toggle_split_zoom,
=> {
log.warn("unimplemented action={}", .{action});
return false;
},
.toggle_split_zoom => return Action.toggleSplitZoom(target),
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
// Unimplemented
.secure_input,
@ -885,10 +882,10 @@ pub const Application = extern struct {
self.syncActionAccelerator("win.reset", .{ .reset = {} });
self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
self.syncActionAccelerator("split-tree.new-left", .{ .new_split = .left });
self.syncActionAccelerator("split-tree.new-right", .{ .new_split = .right });
self.syncActionAccelerator("split-tree.new-up", .{ .new_split = .up });
self.syncActionAccelerator("split-tree.new-down", .{ .new_split = .down });
self.syncActionAccelerator("split-tree.new-split::left", .{ .new_split = .left });
self.syncActionAccelerator("split-tree.new-split::right", .{ .new_split = .right });
self.syncActionAccelerator("split-tree.new-split::up", .{ .new_split = .up });
self.syncActionAccelerator("split-tree.new-split::down", .{ .new_split = .down });
}
fn syncActionAccelerator(
@ -1094,6 +1091,11 @@ pub const Application = extern struct {
self,
.{ .detail = "dark" },
);
// Do an initial color scheme sync. This is idempotent and does nothing
// if our current theme matches what libghostty has so its safe to
// call.
handleStyleManagerDark(style, undefined, self);
}
/// Setup signal handlers
@ -1115,38 +1117,16 @@ pub const Application = extern struct {
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
// The set of actions. Each action has (in order):
// [0] The action name
// [1] The callback function
// [2] The glib.VariantType of the parameter
//
// For action names:
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
const actions = .{
.{ "new-window", actionNewWindow, null },
.{ "new-window-command", actionNewWindow, as_variant_type },
.{ "open-config", actionOpenConfig, null },
.{ "present-surface", actionPresentSurface, t_variant_type },
.{ "quit", actionQuit, null },
.{ "reload-config", actionReloadConfig, null },
const actions = [_]ext.actions.Action(Self){
.init("new-window", actionNewWindow, null),
.init("new-window-command", actionNewWindow, as_variant_type),
.init("open-config", actionOpenConfig, null),
.init("present-surface", actionPresentSurface, t_variant_type),
.init("quit", actionQuit, null),
.init("reload-config", actionReloadConfig, null),
};
const action_map = self.as(gio.ActionMap);
inline for (actions) |entry| {
const action = gio.SimpleAction.new(
entry[0],
entry[2],
);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
*Self,
entry[1],
self,
.{},
);
action_map.addAction(action.as(gio.Action));
}
ext.actions.add(Self, self, &actions);
}
/// Setup our global shortcuts.
@ -1329,14 +1309,25 @@ pub const Application = extern struct {
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
_ = self;
const color_scheme: apprt.ColorScheme = if (style.getDark() == 0)
const scheme: apprt.ColorScheme = if (style.getDark() == 0)
.light
else
.dark;
log.debug("style manager changed scheme={}", .{scheme});
log.debug("style manager changed scheme={}", .{color_scheme});
const priv = self.private();
const core_app = priv.core_app;
core_app.colorSchemeEvent(self.rt(), scheme) catch |err| {
log.warn("error updating app color scheme err={}", .{err});
};
for (core_app.surfaces.items) |surface| {
surface.core().colorSchemeCallback(scheme) catch |err| {
log.warn(
"unable to tell surface about color scheme change err={}",
.{err},
);
};
}
}
fn handleReloadConfig(
@ -1585,13 +1576,23 @@ pub const Application = extern struct {
/// All apprt action handlers
const Action = struct {
pub fn close(
target: apprt.Target,
scope: Surface.CloseScope,
) void {
pub fn closeTab(target: apprt.Target) bool {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.close(scope),
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("tab.close", null) != 0;
},
}
}
pub fn closeWindow(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("win.close", null) != 0;
},
}
}
@ -1813,12 +1814,12 @@ const Action = struct {
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction(switch (direction) {
.right => "split-tree.new-right",
.left => "split-tree.new-left",
.down => "split-tree.new-down",
.up => "split-tree.new-up",
}, null) != 0;
return surface.as(gtk.Widget).activateAction(
"split-tree.new-split",
"&s",
@tagName(direction).ptr,
) != 0;
},
}
}
@ -1955,6 +1956,16 @@ const Action = struct {
};
}
pub fn promptTitle(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |v| {
v.rt_surface.surface.promptTitle();
return true;
},
}
}
/// Reload the configuration for the application and propagate it
/// across the entire application and all terminals.
pub fn reloadConfig(
@ -2003,10 +2014,47 @@ const Action = struct {
}
}
pub fn resizeSplit(
target: apprt.Target,
value: apprt.action.ResizeSplit,
) bool {
switch (target) {
.app => {
log.warn("resize_split to app is unexpected", .{});
return false;
},
.surface => |core| {
const surface = core.rt_surface.surface;
const tree = ext.getAncestor(
SplitTree,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a split tree, ignoring goto_split", .{});
return false;
};
return tree.resize(
switch (value.direction) {
.up => .up,
.down => .down,
.left => .left,
.right => .right,
},
value.amount,
) catch |err| switch (err) {
error.OutOfMemory => {
log.warn("unable to resize split, out of memory", .{});
return false;
},
};
},
}
}
pub fn ringBell(target: apprt.Target) void {
switch (target) {
.app => {},
.surface => |v| v.rt_surface.surface.ringBell(),
.surface => |v| v.rt_surface.surface.setBellRinging(true),
}
}
@ -2083,6 +2131,36 @@ const Action = struct {
return true;
}
pub fn toggleSplitZoom(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("toggle_split_zoom to app is unexpected", .{});
return false;
},
.surface => |core| {
// TODO: pass surface ID when we have that
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("split-tree.zoom", null) != 0;
},
}
}
pub fn showOnScreenKeyboard(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("show_on_screen_keyboard to app is unexpected", .{});
return false;
},
// NOTE: Even though `activateOsk` takes a gdk.Event, it's currently
// unused by all implementations of `activateOsk` as of GTK 4.18.
// The commit that introduced the method (ce6aa73c) clarifies that
// the event *may* be used by other IM backends, but for Linux desktop
// environments this doesn't matter.
.surface => |v| return v.rt_surface.surface.showOnScreenKeyboard(null),
}
}
fn getQuickTerminalWindow() ?*Window {
// Find a quick terminal window.
const list = gtk.Window.listToplevels();
@ -2156,6 +2234,15 @@ const Action = struct {
},
}
}
pub fn controlInspector(target: apprt.Target, value: apprt.Action.Value(.inspector)) bool {
switch (target) {
.app => return false,
.surface => |surface| {
return surface.rt_surface.gobj().controlInspector(value);
},
}
}
};
/// This sets various GTK-related environment variables as necessary

View File

@ -0,0 +1,478 @@
const std = @import("std");
const assert = std.debug.assert;
const cimgui = @import("cimgui");
const gl = @import("opengl");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const input = @import("../../../input.zig");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const log = std.log.scoped(.gtk_ghostty_imgui_widget);
/// A widget for embedding a Dear ImGui application.
///
/// It'd be a lot cleaner to use inheritance here but zig-gobject doesn't
/// currently have a way to define virtual methods, so we have to use
/// composition and signals instead.
pub const ImguiWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyImguiWidget",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {};
pub const signals = struct {
/// Emitted when the child widget should render. During the callback,
/// the Imgui context is valid.
pub const render = struct {
pub const name = "render";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when first realized to allow the embedded ImGui application
/// to initialize itself. When this is called, the ImGui context
/// is properly set.
///
/// This might be called multiple times, but each time it is
/// called a new Imgui context will be created.
pub const setup = struct {
pub const name = "setup";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
};
const Private = struct {
/// GL area where we display the Dear ImGui application.
gl_area: *gtk.GLArea,
/// GTK input method context
im_context: *gtk.IMMulticontext,
/// Dear ImGui context. We create a context per widget so that we can
/// have multiple active imgui views in the same application.
ig_context: ?*cimgui.c.ImGuiContext = null,
/// Our previous instant used to calculate delta time for animations.
instant: ?std.time.Instant = null,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
/// This should be called anytime the underlying data for the UI changes
/// so that the UI can be refreshed.
pub fn queueRender(self: *ImguiWidget) void {
const priv = self.private();
priv.gl_area.queueRender();
}
//---------------------------------------------------------------
// Private Methods
/// Set our imgui context to be current, or return an error. This must be
/// called before any Dear ImGui API calls so that they're made against
/// the proper context.
fn setCurrentContext(self: *Self) error{ContextNotInitialized}!void {
const priv = self.private();
const ig_context = priv.ig_context orelse {
log.warn("Dear ImGui context not initialized", .{});
return error.ContextNotInitialized;
};
cimgui.c.igSetCurrentContext(ig_context);
}
/// Initialize the frame. Expects that the context is already current.
fn newFrame(self: *Self) void {
// If we can't determine the time since the last frame we default to
// 1/60th of a second.
const default_delta_time = 1 / 60;
const priv = self.private();
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
// Determine our delta time
const now = std.time.Instant.now() catch unreachable;
io.DeltaTime = if (priv.instant) |prev| delta: {
const since_ns = now.since(prev);
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
break :delta @max(0.00001, since_s);
} else default_delta_time;
priv.instant = now;
}
/// Handle key press/release events.
fn keyEvent(
self: *ImguiWidget,
action: input.Action,
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
_: c_uint,
gtk_mods: gdk.ModifierType,
) bool {
self.queueRender();
self.setCurrentContext() catch return false;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const mods = key.translateMods(gtk_mods);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
// If our keyval has a key, then we send that key event
if (key.keyFromKeyval(keyval)) |inputkey| {
if (inputkey.imguiKey()) |imgui_key| {
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
}
}
// Try to process the event as text
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
const priv = self.private();
_ = priv.im_context.as(gtk.IMContext).filterKeypress(event);
}
return true;
}
/// Translate a GTK mouse button to a Dear ImGui mouse button.
fn translateMouseButton(button: c_uint) ?c_int {
return switch (button) {
1 => cimgui.c.ImGuiMouseButton_Left,
2 => cimgui.c.ImGuiMouseButton_Middle,
3 => cimgui.c.ImGuiMouseButton_Right,
else => null,
};
}
/// Get the scale factor that the display is operating at.
fn getScaleFactor(self: *Self) f64 {
const priv = self.private();
return @floatFromInt(priv.gl_area.as(gtk.Widget).getScaleFactor());
}
//---------------------------------------------------------------
// Properties
//---------------------------------------------------------------
// Signal Handlers
fn glAreaRealize(_: *gtk.GLArea, self: *Self) callconv(.c) void {
const priv = self.private();
assert(priv.ig_context == null);
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("GLArea for Dear ImGui widget failed to realize: {s}", .{err.f_message orelse "(unknown)"});
return;
}
priv.ig_context = cimgui.c.igCreateContext(null) orelse {
log.warn("unable to initialize Dear ImGui context", .{});
return;
};
self.setCurrentContext() catch return;
// Setup some basic config
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
io.BackendPlatformName = "ghostty_gtk";
// Realize means that our OpenGL context is ready, so we can now
// initialize the ImgUI OpenGL backend for our context.
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
// Setup our app
signals.setup.impl.emit(
self,
null,
.{},
null,
);
}
/// Handle a request to unrealize the GLArea
fn glAreaUnrealize(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
assert(self.private().ig_context != null);
self.setCurrentContext() catch return;
cimgui.ImGui_ImplOpenGL3_Shutdown();
}
/// Handle a request to resize the GLArea
fn glAreaResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *Self) callconv(.c) void {
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = area.as(gtk.Widget).getScaleFactor();
// Our display size is always unscaled. We'll do the scaling in the
// style instead. This creates crisper looking fonts.
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
// Setup a new style and scale it appropriately.
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
defer cimgui.c.ImGuiStyle_destroy(style);
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
const active_style = cimgui.c.igGetStyle();
active_style.* = style.*;
}
/// Handle a request to render the contents of our GLArea
fn glAreaRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Self) callconv(.c) c_int {
self.setCurrentContext() catch return @intFromBool(false);
// Setup our frame. We render twice because some ImGui behaviors
// take multiple renders to process. I don't know how to make this
// more efficient.
for (0..2) |_| {
cimgui.ImGui_ImplOpenGL3_NewFrame();
self.newFrame();
cimgui.c.igNewFrame();
// Use the callback to draw the UI.
signals.render.impl.emit(
self,
null,
.{},
null,
);
// Render
cimgui.c.igRender();
}
// OpenGL final render
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
return @intFromBool(true);
}
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
self.queueRender();
}
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
self.queueRender();
}
fn ecKeyPressed(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) c_int {
return @intFromBool(self.keyEvent(
.press,
ec_key,
keyval,
keycode,
gtk_mods,
));
}
fn ecKeyReleased(
ec_key: *gtk.EventControllerKey,
keyval: c_uint,
keycode: c_uint,
gtk_mods: gdk.ModifierType,
self: *ImguiWidget,
) callconv(.c) void {
_ = self.keyEvent(
.release,
ec_key,
keyval,
keycode,
gtk_mods,
);
}
fn ecMousePressed(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
}
}
fn ecMouseReleased(
gesture: *gtk.GestureClick,
_: c_int,
_: f64,
_: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
if (translateMouseButton(gdk_button)) |button| {
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
}
}
fn ecMouseMotion(
_: *gtk.EventControllerMotion,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
const scale_factor = self.getScaleFactor();
cimgui.c.ImGuiIO_AddMousePosEvent(
io,
@floatCast(x * scale_factor),
@floatCast(y * scale_factor),
);
}
fn ecMouseScroll(
_: *gtk.EventControllerScroll,
x: f64,
y: f64,
self: *ImguiWidget,
) callconv(.c) c_int {
self.queueRender();
self.setCurrentContext() catch return @intFromBool(false);
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddMouseWheelEvent(
io,
@floatCast(x),
@floatCast(-y),
);
return @intFromBool(true);
}
fn imCommit(
_: *gtk.IMMulticontext,
bytes: [*:0]u8,
self: *ImguiWidget,
) callconv(.c) void {
self.queueRender();
self.setCurrentContext() catch return;
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "imgui-widget",
}),
);
// Bindings
class.bindTemplateChildPrivate("gl_area", .{});
class.bindTemplateChildPrivate("im_context", .{});
// Template Callbacks
class.bindTemplateCallback("realize", &glAreaRealize);
class.bindTemplateCallback("unrealize", &glAreaUnrealize);
class.bindTemplateCallback("resize", &glAreaResize);
class.bindTemplateCallback("render", &glAreaRender);
class.bindTemplateCallback("focus_enter", &ecFocusEnter);
class.bindTemplateCallback("focus_leave", &ecFocusLeave);
class.bindTemplateCallback("key_pressed", &ecKeyPressed);
class.bindTemplateCallback("key_released", &ecKeyReleased);
class.bindTemplateCallback("mouse_pressed", &ecMousePressed);
class.bindTemplateCallback("mouse_released", &ecMouseReleased);
class.bindTemplateCallback("mouse_motion", &ecMouseMotion);
class.bindTemplateCallback("scroll", &ecMouseScroll);
class.bindTemplateCallback("im_commit", &imCommit);
// Signals
signals.render.impl.register(.{});
signals.setup.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -0,0 +1,255 @@
const std = @import("std");
const adw = @import("adw");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const Inspector = @import("../../../inspector/Inspector.zig");
const Common = @import("../class.zig").Common;
const Surface = @import("surface.zig").Surface;
const ImguiWidget = @import("imgui_widget.zig").ImguiWidget;
const log = std.log.scoped(.gtk_ghostty_inspector_widget);
/// Widget for displaying the Ghostty inspector.
pub const InspectorWidget = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWidget",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const surface = struct {
pub const name = "surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = .{
.getter = getSurfaceValue,
.setter = setSurfaceValue,
},
},
);
};
};
pub const signals = struct {};
const Private = struct {
/// The surface that we are attached to. This is NOT referenced.
/// We attach a weak notify to the object.
surface: ?*Surface = null,
/// The embedded Dear ImGui widget.
imgui_widget: *ImguiWidget,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
fn dispose(self: *Self) callconv(.c) void {
// Clear our surface so it deactivates the inspector.
self.setSurface(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
/// Queue a render of the Dear ImGui widget.
pub fn queueRender(self: *Self) void {
const priv = self.private();
priv.imgui_widget.queueRender();
}
//---------------------------------------------------------------
// Properties
fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
gobject.ext.Value.set(
value,
self.private().surface,
);
}
fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
self.setSurface(gobject.ext.Value.get(
value,
?*Surface,
));
}
pub fn getSurface(self: *Self) ?*Surface {
return self.private().surface;
}
pub fn setSurface(self: *Self, surface_: ?*Surface) void {
const priv = self.private();
// Do nothing if we're not changing the value.
if (surface_ == priv.surface) return;
// Setup our notification to happen at the end because we're
// changing values no matter what.
self.as(gobject.Object).freezeNotify();
defer self.as(gobject.Object).thawNotify();
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
// Deactivate the inspector on the old surface if it exists
// and set our value to null.
if (priv.surface) |old| old: {
priv.surface = null;
// Remove our weak ref
old.as(gobject.Object).weakUnref(
surfaceWeakNotify,
self,
);
// Deactivate the inspector
const core_surface = old.core() orelse break :old;
core_surface.deactivateInspector();
}
// Activate the inspector on the new surface.
const surface = surface_ orelse return;
const core_surface = surface.core() orelse return;
core_surface.activateInspector() catch |err| {
log.warn("failed to activate inspector err={}", .{err});
return;
};
// We use a weak reference on surface to determine if the surface
// was closed while our inspector was active.
surface.as(gobject.Object).weakRef(
surfaceWeakNotify,
self,
);
// Store our surface. We don't need to ref this because we setup
// the weak notify above.
priv.surface = surface;
self.queueRender();
}
//---------------------------------------------------------------
// Signal Handlers
fn surfaceWeakNotify(
ud: ?*anyopaque,
surface: *gobject.Object,
) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(ud orelse return));
const priv = self.private();
// The weak notify docs call out that we can specifically use the
// pointer values for comparison, but the objects themselves are unsafe.
if (@intFromPtr(priv.surface) != @intFromPtr(surface)) return;
// According to weak notify docs, "surface" is in the "dispose" state.
// Our surface doesn't clear the core surface until the "finalize"
// state so we should be able to safely access it here. We need to
// be really careful though.
const old = priv.surface orelse return;
const core_surface = old.core() orelse return;
core_surface.deactivateInspector();
priv.surface = null;
self.as(gobject.Object).notifyByPspec(properties.surface.impl.param_spec);
// Note: in the future we should probably show some content on our
// window to note that the surface went away in case our embedding
// widget doesn't close itself. As I type this, our window closes
// immediately when the surface goes away so you don't see this, but
// for completeness sake we should clean this up.
}
fn imguiRender(
_: *ImguiWidget,
self: *Self,
) callconv(.c) void {
const priv = self.private();
const surface = priv.surface orelse return;
const core_surface = surface.core() orelse return;
const inspector = core_surface.inspector orelse return;
inspector.render();
}
fn imguiSetup(
_: *ImguiWidget,
_: *Self,
) callconv(.c) void {
Inspector.setup();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(ImguiWidget);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "inspector-widget",
}),
);
// Bindings
class.bindTemplateChildPrivate("imgui_widget", .{});
// Template callbacks
class.bindTemplateCallback("imgui_render", &imguiRender);
class.bindTemplateCallback("imgui_setup", &imguiSetup);
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
});
// Signals
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -0,0 +1,220 @@
const std = @import("std");
const build_config = @import("../../../build_config.zig");
const adw = @import("adw");
const gdk = @import("gdk");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Surface = @import("surface.zig").Surface;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const InspectorWidget = @import("inspector_widget.zig").InspectorWidget;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_inspector_window);
/// Window for displaying the Ghostty inspector.
pub const InspectorWindow = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.ApplicationWindow;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyInspectorWindow",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const surface = struct {
pub const name = "surface";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface,
.{
.accessor = .{
.getter = getSurfaceValue,
.setter = setSurfaceValue,
},
},
);
};
pub const debug = struct {
pub const name = "debug";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = build_config.is_debug,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = struct {
pub fn getter(_: *Self) bool {
return build_config.is_debug;
}
}.getter,
}),
},
);
};
};
pub const signals = struct {};
const Private = struct {
/// The surface that we are attached to
surface: WeakRef(Surface) = .empty,
/// The embedded inspector widget.
inspector_widget: *InspectorWidget,
pub var offset: c_int = 0;
};
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Add our dev CSS class if we're in debug mode.
if (comptime build_config.is_debug) {
self.as(gtk.Widget).addCssClass("devel");
}
// Set our window icon. We can't set this in the blueprint file
// because its dependent on the build config.
self.as(gtk.Window).setIconName(build_config.bundle_id);
}
fn dispose(self: *Self) callconv(.c) void {
// You MUST clear all weak refs in dispose, otherwise it causes
// memory corruption on dispose on the TARGET (weak referenced)
// object. The only way we caught this is via Valgrind. Its not a leak,
// its an invalid memory read. In practice, I found this sometimes
// caused hanging!
self.setSurface(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Public methods
pub fn new(surface: *Surface) *Self {
return gobject.ext.newInstance(Self, .{
.surface = surface,
});
}
/// Present the window.
pub fn present(self: *Self) void {
self.as(gtk.Window).present();
}
/// Queue a render of the embedded widget.
pub fn queueRender(self: *Self) void {
const priv = self.private();
priv.inspector_widget.queueRender();
}
//---------------------------------------------------------------
// Properties
fn setSurface(self: *Self, newvalue: ?*Surface) void {
const priv = self.private();
priv.surface.set(newvalue);
}
fn getSurfaceValue(self: *Self, value: *gobject.Value) void {
// Important: get() refs, so we take to not increase ref twice
gobject.ext.Value.take(
value,
self.private().surface.get(),
);
}
fn setSurfaceValue(self: *Self, value: *const gobject.Value) void {
self.setSurface(gobject.ext.Value.get(
value,
?*Surface,
));
}
//---------------------------------------------------------------
// Signal Handlers
fn propInspectorSurface(
inspector: *InspectorWidget,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// If the inspector's surface went away, we destroy the window.
// The inspector has a weak notify on the surface so it knows
// if it goes nil.
if (inspector.getSurface() == null) {
self.as(gtk.Window).destroy();
}
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(DebugWarning);
gobject.ext.ensureType(InspectorWidget);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "inspector-window",
}),
);
// Template Bindings
class.bindTemplateChildPrivate("inspector_widget", .{});
// Template callbacks
class.bindTemplateCallback("notify_inspector_surface", &propInspectorSurface);
// Properties
gobject.ext.registerProperties(class, &.{
properties.surface.impl,
properties.debug.impl,
});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -79,6 +79,25 @@ pub const SplitTree = extern struct {
);
};
pub const @"is-zoomed" = struct {
pub const name = "is-zoomed";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
bool,
.{
.getter = getIsZoomed,
},
),
},
);
};
pub const tree = struct {
pub const name = "tree";
const impl = gobject.ext.defineProperty(
@ -141,57 +160,26 @@ pub const SplitTree = extern struct {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Initialize our actions
self.initActions();
self.initActionMap();
// Initialize some basic state
const priv = self.private();
priv.pending_close = null;
}
fn initActions(self: *Self) void {
// The set of actions. Each action has (in order):
// [0] The action name
// [1] The callback function
// [2] The glib.VariantType of the parameter
//
// For action names:
// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
const actions = .{
fn initActionMap(self: *Self) void {
const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_variant_type.free();
const actions = [_]ext.actions.Action(Self){
// All of these will eventually take a target surface parameter.
// For now all our targets originate from the focused surface.
.{ "new-left", actionNewLeft, null },
.{ "new-right", actionNewRight, null },
.{ "new-up", actionNewUp, null },
.{ "new-down", actionNewDown, null },
.{ "equalize", actionEqualize, null },
.init("new-split", actionNewSplit, s_variant_type),
.init("equalize", actionEqualize, null),
.init("zoom", actionZoom, null),
};
// We need to collect our actions into a group since we're just
// a plain widget that doesn't implement ActionGroup directly.
const group = gio.SimpleActionGroup.new();
errdefer group.unref();
const map = group.as(gio.ActionMap);
inline for (actions) |entry| {
const action = gio.SimpleAction.new(
entry[0],
entry[2],
);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
*Self,
entry[1],
self,
.{},
);
map.addAction(action.as(gio.Action));
}
self.as(gtk.Widget).insertActionGroup(
"split-tree",
group.as(gio.ActionGroup),
);
ext.actions.addAsGroup(Self, self, "split-tree", &actions);
}
/// Create a new split in the given direction from the currently
@ -241,7 +229,7 @@ pub const SplitTree = extern struct {
// The handle we create the split relative to. Today this is the active
// surface but this might be the handle of the given parent if we want.
const handle = self.getActiveSurfaceHandle() orelse 0;
const handle = self.getActiveSurfaceHandle() orelse .root;
// Create our split!
var new_tree = try old_tree.split(
@ -261,6 +249,51 @@ pub const SplitTree = extern struct {
self.setTree(&new_tree);
}
pub fn resize(
self: *Self,
direction: Surface.Tree.Split.Direction,
amount: u16,
) Allocator.Error!bool {
// Avoid useless work
if (amount == 0) return false;
const old_tree = self.getTree() orelse return false;
const active = self.getActiveSurfaceHandle() orelse return false;
// Get all our dimensions we're going to need to turn our
// amount into a percentage.
const priv = self.private();
const width = priv.tree_bin.as(gtk.Widget).getWidth();
const height = priv.tree_bin.as(gtk.Widget).getHeight();
if (width == 0 or height == 0) return false;
const width_f64: f64 = @floatFromInt(width);
const height_f64: f64 = @floatFromInt(height);
const amount_f64: f64 = @floatFromInt(amount);
// Get our ratio and use positive/neg for directions.
const ratio: f64 = switch (direction) {
.right => amount_f64 / width_f64,
.left => -(amount_f64 / width_f64),
.down => amount_f64 / height_f64,
.up => -(amount_f64 / height_f64),
};
const layout: Surface.Tree.Split.Layout = switch (direction) {
.left, .right => .horizontal,
.up, .down => .vertical,
};
var new_tree = try old_tree.resize(
Application.default().allocator(),
active,
layout,
@floatCast(ratio),
);
defer new_tree.deinit();
self.setTree(&new_tree);
return true;
}
/// Move focus from the currently focused surface to the given
/// direction. Returns true if focus switched to a new surface.
pub fn goto(self: *Self, to: Surface.Tree.Goto) bool {
@ -283,7 +316,7 @@ pub const SplitTree = extern struct {
if (active == target) return false;
// Get the surface at the target location and grab focus.
const surface = tree.nodes[target].leaf;
const surface = tree.nodes[target.idx()].leaf;
surface.grabFocus();
return true;
@ -338,12 +371,28 @@ pub const SplitTree = extern struct {
//---------------------------------------------------------------
// Properties
/// Returns true if this split tree needs confirmation before quitting based
/// on the various Ghostty configurations.
pub fn getNeedsConfirmQuit(self: *Self) bool {
const tree = self.getTree() orelse return false;
var it = tree.iterator();
while (it.next()) |entry| {
if (entry.view.core()) |core| {
if (core.needsConfirmQuit()) {
return true;
}
}
}
return false;
}
/// Get the currently active surface. See the "active-surface" property.
/// This does not ref the value.
pub fn getActiveSurface(self: *Self) ?*Surface {
const tree = self.getTree() orelse return null;
const handle = self.getActiveSurfaceHandle() orelse return null;
return tree.nodes[handle].leaf;
return tree.nodes[handle.idx()].leaf;
}
fn getActiveSurfaceHandle(self: *Self) ?Surface.Tree.Node.Handle {
@ -353,6 +402,20 @@ pub const SplitTree = extern struct {
if (entry.view.getFocused()) return entry.handle;
}
// If none are currently focused, the most previously focused
// surface (if it exists) is our active surface. This lets things
// like apprt actions and bell ringing continue to work in the
// background.
if (self.private().last_focused.get()) |v| {
defer v.unref();
// We need to find the handle of the last focused surface.
it = tree.iterator();
while (it.next()) |entry| {
if (entry.view == v) return entry.handle;
}
}
return null;
}
@ -384,6 +447,11 @@ pub const SplitTree = extern struct {
return !tree.isEmpty();
}
pub fn getIsZoomed(self: *Self) bool {
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
return tree.zoomed != null;
}
/// Get the tree data model that we're showing in this widget. This
/// does not clone the tree.
pub fn getTree(self: *Self) ?*Surface.Tree {
@ -483,56 +551,30 @@ pub const SplitTree = extern struct {
//---------------------------------------------------------------
// Signal handlers
pub fn actionNewLeft(
pub fn actionNewSplit(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
args_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = parameter_;
self.newSplit(
.left,
self.getActiveSurface(),
) catch |err| {
log.warn("new split failed error={}", .{err});
const args = args_ orelse {
log.warn("split-tree.new-split called without a parameter", .{});
return;
};
}
pub fn actionNewRight(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = parameter_;
self.newSplit(
.right,
self.getActiveSurface(),
) catch |err| {
log.warn("new split failed error={}", .{err});
var dir: ?[*:0]const u8 = null;
args.get("&s", &dir);
const direction = std.meta.stringToEnum(
Surface.Tree.Split.Direction,
std.mem.span(dir) orelse return,
) orelse {
// Need to be defensive here since actions can be triggered externally.
log.warn("invalid split direction for split-tree.new-split: {s}", .{dir.?});
return;
};
}
pub fn actionNewUp(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = parameter_;
self.newSplit(
.up,
self.getActiveSurface(),
) catch |err| {
log.warn("new split failed error={}", .{err});
};
}
pub fn actionNewDown(
_: *gio.SimpleAction,
parameter_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
_ = parameter_;
self.newSplit(
.down,
direction,
self.getActiveSurface(),
) catch |err| {
log.warn("new split failed error={}", .{err});
@ -555,20 +597,27 @@ pub const SplitTree = extern struct {
self.setTree(&new_tree);
}
fn surfaceCloseRequest(
surface: *Surface,
scope: *const Surface.CloseScope,
pub fn actionZoom(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
switch (scope.*) {
// Handled upstream... this will probably go away for widget
// actions eventually.
.window, .tab => return,
// Remove the surface from the tree.
.surface => {},
const tree = self.getTree() orelse return;
if (tree.zoomed != null) {
tree.zoomed = null;
} else {
const active = self.getActiveSurfaceHandle() orelse return;
if (tree.zoomed == active) return;
tree.zoom(active);
}
self.as(gobject.Object).notifyByPspec(properties.tree.impl.param_spec);
}
fn surfaceCloseRequest(
surface: *Surface,
self: *Self,
) callconv(.c) void {
const core = surface.core() orelse return;
// Reset our pending close state
@ -634,7 +683,7 @@ pub const SplitTree = extern struct {
// Note: we don't need to ref this or anything because its
// guaranteed to remain in the new tree since its not part
// of the handle we're removing.
break :next_focus old_tree.nodes[next_handle].leaf;
break :next_focus old_tree.nodes[next_handle.idx()].leaf;
};
// Remove it from the tree.
@ -736,6 +785,7 @@ pub const SplitTree = extern struct {
// Dependent properties
self.as(gobject.Object).notifyByPspec(properties.@"has-surfaces".impl.param_spec);
self.as(gobject.Object).notifyByPspec(properties.@"is-zoomed".impl.param_spec);
}
fn onRebuild(ud: ?*anyopaque) callconv(.c) c_int {
@ -752,7 +802,10 @@ pub const SplitTree = extern struct {
// Rebuild our tree
const tree: *const Surface.Tree = self.private().tree orelse &.empty;
if (!tree.isEmpty()) {
priv.tree_bin.setChild(self.buildTree(tree, 0));
priv.tree_bin.setChild(self.buildTree(
tree,
tree.zoomed orelse .root,
));
}
// If we have a last focused surface, we need to refocus it, because
@ -778,7 +831,7 @@ pub const SplitTree = extern struct {
tree: *const Surface.Tree,
current: Surface.Tree.Node.Handle,
) *gtk.Widget {
return switch (tree.nodes[current]) {
return switch (tree.nodes[current.idx()]) {
.leaf => |v| v.as(gtk.Widget),
.split => |s| SplitTreeSplit.new(
current,
@ -818,6 +871,7 @@ pub const SplitTree = extern struct {
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.@"has-surfaces".impl,
properties.@"is-zoomed".impl,
properties.tree.impl,
});
@ -937,7 +991,7 @@ const SplitTreeSplit = extern struct {
self.as(gtk.Widget),
) orelse return 0;
const tree = split_tree.getTree() orelse return 0;
const split: *const Surface.Tree.Split = &tree.nodes[priv.handle].split;
const split: *const Surface.Tree.Split = &tree.nodes[priv.handle.idx()].split;
// Current, min, and max positions as pixels.
const pos = paned.getPosition();

View File

@ -27,7 +27,10 @@ const Config = @import("config.zig").Config;
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
const ChildExited = @import("surface_child_exited.zig").SurfaceChildExited;
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
const Window = @import("window.zig").Window;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const log = std.log.scoped(.gtk_ghostty_surface);
@ -47,6 +50,19 @@ pub const Surface = extern struct {
pub const Tree = datastruct.SplitTree(Self);
pub const properties = struct {
pub const @"bell-ringing" = struct {
pub const name = "bell-ringing";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = C.privateShallowFieldAccessor("bell_ringing"),
},
);
};
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
@ -173,8 +189,6 @@ pub const Surface = extern struct {
pub const @"mouse-hover-url" = struct {
pub const name = "mouse-hover-url";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
@ -188,8 +202,6 @@ pub const Surface = extern struct {
pub const pwd = struct {
pub const name = "pwd";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
@ -203,8 +215,6 @@ pub const Surface = extern struct {
pub const title = struct {
pub const name = "title";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
@ -216,6 +226,19 @@ pub const Surface = extern struct {
);
};
pub const @"title-override" = struct {
pub const name = "title-override";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = C.privateStringFieldAccessor("title_override"),
},
);
};
pub const zoom = struct {
pub const name = "zoom";
const impl = gobject.ext.defineProperty(
@ -249,21 +272,6 @@ pub const Surface = extern struct {
pub const @"close-request" = struct {
pub const name = "close-request";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{*const CloseScope},
void,
);
};
/// The bell is rung.
///
/// The surface view handles the audio bell feature but none of the
/// others so it is up to the embedding widget to react to this.
pub const bell = struct {
pub const name = "bell";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
@ -403,6 +411,9 @@ pub const Surface = extern struct {
/// The title of this surface, if any has been set.
title: ?[:0]const u8 = null,
/// The manually overridden title of this surface from `promptTitle`.
title_override: ?[:0]const u8 = null,
/// The current focus state of the terminal based on the
/// focus events.
focused: bool = true,
@ -456,6 +467,14 @@ pub const Surface = extern struct {
// Progress bar
progress_bar_timer: ?c_uint = null,
// True while the bell is ringing. This will be set to false (after
// true) under various scenarios, but can also manually be set to
// false by a parent widget.
bell_ringing: bool = false,
/// A weak reference to an inspector window.
inspector: ?*InspectorWindow = null,
// Template binds
child_exited_overlay: *ChildExited,
context_menu: *gtk.PopoverMenu,
@ -504,10 +523,18 @@ pub const Surface = extern struct {
priv.font_size_request = font_size_ptr;
self.as(gobject.Object).notifyByPspec(properties.@"font-size-request".impl.param_spec);
// Setup our pwd
if (parent.rt_surface.surface.getPwd()) |pwd| {
priv.pwd = glib.ext.dupeZ(u8, pwd);
self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec);
// Remainder needs a config. If there is no config we just assume
// we aren't inheriting any of these values.
if (priv.config) |config_obj| {
const config = config_obj.get();
// Setup our pwd if configured to inherit
if (config.@"window-inherit-working-directory") {
if (parent.rt_surface.surface.getPwd()) |pwd| {
priv.pwd = glib.ext.dupeZ(u8, pwd);
self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec);
}
}
}
}
@ -520,16 +547,20 @@ pub const Surface = extern struct {
priv.gl_area.queueRender();
}
/// Ring the bell.
pub fn ringBell(self: *Self) void {
// TODO: Audio feature
/// Callback used to determine whether border should be shown around the
/// surface.
fn closureShouldBorderBeShown(
_: *Self,
config_: ?*Config,
bell_ringing_: c_int,
) callconv(.c) c_int {
const config = if (config_) |v| v.get() else {
log.warn("config unavailable for computing whether border should be shown , likely bug", .{});
return @intFromBool(false);
};
signals.bell.impl.emit(
self,
null,
.{},
null,
);
const bell_ringing = bell_ringing_ != 0;
return @intFromBool(config.@"bell-features".border and bell_ringing);
}
pub fn toggleFullscreen(self: *Self) void {
@ -555,6 +586,41 @@ pub const Surface = extern struct {
return self.as(gtk.Widget).activateAction("win.toggle-command-palette", null) != 0;
}
pub fn controlInspector(
self: *Self,
value: apprt.Action.Value(.inspector),
) bool {
// Let's see if we have an inspector already.
const priv = self.private();
if (priv.inspector) |inspector| switch (value) {
.show => {},
// Our weak ref will set our private value to null
.toggle, .hide => inspector.as(gtk.Window).destroy(),
} else switch (value) {
.toggle, .show => {
const inspector = InspectorWindow.new(self);
inspector.present();
inspector.as(gobject.Object).weakRef(inspectorWeakNotify, self);
priv.inspector = inspector;
},
.hide => {},
}
return true;
}
/// Redraw our inspector, if there is one associated with this surface.
pub fn redrawInspector(self: *Self) void {
const priv = self.private();
if (priv.inspector) |v| v.queueRender();
}
pub fn showOnScreenKeyboard(self: *Self, event: ?*gdk.Event) bool {
const priv = self.private();
return priv.im_context.as(gtk.IMContext).activateOsk(event) != 0;
}
/// Set the current progress report state.
pub fn setProgressReport(
self: *Self,
@ -691,7 +757,7 @@ pub const Surface = extern struct {
keycode: c_uint,
gtk_mods: gdk.ModifierType,
) bool {
log.warn("keyEvent action={}", .{action});
//log.warn("keyEvent action={}", .{action});
const event = ec_key.as(gtk.EventController).getCurrentEvent() orelse return false;
const key_event = gobject.ext.cast(gdk.KeyEvent, event) orelse return false;
const priv = self.private();
@ -881,6 +947,10 @@ pub const Surface = extern struct {
surface.preeditCallback(null) catch {};
}
// Bell stops ringing when any key is pressed that is used by
// the core in any way.
self.setBellRinging(false);
return true;
},
}
@ -888,6 +958,26 @@ pub const Surface = extern struct {
return false;
}
/// Prompt for a manual title change for the surface.
pub fn promptTitle(self: *Self) void {
const priv = self.private();
const dialog = gobject.ext.newInstance(
TitleDialog,
.{
.@"initial-value" = priv.title_override orelse priv.title,
},
);
_ = TitleDialog.signals.set.connect(
dialog,
*Self,
titleDialogSet,
self,
.{},
);
dialog.present(self.as(gtk.Widget));
}
/// Scale x/y by the GDK device scale.
fn scaledCoordinates(
self: *Self,
@ -965,11 +1055,11 @@ pub const Surface = extern struct {
//---------------------------------------------------------------
// Libghostty Callbacks
pub fn close(self: *Self, scope: CloseScope) void {
pub fn close(self: *Self) void {
signals.@"close-request".impl.emit(
self,
null,
.{&scope},
.{},
null,
);
}
@ -1150,6 +1240,9 @@ pub const Surface = extern struct {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Initialize our actions
self.initActionMap();
const priv = self.private();
// Initialize some private fields so they aren't undefined
@ -1190,18 +1283,28 @@ pub const Surface = extern struct {
renderer.OpenGL.MIN_VERSION_MAJOR,
renderer.OpenGL.MIN_VERSION_MINOR,
);
gl_area.as(gtk.Widget).setCursorFromName("text");
self.as(gtk.Widget).setCursorFromName("text");
// Initialize our config
self.propConfig(undefined, null);
}
fn initActionMap(self: *Self) void {
const actions = [_]ext.actions.Action(Self){
.init("prompt-title", actionPromptTitle, null),
};
ext.actions.addAsGroup(Self, self, "surface", &actions);
}
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
priv.config = null;
}
if (priv.progress_bar_timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove progress bar timer", .{});
@ -1228,6 +1331,10 @@ pub const Surface = extern struct {
// searching for this surface.
Application.default().core().deleteSurface(self.rt());
// NOTE: We must deinit the surface in the finalize call and NOT
// the dispose call because the inspector widget relies on this
// behavior with a weakRef to properly deactivate.
// Deinit the surface
v.deinit();
const alloc = Application.default().allocator();
@ -1259,6 +1366,10 @@ pub const Surface = extern struct {
glib.free(@constCast(@ptrCast(v)));
priv.title = null;
}
if (priv.title_override) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.title_override = null;
}
self.clearCgroup();
gobject.Object.virtual_methods.finalize.call(
@ -1275,7 +1386,9 @@ pub const Surface = extern struct {
return self.private().title;
}
/// Set the title for this surface, copies the value.
/// Set the title for this surface, copies the value. This should always
/// be the title as set by the terminal program, not any manually set
/// title. For manually set titles see `setTitleOverride`.
pub fn setTitle(self: *Self, title: ?[:0]const u8) void {
const priv = self.private();
if (priv.title) |v| glib.free(@constCast(@ptrCast(v)));
@ -1284,6 +1397,16 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec);
}
/// Overridden title. This will be generally be shown over the title
/// unless this is unset (null).
pub fn setTitleOverride(self: *Self, title: ?[:0]const u8) void {
const priv = self.private();
if (priv.title_override) |v| glib.free(@constCast(@ptrCast(v)));
priv.title_override = null;
if (title) |v| priv.title_override = glib.ext.dupeZ(u8, v);
self.as(gobject.Object).notifyByPspec(properties.@"title-override".impl.param_spec);
}
/// Returns the pwd property without a copy.
pub fn getPwd(self: *Self) ?[:0]const u8 {
return self.private().pwd;
@ -1383,6 +1506,17 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec);
}
pub fn getBellRinging(self: *Self) bool {
return self.private().bell_ringing;
}
pub fn setBellRinging(self: *Self, ringing: bool) void {
const priv = self.private();
if (priv.bell_ringing == ringing) return;
priv.bell_ringing = ringing;
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
}
fn propConfig(
self: *Self,
_: *gobject.ParamSpec,
@ -1454,7 +1588,7 @@ pub const Surface = extern struct {
// If we're hidden we set it to "none"
if (priv.mouse_hidden) {
priv.gl_area.as(gtk.Widget).setCursorFromName("none");
self.as(gtk.Widget).setCursorFromName("none");
return;
}
@ -1512,18 +1646,87 @@ pub const Surface = extern struct {
};
// Set our new cursor.
priv.gl_area.as(gtk.Widget).setCursorFromName(name.ptr);
self.as(gtk.Widget).setCursorFromName(name.ptr);
}
fn propBellRinging(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (!priv.bell_ringing) return;
// Activate actions if they exist
_ = self.as(gtk.Widget).activateAction("tab.ring-bell", null);
_ = self.as(gtk.Widget).activateAction("win.ring-bell", null);
// Do our sound
const config = if (priv.config) |c| c.get() else return;
if (config.@"bell-features".audio) audio: {
const config_path = config.@"bell-audio-path" orelse break :audio;
const path, const required = switch (config_path) {
.optional => |path| .{ path, false },
.required => |path| .{ path, true },
};
const volume = std.math.clamp(
config.@"bell-audio-volume",
0.0,
1.0,
);
assert(std.fs.path.isAbsolute(path));
const media_file = gtk.MediaFile.newForFilename(path);
// If the audio file is marked as required, we'll emit an error if
// there was a problem playing it. Otherwise there will be silence.
if (required) {
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileError,
null,
.{ .detail = "error" },
);
}
// Watch for the "ended" signal so that we can clean up after
// ourselves.
_ = gobject.Object.signals.notify.connect(
media_file,
?*anyopaque,
mediaFileEnded,
null,
.{ .detail = "ended" },
);
const media_stream = media_file.as(gtk.MediaStream);
media_stream.setVolume(volume);
media_stream.play();
}
}
//---------------------------------------------------------------
// Signal Handlers
pub fn actionPromptTitle(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const surface = self.core() orelse return;
_ = surface.performBindingAction(.prompt_surface_title) catch |err| {
log.warn("unable to perform prompt title action err={}", .{err});
};
}
fn childExitedClose(
_: *ChildExited,
self: *Self,
) callconv(.c) void {
// This closes the surface with no confirmation.
self.close(.{ .surface = false });
self.close();
}
fn contextMenuClosed(
@ -1536,6 +1739,15 @@ pub const Surface = extern struct {
self.grabFocus();
}
fn inspectorWeakNotify(
ud: ?*anyopaque,
_: *gobject.Object,
) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(ud orelse return));
const priv = self.private();
priv.inspector = null;
}
fn dtDrop(
_: *gtk.DropTarget,
value: *gobject.Value,
@ -1545,10 +1757,7 @@ pub const Surface = extern struct {
) callconv(.c) c_int {
const alloc = Application.default().allocator();
if (g_value_holds(
value,
gdk.FileList.getGObjectType(),
)) {
if (ext.gValueHolds(value, gdk.FileList.getGObjectType())) {
var data = std.ArrayList(u8).init(alloc);
defer data.deinit();
@ -1592,7 +1801,7 @@ pub const Surface = extern struct {
return 1;
}
if (g_value_holds(value, gio.File.getGObjectType())) {
if (ext.gValueHolds(value, gio.File.getGObjectType())) {
const object = value.getObject() orelse return 0;
const file = gobject.ext.cast(gio.File, object) orelse return 0;
const path = file.getPath() orelse return 0;
@ -1620,7 +1829,7 @@ pub const Surface = extern struct {
return 1;
}
if (g_value_holds(value, gobject.ext.types.string)) {
if (ext.gValueHolds(value, gobject.ext.types.string)) {
if (value.getString()) |string| {
Clipboard.paste(self, std.mem.span(string));
}
@ -1668,6 +1877,9 @@ pub const Surface = extern struct {
priv.im_context.as(gtk.IMContext).focusIn();
_ = glib.idleAddOnce(idleFocus, self.ref());
self.as(gobject.Object).notifyByPspec(properties.focused.impl.param_spec);
// Bell stops ringing as soon as we gain focus
self.setBellRinging(false);
}
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
@ -1704,6 +1916,9 @@ pub const Surface = extern struct {
) callconv(.c) void {
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
// Bell stops ringing if any mouse button is pressed.
self.setBellRinging(false);
// If we don't have focus, grab it.
const priv = self.private();
const gl_area_widget = priv.gl_area.as(gtk.Widget);
@ -1760,18 +1975,29 @@ pub const Surface = extern struct {
const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return;
const priv = self.private();
if (priv.core_surface) |surface| {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const mods = gtk_key.translateMods(gtk_mods);
_ = surface.mouseButtonCallback(
.release,
button,
mods,
) catch |err| {
log.warn("error in key callback err={}", .{err});
return;
};
const surface = priv.core_surface orelse return;
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const mods = gtk_key.translateMods(gtk_mods);
const consumed = surface.mouseButtonCallback(
.release,
button,
mods,
) catch |err| {
log.warn("error in key callback err={}", .{err});
return;
};
// Trigger the on-screen keyboard if we have no selection,
// and that the mouse event hasn't been intercepted by the callback.
//
// It's better to do this here rather than within the core callback
// since we have direct access to the underlying gdk.Event here.
if (!consumed and button == .left and !surface.hasSelection()) {
if (!self.showOnScreenKeyboard(event)) {
log.warn("failed to activate the on-screen keyboard", .{});
}
}
}
@ -2314,6 +2540,44 @@ pub const Surface = extern struct {
right.setVisible(0);
}
fn mediaFileError(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const path = path: {
const file = media_file.getFile() orelse break :path null;
break :path file.getPath();
};
defer if (path) |p| glib.free(p);
const media_stream = media_file.as(gtk.MediaStream);
const err = media_stream.getError() orelse return;
log.warn("error playing bell from {s}: {s} {d} {s}", .{
path orelse "<<unknown>>",
glib.quarkToString(err.f_domain),
err.f_code,
err.f_message orelse "",
});
}
fn mediaFileEnded(
media_file: *gtk.MediaFile,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
media_file.unref();
}
fn titleDialogSet(
_: *TitleDialog,
title_ptr: [*:0]const u8,
self: *Self,
) callconv(.c) void {
const title = std.mem.span(title_ptr);
self.setTitleOverride(if (title.len == 0) null else title);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -2378,9 +2642,12 @@ pub const Surface = extern struct {
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"bell-ringing".impl,
properties.config.impl,
properties.@"child-exited".impl,
properties.@"default-size".impl,
@ -2392,12 +2659,12 @@ pub const Surface = extern struct {
properties.@"mouse-hover-url".impl,
properties.pwd.impl,
properties.title.impl,
properties.@"title-override".impl,
properties.zoom.impl,
});
// Signals
signals.@"close-request".impl.register(.{});
signals.bell.impl.register(.{});
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
@ -2416,25 +2683,6 @@ pub const Surface = extern struct {
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
/// The scope of a close request.
pub const CloseScope = union(enum) {
/// Close the surface. The boolean determines if there is a
/// process active.
surface: bool,
/// Close the tab. We can't know if there are processes active
/// for the entire tab scope so listeners must query the app.
tab,
/// Close the window.
window,
pub const getGObjectType = gobject.ext.defineBoxed(
CloseScope,
.{ .name = "GhosttySurfaceCloseScope" },
);
};
/// Simple dimensions struct for the surface used by various properties.
pub const Size = extern struct {
width: u32,
@ -2765,16 +3013,6 @@ const Clipboard = struct {
};
};
/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool {
if (value_) |value| {
if (value.f_g_type == g_type) return true;
return gobject.typeCheckValueHolds(value, g_type) != 0;
}
return false;
}
/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
/// to [0, 100].
fn computeFraction(progress: u8) f64 {

View File

@ -0,0 +1,190 @@
const std = @import("std");
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const ext = @import("../ext.zig");
const Common = @import("../class.zig").Common;
const log = std.log.scoped(.gtk_ghostty_surface_title_dialog);
pub const SurfaceTitleDialog = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.AlertDialog;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttySurfaceTitleDialog",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const @"initial-value" = struct {
pub const name = "initial-value";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = C.privateStringFieldAccessor("initial_value"),
},
);
};
};
pub const signals = struct {
/// Set the title to the given value.
pub const set = struct {
pub const name = "set";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{[*:0]const u8},
void,
);
};
};
const Private = struct {
/// The initial value of the entry field.
initial_value: ?[:0]const u8 = null,
// Template bindings
entry: *gtk.Entry,
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
pub fn present(self: *Self, parent_: *gtk.Widget) void {
// If we have a window we can attach to, we prefer that.
const parent: *gtk.Widget = if (ext.getAncestor(
adw.ApplicationWindow,
parent_,
)) |window|
window.as(gtk.Widget)
else if (ext.getAncestor(
adw.Window,
parent_,
)) |window|
window.as(gtk.Widget)
else
parent_;
// Set our initial value
const priv = self.private();
if (priv.initial_value) |v| {
priv.entry.getBuffer().setText(v, -1);
}
// Show it. We could also just use virtual methods to bind to
// response but this is pretty simple.
self.as(adw.AlertDialog).choose(
parent,
null,
alertDialogReady,
self,
);
}
fn alertDialogReady(
_: ?*gobject.Object,
result: *gio.AsyncResult,
ud: ?*anyopaque,
) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(ud));
const response = self.as(adw.AlertDialog).chooseFinish(result);
// If we didn't hit "okay" then we do nothing.
if (std.mem.orderZ(u8, "ok", response) != .eq) return;
// Emit our signal with the new title.
const title = std.mem.span(self.private().entry.getBuffer().getText());
signals.set.impl.emit(
self,
null,
.{title.ptr},
null,
);
}
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.initial_value) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.initial_value = null;
}
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "surface-title-dialog",
}),
);
// Signals
signals.set.impl.register(.{});
// Bindings
class.bindTemplateChildPrivate("entry", .{});
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"initial-value".impl,
});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};

View File

@ -11,6 +11,7 @@ const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const gtk_version = @import("../gtk_version.zig");
const adw_version = @import("../adw_version.zig");
const gresource = @import("../build/gresource.zig");
@ -70,6 +71,24 @@ pub const Tab = extern struct {
);
};
pub const @"split-tree" = struct {
pub const name = "split-tree";
const impl = gobject.ext.defineProperty(
name,
Self,
?*SplitTree,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*SplitTree,
.{
.getter = getSplitTree,
},
),
},
);
};
pub const @"surface-tree" = struct {
pub const name = "surface-tree";
const impl = gobject.ext.defineProperty(
@ -88,10 +107,21 @@ pub const Tab = extern struct {
);
};
pub const tooltip = struct {
pub const name = "tooltip";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = C.privateStringFieldAccessor("tooltip"),
},
);
};
pub const title = struct {
pub const name = "title";
pub const get = impl.get;
pub const set = impl.set;
const impl = gobject.ext.defineProperty(
name,
Self,
@ -122,12 +152,11 @@ pub const Tab = extern struct {
/// The configuration that this surface is using.
config: ?*Config = null,
/// The title to show for this tab. This is usually set to a binding
/// with the active surface but can be manually set to anything.
/// The title of this tab. This is usually bound to the active surface.
title: ?[:0]const u8 = null,
/// The binding groups for the current active surface.
surface_bindings: *gobject.BindingGroup,
/// The tooltip of this tab. This is usually bound to the active surface.
tooltip: ?[:0]const u8 = null,
// Template bindings
split_tree: *SplitTree,
@ -147,6 +176,9 @@ pub const Tab = extern struct {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Init our actions
self.initActionMap();
// If our configuration is null then we get the configuration
// from the application.
const priv = self.private();
@ -155,15 +187,6 @@ pub const Tab = extern struct {
priv.config = app.getConfig();
}
// Setup binding groups for surface properties
priv.surface_bindings = gobject.BindingGroup.new();
priv.surface_bindings.bind(
"title",
self.as(gobject.Object),
"title",
.{},
);
// Create our initial surface in the split tree.
priv.split_tree.newSplit(.right, null) catch |err| switch (err) {
error.OutOfMemory => {
@ -175,6 +198,15 @@ pub const Tab = extern struct {
};
}
fn initActionMap(self: *Self) void {
const actions = [_]ext.actions.Action(Self){
.init("close", actionClose, null),
.init("ring-bell", actionRingBell, null),
};
ext.actions.addAsGroup(Self, self, "tab", &actions);
}
//---------------------------------------------------------------
// Properties
@ -199,9 +231,17 @@ pub const Tab = extern struct {
/// Returns true if this tab needs confirmation before quitting based
/// on the various Ghostty configurations.
pub fn getNeedsConfirmQuit(self: *Self) bool {
const surface = self.getActiveSurface() orelse return false;
const core_surface = surface.core() orelse return false;
return core_surface.needsConfirmQuit();
const tree = self.getSplitTree();
return tree.getNeedsConfirmQuit();
}
/// Get the tab page holding this tab, if any.
fn getTabPage(self: *Self) ?*adw.TabPage {
const tab_view = ext.getAncestor(
adw.TabView,
self.as(gtk.Widget),
) orelse return null;
return tab_view.getPage(self.as(gtk.Widget));
}
//---------------------------------------------------------------
@ -213,7 +253,6 @@ pub const Tab = extern struct {
v.unref();
priv.config = null;
}
priv.surface_bindings.setSource(null);
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
@ -228,11 +267,14 @@ pub const Tab = extern struct {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.tooltip) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.tooltip = null;
}
if (priv.title) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.title = null;
}
priv.surface_bindings.unref();
gobject.Object.virtual_methods.finalize.call(
Class.parent,
@ -267,13 +309,90 @@ pub const Tab = extern struct {
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
priv.surface_bindings.setSource(null);
if (self.getActiveSurface()) |surface| {
priv.surface_bindings.setSource(surface.as(gobject.Object));
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
fn actionClose(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const tab_view = ext.getAncestor(
adw.TabView,
self.as(gtk.Widget),
) orelse return;
const page = tab_view.getPage(self.as(gtk.Widget));
// Delegate to our parent to handle this, since this will emit
// a close-page signal that the parent can intercept.
tab_view.closePage(page);
}
fn actionRingBell(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
// Future note: I actually don't like this logic living here at all.
// I think a better approach will be for the ring bell action to
// specify its sending surface and then do all this in the window.
// If the page is selected already we don't mark it as needing
// attention. We only want to mark unfocused pages. This will then
// clear when the page is selected.
const page = self.getTabPage() orelse return;
if (page.getSelected() != 0) return;
page.setNeedsAttention(@intFromBool(true));
}
fn closureComputedTitle(
_: *Self,
config_: ?*Config,
terminal_: ?[*:0]const u8,
override_: ?[*:0]const u8,
zoomed_: c_int,
bell_ringing_: c_int,
_: *gobject.ParamSpec,
) callconv(.c) ?[*:0]const u8 {
const zoomed = zoomed_ != 0;
const bell_ringing = bell_ringing_ != 0;
// Our plain title is the overridden title if it exists, otherwise
// the terminal title if it exists, otherwise a default string.
const plain = plain: {
const default = "Ghostty";
const plain = override_ orelse
terminal_ orelse
break :plain default;
break :plain std.mem.span(plain);
};
// We don't need a config in every case, but if we don't have a config
// let's just assume something went terribly wrong and use our
// default title. Its easier then guarding on the config existing
// in every case for something so unlikely.
const config = if (config_) |v| v.get() else {
log.warn("config unavailable for computed title, likely bug", .{});
return glib.ext.dupeZ(u8, plain);
};
// Use an allocator to build up our string as we write it.
var buf: std.ArrayList(u8) = .init(Application.default().allocator());
defer buf.deinit();
const writer = buf.writer();
// If our bell is ringing, then we prefix the bell icon to the title.
if (bell_ringing and config.@"bell-features".title) {
writer.writeAll("🔔 ") catch {};
}
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
// If we're zoomed, prefix with the magnifying glass emoji.
if (zoomed) {
writer.writeAll("🔍 ") catch {};
}
writer.writeAll(plain) catch return glib.ext.dupeZ(u8, plain);
return glib.ext.dupeZ(u8, buf.items);
}
const C = Common(Self, Private);
@ -303,14 +422,17 @@ pub const Tab = extern struct {
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.config.impl,
properties.@"split-tree".impl,
properties.@"surface-tree".impl,
properties.title.impl,
properties.tooltip.impl,
});
// Bindings
class.bindTemplateChildPrivate("split_tree", .{});
// Template Callbacks
class.bindTemplateCallback("computed_title", &closureComputedTitle);
class.bindTemplateCallback("notify_active_surface", &propActiveSurface);
class.bindTemplateCallback("notify_tree", &propSplitTree);

View File

@ -28,6 +28,7 @@ const Surface = @import("surface.zig").Surface;
const Tab = @import("tab.zig").Tab;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const CommandPalette = @import("command_palette.zig").CommandPalette;
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_window);
@ -318,8 +319,15 @@ pub const Window = extern struct {
);
}
// Start states based on config.
if (priv.config) |config_obj| {
const config = config_obj.get();
if (config.maximize) self.as(gtk.Window).maximize();
if (config.fullscreen) self.as(gtk.Window).fullscreen();
}
// We always sync our appearance at the end because loading our
// config and such can affect our bindings which ar setup initially
// config and such can affect our bindings which are setup initially
// in initTemplate.
self.syncAppearance();
@ -330,40 +338,27 @@ pub const Window = extern struct {
/// Setup our action map.
fn initActionMap(self: *Self) void {
const actions = .{
.{ "about", actionAbout, null },
.{ "close", actionClose, null },
.{ "close-tab", actionCloseTab, null },
.{ "new-tab", actionNewTab, null },
.{ "new-window", actionNewWindow, null },
.{ "split-right", actionSplitRight, null },
.{ "split-left", actionSplitLeft, null },
.{ "split-up", actionSplitUp, null },
.{ "split-down", actionSplitDown, null },
.{ "copy", actionCopy, null },
.{ "paste", actionPaste, null },
.{ "reset", actionReset, null },
.{ "clear", actionClear, null },
const actions = [_]ext.actions.Action(Self){
.init("about", actionAbout, null),
.init("close", actionClose, null),
.init("close-tab", actionCloseTab, null),
.init("new-tab", actionNewTab, null),
.init("new-window", actionNewWindow, null),
.init("ring-bell", actionRingBell, null),
.init("split-right", actionSplitRight, null),
.init("split-left", actionSplitLeft, null),
.init("split-up", actionSplitUp, null),
.init("split-down", actionSplitDown, null),
.init("copy", actionCopy, null),
.init("paste", actionPaste, null),
.init("reset", actionReset, null),
.init("clear", actionClear, null),
// TODO: accept the surface that toggled the command palette
.{ "toggle-command-palette", actionToggleCommandPalette, null },
.init("toggle-command-palette", actionToggleCommandPalette, null),
.init("toggle-inspector", actionToggleInspector, null),
};
const action_map = self.as(gio.ActionMap);
inline for (actions) |entry| {
const action = gio.SimpleAction.new(
entry[0],
entry[2],
);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
*Self,
entry[1],
self,
.{},
);
action_map.addAction(action.as(gio.Action));
}
ext.actions.add(Self, self, &actions);
}
/// Winproto backend for this window.
@ -416,6 +411,12 @@ pub const Window = extern struct {
"title",
.{ .sync_create = true },
);
_ = tab.as(gobject.Object).bindProperty(
"tooltip",
page.as(gobject.Object),
"tooltip",
.{ .sync_create = true },
);
// Bind signals
const split_tree = tab.getSplitTree();
@ -556,16 +557,46 @@ pub const Window = extern struct {
/// fullscreen, etc.).
fn syncAppearance(self: *Self) void {
const priv = self.private();
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled));
const widget = self.as(gtk.Widget);
// Fix any artifacting that may occur in window corners. The .ssd CSS
// class is defined in the GtkWindow documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
// for .ssd is provided by GTK and Adwaita.
self.toggleCssClass("csd", csd_enabled);
self.toggleCssClass("ssd", !csd_enabled);
self.toggleCssClass("no-border-radius", !csd_enabled);
// Toggle style classes based on whether we're using CSDs or SSDs.
//
// These classes are defined in the gtk.Window documentation:
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes.
{
// Reset all style classes first
inline for (&.{
"ssd",
"csd",
"solid-csd",
"no-border-radius",
}) |class|
widget.removeCssClass(class);
const csd_enabled = priv.winproto.clientSideDecorationEnabled();
self.as(gtk.Window).setDecorated(@intFromBool(csd_enabled));
if (csd_enabled) {
const display = widget.getDisplay();
// We do the exact same check GTK is doing internally and toggle
// either the `csd` or `solid-csd` style, based on whether the user's
// window manager is deemed _non-compositing_.
//
// In practice this only impacts users of traditional X11 window
// managers (e.g. i3, dwm, awesomewm, etc.) and not X11 desktop
// environments or Wayland compositors/DEs.
if (display.isRgba() != 0 and display.isComposited() != 0) {
widget.addCssClass("csd");
} else {
widget.addCssClass("solid-csd");
}
} else {
widget.addCssClass("ssd");
// Fix any artifacting that may occur in window corners.
widget.addCssClass("no-border-radius");
}
}
// Trigger all our dynamic properties that depend on the config.
inline for (&.{
@ -674,13 +705,6 @@ pub const Window = extern struct {
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = Surface.signals.@"close-request".connect(
surface,
*Self,
surfaceCloseRequest,
self,
.{},
);
_ = Surface.signals.@"present-request".connect(
surface,
*Self,
@ -1060,6 +1084,21 @@ pub const Window = extern struct {
});
}
fn closureSubtitle(
_: *Self,
config_: ?*Config,
pwd_: ?[*:0]const u8,
) callconv(.c) ?[*:0]const u8 {
const config = if (config_) |v| v.get() else return null;
return switch (config.@"window-subtitle") {
.false => null,
.@"working-directory" => pwd: {
const pwd = pwd_ orelse return null;
break :pwd glib.ext.dupeZ(u8, std.mem.span(pwd));
},
};
}
//---------------------------------------------------------------
// Virtual methods
@ -1296,6 +1335,10 @@ pub const Window = extern struct {
// Setup our binding group. This ensures things like the title
// are synced from the active tab.
priv.tab_bindings.setSource(child.as(gobject.Object));
// If the tab was previously marked as needing attention
// (e.g. due to a bell character), we now unmark that
page.setNeedsAttention(@intFromBool(false));
}
fn tabViewPageAttached(
@ -1430,25 +1473,6 @@ pub const Window = extern struct {
self.addToast(i18n._("Cleared clipboard"));
}
fn surfaceCloseRequest(
_: *Surface,
scope: *const Surface.CloseScope,
self: *Self,
) callconv(.c) void {
switch (scope.*) {
// Handled directly by the tab. If the surface is the last
// surface then the tab will emit its own signal to request
// closing itself.
.surface => return,
// Also handled directly by the tab.
.tab => return,
// The only one we care about!
.window => self.as(gtk.Window).close(),
}
}
fn surfaceMenu(
_: *Surface,
self: *Self,
@ -1708,6 +1732,30 @@ pub const Window = extern struct {
self.performBindingAction(.clear_screen);
}
fn actionRingBell(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return;
if (config.@"bell-features".system) system: {
const native = self.as(gtk.Native).getSurface() orelse {
log.warn("unable to get native surface from window", .{});
break :system;
};
native.beep();
}
if (config.@"bell-features".attention) {
// Request user attention
self.winproto().setUrgent(true) catch |err| {
log.warn("failed to request user attention={}", .{err});
};
}
}
/// Toggle the command palette.
///
/// TODO: accept the surface that toggled the command palette as a parameter
@ -1770,6 +1818,23 @@ pub const Window = extern struct {
self.toggleCommandPalette();
}
/// Toggle the Ghostty inspector for the active surface.
fn toggleInspector(self: *Self) void {
const surface = self.getActiveSurface() orelse return;
_ = surface.controlInspector(.toggle);
}
/// React to a GTK action requesting that the Ghostty inspector be toggled.
fn actionToggleInspector(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
// TODO: accept the surface that toggled the command palette as a
// parameter
self.toggleInspector();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -1783,6 +1848,9 @@ pub const Window = extern struct {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(DebugWarning);
gobject.ext.ensureType(SplitTree);
gobject.ext.ensureType(Surface);
gobject.ext.ensureType(Tab);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
@ -1832,6 +1900,7 @@ pub const Window = extern struct {
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
class.bindTemplateCallback("computed_subtitle", &closureSubtitle);
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);

View File

@ -102,6 +102,12 @@ label.resize-overlay {
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */
}
.surface .bell-overlay {
border-color: color-mix(in srgb, var(--accent-color), transparent 50%);
border-width: 3px;
border-style: solid;
}
/*
* Command Palette
*/

View File

@ -5,11 +5,15 @@
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
pub const actions = @import("ext/actions.zig");
/// Wrapper around `gobject.boxedCopy` to copy a boxed type `T`.
pub fn boxedCopy(comptime T: type, ptr: *const T) *T {
const copy = gobject.boxedCopy(T.getGObjectType(), ptr);
@ -50,3 +54,15 @@ pub fn getAncestor(comptime T: type, widget: *gtk.Widget) ?*T {
// We can assert the unwrap because getAncestor above
return gobject.ext.cast(T, ancestor).?;
}
/// Check a gobject.Value to see what type it is wrapping. This is equivalent to GTK's
/// `G_VALUE_HOLDS()` macro but Zig's C translator does not like it.
pub fn gValueHolds(value_: ?*const gobject.Value, g_type: gobject.Type) bool {
const value = value_ orelse return false;
if (value.f_g_type == g_type) return true;
return gobject.typeCheckValueHolds(value, g_type) != 0;
}
test {
_ = actions;
}

View File

@ -0,0 +1,158 @@
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const gValueHolds = @import("../ext.zig").gValueHolds;
/// Check that an action name is valid.
///
/// Reimplementation of `g_action_name_is_valid()` so that it can be
/// used at comptime.
///
/// See:
/// https://docs.gtk.org/gio/type_func.Action.name_is_valid.html
fn gActionNameIsValid(name: [:0]const u8) bool {
if (name.len == 0) return false;
for (name) |c| switch (c) {
'-' => continue,
'.' => continue,
'0'...'9' => continue,
'a'...'z' => continue,
'A'...'Z' => continue,
else => return false,
};
return true;
}
test "gActionNameIsValid" {
try testing.expect(gActionNameIsValid("ring-bell"));
try testing.expect(!gActionNameIsValid("ring_bell"));
}
/// Function to create a structure for describing an action.
pub fn Action(comptime T: type) type {
return struct {
pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void;
name: [:0]const u8,
callback: Callback,
parameter_type: ?*const glib.VariantType,
/// Function to initialize a new action so that we can comptime check the name.
pub fn init(comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType) @This() {
comptime assert(gActionNameIsValid(name));
return .{
.name = name,
.callback = callback,
.parameter_type = parameter_type,
};
}
};
}
/// Add actions to a widget that implements gio.ActionMap.
pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void {
addToMap(T, self, self.as(gio.ActionMap), actions);
}
/// Add actions to the given map.
pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void {
for (actions) |entry| {
assert(gActionNameIsValid(entry.name));
const action = gio.SimpleAction.new(
entry.name,
entry.parameter_type,
);
defer action.unref();
_ = gio.SimpleAction.signals.activate.connect(
action,
*T,
entry.callback,
self,
.{},
);
map.addAction(action.as(gio.Action));
}
}
/// Add actions to a widget that doesn't implement ActionGroup directly.
pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) void {
comptime assert(gActionNameIsValid(name));
// Collect our actions into a group since we're just a plain widget that
// doesn't implement ActionGroup directly.
const group = gio.SimpleActionGroup.new();
errdefer group.unref();
addToMap(T, self, group.as(gio.ActionMap), actions);
self.as(gtk.Widget).insertActionGroup(
name,
group.as(gio.ActionGroup),
);
}
test "adding actions to an object" {
// This test requires a connection to an active display environment.
if (gtk.initCheck() == 0) return;
const callbacks = struct {
fn callback(_: *gio.SimpleAction, variant_: ?*glib.Variant, self: *gtk.Box) callconv(.c) void {
const i32_variant_type = glib.ext.VariantType.newFor(i32);
defer i32_variant_type.free();
const variant = variant_ orelse return;
assert(variant.isOfType(i32_variant_type) != 0);
var value = std.mem.zeroes(gobject.Value);
_ = value.init(gobject.ext.types.int);
defer value.unset();
value.setInt(variant.getInt32());
self.as(gobject.Object).setProperty("spacing", &value);
}
};
const box = gtk.Box.new(.vertical, 0);
_ = box.as(gobject.Object).refSink();
defer box.unref();
{
const i32_variant_type = glib.ext.VariantType.newFor(i32);
defer i32_variant_type.free();
const actions = [_]Action(gtk.Box){
.init("test", callbacks.callback, i32_variant_type),
};
addAsGroup(gtk.Box, box, "test", &actions);
}
const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31));
const parameter = glib.Variant.newInt32(expected);
try testing.expect(box.as(gtk.Widget).activateActionVariant("test.test", parameter) != 0);
_ = glib.MainContext.iteration(null, @intFromBool(true));
var value = std.mem.zeroes(gobject.Value);
_ = value.init(gobject.ext.types.int);
defer value.unset();
box.as(gobject.Object).getProperty("spacing", &value);
try testing.expect(gValueHolds(&value, gobject.ext.types.int));
const actual = value.getInt();
try testing.expectEqual(expected, actual);
}

View File

@ -6,6 +6,7 @@ template $GhosttySurface: Adw.Bin {
"surface",
]
notify::bell-ringing => $notify_bell_ringing();
notify::config => $notify_config();
notify::mouse-hover-url => $notify_mouse_hover_url();
notify::mouse-hidden => $notify_mouse_hidden();
@ -53,6 +54,27 @@ template $GhosttySurface: Adw.Bin {
valign: start;
}
[overlay]
// The "border" bell feature is implemented here as an overlay rather than
// just adding a border to the GLArea or other widget for two reasons.
// First, adding a border to an existing widget causes a resize of the
// widget which undesirable side effects. Second, we can make it reactive
// here in the blueprint with relatively little code.
Revealer {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
Box bell_overlay {
styles [
"bell-overlay",
]
halign: fill;
valign: fill;
}
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;
@ -130,6 +152,7 @@ template $GhosttySurface: Adw.Bin {
}
IMMulticontext im_context {
input-purpose: terminal;
preedit-start => $im_preedit_start();
preedit-changed => $im_preedit_changed();
preedit-end => $im_preedit_end();
@ -167,27 +190,31 @@ menu context_menu_model {
item {
label: _("Change Title…");
action: "win.prompt-title";
action: "surface.prompt-title";
}
item {
label: _("Split Up");
action: "split-tree.new-up";
action: "split-tree.new-split";
target: "up";
}
item {
label: _("Split Down");
action: "split-tree.new-down";
action: "split-tree.new-split";
target: "down";
}
item {
label: _("Split Left");
action: "split-tree.new-left";
action: "split-tree.new-split";
target: "left";
}
item {
label: _("Split Right");
action: "split-tree.new-right";
action: "split-tree.new-split";
target: "right";
}
}

View File

@ -0,0 +1,51 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyImguiWidget: Adw.Bin {
styles [
"imgui",
]
Adw.Bin {
Gtk.GLArea gl_area {
auto-render: true;
// needs to be focusable so that we can receive events
focusable: true;
focus-on-click: true;
allowed-apis: gl;
realize => $realize();
unrealize => $unrealize();
resize => $resize();
render => $render();
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
GestureClick {
pressed => $mouse_pressed();
released => $mouse_released();
button: 0;
}
EventControllerMotion {
motion => $mouse_motion();
}
EventControllerScroll {
scroll => $scroll();
flags: both_axes;
}
}
}
}
IMMulticontext im_context {
commit => $im_commit();
}

View File

@ -0,0 +1,18 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyInspectorWidget: Adw.Bin {
styles [
"inspector",
]
hexpand: true;
vexpand: true;
Adw.Bin {
$GhosttyImguiWidget imgui_widget {
render => $imgui_render();
setup => $imgui_setup();
}
}
}

View File

@ -0,0 +1,38 @@
using Gtk 4.0;
using Adw 1;
template $GhosttyInspectorWindow: Adw.ApplicationWindow {
title: _("Ghostty: Terminal Inspector");
icon-name: "com.mitchellh.ghostty";
default-width: 1000;
default-height: 600;
styles [
"inspector",
]
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: bind template.title;
};
}
Gtk.Box {
orientation: vertical;
spacing: 0;
hexpand: true;
vexpand: true;
$GhosttyDebugWarning {
visible: bind template.debug;
}
$GhosttyInspectorWidget inspector_widget {
notify::surface => $notify_inspector_surface();
surface: bind template.surface;
}
}
};
}

View File

@ -0,0 +1,16 @@
using Gtk 4.0;
using Adw 1;
template $GhosttySurfaceTitleDialog: Adw.AlertDialog {
heading: _("Change Terminal Title");
body: _("Leave blank to restore the default title.");
responses [
cancel: _("Cancel") suggested,
ok: _("OK") destructive,
]
focus-widget: entry;
extra-child: Entry entry {};
}

View File

@ -8,6 +8,8 @@ template $GhosttyTab: Box {
orientation: vertical;
hexpand: true;
vexpand: true;
title: bind $computed_title(template.config, split_tree.active-surface as <$GhosttySurface>.title, split_tree.active-surface as <$GhosttySurface>.title-override, split_tree.is-zoomed, split_tree.active-surface as <$GhosttySurface>.bell-ringing) as <string>;
tooltip: bind split_tree.active-surface as <$GhosttySurface>.pwd;
$GhosttySplitTree split_tree {
notify::active-surface => $notify_active_surface();

View File

@ -40,6 +40,13 @@ template $GhosttyWindow: Adw.ApplicationWindow {
title-widget: Adw.WindowTitle {
title: bind template.title;
// Blueprint auto-formatter won't let me split this into multiple
// lines. Let me explain myself. All parameters to a closure are used
// as notifications to recompute the value of the closure. All
// elements of a property chain are also subscribed to for changes.
// This one long, ugly line saves us from manually building up this
// massive notify chain in code.
subtitle: bind $computed_subtitle(template.config, tab_view.selected-page.child as <$GhosttyTab>.active-surface as <$GhosttySurface>.pwd) as <string>;
};
[start]

View File

@ -539,6 +539,7 @@ pub fn performAction(
.check_for_updates,
.undo,
.redo,
.show_on_screen_keyboard,
=> {
log.warn("unimplemented action={}", .{action});
return false;

View File

@ -25,6 +25,11 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
try resources.append(alloc, gtk.resources_c);
try resources.append(alloc, gtk.resources_h);
}
{
const gtk = SharedDeps.gtkNgDistResources(b);
try resources.append(alloc, gtk.resources_c);
try resources.append(alloc, gtk.resources_h);
}
// git archive to create the final tarball. "git archive" is the
// easiest way I can find to create a tarball that ignores stuff

View File

@ -1,10 +1,11 @@
ARG DISTRO_VERSION="12"
ARG DISTRO_VERSION="13"
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
blueprint-compiler \
build-essential \
curl \
libbz2-dev \
@ -16,33 +17,28 @@ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
pandoc \
# Ghostty Dependencies
libadwaita-1-dev \
libgtk-4-dev && \
# TODO: Add when this is updated to Debian 13++
# gtk4-layer-shell
libgtk-4-dev \
libgtk4-layer-shell-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
WORKDIR /src
COPY ./build.zig /src
# Install zig
# https://ziglang.org/download/
COPY . /src
WORKDIR /src
RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' build.zig) && curl -L -o /tmp/zig.tar.xz "https://ziglang.org/download/$ZIG_VERSION/zig-linux-$(uname -m)-$ZIG_VERSION.tar.xz" && \
tar -xf /tmp/zig.tar.xz -C /opt && \
rm /tmp/zig.tar.xz && \
ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig
# Debian 12 doesn't have gtk4-layer-shell, so we have to manually compile it ourselves
COPY . /src
RUN zig build \
-Doptimize=Debug \
-Dcpu=baseline \
-Dapp-runtime=gtk \
-fno-sys=gtk4-layer-shell
-Dcpu=baseline
RUN ./zig-out/bin/ghostty +version

View File

@ -2433,7 +2433,12 @@ keybind: Keybinds = .{},
/// Prepend a bell emoji (🔔) to the title of the alerted surface until the
/// terminal is re-focused or interacted with (such as on keyboard input).
///
/// Only implemented on macOS.
/// * `border`
///
/// Display a border around the alerted surface until the terminal is
/// re-focused or interacted with (such as on keyboard input).
///
/// GTK only.
///
/// Example: `audio`, `no-audio`, `system`, `no-system`
///
@ -6988,6 +6993,7 @@ pub const BellFeatures = packed struct {
audio: bool = false,
attention: bool = true,
title: bool = true,
border: bool = false,
};
/// See mouse-shift-capture

File diff suppressed because it is too large Load Diff

View File

@ -86,6 +86,11 @@ pub const Region = extern struct {
height: u32,
};
/// Number of nodes to preallocate in the list on init.
///
/// TODO: figure out optimal prealloc based on real world usage
const node_prealloc: usize = 64;
pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
var result = Atlas{
.data = try alloc.alloc(u8, size * size * format.depth()),
@ -95,8 +100,8 @@ pub fn init(alloc: Allocator, size: u32, format: Format) Allocator.Error!Atlas {
};
errdefer result.deinit(alloc);
// TODO: figure out optimal prealloc based on real world usage
try result.nodes.ensureUnusedCapacity(alloc, 64);
// Prealloc some nodes.
result.nodes = try .initCapacity(alloc, node_prealloc);
// This sets up our initial state
result.clear();
@ -287,30 +292,30 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void
assert(size_new >= self.size);
if (size_new == self.size) return;
// Preserve our old values so we can copy the old data
// We reserve space ahead of time for the new node, so that we
// won't have to handle any errors after allocating our new data.
try self.nodes.ensureUnusedCapacity(alloc, 1);
const data_new = try alloc.alloc(
u8,
size_new * size_new * self.format.depth(),
);
// Function is infallible from this point.
errdefer comptime unreachable;
// Keep track of our old data so that we can copy it.
const data_old = self.data;
const size_old = self.size;
// Allocate our new data
self.data = try alloc.alloc(u8, size_new * size_new * self.format.depth());
defer alloc.free(data_old);
errdefer {
alloc.free(self.data);
self.data = data_old;
}
// Add our new rectangle for our added righthand space. We do this
// right away since its the only operation that can fail and we want
// to make error cleanup easier.
try self.nodes.append(alloc, .{
.x = size_old - 1,
.y = 1,
.width = size_new - size_old,
});
// If our allocation and rectangle add succeeded, we can go ahead
// and persist our new size and copy over the old data.
// Update our data and size to our new ones.
self.data = data_new;
self.size = size_new;
// Free the old data once we're done with it.
defer alloc.free(data_old);
// Zero the new data out and copy the old data over.
@memset(self.data, 0);
self.set(.{
.x = 0, // don't bother skipping border so we can avoid strides
@ -319,6 +324,13 @@ pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void
.height = size_old - 2, // skip the last border row
}, data_old[size_old * self.format.depth() ..]);
// Add the new rectangle for our added righthand space.
self.nodes.appendAssumeCapacity(.{
.x = size_old - 1,
.y = 1,
.width = size_new - size_old,
});
// We are both modified and resized
_ = self.modified.fetchAdd(1, .monotonic);
_ = self.resized.fetchAdd(1, .monotonic);
@ -737,3 +749,49 @@ test "grow BGR" {
_ = try atlas.reserve(alloc, 2, 1);
try testing.expectError(Error.AtlasFull, atlas.reserve(alloc, 1, 1));
}
test "grow OOM" {
// We use a fixed buffer allocator so that we can consistently hit OOM.
//
// We calculate the size to exactly fit the 4x4 pixels and node list.
var buf: [
4 * 4 * 1 // 4x4 pixels, each 1 byte.
+ node_prealloc * @sizeOf(Node) // preallocated nodes.
]u8 = undefined;
var fba: std.heap.FixedBufferAllocator = .init(&buf);
const alloc = fba.allocator();
var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border
defer atlas.deinit(alloc);
const reg = try atlas.reserve(alloc, 2, 2);
try testing.expectError(
Error.AtlasFull,
atlas.reserve(alloc, 1, 1),
);
// Write some data so we can verify that attempted growing doesn't mess it up.
atlas.set(reg, &[_]u8{ 1, 2, 3, 4 });
try testing.expectEqual(@as(u8, 1), atlas.data[5]);
try testing.expectEqual(@as(u8, 2), atlas.data[6]);
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
// Expand by 1, should give OOM, modified and resized should be unchanged.
const old_modified = atlas.modified.load(.monotonic);
const old_resized = atlas.resized.load(.monotonic);
try testing.expectError(
Allocator.Error.OutOfMemory,
atlas.grow(alloc, atlas.size + 1),
);
const new_modified = atlas.modified.load(.monotonic);
const new_resized = atlas.resized.load(.monotonic);
try testing.expectEqual(old_modified, new_modified);
try testing.expectEqual(old_resized, new_resized);
// Ensure our data is still set.
try testing.expectEqual(@as(u8, 1), atlas.data[5]);
try testing.expectEqual(@as(u8, 2), atlas.data[6]);
try testing.expectEqual(@as(u8, 3), atlas.data[9]);
try testing.expectEqual(@as(u8, 4), atlas.data[10]);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -524,6 +524,14 @@ pub const Action = union(enum) {
/// Has no effect on macOS.
show_gtk_inspector,
/// Show the on-screen keyboard if one is present.
///
/// Only implemented on Linux (GTK). On GNOME, the "Screen Keyboard"
/// accessibility feature must be turned on, which can be found under
/// Settings > Accessibility > Typing. Other platforms are as of now
/// untested.
show_on_screen_keyboard,
/// Open the configuration file in the default OS editor.
///
/// If your default OS editor isn't configured then this will fail.
@ -1051,6 +1059,7 @@ pub const Action = union(enum) {
.toggle_window_float_on_top,
.toggle_secure_input,
.toggle_command_palette,
.show_on_screen_keyboard,
.reset_window_size,
.crash,
=> .surface,

View File

@ -369,6 +369,12 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Show the GTK inspector.",
}},
.show_on_screen_keyboard => comptime &.{.{
.action = .show_on_screen_keyboard,
.title = "Show On-Screen Keyboard",
.description = "Show the on-screen keyboard if present.",
}},
.open_config => comptime &.{.{
.action = .open_config,
.title = "Open Config",

View File

@ -141,7 +141,12 @@ pub const Contents = struct {
}
/// Set the cursor value. If the value is null then the cursor is hidden.
pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText, cursor_style: ?renderer.CursorStyle) void {
pub fn setCursor(
self: *Contents,
v: ?shaderpkg.CellText,
cursor_style: ?renderer.CursorStyle,
) void {
if (self.size.rows == 0) return;
self.fg_rows.lists[0].clearRetainingCapacity();
self.fg_rows.lists[self.size.rows + 1].clearRetainingCapacity();
@ -158,6 +163,7 @@ pub const Contents = struct {
/// Returns the current cursor glyph if present, checking both cursor lists.
pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText {
if (self.size.rows == 0) return null;
if (self.fg_rows.lists[0].items.len > 0) {
return self.fg_rows.lists[0].items[0];
}
@ -469,3 +475,14 @@ test "Contents clear last added content" {
// Fg row index is +1 because of cursor list at start
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
}
test "Contents with zero-sized screen" {
const testing = std.testing;
const alloc = testing.allocator;
var c: Contents = .{};
defer c.deinit(alloc);
c.setCursor(null, null);
try testing.expect(c.getCursorGlyph() == null);
}

View File

@ -395,6 +395,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
}
pub fn deinit(self: *FrameState) void {
self.target.deinit();
self.uniforms.deinit();
self.cells.deinit();
self.cells_bg.deinit();

View File

@ -529,6 +529,17 @@
...
}
{
pango fontset
Memcheck:Leak
match-leak-kinds: possible
fun:*alloc
...
fun:FcFontRenderPrepare
fun:pango_fc_fontset_get_font_at
...
}
{
pango and fontconfig
Memcheck:Leak