Merge remote-tracking branch 'upstream/main' into jacob/uucode
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = .{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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 {};
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -539,6 +539,7 @@ pub fn performAction(
|
|||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.show_on_screen_keyboard,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||