diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 522847c88..d253688e6 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,13 +36,13 @@ jobs: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index f7fb72f65..f4983412f 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,13 +83,13 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index bae096054..009708f56 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -107,12 +107,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8910d8c07..419e83235 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: - build-bench - build-dist - build-flatpak + - build-freebsd - build-linux - build-linux-libghostty - build-nix @@ -20,7 +21,6 @@ jobs: - build-macos - build-macos-matrix - build-windows - - flatpak-check-zig-cache - test - test-gtk - test-gtk-ng @@ -70,14 +70,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -101,14 +101,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -137,14 +137,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -166,14 +166,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -199,14 +199,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -243,14 +243,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -378,7 +378,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix @@ -474,14 +474,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -519,14 +519,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -568,14 +568,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -616,14 +616,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -674,12 +674,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -702,12 +702,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -729,12 +729,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -756,12 +756,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -783,12 +783,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -810,12 +810,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -844,12 +844,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -871,12 +871,12 @@ jobs: steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -906,14 +906,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -954,33 +954,6 @@ jobs: build-args: | DISTRO_VERSION=13 - flatpak-check-zig-cache: - if: github.repository == 'ghostty-org/ghostty' - runs-on: namespace-profile-ghostty-xsm - env: - ZIG_LOCAL_CACHE_DIR: /zig/local-cache - ZIG_GLOBAL_CACHE_DIR: /zig/global-cache - steps: - - name: Checkout code - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 - with: - path: | - /nix - /zig - - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - useDaemon: false # sometimes fails on short jobs - - name: Check Flatpak Zig Dependencies - run: nix develop -c ./flatpak/build-support/check-zig-cache.sh - flatpak: if: github.repository == 'ghostty-org/ghostty' name: "Flatpak" @@ -996,7 +969,7 @@ jobs: - arch: aarch64 runner: namespace-profile-ghostty-md-arm64 runs-on: ${{ matrix.variant.runner }} - needs: [flatpak-check-zig-cache, test] + needs: test steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5 @@ -1020,14 +993,14 @@ jobs: uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -1043,3 +1016,57 @@ jobs: - name: valgrind run: | nix develop -c zig build test-valgrind + + build-freebsd: + name: Build on FreeBSD + needs: test + runs-on: namespace-profile-mitchellh-sm-systemd + strategy: + matrix: + release: + - "14.3" + # - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108 + steps: + - name: Checkout Ghostty + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Start SSH + run: | + sudo systemctl start ssh + + - name: Set up FreeBSD VM + uses: vmactions/freebsd-vm@05856381fab64eeee9b038a0818f6cec649ca17a # v1.2.3 + with: + release: ${{ matrix.release }} + copyback: false + usesh: true + prepare: | + pkg install -y \ + devel/blueprint-compiler \ + devel/gettext \ + devel/git \ + devel/pkgconf \ + graphics/wayland \ + lang/zig \ + security/ca_root_nss \ + textproc/hs-pandoc \ + x11-fonts/jetbrains-mono \ + x11-toolkits/libadwaita \ + x11-toolkits/gtk40 \ + x11-toolkits/gtk4-layer-shell + + run: | + zig env + + - name: Run tests + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE + zig build test + + - name: Build GTK-NG app runtime + shell: freebsd {0} + run: | + cd $GITHUB_WORKSPACE + zig build + ./zig-out/bin/ghostty +version diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index e1ee92168..591762e72 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,14 +22,14 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16 + uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17 with: path: | /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2 + uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 @@ -50,8 +50,6 @@ jobs: if ! git diff --exit-code build.zig.zon; then nix develop -c ./nix/build-support/check-zig-cache.sh --update nix develop -c ./nix/build-support/check-zig-cache.sh - nix develop -c ./flatpak/build-support/check-zig-cache.sh --update - nix develop -c ./flatpak/build-support/check-zig-cache.sh fi # Verify the build still works. We choose an arbitrary build type diff --git a/CODEOWNERS b/CODEOWNERS index 0fb60758e..770c08860 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -169,6 +169,7 @@ /po/es_BO.UTF-8.po @ghostty-org/es_BO /po/es_AR.UTF-8.po @ghostty-org/es_AR /po/fr_FR.UTF-8.po @ghostty-org/fr_FR +/po/hu_HU.UTF-8.po @ghostty-org/hu_HU /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/mk_MK.UTF-8.po @ghostty-org/mk_MK diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e988704b..777771145 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -# Ghostty Development Process +# Contributing to Ghostty -This document describes the development process for Ghostty. It is intended for -anyone considering opening an **issue** or **pull request**. If in doubt, -please open a [discussion](https://github.com/ghostty-org/ghostty/discussions); -we can always convert that to an issue later. +This document describes the process of contributing to Ghostty. It is intended +for anyone considering opening an **issue**, **discussion** or **pull request**. +For people who are interested in developing Ghostty and technical details behind +it, please check out our ["Developing Ghostty"](HACKING.md) document as well. > [!NOTE] > @@ -49,13 +49,16 @@ Please be respectful to maintainers and disclose AI assistance. ## Quick Guide -**I'd like to contribute!** +### I'd like to contribute! -All issues are actionable. Pick one and start working on it. Thank you. -If you need help or guidance, comment on the issue. Issues that are extra -friendly to new contributors are tagged with "contributor friendly". +[All issues are actionable](#issues-are-actionable). Pick one and start +working on it. Thank you. If you need help or guidance, comment on the issue. +Issues that are extra friendly to new contributors are tagged with +["contributor friendly"]. -**I'd like to translate Ghostty to my language!** +["contributor friendly"]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22 + +### I'd like to translate Ghostty to my language! We have written a [Translator's Guide](po/README_TRANSLATORS.md) for everyone interested in contributing translations to Ghostty. @@ -64,25 +67,39 @@ and you can submit pull requests directly, although please make sure that our [Style Guide](po/README_TRANSLATORS.md#style-guide) is followed before submission. -**I have a bug!** +### I have a bug! / Something isn't working! -1. Search the issue tracker and discussions for similar issues. -2. If you don't have steps to reproduce, open a discussion. -3. If you have steps to reproduce, open an issue. +1. Search the issue tracker and discussions for similar issues. Tip: also + search for [closed issues] and [discussions] — your issue might have already + been fixed! +2. If your issue hasn't been reported already, open an ["Issue Triage" discussion] + and make sure to fill in the template **completely**. They are vital for + maintainers to figure out important details about your setup. Because of + this, please make sure that you _only_ use the "Issue Triage" category for + reporting bugs — thank you! -**I have an idea for a feature!** +[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed +[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed +["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage -1. Open a discussion. +### I have an idea for a feature! -**I've implemented a feature!** +Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas). -1. If there is an issue for the feature, open a pull request. +### I've implemented a feature! + +1. If there is an issue for the feature, open a pull request straight away. 2. If there is no issue, open a discussion and link to your branch. -3. If you want to live dangerously, open a pull request and hope for the best. +3. If you want to live dangerously, open a pull request and + [hope for the best](#pull-requests-implement-an-issue). -**I have a question!** +### I have a question! -1. Open a discussion or use Discord. +Open an [Q&A discussion], or join our [Discord Server] and ask away in the +`#help` channel. + +[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a +[Discord Server]: https://discord.gg/ghostty ## General Patterns @@ -120,209 +137,3 @@ pull request will be accepted with a high degree of certainty. > **Pull requests are NOT a place to discuss feature design.** Please do > not open a WIP pull request to discuss a feature. Instead, use a discussion > and link to your branch. - -# Developer Guide - -> [!NOTE] -> -> **The remainder of this file is dedicated to developers actively -> working on Ghostty.** If you're a user reporting an issue, you can -> ignore the rest of this document. - -## Including and Updating Translations - -See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details. - -## Checking for Memory Leaks - -While Zig does an amazing job of finding and preventing memory leaks, -Ghostty uses many third-party libraries that are written in C. Improper usage -of those libraries or bugs in those libraries can cause memory leaks that -Zig cannot detect by itself. - -### On Linux - -On Linux the recommended tool to check for memory leaks is Valgrind. The -recommended way to run Valgrind is via `zig build`: - -```sh -zig build run-valgrind -``` - -This builds a Ghostty executable with Valgrind support and runs Valgrind -with the proper flags to ensure we're suppressing known false positives. - -You can combine the same build args with `run-valgrind` that you can with -`run`, such as specifying additional configurations after a trailing `--`. - -## Input Stack Testing - -The input stack is the part of the codebase that starts with a -key event and ends with text encoding being sent to the pty (it -does not include _rendering_ the text, which is part of the -font or rendering stack). - -If you modify any part of the input stack, you must manually verify -all the following input cases work properly. We unfortunately do -not automate this in any way, but if we can do that one day that'd -save a LOT of grief and time. - -Note: this list may not be exhaustive, I'm still working on it. - -### Linux IME - -IME (Input Method Editors) are a common source of bugs in the input stack, -especially on Linux since there are multiple different IME systems -interacting with different windowing systems and application frameworks -all written by different organizations. - -The following matrix should be tested to ensure that all IME input works -properly: - -1. Wayland, X11 -2. ibus, fcitx, none -3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex -4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors) - -> [!NOTE] -> -> This is a **work in progress**. I'm still working on this list and it -> is not complete. As I find more test cases, I will add them here. - -#### Dead Key Input - -Set your keyboard layout to "Spanish" (or another layout that uses dead keys). - -1. Launch Ghostty -2. Press `'` -3. Press `a` -4. Verify that `á` is displayed - -Note that the dead key may or may not show a preedit state visually. -For ibus and fcitx it does but for the "none" case it does not. Importantly, -the text should be correct when it is sent to the pty. - -We should also test canceling dead key input: - -1. Launch Ghostty -2. Press `'` -3. Press escape -4. Press `a` -5. Verify that `a` is displayed (no diacritic) - -#### CJK Input - -Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The -exact layout doesn't matter. - -1. Launch Ghostty -2. Press `Ctrl+Shift` to switch to "Hiragana" -3. On a US physical layout, type: `konn`, you should see `こん` in preedit. -4. Press `Enter` -5. Verify that `こん` is displayed in the terminal. - -We should also test switching input methods while preedit is active, which -should commit the text: - -1. Launch Ghostty -2. Press `Ctrl+Shift` to switch to "Hiragana" -3. On a US physical layout, type: `konn`, you should see `こん` in preedit. -4. Press `Ctrl+Shift` to switch to another layout (any) -5. Verify that `こん` is displayed in the terminal as committed text. - -## Nix Virtual Machines - -Several Nix virtual machine definitions are provided by the project for testing -and developing Ghostty against multiple different Linux desktop environments. - -Running these requires a working Nix installation, either Nix on your -favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further -requirements for macOS are detailed below. - -VMs should only be run on your local desktop and then powered off when not in -use, which will discard any changes to the VM. - -The VM definitions provide minimal software "out of the box" but additional -software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`. - -### Linux - -1. Check out the Ghostty source and change to the directory. -2. Run `nix run .#`. `` can be any of the VMs defined in the - `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed - with `common` or `create`. -3. The VM will build and then launch. Depending on the speed of your system, this - can take a while, but eventually you should get a new VM window. -4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending - on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be - writable by the VM user, so be careful! - -### macOS - -1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` - config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your - configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) - blog post for more information about the Linux builder and how to tune the performance. -2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions - above to launch a VM. - -### Custom VMs - -To easily create a custom VM without modifying the Ghostty source, create a new -directory, then create a file called `flake.nix` with the following text in the -new directory. - -``` -{ - inputs = { - nixpkgs.url = "nixpkgs/nixpkgs-unstable"; - ghostty.url = "github:ghostty-org/ghostty"; - }; - outputs = { - nixpkgs, - ghostty, - ... - }: { - nixosConfigurations.custom-vm = ghostty.create-gnome-vm { - nixpkgs = nixpkgs; - system = "x86_64-linux"; - overlay = ghostty.overlays.releasefast; - # module = ./configuration.nix # also works - module = {pkgs, ...}: { - environment.systemPackages = [ - pkgs.btop - ]; - }; - }; - }; -} -``` - -The custom VM can then be run with a command like this: - -``` -nix run .#nixosConfigurations.custom-vm.config.system.build.vm -``` - -A file named `ghostty.qcow2` will be created that is used to persist any changes -made in the VM. To "reset" the VM to default delete the file and it will be -recreated the next time you run the VM. - -### Contributing new VM definitions - -#### VM Acceptance Criteria - -We welcome the contribution of new VM definitions, as long as they meet the following criteria: - -1. They should be different enough from existing VM definitions that they represent a distinct - user (and developer) experience. -2. There's a significant Ghostty user population that uses a similar environment. -3. The VMs can be built using only packages from the current stable NixOS release. - -#### VM Definition Criteria - -1. VMs should be as minimal as possible so that they build and launch quickly. - Additional software can be added at runtime with a command like `nix run nixpkgs#`. -2. VMs should not expose any services to the network, or run any remote access - software like SSH daemons, VNC or RDP. -3. VMs should auto-login using the "ghostty" user. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 000000000..d79d15a4a --- /dev/null +++ b/HACKING.md @@ -0,0 +1,329 @@ +# Developing Ghostty + +This document describes the technical details behind Ghostty's development. +If you'd like to open any pull requests or would like to implement new features +into Ghostty, please make sure to read our ["Contributing to Ghostty"](CONTRIBUTING.md) +document first. + +To start development on Ghostty, you need to build Ghostty from a Git checkout, +which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously +you need to clone the Git repository instead of unpacking the source tarball: + +```shell +git clone https://github.com/ghostty-org/ghostty +cd ghostty +``` + +> [!NOTE] +> +> Ghostty may require [extra dependencies](#extra-dependencies) +> when building from a Git checkout compared to a source tarball. +> Tip versions may also require a different version of Zig or other toolchains +> (e.g. the Xcode SDK on macOS) compared to stable versions — make sure to +> follow the steps closely! + +When you're developing Ghostty, it's very likely that you will want to build a +_debug_ build to diagnose issues more easily. This is already the default for +Zig builds, so simply run `zig build` **without any `-Doptimize` flags**. + +There are many more build steps than just `zig build`, some of which are listed +here: + +| Command | Description | +| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `zig build run` | Runs Ghostty | +| `zig build run-valgrind` | Runs Ghostty under Valgrind to [check for memory leaks](#checking-for-memory-leaks) | +| `zig build test` | Runs unit tests (accepts `-Dtest-filter=` to only run tests whose name matches the filter) | +| `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) | +| `zig build dist` | Builds a source tarball | +| `zig build distcheck` | Installs and validates a source tarball | + +## Extra Dependencies + +Building Ghostty from a Git checkout on Linux requires some additional +dependencies: + +- `blueprint-compiler` (version 0.16.0 or newer) + +macOS users don't require any additional dependencies. + +## Xcode Version and SDKs + +Building the Ghostty macOS app requires that Xcode, the macOS SDK, +and the iOS SDK are all installed. + +A common issue is that the incorrect version of Xcode is either +installed or selected. Use the `xcode-select` command to +ensure that the correct version of Xcode is selected: + +```shell-session +sudo xcode-select --switch /Applications/Xcode-beta.app +``` + +> [!IMPORTANT] +> +> Main branch development of Ghostty is preparing for the next major +> macOS release, Tahoe (macOS 26). Therefore, the main branch requires +> **Xcode 26 and the macOS 26 SDK**. +> +> You do not need to be running on macOS 26 to build Ghostty, you can +> still use Xcode 26 beta on macOS 15 stable. + +## Linting + +### Prettier + +Ghostty's docs and resources (not including Zig code) are linted using +[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI +check will fail builds with improper formatting. Therefore, if you are +modifying anything Prettier will lint, you may want to install it locally and +run this from the repo root before you commit: + +``` +prettier --write . +``` + +Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). + +Nix users can use the following command to format with Prettier: + +``` +nix develop -c prettier --write . +``` + +### Alejandra + +Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check +will fail builds with improper formatting. + +Nix users can use the following command to format with Alejandra: + +``` +nix develop -c alejandra . +``` + +Non-Nix users should install Alejandra and use the following command to format with Alejandra: + +``` +alejandra . +``` + +Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). + +### Updating the Zig Cache Fixed-Output Derivation Hash + +The Nix package depends on a [fixed-output +derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash) +that manages the Zig package cache. This allows the package to be built in the +Nix sandbox. + +Occasionally (usually when `build.zig.zon` is updated), the hash that +identifies the cache will need to be updated. There are jobs that monitor the +hash in CI, and builds will fail if it drifts. + +To update it, you can run the following in the repository root: + +``` +./nix/build-support/check-zig-cache-hash.sh --update +``` + +This will write out the `nix/zigCacheHash.nix` file with the updated hash +that can then be committed and pushed to fix the builds. + +## Including and Updating Translations + +See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details. + +## Checking for Memory Leaks + +While Zig does an amazing job of finding and preventing memory leaks, +Ghostty uses many third-party libraries that are written in C. Improper usage +of those libraries or bugs in those libraries can cause memory leaks that +Zig cannot detect by itself. + +### On Linux + +On Linux the recommended tool to check for memory leaks is Valgrind. The +recommended way to run Valgrind is via `zig build`: + +```sh +zig build run-valgrind +``` + +This builds a Ghostty executable with Valgrind support and runs Valgrind +with the proper flags to ensure we're suppressing known false positives. + +You can combine the same build args with `run-valgrind` that you can with +`run`, such as specifying additional configurations after a trailing `--`. + +## Input Stack Testing + +The input stack is the part of the codebase that starts with a +key event and ends with text encoding being sent to the pty (it +does not include _rendering_ the text, which is part of the +font or rendering stack). + +If you modify any part of the input stack, you must manually verify +all the following input cases work properly. We unfortunately do +not automate this in any way, but if we can do that one day that'd +save a LOT of grief and time. + +Note: this list may not be exhaustive, I'm still working on it. + +### Linux IME + +IME (Input Method Editors) are a common source of bugs in the input stack, +especially on Linux since there are multiple different IME systems +interacting with different windowing systems and application frameworks +all written by different organizations. + +The following matrix should be tested to ensure that all IME input works +properly: + +1. Wayland, X11 +2. ibus, fcitx, none +3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex +4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors) + +> [!NOTE] +> +> This is a **work in progress**. I'm still working on this list and it +> is not complete. As I find more test cases, I will add them here. + +#### Dead Key Input + +Set your keyboard layout to "Spanish" (or another layout that uses dead keys). + +1. Launch Ghostty +2. Press `'` +3. Press `a` +4. Verify that `á` is displayed + +Note that the dead key may or may not show a preedit state visually. +For ibus and fcitx it does but for the "none" case it does not. Importantly, +the text should be correct when it is sent to the pty. + +We should also test canceling dead key input: + +1. Launch Ghostty +2. Press `'` +3. Press escape +4. Press `a` +5. Verify that `a` is displayed (no diacritic) + +#### CJK Input + +Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The +exact layout doesn't matter. + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Enter` +5. Verify that `こん` is displayed in the terminal. + +We should also test switching input methods while preedit is active, which +should commit the text: + +1. Launch Ghostty +2. Press `Ctrl+Shift` to switch to "Hiragana" +3. On a US physical layout, type: `konn`, you should see `こん` in preedit. +4. Press `Ctrl+Shift` to switch to another layout (any) +5. Verify that `こん` is displayed in the terminal as committed text. + +## Nix Virtual Machines + +Several Nix virtual machine definitions are provided by the project for testing +and developing Ghostty against multiple different Linux desktop environments. + +Running these requires a working Nix installation, either Nix on your +favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further +requirements for macOS are detailed below. + +VMs should only be run on your local desktop and then powered off when not in +use, which will discard any changes to the VM. + +The VM definitions provide minimal software "out of the box" but additional +software can be installed by using standard Nix mechanisms like `nix run nixpkgs#`. + +### Linux + +1. Check out the Ghostty source and change to the directory. +2. Run `nix run .#`. `` can be any of the VMs defined in the + `nix/vm` directory (without the `.nix` suffix) excluding any file prefixed + with `common` or `create`. +3. The VM will build and then launch. Depending on the speed of your system, this + can take a while, but eventually you should get a new VM window. +4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending + on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be + writable by the VM user, so be careful! + +### macOS + +1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin` + config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your + configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/) + blog post for more information about the Linux builder and how to tune the performance. +2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions + above to launch a VM. + +### Custom VMs + +To easily create a custom VM without modifying the Ghostty source, create a new +directory, then create a file called `flake.nix` with the following text in the +new directory. + +``` +{ + inputs = { + nixpkgs.url = "nixpkgs/nixpkgs-unstable"; + ghostty.url = "github:ghostty-org/ghostty"; + }; + outputs = { + nixpkgs, + ghostty, + ... + }: { + nixosConfigurations.custom-vm = ghostty.create-gnome-vm { + nixpkgs = nixpkgs; + system = "x86_64-linux"; + overlay = ghostty.overlays.releasefast; + # module = ./configuration.nix # also works + module = {pkgs, ...}: { + environment.systemPackages = [ + pkgs.btop + ]; + }; + }; + }; +} +``` + +The custom VM can then be run with a command like this: + +``` +nix run .#nixosConfigurations.custom-vm.config.system.build.vm +``` + +A file named `ghostty.qcow2` will be created that is used to persist any changes +made in the VM. To "reset" the VM to default delete the file and it will be +recreated the next time you run the VM. + +### Contributing new VM definitions + +#### VM Acceptance Criteria + +We welcome the contribution of new VM definitions, as long as they meet the following criteria: + +1. They should be different enough from existing VM definitions that they represent a distinct + user (and developer) experience. +2. There's a significant Ghostty user population that uses a similar environment. +3. The VMs can be built using only packages from the current stable NixOS release. + +#### VM Definition Criteria + +1. VMs should be as minimal as possible so that they build and launch quickly. + Additional software can be added at runtime with a command like `nix run nixpkgs#`. +2. VMs should not expose any services to the network, or run any remote access + software like SSH daemons, VNC or RDP. +3. VMs should auto-login using the "ghostty" user. diff --git a/README.md b/README.md index a761e25ce..df86f7830 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ · Documentation · - Developing + Contributing + · + Developing

@@ -49,6 +51,14 @@ See the [download page](https://ghostty.org/download) on the Ghostty website. See the [documentation](https://ghostty.org/docs) on the Ghostty website. +## Contributing and Developing + +If you have any ideas, issues, etc. regarding Ghostty, or would like to +contribute to Ghostty through pull requests, please check out our +["Contributing to Ghostty"](CONTRIBUTING.md) document. Those who would like +to get involved with Ghostty's development as well should also read the +["Developing Ghostty"](HACKING.md) document for more technical details. + ## Roadmap and Status The high-level ambitious plan for the project, in order: @@ -184,119 +194,3 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. > stack memory of each thread at the time of the crash. This information > is used to rebuild the stack trace but can also contain sensitive data > depending when the crash occurred. - -## Developing Ghostty - -See the documentation on the Ghostty website for -[building Ghostty from a source tarball](http://ghostty.org/docs/install/build). -Building Ghostty from a Git checkout is very similar, except you want to -omit the `-Doptimize` flag to build a debug build, and you may require -additional dependencies since the source tarball includes some processed -files that are not in the Git repository. - -Other useful commands: - -- `zig build test` for running unit tests. -- `zig build test -Dtest-filter=` for running a specific subset of those unit tests -- `zig build run -Dconformance=` runs a conformance test case from - the `conformance` directory. The `name` is the name of the file. This runs - in the current running terminal emulator so if you want to check the - behavior of this project, you must run this command in Ghostty. - -### Extra Dependencies - -Building Ghostty from a Git checkout on Linux requires some additional -dependencies: - -- `blueprint-compiler` - -macOS users don't require any additional dependencies. - -> [!NOTE] -> This only applies to building from a _Git checkout_. This section does -> not apply if you're building from a released _source tarball_. For -> source tarballs, see the -> [website](http://ghostty.org/docs/install/build). - -### Xcode Version and SDKs - -Building the Ghostty macOS app requires that Xcode, the macOS SDK, -and the iOS SDK are all installed. - -A common issue is that the incorrect version of Xcode is either -installed or selected. Use the `xcode-select` command to -ensure that the correct version of Xcode is selected: - -```shell-session -sudo xcode-select --switch /Applications/Xcode-beta.app -``` - -> [!IMPORTANT] -> -> Main branch development of Ghostty is preparing for the next major -> macOS release, Tahoe (macOS 26). Therefore, the main branch requires -> **Xcode 26 and the macOS 26 SDK**. -> -> You do not need to be running on macOS 26 to build Ghostty, you can -> still use Xcode 26 beta on macOS 15 stable. - -### Linting - -#### Prettier - -Ghostty's docs and resources (not including Zig code) are linted using -[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI -check will fail builds with improper formatting. Therefore, if you are -modifying anything Prettier will lint, you may want to install it locally and -run this from the repo root before you commit: - -``` -prettier --write . -``` - -Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). - -Nix users can use the following command to format with Prettier: - -``` -nix develop -c prettier --write . -``` - -#### Alejandra - -Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check -will fail builds with improper formatting. - -Nix users can use the following command to format with Alejandra: - -``` -nix develop -c alejandra . -``` - -Non-Nix users should install Alejandra and use the following command to format with Alejandra: - -``` -alejandra . -``` - -Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix). - -#### Updating the Zig Cache Fixed-Output Derivation Hash - -The Nix package depends on a [fixed-output -derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash) -that manages the Zig package cache. This allows the package to be built in the -Nix sandbox. - -Occasionally (usually when `build.zig.zon` is updated), the hash that -identifies the cache will need to be updated. There are jobs that monitor the -hash in CI, and builds will fail if it drifts. - -To update it, you can run the following in the repository root: - -``` -./nix/build-support/check-zig-cache-hash.sh --update -``` - -This will write out the `nix/zigCacheHash.nix` file with the updated hash -that can then be committed and pushed to fix the builds. diff --git a/build.zig.zon b/build.zig.zon index 02f74ec0a..79b06cf8b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -111,8 +111,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - .hash = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + .hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 396ca85d0..9b2ee604f 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls": { + "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - "hash": "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + "hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 0b1aa7654..ea4ef3dc9 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -49,6 +49,7 @@ inherit name rev hash; url = url_without_query; deepClone = false; + fetchSubmodules = false; }; fetchZigArtifact = { @@ -162,11 +163,11 @@ in }; } { - name = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls"; + name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz"; - hash = "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz"; + hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index f31cfcc50..203e5dcf2 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jacobsandlund/uucode/archive/38b82297e69a3b2dc55dc8df25f3851be37f9327.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flake.lock b/flake.lock index 0374b3e5a..ba1adb08a 100644 --- a/flake.lock +++ b/flake.lock @@ -47,6 +47,19 @@ "url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1755972213, + "narHash": "sha256-VYK7aDAv8H1enXn1ECRHmGbeY6RqLnNwUJkOwloIsko=", + "rev": "73e96df7cff5783f45e21342a75a1540c4eddce4", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/unstable-small/nixos-25.11pre850642.73e96df7cff5/nixexprs.tar.xz" + }, + "original": { + "type": "tarball", + "url": "https://channels.nixos.org/nixos-unstable-small/nixexprs.tar.xz" + } + }, "root": { "inputs": { "flake-compat": "flake-compat", @@ -102,22 +115,20 @@ "flake-utils": [ "flake-utils" ], - "nixpkgs": [ - "nixpkgs" - ] + "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1742104771, - "narHash": "sha256-LhidlyEA9MP8jGe1rEnyjGFCzLLgCdDpYeWggibayr0=", + "lastModified": 1756000480, + "narHash": "sha256-fR5pdcjO0II5MNdCzqvyokyuFkmff7/FyBAjUS6sMfA=", "owner": "jcollie", "repo": "zon2nix", - "rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613", + "rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60", "type": "github" }, "original": { "owner": "jcollie", "repo": "zon2nix", - "rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613", + "rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60", "type": "github" } } diff --git a/flake.nix b/flake.nix index 7cf58b27c..99f7fcb7c 100644 --- a/flake.nix +++ b/flake.nix @@ -24,9 +24,12 @@ }; zon2nix = { - url = "github:jcollie/zon2nix?rev=56c159be489cc6c0e73c3930bd908ddc6fe89613"; + url = "github:jcollie/zon2nix?rev=d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60"; inputs = { - nixpkgs.follows = "nixpkgs"; + # Don't override nixpkgs until Zig 0.15 is available in the Nix branch + # we are using for "normal" builds. + # + # nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; }; }; diff --git a/flatpak/build-support/check-zig-cache.sh b/flatpak/build-support/check-zig-cache.sh deleted file mode 100755 index bea718640..000000000 --- a/flatpak/build-support/check-zig-cache.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash -# -# This script checks if the flatpak/zig-packages.json file is up-to-date. -# If the `--update` flag is passed, it will update all necessary -# files to be up to date. -# -# The files owned by this are: -# -# - flatpak/zig-packages.json -# -# All of these are auto-generated and should not be edited manually. - -# Nothing in this script should fail. -set -eu -set -o pipefail - -WORK_DIR=$(mktemp -d) - -if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then - echo "could not create temp dir" - exit 1 -fi - -function cleanup { - rm -rf "$WORK_DIR" -} - -trap cleanup EXIT - -help() { - echo "" - echo "To fix, please (manually) re-run the script from the repository root," - echo "commit, and submit a PR with the update:" - echo "" - echo " ./flatpak/build-support/check-zig-cache.sh --update" - echo " git add flatpak/zig-packages.json" - echo " git commit -m \"flatpak: update zig-packages.json\"" - echo "" -} - -# Turn Nix's base64 hashes into regular hexadecimal form -decode_hash() { - input=$1 - input=${input#sha256-} - echo "$input" | base64 -d | od -vAn -t x1 | tr -d ' \n' -} - -ROOT="$(realpath "$(dirname "$0")/../../")" -ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json" -BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json" - -if [ ! -f "${BUILD_ZIG_ZON_JSON}" ]; then - echo -e "\nERROR: build.zig.zon2json-lock missing." - help - exit 1 -fi - -if [ -f "${ZIG_PACKAGES_JSON}" ]; then - OLD_HASH=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}') -fi - -while read -r url sha256 dest; do - src_type=archive - sha256=$(decode_hash "$sha256") - git_commit= - if [[ "$url" =~ ^git\+* ]]; then - src_type=git - sha256= - url=${url#git+} - git_commit=${url##*#} - url=${url%%/\?ref*} - url=${url%%#*} - fi - - jq \ - -nec \ - --arg type "$src_type" \ - --arg url "$url" \ - --arg git_commit "$git_commit" \ - --arg dest "$dest" \ - --arg sha256 "$sha256" \ - '{ - type: $type, - url: $url, - commit: $git_commit, - dest: $dest, - sha256: $sha256, - } | with_entries(select(.value != ""))' -done < <(jq -rc 'to_entries[] | [.value.url, .value.hash, "vendor/p/\(.key)"] | @tsv' "$BUILD_ZIG_ZON_JSON") | - jq -s '.' >"$WORK_DIR/zig-packages.json" - -NEW_HASH=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}') - -if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then - echo -e "\nOK: flatpak/zig-packages.json unchanged." - exit 0 -elif [ "${1:-}" != "--update" ]; then - echo -e "\nERROR: flatpak/zig-packages.json needs to be updated." - echo "" - echo " * Old hash: ${OLD_HASH}" - echo " * New hash: ${NEW_HASH}" - help - exit 1 -else - mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON" - echo -e "\nOK: flatpak/zig-packages.json updated." - exit 0 -fi diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 3eae3c8d2..a1cab7817 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz", - "dest": "vendor/p/N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls", - "sha256": "3f249617ff4800ae0364291de4546989a225e9eb76426eadf082824f387f5dad" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz", + "dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME", + "sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27" }, { "type": "archive", diff --git a/include/ghostty.h b/include/ghostty.h index 082711836..c871dd593 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -419,6 +419,7 @@ typedef struct { ghostty_env_var_s* env_vars; size_t env_var_count; const char* initial_input; + bool wait_after_command; } ghostty_surface_config_s; typedef struct { @@ -450,6 +451,28 @@ typedef struct { ghostty_config_color_s colors[256]; } ghostty_config_palette_s; +// config.QuickTerminalSize +typedef enum { + GHOSTTY_QUICK_TERMINAL_SIZE_NONE, + GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE, + GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS, +} ghostty_quick_terminal_size_tag_e; + +typedef union { + float percentage; + uint32_t pixels; +} ghostty_quick_terminal_size_value_u; + +typedef struct { + ghostty_quick_terminal_size_tag_e tag; + ghostty_quick_terminal_size_value_u value; +} ghostty_quick_terminal_size_s; + +typedef struct { + ghostty_quick_terminal_size_s primary; + ghostty_quick_terminal_size_s secondary; +} ghostty_config_quick_terminal_size_s; + // apprt.Target.Key typedef enum { GHOSTTY_TARGET_APP, @@ -680,6 +703,12 @@ typedef struct { uintptr_t len; } ghostty_action_open_url_s; +// apprt.action.CloseTabMode +typedef enum { + GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS, + GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER, +} ghostty_action_close_tab_mode_e; + // apprt.surface.Message.ChildExited typedef struct { uint32_t exit_code; @@ -693,15 +722,15 @@ typedef enum { GHOSTTY_PROGRESS_STATE_ERROR, GHOSTTY_PROGRESS_STATE_INDETERMINATE, GHOSTTY_PROGRESS_STATE_PAUSE, -} ghostty_terminal_osc_command_progressreport_state_e; +} ghostty_action_progress_report_state_e; // terminal.osc.Command.ProgressReport.C typedef struct { - ghostty_terminal_osc_command_progressreport_state_e state; + ghostty_action_progress_report_state_e state; // -1 if no progress was reported, otherwise 0-100 indicating percent // completeness. int8_t progress; -} ghostty_terminal_osc_command_progressreport_s; +} ghostty_action_progress_report_s; // apprt.Action.Key typedef enum { @@ -786,8 +815,9 @@ typedef union { ghostty_action_reload_config_s reload_config; ghostty_action_config_change_s config_change; ghostty_action_open_url_s open_url; + ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; - ghostty_terminal_osc_command_progressreport_s progress_report; + ghostty_action_progress_report_s progress_report; } ghostty_action_u; typedef struct { diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 0c54ba693..6a6adb494 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; + A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; @@ -126,6 +127,7 @@ A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; }; A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; }; A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; }; + A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; }; A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; }; A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; @@ -252,6 +254,7 @@ A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = ""; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; @@ -637,6 +640,7 @@ CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */, A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */, A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */, + A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */, A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */, ); path = QuickTerminal; @@ -939,6 +943,10 @@ A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, + A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, + A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, @@ -980,6 +988,7 @@ A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */, A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */, A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, + A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */, A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */, A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c00025bf5..310a46d6c 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -394,35 +394,69 @@ class AppDelegate: NSObject, // Ghostty will validate as well but we can avoid creating an entirely new // surface by doing our own validation here. We can also show a useful error // this way. - + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false } - + + // Set to true if confirmation is required before starting up the + // new terminal. + var requiresConfirm: Bool = false + // Initialize the surface config which will be used to create the tab or window for the opened file. var config = Ghostty.SurfaceConfiguration() - + if (isDirectory.boolValue) { // When opening a directory, check the configuration to decide // whether to open in a new tab or new window. config.workingDirectory = filename } else { + // Unconditionally require confirmation in the file execution case. + // In the future I have ideas about making this more fine-grained if + // we can not inherit of unsandboxed state. For now, we need to confirm + // because there is a sandbox escape possible if a sandboxed application + // somehow is tricked into `open`-ing a non-sandboxed application. + requiresConfirm = true + // When opening a file, we want to execute the file. To do this, we // don't override the command directly, because it won't load the // profile/rc files for the shell, which is super important on macOS // due to things like Homebrew. Instead, we set the command to // `; exit` which is what Terminal and iTerm2 do. config.initialInput = "\(filename); exit\n" - + + // For commands executed directly, we want to ensure we wait after exit + // because in most cases scripts don't block on exit and we don't want + // the window to just flash closed once complete. + config.waitAfterCommand = true + // Set the parent directory to our working directory so that relative // paths in scripts work. config.workingDirectory = (filename as NSString).deletingLastPathComponent } + if requiresConfirm { + // Confirmation required. We use an app-wide NSAlert for now. In the future we + // may want to show this as a sheet on the focused window (especially if we're + // opening a tab). I'm not sure. + let alert = NSAlert() + alert.messageText = "Allow Ghostty to execute \"\(filename)\"?" + alert.addButton(withTitle: "Allow") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + switch (alert.runModal()) { + case .alertFirstButtonReturn: + break + + default: + return false + } + } + switch ghostty.config.macosDockDropBehavior { case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config) case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } - + return true } diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 5cd6d9bec..c97ed7c61 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 1f608f767..68b9ba337 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController { private var previousActiveSpace: CGSSpace? = nil /// The window frame saved when the quick terminal's surface tree becomes empty. - /// + /// /// This preserves the user's window size and position when all terminal surfaces /// are closed (e.g., via the `exit` command). When a new surface is created, /// the window will be restored to this frame, preventing SwiftUI from resetting @@ -34,6 +34,9 @@ class QuickTerminalController: BaseTerminalController { /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + + /// Tracks if we're currently handling a manual resize to prevent recursion + private var isHandlingResize: Bool = false init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, @@ -76,6 +79,11 @@ class QuickTerminalController: BaseTerminalController { selector: #selector(onNewTab), name: Ghostty.Notification.ghosttyNewTab, object: nil) + center.addObserver( + self, + selector: #selector(windowDidResize(_:)), + name: NSWindow.didResizeNotification, + object: nil) } required init?(coder: NSCoder) { @@ -109,7 +117,7 @@ class QuickTerminalController: BaseTerminalController { syncAppearance() // Setup our initial size based on our configured position - position.setLoaded(window) + position.setLoaded(window, size: derivedConfig.quickTerminalSize) // Upon first adding this Window to its host view, older SwiftUI // seems to have a "hiccup" and corrupts the frameRect, @@ -209,11 +217,28 @@ class QuickTerminalController: BaseTerminalController { } } - func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize { - // We use the actual screen the window is on for this, since it should - // be on the proper screen. - guard let screen = window?.screen ?? NSScreen.main else { return frameSize } - return position.restrictFrameSize(frameSize, on: screen) + override func windowDidResize(_ notification: Notification) { + guard let window = notification.object as? NSWindow, + window == self.window, + visible, + !isHandlingResize else { return } + guard let screen = window.screen ?? NSScreen.main else { return } + + // Prevent recursive loops + isHandlingResize = true + defer { isHandlingResize = false } + + switch position { + case .top, .bottom, .center: + // For centered positions (top, bottom, center), we need to recenter the window + // when it's manually resized to maintain proper positioning + let newOrigin = position.centeredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) + case .left, .right: + // For side positions, we may need to adjust vertical centering + let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen) + window.setFrameOrigin(newOrigin) + } } // MARK: Base Controller Overrides @@ -333,15 +358,17 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } + + // Grab our last closed frame to use, and clear our state since we're animating in. + let lastClosedFrame = self.lastClosedFrame + self.lastClosedFrame = nil - // Restore our previous frame if we have one - if let lastClosedFrame { - window.setFrame(lastClosedFrame, display: false) - self.lastClosedFrame = nil - } - - // Move our window off screen to the top - position.setInitial(in: window, on: screen) + // Move our window off screen to the initial animation position. + position.setInitial( + in: window, + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: lastClosedFrame) // We need to set our window level to a high value. In testing, only // popUpMenu and above do what we want. This gets it above the menu bar @@ -372,7 +399,11 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setFinal(in: window.animator(), on: screen) + position.setFinal( + in: window.animator(), + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: lastClosedFrame) }, completionHandler: { // There is a very minor delay here so waiting at least an event loop tick // keeps us safe from the view not being on the window. @@ -450,11 +481,19 @@ class QuickTerminalController: BaseTerminalController { } private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) { + // If we are in fullscreen, then we exit fullscreen. We do this immediately so + // we have th correct window.frame for the save state below. + if let fullscreenStyle, fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + } + // Save the current window frame before animating out. This preserves // the user's preferred window size and position for when the quick // terminal is reactivated with a new surface. Without this, SwiftUI // would reset the window to its minimum content size. - lastClosedFrame = window.frame + if window.frame.width > 0 && window.frame.height > 0 { + lastClosedFrame = window.frame + } // If we hid the dock then we unhide it. hiddenDock = nil @@ -470,11 +509,6 @@ class QuickTerminalController: BaseTerminalController { // We always animate out to whatever screen the window is actually on. guard let screen = window.screen ?? NSScreen.main else { return } - // If we are in fullscreen, then we exit fullscreen. - if let fullscreenStyle, fullscreenStyle.isFullscreen { - fullscreenStyle.exit() - } - // If we have a previously active application, restore focus to it. We // do this BEFORE the animation below because when the animation completes // macOS will bring forward another window. @@ -496,7 +530,11 @@ class QuickTerminalController: BaseTerminalController { NSAnimationContext.runAnimationGroup({ context in context.duration = derivedConfig.quickTerminalAnimationDuration context.timingFunction = .init(name: .easeIn) - position.setInitial(in: window.animator(), on: screen) + position.setInitial( + in: window.animator(), + on: screen, + terminalSize: derivedConfig.quickTerminalSize, + closedFrame: window.frame) }, completionHandler: { // This causes the window to be removed from the screen list and macOS // handles what should be focused next. @@ -627,6 +665,7 @@ class QuickTerminalController: BaseTerminalController { let quickTerminalAnimationDuration: Double let quickTerminalAutoHide: Bool let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior + let quickTerminalSize: QuickTerminalSize let backgroundOpacity: Double init() { @@ -634,6 +673,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalAnimationDuration = 0.2 self.quickTerminalAutoHide = true self.quickTerminalSpaceBehavior = .move + self.quickTerminalSize = QuickTerminalSize() self.backgroundOpacity = 1.0 } @@ -642,6 +682,7 @@ class QuickTerminalController: BaseTerminalController { self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration self.quickTerminalAutoHide = config.quickTerminalAutoHide self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior + self.quickTerminalSize = config.quickTerminalSize self.backgroundOpacity = config.backgroundOpacity } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift index 7ba124a30..d7660f77a 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalPosition.swift @@ -7,95 +7,86 @@ enum QuickTerminalPosition : String { case right case center - /// Set the loaded state for a window. - func setLoaded(_ window: NSWindow) { + /// Set the loaded state for a window. This should only be called when the window is first loaded, + /// usually in `windowDidLoad` or in a similar callback. This is the initial state. + func setLoaded(_ window: NSWindow, size: QuickTerminalSize) { guard let screen = window.screen ?? NSScreen.main else { return } - switch (self) { - case .top, .bottom: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width, - height: screen.frame.height / 4) - ), display: false) - - case .left, .right: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width / 4, - height: screen.frame.height) - ), display: false) - - case .center: - window.setFrame(.init( - origin: window.frame.origin, - size: .init( - width: screen.frame.width / 2, - height: screen.frame.height / 3) - ), display: false) - } + window.setFrame(.init( + origin: window.frame.origin, + size: size.calculate(position: self, screenDimensions: screen.visibleFrame.size) + ), display: false) } - /// Set the initial state for a window for animating out of this position. - func setInitial(in window: NSWindow, on screen: NSScreen) { - // We always start invisible + /// Set the initial state for a window NOT yet into position (either before animating in or + /// after animating out). + func setInitial( + in window: NSWindow, + on screen: NSScreen, + terminalSize: QuickTerminalSize, + closedFrame: NSRect? = nil + ) { + // Invisible window.alphaValue = 0 // Position depends window.setFrame(.init( origin: initialOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen) + size: closedFrame?.size ?? configuredFrameSize( + on: screen, + terminalSize: terminalSize) ), display: false) } /// Set the final state for a window in this position. - func setFinal(in window: NSWindow, on screen: NSScreen) { + func setFinal( + in window: NSWindow, + on screen: NSScreen, + terminalSize: QuickTerminalSize, + closedFrame: NSRect? = nil + ) { // We always end visible window.alphaValue = 1 // Position depends window.setFrame(.init( origin: finalOrigin(for: window, on: screen), - size: restrictFrameSize(window.frame.size, on: screen) + size: closedFrame?.size ?? configuredFrameSize( + on: screen, + terminalSize: terminalSize) ), display: true) } - /// Restrict the frame size during resizing. - func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize { - var finalSize = size - switch (self) { - case .top, .bottom: - finalSize.width = screen.frame.width - - case .left, .right: - finalSize.height = screen.visibleFrame.height - - case .center: - finalSize.width = screen.frame.width / 2 - finalSize.height = screen.frame.height / 3 - } - - return finalSize + /// Get the configured frame size for initial positioning and animations. + func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize { + let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.visibleFrame.size) + return NSSize(width: dimensions.width, height: dimensions.height) } /// The initial point origin for this position. func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.minX, y: screen.frame.maxY) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.maxY) case .bottom: - return .init(x: screen.frame.minX, y: -window.frame.height) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: -window.frame.height) case .left: - return .init(x: screen.frame.minX-window.frame.width, y: 0) + return .init( + x: screen.visibleFrame.minX-window.frame.width, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.frame.maxX, y: 0) + return .init( + x: screen.visibleFrame.maxX, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: - return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width) } } @@ -103,19 +94,27 @@ enum QuickTerminalPosition : String { func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { switch (self) { case .top: - return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.maxY - window.frame.height) case .bottom: - return .init(x: screen.frame.minX, y: screen.frame.minY) + return .init( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: screen.visibleFrame.minY) case .left: - return .init(x: screen.frame.minX, y: window.frame.origin.y) + return .init( + x: screen.visibleFrame.minX, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .right: - return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y) + return .init( + x: screen.visibleFrame.maxX - window.frame.width, + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) case .center: - return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) + return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)) } } @@ -136,4 +135,52 @@ enum QuickTerminalPosition : String { case .right: self == .top || self == .bottom } } + + /// Calculate the centered origin for a window, keeping it properly positioned after manual resizing + func centeredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { + switch self { + case .top: + return CGPoint( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: window.frame.origin.y // Keep the same Y position + ) + + case .bottom: + return CGPoint( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: window.frame.origin.y // Keep the same Y position + ) + + case .center: + return CGPoint( + x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) + ) + + case .left, .right: + // For left/right positions, only adjust horizontal centering if needed + return window.frame.origin + } + } + + /// Calculate the vertically centered origin for side-positioned windows + func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint { + switch self { + case .left: + return CGPoint( + x: window.frame.origin.x, // Keep the same X position + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) + ) + + case .right: + return CGPoint( + x: window.frame.origin.x, // Keep the same X position + y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2) + ) + + case .top, .bottom, .center: + // These positions don't need vertical recentering during resize + return window.frame.origin + } + } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift new file mode 100644 index 000000000..9f86a7c2b --- /dev/null +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalSize.swift @@ -0,0 +1,84 @@ +import GhosttyKit + +/// Represents the Ghostty `quick-terminal-size` configuration. See the documentation for +/// that for more details on exactly how it works. Some of those docs will be reproduced in various comments +/// in this file but that is the best source of truth for it. +/// +/// The size determines the size of the quick terminal along the primary and secondary axis. The primary and +/// secondary axis is defined by the `quick-terminal-position`. +struct QuickTerminalSize { + let primary: Size? + let secondary: Size? + + init(primary: Size? = nil, secondary: Size? = nil) { + self.primary = primary + self.secondary = secondary + } + + init(from cStruct: ghostty_config_quick_terminal_size_s) { + self.primary = Size(from: cStruct.primary) + self.secondary = Size(from: cStruct.secondary) + } + + enum Size { + case percentage(Float) + case pixels(UInt32) + + init?(from cStruct: ghostty_quick_terminal_size_s) { + switch cStruct.tag { + case GHOSTTY_QUICK_TERMINAL_SIZE_NONE: + return nil + case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE: + self = .percentage(cStruct.value.percentage) + case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS: + self = .pixels(cStruct.value.pixels) + default: + return nil + } + } + + func toPixels(parentDimension: CGFloat) -> CGFloat { + switch self { + case .percentage(let value): + return parentDimension * CGFloat(value) / 100.0 + case .pixels(let value): + return CGFloat(value) + } + } + } + + + /// This is an almost direct port of th Zig function QuickTerminalSize.calculate + func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize { + let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height) + + switch position { + case .left, .right: + return CGSize( + width: primary?.toPixels(parentDimension: dims.width) ?? 400, + height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height + ) + + case .top, .bottom: + return CGSize( + width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width, + height: primary?.toPixels(parentDimension: dims.height) ?? 400 + ) + + case .center: + if dims.width >= dims.height { + // Landscape + return CGSize( + width: primary?.toPixels(parentDimension: dims.width) ?? 800, + height: secondary?.toPixels(parentDimension: dims.height) ?? 400 + ) + } else { + // Portrait + return CGSize( + width: secondary?.toPixels(parentDimension: dims.width) ?? 400, + height: primary?.toPixels(parentDimension: dims.height) ?? 800 + ) + } + } + } +} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 644a0c8ac..414f38d81 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -95,6 +95,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr selector: #selector(onCloseTab), name: .ghosttyCloseTab, object: nil) + center.addObserver( + self, + selector: #selector(onCloseOtherTabs), + name: .ghosttyCloseOtherTabs, + object: nil) center.addObserver( self, selector: #selector(onResetWindowSize), @@ -559,7 +564,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeWindow(nil) } - private func closeTabImmediately() { + private func closeTabImmediately(registerRedo: Bool = true) { guard let window = window else { return } guard let tabGroup = window.tabGroup, tabGroup.windows.count > 1 else { @@ -576,19 +581,69 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr expiresAfter: undoExpiration ) { ghostty in let newController = TerminalController(ghostty, with: undoState) - - // Register redo action - undoManager.registerUndo( - withTarget: newController, - expiresAfter: newController.undoExpiration - ) { target in - target.closeTabImmediately() + + if registerRedo { + undoManager.registerUndo( + withTarget: newController, + expiresAfter: newController.undoExpiration + ) { target in + target.closeTabImmediately() + } } } } window.close() } + + private func closeOtherTabsImmediately() { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + guard tabGroup.windows.count > 1 else { return } + + // Start an undo grouping + if let undoManager { + undoManager.beginUndoGrouping() + } + defer { + undoManager?.endUndoGrouping() + } + + // Iterate through all tabs except the current one. + for window in tabGroup.windows where window != self.window { + // We ignore any non-terminal tabs. They don't currently exist and we can't + // properly undo them anyways so I'd rather ignore them and get a bug report + // later if and when we introduce non-terminal tabs. + if let controller = window.windowController as? TerminalController { + // We must not register a redo, because it messes with our own redo + // that we register later. + controller.closeTabImmediately(registerRedo: false) + } + } + + if let undoManager { + undoManager.setActionName("Close Other Tabs") + + // We need to register an undo that refocuses this window. Otherwise, the + // undo operation above for each tab will steal focus. + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration + ) { target in + DispatchQueue.main.async { + target.window?.makeKeyAndOrderFront(nil) + } + + // Register redo action + undoManager.registerUndo( + withTarget: target, + expiresAfter: target.undoExpiration + ) { target in + target.closeOtherTabsImmediately() + } + } + } + } /// Closes the current window (including any other tabs) immediately and without /// confirmation. This will setup proper undo state so the action can be undone. @@ -1023,6 +1078,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } } + @IBAction func closeOtherTabs(_ sender: Any?) { + guard let window = window else { return } + guard let tabGroup = window.tabGroup else { return } + + // If we only have one window then we have no other tabs to close + guard tabGroup.windows.count > 1 else { return } + + // Check if we have to confirm close. + guard tabGroup.windows.contains(where: { window in + // Ignore ourself + if window == self.window { return false } + + // Ignore non-terminals + guard let controller = window.windowController as? TerminalController else { + return false + } + + // Check if any surfaces require confirmation + return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) + }) else { + self.closeOtherTabsImmediately() + return + } + + confirmClose( + messageText: "Close Other Tabs?", + informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed." + ) { + self.closeOtherTabsImmediately() + } + } + @IBAction func returnToDefaultSize(_ sender: Any?) { guard let defaultSize else { return } window?.setFrame(defaultSize, display: true) @@ -1206,6 +1293,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr closeTab(self) } + @objc private func onCloseOtherTabs(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree.contains(target) else { return } + closeOtherTabs(self) + } + @objc private func onCloseWindow(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard surfaceTree.contains(target) else { return } diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index a6559600d..ff265189b 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -70,4 +70,39 @@ extension Ghostty.Action { } } } + + struct ProgressReport { + enum State { + case remove + case set + case error + case indeterminate + case pause + + init(_ c: ghostty_action_progress_report_state_e) { + switch c { + case GHOSTTY_PROGRESS_STATE_REMOVE: + self = .remove + case GHOSTTY_PROGRESS_STATE_SET: + self = .set + case GHOSTTY_PROGRESS_STATE_ERROR: + self = .error + case GHOSTTY_PROGRESS_STATE_INDETERMINATE: + self = .indeterminate + case GHOSTTY_PROGRESS_STATE_PAUSE: + self = .pause + default: + self = .remove + } + } + } + + let state: State + let progress: UInt8? + + init(c: ghostty_action_progress_report_s) { + self.state = State(c.state) + self.progress = c.progress >= 0 ? UInt8(c.progress) : nil + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index c94f40291..65979bd40 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -455,7 +455,7 @@ extension Ghostty { newSplit(app, target: target, direction: action.action.new_split) case GHOSTTY_ACTION_CLOSE_TAB: - closeTab(app, target: target) + closeTab(app, target: target, mode: action.action.close_tab_mode) case GHOSTTY_ACTION_CLOSE_WINDOW: closeWindow(app, target: target) @@ -543,6 +543,9 @@ extension Ghostty { case GHOSTTY_ACTION_KEY_SEQUENCE: keySequence(app, target: target, v: action.action.key_sequence) + + case GHOSTTY_ACTION_PROGRESS_REPORT: + progressReport(app, target: target, v: action.action.progress_report) case GHOSTTY_ACTION_CONFIG_CHANGE: configChange(app, target: target, v: action.action.config_change) @@ -778,20 +781,34 @@ extension Ghostty { } } - private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) { + private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) { switch (target.tag) { case GHOSTTY_TARGET_APP: - Ghostty.logger.warning("close tab does nothing with an app target") + Ghostty.logger.warning("close tabs does nothing with an app target") return case GHOSTTY_TARGET_SURFACE: guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } - NotificationCenter.default.post( - name: .ghosttyCloseTab, - object: surfaceView - ) + switch (mode) { + case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS: + NotificationCenter.default.post( + name: .ghosttyCloseTab, + object: surfaceView + ) + return + + case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER: + NotificationCenter.default.post( + name: .ghosttyCloseOtherTabs, + object: surfaceView + ) + return + + default: + assertionFailure() + } default: @@ -1509,6 +1526,33 @@ extension Ghostty { assertionFailure() } } + + private static func progressReport( + _ app: ghostty_app_t, + target: ghostty_target_s, + v: ghostty_action_progress_report_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("progress report does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + + let progressReport = Ghostty.Action.ProgressReport(c: v) + DispatchQueue.main.async { + if progressReport.state == .remove { + surfaceView.progressReport = nil + } else { + surfaceView.progressReport = progressReport + } + } + + default: + assertionFailure() + } + } private static func configReload( _ app: ghostty_app_t, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 6992f59f6..b106082bb 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -504,6 +504,14 @@ extension Ghostty { let str = String(cString: ptr) return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move } + + var quickTerminalSize: QuickTerminalSize { + guard let config = self.config else { return QuickTerminalSize() } + var v = ghostty_config_quick_terminal_size_s() + let key = "quick-terminal-size" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() } + return QuickTerminalSize(from: v) + } #endif var resizeOverlay: ResizeOverlay { diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 73487f1bd..85040d390 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -329,6 +329,9 @@ extension Notification.Name { /// Close tab static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") + /// Close other tabs + static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs") + /// Close window static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index aa4de5178..38efef646 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -113,6 +113,11 @@ extension Ghostty { } } .ghosttySurfaceView(surfaceView) + + // Progress report overlay + if let progressReport = surfaceView.progressReport { + ProgressReportOverlay(report: progressReport) + } #if canImport(AppKit) // If we are in the middle of a key sequence, then we show a visual element. We only @@ -267,6 +272,49 @@ extension Ghostty { } } + // Progress report overlay that shows a progress bar at the top of the terminal + struct ProgressReportOverlay: View { + let report: Action.ProgressReport + + @ViewBuilder + private var progressBar: some View { + if let progress = report.progress { + // Determinate progress bar + ProgressView(value: Double(progress), total: 100) + .progressViewStyle(.linear) + .tint(report.state == .error ? .red : report.state == .pause ? .orange : nil) + .animation(.easeInOut(duration: 0.2), value: progress) + } else { + // Indeterminate states + switch report.state { + case .indeterminate: + ProgressView() + .progressViewStyle(.linear) + case .error: + ProgressView() + .progressViewStyle(.linear) + .tint(.red) + case .pause: + Rectangle().fill(Color.orange) + default: + EmptyView() + } + } + } + + var body: some View { + VStack(spacing: 0) { + progressBar + .scaleEffect(x: 1, y: 0.5, anchor: .center) + .frame(height: 2) + + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .allowsHitTesting(false) + } + } + // This is the resize overlay that shows on top of a surface to show the current // size during a resize operation. struct SurfaceResizeOverlay: View { @@ -424,6 +472,9 @@ extension Ghostty { /// Extra input to send as stdin var initialInput: String? = nil + + /// Wait after the command + var waitAfterCommand: Bool = false init() {} @@ -475,6 +526,9 @@ extension Ghostty { // Zero is our default value that means to inherit the font size. config.font_size = fontSize ?? 0 + + // Set wait after command + config.wait_after_command = waitAfterCommand // Use withCString to ensure strings remain valid for the duration of the closure return try workingDirectory.withCString { cWorkingDir in diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 97637e737..2d83a8a6b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -41,6 +41,23 @@ extension Ghostty { // The hovered URL string @Published var hoverUrl: String? = nil + + // The progress report (if any) + @Published var progressReport: Action.ProgressReport? = nil { + didSet { + // Cancel any existing timer + progressReportTimer?.invalidate() + progressReportTimer = nil + + // If we have a new progress report, start a timer to remove it after 15 seconds + if progressReport != nil { + progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in + self?.progressReport = nil + self?.progressReportTimer = nil + } + } + } + } // The currently active key sequence. The sequence is not active if this is empty. @Published var keySequence: [KeyboardShortcut] = [] @@ -142,6 +159,9 @@ extension Ghostty { // A timer to fallback to ghost emoji if no title is set within the grace period private var titleFallbackTimer: Timer? + + // Timer to remove progress report after 15 seconds + private var progressReportTimer: Timer? // This is the title from the terminal. This is nil if we're currently using // the terminal title as the main title property. If the title is set manually @@ -348,6 +368,9 @@ extension Ghostty { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) + + // Cancel progress report timer + progressReportTimer?.invalidate() } func focusDidChange(_ focused: Bool) { diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index e88ec82e2..29364d4a5 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -30,6 +30,9 @@ extension Ghostty { // The hovered URL @Published var hoverUrl: String? = nil + + // The progress report (if any) + @Published var progressReport: Action.ProgressReport? = nil // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index f3940a9aa..6c70e8cf7 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -407,8 +407,14 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.styleMask = window.styleMask self.toolbar = window.toolbar self.toolbarStyle = window.toolbarStyle - self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false + + self.titlebarAccessoryViewControllers = if (window.hasTitleBar) { + // Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash. + window.titlebarAccessoryViewControllers + } else { + [] + } if let cgWindowId = window.cgWindowId { // We hide the menu only if this window is not on any fullscreen diff --git a/nix/build-support/check-zig-cache.sh b/nix/build-support/check-zig-cache.sh index 49997ac1a..33e57e790 100755 --- a/nix/build-support/check-zig-cache.sh +++ b/nix/build-support/check-zig-cache.sh @@ -9,6 +9,7 @@ # - build.zig.zon.nix # - build.zig.zon.txt # - build.zig.zon.json +# - flatpak/zig-packages.json # # All of these are auto-generated and should not be edited manually. @@ -34,8 +35,8 @@ help() { echo "commit, and submit a PR with the update:" echo "" echo " ./nix/build-support/check-zig-cache-hash.sh --update" - echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json" - echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json\"" + echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json" + echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json\"" echo "" } @@ -44,6 +45,7 @@ BUILD_ZIG_ZON="$ROOT/build.zig.zon" BUILD_ZIG_ZON_NIX="$ROOT/build.zig.zon.nix" BUILD_ZIG_ZON_TXT="$ROOT/build.zig.zon.txt" BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json" +ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json" if [ -f "${BUILD_ZIG_ZON_NIX}" ]; then OLD_HASH_NIX=$(sha512sum "${BUILD_ZIG_ZON_NIX}" | awk '{print $1}') @@ -69,27 +71,40 @@ elif [ "$1" != "--update" ]; then exit 1 fi -zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" +if [ -f "${ZIG_PACKAGES_JSON}" ]; then + OLD_HASH_FLATPAK=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}') +elif [ "$1" != "--update" ]; then + echo -e "\nERROR: flatpak/zig-packages.json missing." + help + exit 1 +fi + +zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json" alejandra --quiet "$WORK_DIR/build.zig.zon.nix" -prettier --write "$WORK_DIR/build.zig.zon.json" +prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json" +prettier --log-level warn --write "$WORK_DIR/zig-packages.json" NEW_HASH_NIX=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}') NEW_HASH_TXT=$(sha512sum "$WORK_DIR/build.zig.zon.txt" | awk '{print $1}') NEW_HASH_JSON=$(sha512sum "$WORK_DIR/build.zig.zon.json" | awk '{print $1}') +NEW_HASH_FLATPAK=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}') -if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ]; then +if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ] && [ "${OLD_HASH_FLATPAK}" == "${NEW_HASH_FLATPAK}" ]; then echo -e "\nOK: build.zig.zon.nix unchanged." echo -e "OK: build.zig.zon.txt unchanged." echo -e "OK: build.zig.zon.json unchanged." + echo -e "OK: flatpak/zig-packages.json unchanged." exit 0 elif [ "$1" != "--update" ]; then echo -e "\nERROR: build.zig.zon.nix, build.zig.zon.txt, or build.zig.zon.json needs to be updated.\n" - echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}" - echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}" - echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}" - echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}" - echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}" - echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}" + echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}" + echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}" + echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}" + echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}" + echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}" + echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}" + echo " * Old flatpak/zig-packages.json hash: ${OLD_HASH_FLATPAK}" + echo " * New flatpak/zig-packages.json hash: ${NEW_HASH_FLATPAK}" help exit 1 else @@ -99,6 +114,8 @@ else echo -e "OK: build.zig.zon.txt updated." mv "$WORK_DIR/build.zig.zon.json" "$BUILD_ZIG_ZON_JSON" echo -e "OK: build.zig.zon.json updated." + mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON" + echo -e "OK: flatpak/zig-packages.json updated." exit 0 fi diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 68aa63595..47954124d 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # Damyan Bogoev , 2025. +# reo101 , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-05-19 11:34+0300\n" -"Last-Translator: Damyan Bogoev \n" +"PO-Revision-Date: 2025-08-22 14:52+0300\n" +"Last-Translator: reo101 \n" "Language-Team: Bulgarian \n" "Language: bg\n" "MIME-Version: 1.0\n" @@ -208,12 +209,12 @@ msgstr "Позволи" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запомни избора за това разделяне" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "За да покажеш това съобщение отново, презареди конфигурацията" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Копирано в клипборда" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Клипбордът е изчистен" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Командата завърши успешно" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Командата завърши неуспешно" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index c2c969a66..a776284ac 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Francesc Arpi , 2025. +# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-20 08:07+0100\n" -"Last-Translator: Francesc Arpi \n" +"PO-Revision-Date: 2025-08-24 19:22+0200\n" +"Last-Translator: Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>\n" "Language-Team: \n" "Language: ca\n" "MIME-Version: 1.0\n" @@ -87,7 +88,7 @@ msgstr "Divideix a la dreta" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Executa una ordre…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +161,7 @@ msgstr "Obre la configuració" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandes" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +209,12 @@ msgstr "Permet" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Recorda l’opció per a aquest panell dividit" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recarrega la configuració per tornar a mostrar aquest missatge" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Copiat al porta-retalls" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Porta-retalls netejat" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comanda completada amb èxit" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comanda fallida" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +299,7 @@ msgstr "Mostra les pestanyes obertes" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nova divisió" #: src/apprt/gtk/Window.zig:329 msgid "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 153c298c0..c7fc6643f 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-06 14:57+0100\n" +"PO-Revision-Date: 2025-08-25 19:38+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" "Language: de\n" @@ -39,7 +39,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "" +msgstr "Konfigurationsfehler" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 @@ -47,11 +47,14 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" +"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe " +"die untenstehenden Fehler und lade entweder deine Konfiguration erneut oder " +"ignoriere die Fehler." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" -msgstr "" +msgstr "Ignorieren" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 @@ -86,7 +89,7 @@ msgstr "Fenster nach rechts teilen" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Einen Befehl ausführen…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -159,7 +162,7 @@ msgstr "Konfiguration öffnen" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Befehlspalette" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -207,12 +210,13 @@ msgstr "Erlauben" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Auswahl für dieses geteilte Fenster beibehalten" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" msgstr "" +"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -277,15 +281,15 @@ msgstr "In die Zwischenablage kopiert" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Zwischenablage geleert" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Befehl erfolgreich" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Befehl fehlgeschlagen" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -297,7 +301,7 @@ msgstr "Offene Tabs einblenden" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Neues geteiltes Fenster" #: src/apprt/gtk/Window.zig:329 msgid "" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index a1b8f75cd..50fcb14e2 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-28 17:46+0200\n" +"PO-Revision-Date: 2025-08-23 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" "Language: es_BO\n" @@ -87,7 +87,7 @@ msgstr "Dividir a la derecha" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Ejecutar comando..." #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +160,7 @@ msgstr "Abrir configuración" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandos" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +208,12 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Recordar su elección para esta división de ventana" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recargar configuración para mostrar este aviso nuevamente" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "El portapapeles está limpio" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comando ejecutado con éxito" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comando fallido" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +298,7 @@ msgstr "Ver pestañas abiertas" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nueva ventana dividida" #: src/apprt/gtk/Window.zig:329 msgid "" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 59959bd49..4c520dd7c 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-22 09:31+0100\n" +"PO-Revision-Date: 2025-08-23 21:01+0200\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" "Language: fr\n" @@ -88,7 +88,7 @@ msgstr "Panneau à droite" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Exécuter une commande…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -161,7 +161,7 @@ msgstr "Ouvrir la configuration" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Palette de commandes" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -209,12 +209,12 @@ msgstr "Autoriser" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Se rappeler du choix pour ce panneau" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recharger la configuration pour afficher à nouveau ce message" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -279,15 +279,15 @@ msgstr "Copié dans le presse-papiers" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Presse-papiers vidé" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Commande réussie" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "La commande a échoué" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -299,7 +299,7 @@ msgstr "Voir les onglets ouverts" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Nouveau panneau" #: src/apprt/gtk/Window.zig:329 msgid "" diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 0771ecbcd..9395d2dec 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-06-29 21:15+0100\n" +"PO-Revision-Date: 2025-08-26 15:46+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" -"X-Generator: Poedit 3.4.4\n" +"X-Generator: Poedit 3.4.2\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -209,12 +209,12 @@ msgstr "Ceadaigh" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Sábháil an rogha don scoilt seo" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -280,15 +280,15 @@ msgstr "Cóipeáilte chuig an ghearrthaisce" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Gearrthaisce glanta" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "D'éirigh leis an ordú" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Theip ar an ordú" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po index 08ffb1b36..4ddeb9584 100644 --- a/po/he_IL.UTF-8.po +++ b/po/he_IL.UTF-8.po @@ -2,15 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # Sl (Shahaf Levi), Sl's Repository Ltd , 2025. +# CraziestOwl , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-13 00:00+0000\n" -"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" +"PO-Revision-Date: 2025-08-23 08:00+0300\n" +"Last-Translator: CraziestOwl \n" "Language-Team: Hebrew \n" "Language: he\n" "MIME-Version: 1.0\n" @@ -276,15 +276,15 @@ msgstr "הועתק ללוח ההעתקה" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "לוח ההעתקה רוקן" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "הפקודה הצליחה" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "הפקודה נכשלה" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/hu_HU.UTF-8.po b/po/hu_HU.UTF-8.po new file mode 100644 index 000000000..2ad48eadb --- /dev/null +++ b/po/hu_HU.UTF-8.po @@ -0,0 +1,320 @@ +# Hungarian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Balázs Szücs , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-07-22 17:18+0000\n" +"PO-Revision-Date: 2025-08-23 17:14+0200\n" +"Last-Translator: Balázs Szücs \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Terminál címének módosítása" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Hagyja üresen az alapértelmezett cím visszaállításához." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Mégse" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Rendben" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Konfigurációs hibák" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Egy vagy több konfigurációs hiba található. Kérjük, ellenőrizze az alábbi " +"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a " +"hibákat." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Figyelmen kívül hagyás" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Konfiguráció frissítése" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Felosztás felfelé" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Felosztás lefelé" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Felosztás balra" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Felosztás jobbra" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Parancs végrehajtása…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Másolás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Beillesztés" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Törlés" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Visszaállítás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Felosztás" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Cím módosítása…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Fül" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:265 +msgid "New Tab" +msgstr "Új fül" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Fül bezárása" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Ablak" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Új ablak" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Ablak bezárása" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Konfiguráció" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Konfiguráció megnyitása" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Parancspaletta" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Terminálvizsgáló" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "A Ghostty névjegye" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Kilépés" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Vágólap-hozzáférés engedélyezése" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma " +"lent látható." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Elutasítás" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Engedélyezés" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Választás megjegyzése erre a felosztásra" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent " +"látható." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Figyelem: potenciálisan veszélyes beillesztés" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel " +"néhány parancs végrehajtásra kerülhet." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Bezárás" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Kilép a Ghostty-ból?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Ablak bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fül bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Felosztás bezárása?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Ebben az ablakban minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Ezen a fülön minden terminál munkamenet lezárul." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Vágólapra másolva" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Vágólap törölve" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Parancs sikeres" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Parancs sikertelen" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Főmenü" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Megnyitott fülek megtekintése" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Új felosztás" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ A Ghostty hibakereső verzióját futtatja! A teljesítmény csökkenni fog." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfiguráció frissítve" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty fejlesztők" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminálvizsgáló" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index d07837ee0..0bb4aea40 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Andrej Daskalov , 2025. +# Marija Gjorgjieva Gjondeva , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-23 14:17+0100\n" -"Last-Translator: Andrej Daskalov \n" +"PO-Revision-Date: 2025-08-25 22:17+0200\n" +"Last-Translator: Marija Gjorgjieva Gjondeva \n" "Language-Team: Macedonian\n" "Language: mk\n" "MIME-Version: 1.0\n" @@ -87,7 +88,7 @@ msgstr "Подели надесно" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Изврши команда…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -160,7 +161,7 @@ msgstr "Отвори конфигурација" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Командна палета" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -208,12 +209,12 @@ msgstr "Дозволи" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запомни го изборот за оваа поделба" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Одново вчитај конфигурација за да се повторно прикаже пораката" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -278,15 +279,15 @@ msgstr "Копирано во привремена меморија" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Исчистена привремена меморија" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Командата успеа" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Командата не успеа" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -298,7 +299,7 @@ msgstr "Прегледај отворени јазичиња" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Нова поделба" #: src/apprt/gtk/Window.zig:329 msgid "" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 67c436e46..047736470 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -1,7 +1,7 @@ # Norwegian Bokmal translations for com.mitchellh.ghostty package. # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. -# Hanna Rose , 2025. +# Hanna Rose , 2025. # Uzair Aftab , 2025. # Christoffer Tønnessen , 2025. # cryptocode , 2025. @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-04-14 16:25+0200\n" -"Last-Translator: cryptocode \n" +"PO-Revision-Date: 2025-08-23 12:52+0000\n" +"Last-Translator: Hanna Rose \n" "Language-Team: Norwegian Bokmal \n" "Language: nb\n" "MIME-Version: 1.0\n" @@ -90,7 +90,7 @@ msgstr "Del til høyre" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Kjør en kommando..." #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -163,7 +163,7 @@ msgstr "Åpne konfigurasjon" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Kommandopalett" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -211,12 +211,12 @@ msgstr "Tillat" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Husk valget for dette delte vinduet?" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Last inn konfigurasjonen på nytt for å vise denne meldingen igjen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -281,15 +281,15 @@ msgstr "Kopiert til utklippstavlen" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Utklippstavle tømt" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Kommando lyktes" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Kommando mislyktes" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index d49ab9df7..1b7162f90 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -3,14 +3,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Gustavo Peres , 2025. +# Guilherme Tiscoski , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-06-20 10:19-0300\n" -"Last-Translator: Mário Victor Ribeiro Silva \n" +"PO-Revision-Date: 2025-08-25 11:46-0500\n" +"Last-Translator: Guilherme Tiscoski \n" "Language-Team: Brazilian Portuguese \n" "Language: pt_BR\n" @@ -89,7 +90,7 @@ msgstr "Dividir à direita" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Executar um comando…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -162,7 +163,7 @@ msgstr "Abrir configuração" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Paleta de comandos" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -210,12 +211,12 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Lembrar escolha para esta divisão" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Recarregue a configuração para mostrar este aviso novamente" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -280,15 +281,15 @@ msgstr "Copiado para a área de transferência" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Área de transferência limpa" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Comando executado com sucesso" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Comando falhou" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index ab6c0554f..9e85484f5 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-24 22:01+0300\n" +"PO-Revision-Date: 2025-08-23 17:30+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" "Language: tr\n" @@ -88,7 +88,7 @@ msgstr "Sağa Doğru Böl" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Bir komut çalıştır…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -161,7 +161,7 @@ msgstr "Yapılandırmayı Aç" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Komut Paleti" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -209,12 +209,12 @@ msgstr "İzin Ver" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Bu bölme için tercihi anımsa" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -279,15 +279,15 @@ msgstr "Panoya kopyalandı" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Pano temizlendi" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Komut başarılı oldu" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Komut başarısız oldu" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index c5fe465b1..9d9c58b6e 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -2,14 +2,15 @@ # Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Danylo Zalizchuk , 2025. +# Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>, 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-16 20:16+0200\n" -"Last-Translator: Danylo Zalizchuk \n" +"PO-Revision-Date: 2025-08-25 19:59+0100\n" +"Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n" "Language-Team: Ukrainian \n" "Language: uk\n" "MIME-Version: 1.0\n" @@ -20,17 +21,17 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Змінити назву терміналу" +msgstr "Змінити заголовок терміналу" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." -msgstr "Залиште порожнім, щоб відновити назву за замовчуванням." +msgstr "Залиште порожнім, щоб відновити заголовок за замовчуванням." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 #: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" -msgstr "Відмінити" +msgstr "Скасувати" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 msgid "OK" @@ -39,7 +40,7 @@ msgstr "ОК" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Помилки конфігурації" +msgstr "Помилки налаштування" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 @@ -47,9 +48,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Виявлено одну або декілька помилок у конфігурації. Будь ласка, перегляньте " -"наведені нижче помилки і або перезавантажте конфігурацію, або проігноруйте " -"ці помилки." +"Виявлено одну або декілька помилок налаштування. Будь ласка, перегляньте " +"помилки нижче і або перезавантажте налаштування, або проігноруйте ці помилки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 @@ -61,35 +61,35 @@ msgstr "Ігнорувати" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" -msgstr "Перезавантажити конфігурацію" +msgstr "Перезавантажити налаштування" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 msgid "Split Up" -msgstr "Розділити панель вгору" +msgstr "Нова панель зверху" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 msgid "Split Down" -msgstr "Розділити панель вниз" +msgstr "Нова панель знизу" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 msgid "Split Left" -msgstr "Розділити панель ліворуч" +msgstr "Нова панель ліворуч" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 msgid "Split Right" -msgstr "Розділити панель праворуч" +msgstr "Нова панель праворуч" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "" +msgstr "Виконати команду…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -115,7 +115,7 @@ msgstr "Скинути" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 msgid "Split" -msgstr "Розділена панель" +msgstr "Панель" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 @@ -153,16 +153,16 @@ msgstr "Закрити вікно" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" -msgstr "Конфігурація" +msgstr "Налаштування" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" -msgstr "Відкрити конфігурацію" +msgstr "Відкрити налаштування" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "Палітра команд" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -182,7 +182,7 @@ msgstr "Завершити" #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" -msgstr "Дозволити доступ до буфера обміну" +msgstr "Надати доступ до буфера обміну" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 @@ -190,15 +190,15 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Програма намагається прочитати дані з буфера обміну. Нижче показано поточний " -"вміст буфера обміну." +"Програма намагається прочитати дані з буфера обміну. Нижче наведено вміст " +"буфера обміну." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 #: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" -msgstr "Відхилити" +msgstr "Заборонити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 @@ -210,12 +210,12 @@ msgstr "Дозволити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "Запамʼятати для цієї панелі" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "Перезавантажте налаштування, щоб показати це повідомлення знову" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -223,8 +223,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Програма намагається записати дані до буфера обміну. Нижче показано поточний " -"вміст буфера обміну." +"Програма намагається записати дані до буфера обміну. Нижче наведено вміст " +"буфера обміну." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" @@ -235,8 +235,8 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає " -"так, ніби деякі команди можуть бути виконані." +"Вставка цього тексту в термінал може бути небезпечною, бо схоже, що деякі " +"команди можуть бути виконані." #: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 msgid "Close" @@ -256,7 +256,7 @@ msgstr "Закрити вкладку?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Закрити розділену панель?" +msgstr "Закрити панель?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -272,24 +272,23 @@ msgstr "Всі сесії терміналу в цій вкладці будут #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "" -"Поточний процес, що виконується в цій розділеній панелі, буде завершено." +msgstr "Процес, що виконується в цій панелі, буде завершено." #: src/apprt/gtk/Surface.zig:1266 msgid "Copied to clipboard" -msgstr "Скопійовано в буфер обміну" +msgstr "Скопійовано до буферa обміну" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "Буфер обміну очищено" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "Команда завершилась успішно" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "Команда завершилась з помилкою" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -301,7 +300,7 @@ msgstr "Переглянути відкриті вкладки" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "Нова панель" #: src/apprt/gtk/Window.zig:329 msgid "" @@ -311,7 +310,7 @@ msgstr "" #: src/apprt/gtk/Window.zig:775 msgid "Reloaded the configuration" -msgstr "Конфігурацію перезавантажено" +msgstr "Налаштування перезавантажено" #: src/apprt/gtk/Window.zig:1019 msgid "Ghostty Developers" diff --git a/src/Surface.zig b/src/Surface.zig index 770c2daef..330d25102 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -272,6 +272,7 @@ const DerivedConfig = struct { title_report: bool, links: []Link, link_previews: configpkg.LinkPreviews, + scroll_to_bottom: configpkg.Config.ScrollToBottom, const Link = struct { regex: oni.Regex, @@ -340,6 +341,7 @@ const DerivedConfig = struct { .title_report = config.@"title-report", .links = links, .link_previews = config.@"link-previews", + .scroll_to_bottom = config.@"scroll-to-bottom", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -2280,7 +2282,8 @@ pub fn keyCallback( try self.setSelection(null); } - try self.io.terminal.scrollViewport(.{ .bottom = {} }); + if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom); + try self.queueRender(); } @@ -2766,8 +2769,21 @@ pub fn scrollCallback( // that a wheel tick of 1 results in single scroll event. const yoff_adjusted: f64 = if (scroll_mods.precision) yoff - else - yoff * cell_size * self.config.mouse_scroll_multiplier; + else yoff_adjusted: { + // Round out the yoff to an absolute minimum of 1. macos tries to + // simulate precision scrolling with non precision events by + // ramping up the magnitude of the offsets as it detects faster + // scrolling. Single click (very slow) scrolls are reported with a + // magnitude of 0.1 which would normally require a few clicks + // before we register an actual scroll event (depending on cell + // height and the mouse_scroll_multiplier setting). + const yoff_max: f64 = if (yoff > 0) + @max(yoff, 1) + else + @min(yoff, -1); + + break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier; + }; // Add our previously saved pending amount to the offset to get the // new offset value. The signs of the pending and yoff should match @@ -4701,10 +4717,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), - .close_tab => return try self.rt_app.performAction( + .close_tab => |v| return try self.rt_app.performAction( .{ .surface = self }, .close_tab, - {}, + switch (v) { + .this => .this, + .other => .other, + }, ), inline .previous_tab, diff --git a/src/apprt.zig b/src/apprt.zig index 6c1f040ea..2e3a722a6 100644 --- a/src/apprt.zig +++ b/src/apprt.zig @@ -70,13 +70,15 @@ pub const Runtime = enum { gtk, pub fn default(target: std.Target) Runtime { - // The Linux default is GTK because it is a full featured application. - if (target.os.tag == .linux) return .@"gtk-ng"; - - // Otherwise, we do NONE so we don't create an exe and we - // create libghostty. On macOS, Xcode is used to build the app - // that links to libghostty. - return .none; + return switch (target.os.tag) { + // The Linux and FreeBSD default is GTK because it is a full + // featured application. + .linux, .freebsd => .@"gtk-ng", + // Otherwise, we do NONE so we don't create an exe and we create + // libghostty. On macOS, Xcode is used to build the app that links + // to libghostty. + else => .none, + }; } }; diff --git a/src/apprt/action.zig b/src/apprt/action.zig index d2d444c3a..a41a4627f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -83,8 +83,9 @@ pub const Action = union(Key) { /// the tab should be opened in a new window. new_tab, - /// Closes the tab belonging to the currently focused split. - close_tab, + /// Closes the tab belonging to the currently focused split, or all other + /// tabs, depending on the mode. + close_tab: CloseTabMode, /// Create a new split. The value determines the location of the split /// relative to the target. @@ -701,3 +702,11 @@ pub const OpenUrl = struct { }; } }; + +/// sync with ghostty_action_close_tab_mode_e in ghostty.h +pub const CloseTabMode = enum(c_int) { + /// Close the current tab. + this, + /// Close all other tabs. + other, +}; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index bd1ffd460..e4961ac49 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -447,6 +447,9 @@ pub const Surface = struct { /// Input to send to the command after it is started. initial_input: ?[*:0]const u8 = null, + + /// Wait after the command exits + wait_after_command: bool = false, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -540,6 +543,11 @@ pub const Surface = struct { ); } + // Wait after command + if (opts.wait_after_command) { + config.@"wait-after-command" = true; + } + // Initialize our surface right away. We're given a view that is // ready to use. try self.core_surface.init( diff --git a/src/apprt/gtk-ng/class/application.zig b/src/apprt/gtk-ng/class/application.zig index 29a124798..f0fda2680 100644 --- a/src/apprt/gtk-ng/class/application.zig +++ b/src/apprt/gtk-ng/class/application.zig @@ -116,6 +116,11 @@ pub const Application = extern struct { /// and initialization was successful. transient_cgroup_base: ?[]const u8 = null, + /// This is set to true so long as we request a window exactly + /// once. This prevents quitting the app before we've shown one + /// window. + requested_window: bool = false, + /// This is set to false internally when the event loop /// should exit and the application should quit. This must /// only be set by the main loop thread. @@ -461,7 +466,13 @@ pub const Application = extern struct { // If the quit timer has expired, quit. if (priv.quit_timer == .expired) break :q true; - // There's no quit timer running, or it hasn't expired, don't quit. + // If we have no windows attached to our app, also quit. + if (priv.requested_window and @as( + ?*glib.List, + self.as(gtk.Application).getWindows(), + ) == null) break :q true; + + // No quit conditions met break :q false; }; @@ -488,7 +499,15 @@ pub const Application = extern struct { const parent: ?*gtk.Widget = parent: { const list = gtk.Window.listToplevels(); defer list.free(); - const focused = list.findCustom(null, findActiveWindow); + const focused = @as(?*glib.List, list.findCustom( + null, + findActiveWindow, + )) orelse { + // If we have an active surface then we should have + // a window available but in the rare case we don't we + // should exit so we don't crash. + break :parent null; + }; break :parent @ptrCast(@alignCast(focused.f_data)); }; @@ -542,7 +561,7 @@ pub const Application = extern struct { value: apprt.Action.Value(action), ) !bool { switch (action) { - .close_tab => return Action.closeTab(target), + .close_tab => return Action.closeTab(target, value), .close_window => return Action.closeWindow(target), .config_change => try Action.configChange( @@ -713,27 +732,24 @@ pub const Application = extern struct { } } - fn loadRuntimeCss( - self: *Self, - ) Allocator.Error!void { + fn loadRuntimeCss(self: *Self) Allocator.Error!void { const alloc = self.allocator(); - var buf: std.ArrayListUnmanaged(u8) = .empty; + const config = self.private().config.get(); + + var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048); defer buf.deinit(alloc); const writer = buf.writer(alloc); - const config = self.private().config.get(); - const window_theme = config.@"window-theme"; const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background; - const headerbar_background = config.@"window-titlebar-background" orelse config.background; - const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; try writer.print( \\widget.unfocused-split {{ \\ opacity: {d:.2}; \\ background-color: rgb({d},{d},{d}); \\}} + \\ , .{ 1.0 - config.@"unfocused-split-opacity", unfocused_fill.r, @@ -747,6 +763,7 @@ pub const Application = extern struct { \\ color: rgb({[r]d},{[g]d},{[b]d}); \\ background: rgb({[r]d},{[g]d},{[b]d}); \\}} + \\ , .{ .r = color.r, .g = color.g, @@ -759,9 +776,129 @@ pub const Application = extern struct { \\.window headerbar {{ \\ font-family: "{[font_family]s}"; \\}} + \\ , .{ .font_family = font_family }); } + try loadRuntimeCss414(config, &writer); + try loadRuntimeCss416(config, &writer); + + // ensure that we have a sentinel + try writer.writeByte(0); + + const data = buf.items[0 .. buf.items.len - 1 :0]; + + log.debug("runtime CSS is {d} bytes", .{data.len + 1}); + + // Clears any previously loaded CSS from this provider + loadCssProviderFromData( + self.private().css_provider, + data, + ); + } + + /// Load runtime CSS for older than GTK 4.16 + fn loadRuntimeCss414( + config: *const CoreConfig, + writer: *const std.ArrayListUnmanaged(u8).Writer, + ) Allocator.Error!void { + if (gtk_version.runtimeAtLeast(4, 16, 0)) return; + + const window_theme = config.@"window-theme"; + const headerbar_background = config.@"window-titlebar-background" orelse config.background; + const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; + + switch (window_theme) { + .ghostty => try writer.print( + \\windowhandle {{ + \\ background-color: rgb({d},{d},{d}); + \\ color: rgb({d},{d},{d}); + \\}} + \\windowhandle:backdrop {{ + \\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha); + \\}} + \\ + , .{ + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + headerbar_foreground.r, + headerbar_foreground.g, + headerbar_foreground.b, + headerbar_background.r, + headerbar_background.g, + headerbar_background.b, + }), + else => {}, + } + } + + /// Load runtime for GTK 4.16 and newer + fn loadRuntimeCss416( + config: *const CoreConfig, + writer: *const std.ArrayListUnmanaged(u8).Writer, + ) Allocator.Error!void { + if (gtk_version.runtimeUntil(4, 16, 0)) return; + + const window_theme = config.@"window-theme"; + const headerbar_background = config.@"window-titlebar-background" orelse config.background; + const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground; + + try writer.writeAll( + \\/* + \\ * Child Exited Overlay + \\ */ + \\ + \\.child-exited.normal revealer widget { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--success-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\.child-exited.abnormal revealer widget { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--error-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\/* + \\ * Surface + \\ */ + \\ + \\.surface progressbar.error trough progress { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--error-bg-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\.surface .bell-overlay { + \\ border-color: color-mix( + \\ in srgb, + \\ var(--accent-color), + \\ transparent 50% + \\ ); + \\} + \\ + \\/* + \\ * Splits + \\ */ + \\ + \\.window .split paned > separator { + \\ background-color: color-mix( + \\ in srgb, + \\ var(--window-bg-color), + \\ transparent 0% + \\ ); + \\} + \\ + ); + switch (window_theme) { .ghostty => try writer.print( \\:root {{ @@ -794,15 +931,6 @@ pub const Application = extern struct { }), else => {}, } - - const data = try alloc.dupeZ(u8, buf.items); - defer alloc.free(data); - - // Clears any previously loaded CSS from this provider - loadCssProviderFromData( - self.private().css_provider, - data, - ); } fn loadCustomCss(self: *Self) !void { @@ -872,7 +1000,8 @@ pub const Application = extern struct { self.syncActionAccelerator("win.close", .{ .close_window = {} }); self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this }); + self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this }); self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); @@ -1576,12 +1705,16 @@ pub const Application = extern struct { /// All apprt action handlers const Action = struct { - pub fn closeTab(target: apprt.Target) bool { + pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool { switch (target) { .app => return false, .surface => |core| { const surface = core.rt_surface.surface; - return surface.as(gtk.Widget).activateAction("tab.close", null) != 0; + return surface.as(gtk.Widget).activateAction( + "tab.close", + glib.ext.VariantType.stringFor([:0]const u8), + @as([*:0]const u8, @tagName(value)), + ) != 0; }, } } @@ -1853,6 +1986,13 @@ const Action = struct { self: *Application, parent: ?*CoreSurface, ) !void { + // Note that we've requested a window at least once. This is used + // to trigger quit on no windows. Note I'm not sure if this is REALLY + // necessary, but I don't want to risk a bug where on a slow machine + // or something we quit immediately after starting up because there + // was a delay in the event loop before we created a Window. + self.private().requested_window = true; + const win = Window.new(self); initAndShowWindow(self, win, parent); } diff --git a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig index 3debafbb5..e806eb354 100644 --- a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig @@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; const Dialog = @import("dialog.zig").Dialog; -const log = std.log.scoped(.gtk_ghostty_config_errors_dialog); +const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog); pub const CloseConfirmationDialog = extern struct { const Self = @This(); diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 2debff93b..25ee1f94f 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -105,6 +105,24 @@ pub const Surface = extern struct { ); }; + pub const @"error" = struct { + pub const name = "error"; + const impl = gobject.ext.defineProperty( + name, + Self, + bool, + .{ + .default = false, + .accessor = gobject.ext.privateFieldAccessor( + Self, + Private, + &Private.offset, + "error", + ), + }, + ); + }; + pub const @"font-size-request" = struct { pub const name = "font-size-request"; const impl = gobject.ext.defineProperty( @@ -472,6 +490,12 @@ pub const Surface = extern struct { // false by a parent widget. bell_ringing: bool = false, + /// True if this surface is in an error state. This is currently + /// a simple boolean with no additional information on WHAT the + /// error state is, because we don't yet need it or use it. For now, + /// if this is true, then it means the terminal is non-functional. + @"error": bool = false, + /// A weak reference to an inspector window. inspector: ?*InspectorWindow = null, @@ -571,6 +595,17 @@ pub const Surface = extern struct { return @intFromBool(config.@"bell-features".border); } + fn closureStackChildName( + _: *Self, + error_: c_int, + ) callconv(.c) ?[*:0]const u8 { + const err = error_ != 0; + return if (err) + glib.ext.dupeZ(u8, "error") + else + glib.ext.dupeZ(u8, "terminal"); + } + pub fn toggleFullscreen(self: *Self) void { signals.@"toggle-fullscreen".impl.emit( self, @@ -1540,6 +1575,12 @@ pub const Surface = extern struct { self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec); } + pub fn setError(self: *Self, v: bool) void { + const priv = self.private(); + priv.@"error" = v; + self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec); + } + fn propConfig( self: *Self, _: *gobject.ParamSpec, @@ -1592,6 +1633,28 @@ pub const Surface = extern struct { } } + fn propError( + self: *Self, + _: *gobject.ParamSpec, + _: ?*anyopaque, + ) callconv(.c) void { + const priv = self.private(); + if (priv.@"error") { + // Ensure we have an opaque background. The window will NOT set + // this if we have transparency set and we need an opaque + // background for the error message to be readable. + self.as(gtk.Widget).addCssClass("background"); + } else { + // Regardless of transparency setting, we remove the background + // CSS class from this widget. Parent widgets will set it + // appropriately (see window.zig for example). + self.as(gtk.Widget).removeCssClass("background"); + } + + // Note above: in both cases setting our error view is handled by + // a Gtk.Stack visible-child-name binding. + } + fn propMouseHoverUrl( self: *Self, _: *gobject.ParamSpec, @@ -1942,8 +2005,11 @@ pub const Surface = extern struct { // Bell stops ringing if any mouse button is pressed. self.setBellRinging(false); - // If we don't have focus, grab it. + // Get our surface. If we don't have one, ignore this. const priv = self.private(); + const core_surface = priv.core_surface orelse return; + + // If we don't have focus, grab it. const gl_area_widget = priv.gl_area.as(gtk.Widget); if (gl_area_widget.hasFocus() == 0) { _ = gl_area_widget.grabFocus(); @@ -1951,10 +2017,10 @@ pub const Surface = extern struct { // Report the event const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton()); - const consumed = if (priv.core_surface) |surface| consumed: { + const consumed = consumed: { const gtk_mods = event.getModifierState(); const mods = gtk_key.translateMods(gtk_mods); - break :consumed surface.mouseButtonCallback( + break :consumed core_surface.mouseButtonCallback( .press, button, mods, @@ -1962,7 +2028,7 @@ pub const Surface = extern struct { log.warn("error in key callback err={}", .{err}); break :err false; }; - } else false; + }; // If a right click isn't consumed, mouseButtonCallback selects the hovered // word and returns false. We can use this to handle the context menu @@ -2303,21 +2369,23 @@ pub const Surface = extern struct { ) callconv(.c) void { log.debug("realize", .{}); + // Make the GL area current so we can detect any OpenGL errors. If + // we have errors here we can't render and we switch to the error + // state. + const priv = self.private(); + priv.gl_area.makeCurrent(); + if (priv.gl_area.getError()) |err| { + log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"}); + log.warn("this error is almost always due to a library, driver, or GTK issue", .{}); + log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{}); + self.setError(true); + return; + } + // If we already have an initialized surface then we notify it. // If we don't, we'll initialize it on the first resize so we have // our proper initial dimensions. - const priv = self.private(); if (priv.core_surface) |v| realize: { - // We need to make the context current so we can call GL functions. - // This is required for all surface operations. - priv.gl_area.makeCurrent(); - if (priv.gl_area.getError()) |err| { - log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"}); - log.warn("this error is usually due to a driver or gtk bug", .{}); - log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{}); - break :realize; - } - v.renderer.displayRealized() catch |err| { log.warn("core displayRealized failed err={}", .{err}); break :realize; @@ -2662,11 +2730,13 @@ pub const Surface = extern struct { class.bindTemplateCallback("child_exited_close", &childExitedClose); class.bindTemplateCallback("context_menu_closed", &contextMenuClosed); class.bindTemplateCallback("notify_config", &propConfig); + class.bindTemplateCallback("notify_error", &propError); class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl); class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden); class.bindTemplateCallback("notify_mouse_shape", &propMouseShape); class.bindTemplateCallback("notify_bell_ringing", &propBellRinging); class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown); + class.bindTemplateCallback("stack_child_name", &closureStackChildName); // Properties gobject.ext.registerProperties(class, &.{ @@ -2674,6 +2744,7 @@ pub const Surface = extern struct { properties.config.impl, properties.@"child-exited".impl, properties.@"default-size".impl, + properties.@"error".impl, properties.@"font-size-request".impl, properties.focused.impl, properties.@"min-size".impl, diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 5f1cf50de..d8f9b97f8 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -18,7 +18,6 @@ const gresource = @import("../build/gresource.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; const Application = @import("application.zig").Application; -const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog; const SplitTree = @import("split_tree.zig").SplitTree; const Surface = @import("surface.zig").Surface; @@ -199,8 +198,11 @@ pub const Tab = extern struct { } fn initActionMap(self: *Self) void { + const s_param_type = glib.ext.VariantType.newFor([:0]const u8); + defer s_param_type.free(); + const actions = [_]ext.actions.Action(Self){ - .init("close", actionClose, null), + .init("close", actionClose, s_param_type), .init("ring-bell", actionRingBell, null), }; @@ -314,18 +316,44 @@ pub const Tab = extern struct { fn actionClose( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Self, ) callconv(.c) void { + const param = param_ orelse { + log.warn("tab.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + const tab_view = ext.getAncestor( adw.TabView, self.as(gtk.Widget), ) orelse return; + const page = tab_view.getPage(self.as(gtk.Widget)); + const mode = std.meta.stringToEnum( + apprt.action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to tab.close-tab", .{}); + return; + }, + ), + ) orelse { + // Need to be defensive here since actions can be triggered externally. + log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?}); + return; + }; + // Delegate to our parent to handle this, since this will emit // a close-page signal that the parent can intercept. - tab_view.closePage(page); + switch (mode) { + .this => tab_view.closePage(page), + .other => tab_view.closeOtherPages(page), + } } fn actionRingBell( diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 91e65731b..862455fc8 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -320,10 +320,13 @@ pub const Window = extern struct { /// Setup our action map. 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){ .init("about", actionAbout, null), .init("close", actionClose, null), - .init("close-tab", actionCloseTab, null), + .init("close-tab", actionCloseTab, s_variant_type), .init("new-tab", actionNewTab, null), .init("new-window", actionNewWindow, null), .init("ring-bell", actionRingBell, null), @@ -961,7 +964,14 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - self.addToast(i18n._("Reloaded the configuration")); + const priv = self.private(); + if (priv.config) |config_obj| { + const config = config_obj.get(); + if (config.@"app-notifications".@"config-reload") { + self.addToast(i18n._("Reloaded the configuration")); + } + } + self.syncAppearance(); } @@ -980,6 +990,22 @@ pub const Window = extern struct { }; } + fn propIsActive( + _: *gtk.Window, + _: *gobject.ParamSpec, + self: *Self, + ) callconv(.c) void { + // Don't change urgency if we're not the active window. + if (self.as(gtk.Window).isActive() == 0) return; + + self.winproto().setUrgent(false) catch |err| { + log.warn( + "winproto failed to reset urgency={}", + .{err}, + ); + }; + } + fn propGdkSurfaceWidth( _: *gdk.Surface, _: *gobject.ParamSpec, @@ -1656,10 +1682,31 @@ pub const Window = extern struct { fn actionCloseTab( _: *gio.SimpleAction, - _: ?*glib.Variant, + param_: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.close_tab); + const param = param_ orelse { + log.warn("win.close-tab called without a parameter", .{}); + return; + }; + + var str: ?[*:0]const u8 = null; + param.get("&s", &str); + + const mode = std.meta.stringToEnum( + input.Binding.Action.CloseTabMode, + std.mem.span( + str orelse { + log.warn("invalid mode provided to win.close-tab", .{}); + return; + }, + ), + ) orelse { + log.warn("invalid mode provided to win.close-tab: {s}", .{str.?}); + return; + }; + + self.performBindingAction(.{ .close_tab = mode }); } fn actionNewWindow( @@ -1758,10 +1805,13 @@ pub const Window = extern struct { native.beep(); } - if (config.@"bell-features".attention) { + if (config.@"bell-features".attention) attention: { + // Dont set urgency if the window is already active. + if (self.as(gtk.Window).isActive() != 0) break :attention; + // Request user attention self.winproto().setUrgent(true) catch |err| { - log.warn("failed to request user attention={}", .{err}); + log.warn("winproto failed to set urgency={}", .{err}); }; } } @@ -1905,6 +1955,7 @@ pub const Window = extern struct { class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage); class.bindTemplateCallback("notify_config", &propConfig); class.bindTemplateCallback("notify_fullscreened", &propFullscreened); + class.bindTemplateCallback("notify_is_active", &propIsActive); class.bindTemplateCallback("notify_maximized", &propMaximized); class.bindTemplateCallback("notify_menu_active", &propMenuActive); class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal); diff --git a/src/apprt/gtk-ng/css/style.css b/src/apprt/gtk-ng/css/style.css index 5901d1d7e..5620c9ca4 100644 --- a/src/apprt/gtk-ng/css/style.css +++ b/src/apprt/gtk-ng/css/style.css @@ -12,7 +12,7 @@ window.ssd.no-border-radius { border-radius: 0 0; } -/* +/* * GhosttySurface URL overlay */ label.url-overlay { @@ -83,13 +83,13 @@ label.resize-overlay { */ .child-exited.normal revealer widget { background-color: rgba(38, 162, 105, 0.5); - /* after GTK 4.16 is a requirement, switch to the following: + /* after GTK 4.16 is a requirement, switch to the following: */ /* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */ } .child-exited.abnormal revealer widget { background-color: rgba(192, 28, 40, 0.5); - /* after GTK 4.16 is a requirement, switch to the following: + /* after GTK 4.16 is a requirement, switch to the following: */ /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ } @@ -97,13 +97,15 @@ label.resize-overlay { * Surface */ .surface progressbar.error trough progress { - background-color: rgb(192, 28, 40); + background-color: rgba(192, 28, 40, 0.5); /* after GTK 4.16 is a requirement, switch to the following: */ - /* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */ + /* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */ } .surface .bell-overlay { - border-color: color-mix(in srgb, var(--accent-color), transparent 50%); + border-color: rgba(58, 148, 74, 0.5); + /* after GTK 4.16 is a requirement, switch to the following: */ + /* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */ border-width: 3px; border-style: solid; } @@ -127,6 +129,8 @@ label.resize-overlay { .window .split paned > separator { background-color: rgba(250, 250, 250, 1); + /* after GTK 4.16 is a requirement, switch to the following: */ + /* background-color: color-mix(in srgb, var(--window-bg-color), transparent 0%); */ background-clip: content-box; /* This works around the oversized drag area for the right side of GtkPaned. diff --git a/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp b/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp index c2dcbadbd..f58ef523c 100644 --- a/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.2/close-confirmation-dialog.blp @@ -7,4 +7,6 @@ template $GhosttyCloseConfirmationDialog: $GhosttyDialog { cancel: _("Cancel"), close: _("Close") destructive, ] + + close-response: "cancel"; } diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index 6c027e735..39c88ff33 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin { notify::bell-ringing => $notify_bell_ringing(); notify::config => $notify_config(); + notify::error => $notify_error(); notify::mouse-hover-url => $notify_mouse_hover_url(); notify::mouse-hidden => $notify_mouse_hidden(); notify::mouse-shape => $notify_mouse_shape(); - Overlay { - focusable: false; - focus-on-click: false; + Stack { + StackPage { + name: "terminal"; - child: Box { - hexpand: true; - vexpand: true; + child: Overlay { + focusable: false; + focus-on-click: false; - GLArea gl_area { - realize => $gl_realize(); - unrealize => $gl_unrealize(); - render => $gl_render(); - resize => $gl_resize(); - hexpand: true; - vexpand: true; - focusable: true; - focus-on-click: true; - has-stencil-buffer: false; - has-depth-buffer: false; - allowed-apis: gl; - } + child: Box { + hexpand: true; + vexpand: true; - PopoverMenu context_menu { - closed => $context_menu_closed(); - menu-model: context_menu_model; - flags: nested; - halign: start; - has-arrow: false; - } - }; + GLArea gl_area { + realize => $gl_realize(); + unrealize => $gl_unrealize(); + render => $gl_render(); + resize => $gl_resize(); + hexpand: true; + vexpand: true; + focusable: true; + focus-on-click: true; + has-stencil-buffer: false; + has-depth-buffer: false; + allowed-apis: gl; + } - [overlay] - ProgressBar progress_bar_overlay { - styles [ - "osd", - ] + PopoverMenu context_menu { + closed => $context_menu_closed(); + menu-model: context_menu_model; + flags: nested; + halign: start; + has-arrow: false; + } + }; - visible: false; - halign: fill; - valign: start; + [overlay] + ProgressBar progress_bar_overlay { + styles [ + "osd", + ] + + visible: false; + halign: fill; + 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 ; + 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; + close-request => $child_exited_close(); + } + + [overlay] + $GhosttyResizeOverlay resize_overlay {} + + [overlay] + Label url_left { + styles [ + "background", + "url-overlay", + ] + + visible: false; + halign: start; + valign: end; + label: bind template.mouse-hover-url; + + EventControllerMotion url_ec_motion { + enter => $url_mouse_enter(); + leave => $url_mouse_leave(); + } + } + + [overlay] + Label url_right { + styles [ + "background", + "url-overlay", + ] + + visible: false; + halign: end; + valign: end; + label: bind template.mouse-hover-url; + } + + // Event controllers for interactivity + EventControllerFocus { + enter => $focus_enter(); + leave => $focus_leave(); + } + + EventControllerKey { + key-pressed => $key_pressed(); + key-released => $key_released(); + } + + EventControllerMotion { + motion => $mouse_motion(); + leave => $mouse_leave(); + } + + EventControllerScroll { + scroll => $scroll(); + scroll-begin => $scroll_begin(); + scroll-end => $scroll_end(); + flags: both_axes; + } + + GestureClick { + pressed => $mouse_down(); + released => $mouse_up(); + button: 0; + } + + DropTarget drop_target { + drop => $drop(); + actions: copy; + } + }; } - [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 ; - transition-type: crossfade; - transition-duration: 500; + StackPage { + name: "error"; - Box bell_overlay { - styles [ - "bell-overlay", - ] + child: Adw.StatusPage { + icon-name: "computer-fail-symbolic"; + title: _("Oh, no."); + description: _("Unable to acquire an OpenGL context for rendering."); - halign: fill; - valign: fill; - } + child: LinkButton { + label: "https://ghostty.org/docs/help/gtk-opengl-context"; + uri: "https://ghostty.org/docs/help/gtk-opengl-context"; + }; + }; } - [overlay] - $GhosttySurfaceChildExited child_exited_overlay { - visible: bind template.child-exited; - close-request => $child_exited_close(); - } - - [overlay] - $GhosttyResizeOverlay resize_overlay {} - - [overlay] - Label url_left { - styles [ - "background", - "url-overlay", - ] - - visible: false; - halign: start; - valign: end; - label: bind template.mouse-hover-url; - - EventControllerMotion url_ec_motion { - enter => $url_mouse_enter(); - leave => $url_mouse_leave(); - } - } - - [overlay] - Label url_right { - styles [ - "background", - "url-overlay", - ] - - visible: false; - halign: end; - valign: end; - label: bind template.mouse-hover-url; - } - } - - // Event controllers for interactivity - EventControllerFocus { - enter => $focus_enter(); - leave => $focus_leave(); - } - - EventControllerKey { - key-pressed => $key_pressed(); - key-released => $key_released(); - } - - EventControllerMotion { - motion => $mouse_motion(); - leave => $mouse_leave(); - } - - EventControllerScroll { - scroll => $scroll(); - scroll-begin => $scroll_begin(); - scroll-end => $scroll_end(); - flags: both_axes; - } - - GestureClick { - pressed => $mouse_down(); - released => $mouse_up(); - button: 0; - } - - DropTarget drop_target { - drop => $drop(); - actions: copy; + // The order matters here: we can only set this after the stack + // pages above have been created. + visible-child-name: bind $stack_child_name(template.error) as ; } } @@ -228,7 +254,8 @@ menu context_menu_model { item { label: _("Close Tab"); - action: "win.close-tab"; + action: "tab.close"; + target: "this"; } } diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index b09c0d9b3..8c0a7bedb 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -10,6 +10,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { realize => $realize(); notify::config => $notify_config(); notify::fullscreened => $notify_fullscreened(); + notify::is-active => $notify_is_active(); notify::maximized => $notify_maximized(); notify::quick-terminal => $notify_quick_terminal(); notify::scale-factor => $notify_scale_factor(); @@ -225,6 +226,7 @@ menu main_menu { item { label: _("Close Tab"); action: "win.close-tab"; + target: "this"; } } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0f75a2d97..ee5f3eb96 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -491,7 +491,7 @@ pub fn performAction( .toggle_maximize => self.toggleMaximize(target), .toggle_fullscreen => self.toggleFullscreen(target, value), .new_tab => try self.newTab(target), - .close_tab => return try self.closeTab(target), + .close_tab => return try self.closeTab(target, value), .goto_tab => return self.gotoTab(target, value), .move_tab => self.moveTab(target, value), .new_split => try self.newSplit(target, value), @@ -585,7 +585,7 @@ fn newTab(_: *App, target: apprt.Target) !void { } } -fn closeTab(_: *App, target: apprt.Target) !bool { +fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool { switch (target) { .app => return false, .surface => |v| { @@ -597,8 +597,16 @@ fn closeTab(_: *App, target: apprt.Target) !bool { return false; }; - tab.closeWithConfirmation(); - return true; + switch (value) { + .this => { + tab.closeWithConfirmation(); + return true; + }, + .other => { + log.warn("close-tab:other is not implemented", .{}); + return false; + }, + } }, } } @@ -1145,7 +1153,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); - try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} }); + try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this }); try self.syncActionAccelerator("win.split-right", .{ .new_split = .right }); try self.syncActionAccelerator("win.split-down", .{ .new_split = .down }); try self.syncActionAccelerator("win.split-left", .{ .new_split = .left }); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 2f026e33c..8c02396a6 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -772,7 +772,9 @@ pub fn focusCurrentTab(self: *Window) void { } pub fn onConfigReloaded(self: *Window) void { - self.sendToast(i18n._("Reloaded the configuration")); + if (self.app.config.@"app-notifications".@"config-reload") { + self.sendToast(i18n._("Reloaded the configuration")); + } } pub fn sendToast(self: *Window, title: [*:0]const u8) void { @@ -1074,7 +1076,7 @@ fn gtkActionCloseTab( _: ?*glib.Variant, self: *Window, ) callconv(.c) void { - self.performBindingAction(.{ .close_tab = {} }); + self.performBindingAction(.{ .close_tab = .this }); } fn gtkActionSplitRight( diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index 4128a7adc..0ca154414 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -131,6 +131,13 @@ pub const VTable = struct { }; test Benchmark { + // This test fails on FreeBSD so skip: + // + // /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test) + // try testing.expect(result.duration > 0); + // ^ + if (builtin.os.tag == .freebsd) return error.SkipZigTest; + const testing = std.testing; const Simple = struct { const Self = @This(); diff --git a/src/cli/list_colors.zig b/src/cli/list_colors.zig index e43a43c86..63945de99 100644 --- a/src/cli/list_colors.zig +++ b/src/cli/list_colors.zig @@ -1,13 +1,20 @@ const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const x11_color = @import("../terminal/main.zig").x11_color; +const vaxis = @import("vaxis"); +const tui = @import("tui.zig"); pub const Options = struct { pub fn deinit(self: Options) void { _ = self; } + /// If `true`, print without formatting even if printing to a tty + plain: bool = false, + /// Enables "-h" and "--help" to work. pub fn help(self: Options) !void { _ = self; @@ -17,7 +24,12 @@ pub const Options = struct { /// The `list-colors` command is used to list all the named RGB colors in /// Ghostty. -pub fn run(alloc: std.mem.Allocator) !u8 { +/// +/// Flags: +/// +/// * `--plain`: will disable formatting and make the output more +/// friendly for Unix tooling. This is default when not printing to a tty. +pub fn run(alloc: Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -27,7 +39,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 { try args.parse(Options, alloc, &opts, &iter); } - const stdout = std.io.getStdOut().writer(); + const stdout = std.io.getStdOut(); var keys = std.ArrayList([]const u8).init(alloc); defer keys.deinit(); @@ -39,15 +51,163 @@ pub fn run(alloc: std.mem.Allocator) !u8 { } }.lessThan); - for (keys.items) |name| { - const rgb = x11_color.map.get(name).?; - try stdout.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ - name, - rgb.r, - rgb.g, - rgb.b, - }); + // Despite being under the posix namespace, this also works on Windows as of zig 0.13.0 + if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) { + var arena = std.heap.ArenaAllocator.init(alloc); + defer arena.deinit(); + return prettyPrint(arena.allocator(), keys.items); + } else { + const writer = stdout.writer(); + for (keys.items) |name| { + const rgb = x11_color.map.get(name).?; + try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{ + name, + rgb.r, + rgb.g, + rgb.b, + }); + } } return 0; } + +fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 { + // Set up vaxis + var tty = try vaxis.Tty.init(); + defer tty.deinit(); + var vx = try vaxis.init(alloc, .{}); + defer vx.deinit(alloc, tty.anyWriter()); + + // We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an + // event loop to auto-enable it. + vx.caps.unicode = .unicode; + try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set); + defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {}; + + var buf_writer = tty.bufferedWriter(); + const writer = buf_writer.writer().any(); + + const winsize: vaxis.Winsize = switch (builtin.os.tag) { + // We use some default, it doesn't really matter for what + // we're doing because we don't do any wrapping. + .windows => .{ + .rows = 24, + .cols = 120, + .x_pixel = 1024, + .y_pixel = 768, + }, + + else => try vaxis.Tty.getWinsize(tty.fd), + }; + try vx.resize(alloc, tty.anyWriter(), winsize); + + const win = vx.window(); + + var max_name_len: usize = 0; + for (keys) |name| { + if (name.len > max_name_len) max_name_len = name.len; + } + + // max name length plus " = #RRGGBB XX" plus " " gutter between columns + const column_size = max_name_len + 15; + // add two to take into account lack of gutter after last column + const columns: usize = @divFloor(win.width + 2, column_size); + + var i: usize = 0; + const step = @divFloor(keys.len, columns) + 1; + while (i < step) : (i += 1) { + win.clear(); + + var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false }; + + for (0..columns) |j| { + const k = i + (step * j); + if (k >= keys.len) continue; + + const name = keys[k]; + const rgb = x11_color.map.get(name).?; + + const style1: vaxis.Style = .{ + .fg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + }; + const style2: vaxis.Style = .{ + .fg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + .bg = .{ + .rgb = .{ rgb.r, rgb.g, rgb.b }, + }, + }; + + // name of the color + result = win.printSegment( + .{ .text = name }, + .{ .col_offset = result.col }, + ); + // push the color data to the end of the column + for (0..max_name_len - name.len) |_| { + result = win.printSegment( + .{ .text = " " }, + .{ .col_offset = result.col }, + ); + } + result = win.printSegment( + .{ .text = " = " }, + .{ .col_offset = result.col }, + ); + // rgb triple + result = win.printSegment(.{ + .text = try std.fmt.allocPrint( + alloc, + "#{x:0>2}{x:0>2}{x:0>2}", + .{ + rgb.r, rgb.g, rgb.b, + }, + ), + .style = style1, + }, .{ .col_offset = result.col }); + result = win.printSegment( + .{ .text = " " }, + .{ .col_offset = result.col }, + ); + // colored block + result = win.printSegment( + .{ + .text = " ", + .style = style2, + }, + .{ .col_offset = result.col }, + ); + // add the gutter if needed + if (j + 1 < columns) { + result = win.printSegment( + .{ + .text = " ", + }, + .{ .col_offset = result.col }, + ); + } + } + + // clear the rest of the line + while (result.col != 0) { + result = win.printSegment( + .{ + .text = " ", + }, + .{ .col_offset = result.col }, + ); + } + + // output the data + try vx.prettyPrint(writer); + } + + // be sure to flush! + try buf_writer.flush(); + + return 0; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index d8fcfa1d7..5ffd01871 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -767,6 +767,22 @@ palette: Palette = .{}, /// the mouse is shown again when a new window, tab, or split is created. @"mouse-hide-while-typing": bool = false, +/// When to scroll the surface to the bottom. The format of this is a list of +/// options to enable separated by commas. If you prefix an option with `no-` +/// then it is disabled. If you omit an option, its default value is used. +/// +/// Available options: +/// +/// - `keystroke` If set, scroll the surface to the bottom when the user +/// presses a key that results in data being sent to the PTY (basically +/// anything but modifiers or keybinds that are processed by Ghostty). +/// +/// - `output` If set, scroll the surface to the bottom if there is new data +/// to display. (Currently unimplemented.) +/// +/// The default is `keystroke, no-output`. +@"scroll-to-bottom": ScrollToBottom = .default, + /// Determines whether running programs can detect the shift key pressed with a /// mouse click. Typically, the shift key is used to extend mouse selection. /// @@ -2499,6 +2515,8 @@ keybind: Keybinds = .{}, /// /// - `clipboard-copy` (default: true) - Show a notification when text is copied /// to the clipboard. +/// - `config-reload` (default: true) - Show a notification when +/// the configuration is reloaded. /// /// To specify a notification to enable, specify the name of the notification. /// To specify a notification to disable, prefix the name with `no-`. For @@ -3017,6 +3035,13 @@ else /// Available since Ghostty 1.2.0. @"bold-color": ?BoldColor = null, +/// The opacity level (opposite of transparency) of the faint text. A value of +/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0 +/// or greater than 1 will be clamped to the nearest valid value. +/// +/// Available since Ghostty 1.2.0. +@"faint-opacity": f64 = 0.5, + /// This will be used to set the `TERM` environment variable. /// HACK: We set this with an `xterm` prefix because vim uses that to enable key /// protocols (specifically this will enable `modifyOtherKeys`), among other @@ -3999,6 +4024,8 @@ pub fn finalize(self: *Config) !void { if (self.@"auto-update-channel" == null) { self.@"auto-update-channel" = build_config.release_channel; } + + self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0); } /// Callback for src/cli/args.zig to allow us to handle special cases @@ -5596,7 +5623,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.putFlags( alloc, @@ -5902,7 +5929,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, - .{ .close_tab = {} }, + .{ .close_tab = .this }, ); try self.set.put( alloc, @@ -7058,6 +7085,7 @@ pub const GtkTitlebarStyle = enum(c_int) { /// See app-notifications pub const AppNotifications = packed struct { @"clipboard-copy": bool = true, + @"config-reload": bool = true, }; /// See bell-features @@ -7195,6 +7223,53 @@ pub const QuickTerminalSize = struct { height: u32, }; + /// C API structure for QuickTerminalSize + pub const C = extern struct { + primary: C.Size, + secondary: C.Size, + + pub const Size = extern struct { + tag: Tag, + value: Value, + + pub const Tag = enum(u8) { none, percentage, pixels }; + + pub const Value = extern union { + percentage: f32, + pixels: u32, + }; + + pub const none: C.Size = .{ .tag = .none, .value = undefined }; + + pub fn percentage(v: f32) C.Size { + return .{ + .tag = .percentage, + .value = .{ .percentage = v }, + }; + } + + pub fn pixels(v: u32) C.Size { + return .{ + .tag = .pixels, + .value = .{ .pixels = v }, + }; + } + }; + }; + + pub fn cval(self: QuickTerminalSize) C { + return .{ + .primary = if (self.primary) |p| switch (p) { + .percentage => |v| .percentage(v), + .pixels => |v| .pixels(v), + } else .none, + .secondary = if (self.secondary) |s| switch (s) { + .percentage => |v| .percentage(v), + .pixels => |v| .pixels(v), + } else .none, + }; + } + pub fn calculate( self: QuickTerminalSize, position: QuickTerminalPosition, @@ -7268,6 +7343,7 @@ pub const QuickTerminalSize = struct { try formatter.formatEntry([]const u8, fbs.getWritten()); } + test "parse QuickTerminalSize" { const testing = std.testing; var v: QuickTerminalSize = undefined; @@ -7980,6 +8056,14 @@ pub const WindowPadding = struct { } }; +/// See scroll-to-bottom +pub const ScrollToBottom = packed struct { + keystroke: bool = true, + output: bool = false, + + pub const default: ScrollToBottom = .{}; +}; + test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 78760a9a7..bc7dae026 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -552,11 +552,15 @@ pub const Action = union(enum) { /// of the `confirm-close-surface` configuration setting. close_surface, - /// Close the current tab and all splits therein. + /// Close the current tab and all splits therein _or_ close all tabs and + /// splits thein of tabs _other_ than the current tab, depending on the + /// mode. + /// + /// If the mode is not specified, defaults to closing the current tab. /// /// This might trigger a close confirmation popup, depending on the value /// of the `confirm-close-surface` configuration setting. - close_tab, + close_tab: CloseTabMode, /// Close the current window and all tabs and splits therein. /// @@ -858,6 +862,13 @@ pub const Action = union(enum) { hide, }; + pub const CloseTabMode = enum { + this, + other, + + pub const default: CloseTabMode = .this; + }; + fn parseEnum(comptime T: type, value: []const u8) !T { return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; } diff --git a/src/input/command.zig b/src/input/command.zig index 615ffb713..63feb2edf 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -393,11 +393,18 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close the current terminal.", }}, - .close_tab => comptime &.{.{ - .action = .close_tab, - .title = "Close Tab", - .description = "Close the current tab.", - }}, + .close_tab => comptime &.{ + .{ + .action = .{ .close_tab = .this }, + .title = "Close Tab", + .description = "Close the current tab.", + }, + .{ + .action = .{ .close_tab = .other }, + .title = "Close Other Tabs", + .description = "Close all tabs in this window except the current one.", + }, + }, .close_window => comptime &.{.{ .action = .close_window, diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 29f7f6bc3..d39976811 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{ "ca_ES.UTF-8", "bg_BG.UTF-8", "ga_IE.UTF-8", + "hu_HU.UTF-8", "he_IL.UTF-8", }; diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d975f0f96..1305dc3dc 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -522,6 +522,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { selection_background: ?configpkg.Config.TerminalColor, selection_foreground: ?configpkg.Config.TerminalColor, bold_color: ?configpkg.BoldColor, + faint_opacity: u8, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, custom_shaders: configpkg.RepeatablePath, @@ -584,6 +585,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), .bold_color = config.@"bold-color", + .faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)), .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", @@ -2225,23 +2227,44 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]); const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]); + // Left edge of the cell the cursor is in. var pixel_x: f32 = @floatFromInt( cursor.grid_pos[0] * cell.width + padding.left, ); + // Top edge, relative to the top of the + // screen, of the cell the cursor is in. var pixel_y: f32 = @floatFromInt( cursor.grid_pos[1] * cell.height + padding.top, ); - pixel_x += @floatFromInt(cursor.bearings[0]); - pixel_y += @floatFromInt(cursor.bearings[1]); - - // If +Y is up in our shaders, we need to flip the coordinate. + // If +Y is up in our shaders, we need to flip the coordinate + // so that it's instead the top edge of the cell relative to + // the *bottom* of the screen. if (!GraphicsAPI.custom_shader_y_is_down) { pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y; - // We need to add the cursor height because we need the +Y - // edge for the Y coordinate, and flipping means that it's - // the -Y edge now. - pixel_y += cursor_height; + } + + // Add the X bearing to get the -X (left) edge of the cursor. + pixel_x += @floatFromInt(cursor.bearings[0]); + + // How we deal with the Y bearing depends on which direction + // is "up", since we want our final `pixel_y` value to be the + // +Y edge of the cursor. + if (GraphicsAPI.custom_shader_y_is_down) { + // As a reminder, the Y bearing is the distance from the + // bottom of the cell to the top of the glyph, so to get + // the +Y edge we need to add the cell height, subtract + // the Y bearing, and add the glyph height to get the +Y + // (bottom) edge of the cursor. + pixel_y += @floatFromInt(cell.height); + pixel_y -= @floatFromInt(cursor.bearings[1]); + pixel_y += @floatFromInt(cursor.glyph_size[1]); + } else { + // If the Y direction is reversed though, we instead want + // the *top* edge of the cursor, which means we just need + // to subtract the cell height and add the Y bearing. + pixel_y -= @floatFromInt(cell.height); + pixel_y += @floatFromInt(cursor.bearings[1]); } const new_cursor: [4]f32 = .{ @@ -2612,7 +2635,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; + const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255; // Set the cell's background color. { diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0223545e5..428274878 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -193,7 +193,7 @@ pub const Action = union(enum) { /// Maximum number of intermediate characters during parsing. This is /// 4 because we also use the intermediates array for UTF8 decoding which /// can be at most 4 bytes. -const MAX_INTERMEDIATE = 4; +pub const MAX_INTERMEDIATE = 4; /// Maximum number of CSI parameters. This is arbitrary. Practically, the /// only CSI command that uses more than 3 parameters is the SGR command @@ -206,7 +206,7 @@ const MAX_INTERMEDIATE = 4; /// number. I implore TUI authors to not use more than this number of CSI /// params, but I suspect we'll introduce a slow path with heap allocation /// one day. -const MAX_PARAMS = 24; +pub const MAX_PARAMS = 24; /// Current state of the state machine state: State, @@ -949,6 +949,55 @@ test "csi: too many params" { } } +test "csi: sgr with up to our max parameters" { + for (1..MAX_PARAMS + 1) |max| { + var p = init(); + _ = p.next(0x1B); + _ = p.next('['); + + for (0..max - 1) |_| { + _ = p.next('1'); + _ = p.next(';'); + } + _ = p.next('2'); + + { + const a = p.next('H'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1].? == .csi_dispatch); + try testing.expect(a[2] == null); + + const csi = a[1].?.csi_dispatch; + try testing.expectEqual(@as(usize, max), csi.params.len); + try testing.expectEqual(@as(u16, 2), csi.params[max - 1]); + } + } +} + +test "csi: sgr beyond our max drops it" { + // Has to be +2 for the loops below + const max = MAX_PARAMS + 2; + + var p = init(); + _ = p.next(0x1B); + _ = p.next('['); + + for (0..max - 1) |_| { + _ = p.next('1'); + _ = p.next(';'); + } + _ = p.next('2'); + + { + const a = p.next('H'); + try testing.expect(p.state == .ground); + try testing.expect(a[0] == null); + try testing.expect(a[1] == null); + try testing.expect(a[2] == null); + } +} + test "dcs: XTGETTCAP" { var p = init(); _ = p.next(0x1B); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 6090166da..7f4f32597 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -147,25 +147,28 @@ pub const Command = union(enum) { /// End a hyperlink (OSC 8) hyperlink_end: void, - /// Sleep (OSC 9;1) - sleep: struct { + /// ConEmu sleep (OSC 9;1) + conemu_sleep: struct { duration_ms: u16, }, - /// Show GUI message Box (OSC 9;2) - show_message_box: []const u8, + /// ConEmu show GUI message box (OSC 9;2) + conemu_show_message_box: []const u8, - /// Change ConEmu tab (OSC 9;3) - change_conemu_tab_title: union(enum) { - reset: void, + /// ConEmu change tab title (OSC 9;3) + conemu_change_tab_title: union(enum) { + reset, value: []const u8, }, - /// Set progress state (OSC 9;4) - progress_report: ProgressReport, + /// ConEmu progress report (OSC 9;4) + conemu_progress_report: ProgressReport, - /// Wait input (OSC 9;5) - wait_input: void, + /// ConEmu wait input (OSC 9;5) + conemu_wait_input, + + /// ConEmu GUI macro (OSC 9;6) + conemu_guimacro: []const u8, pub const ColorOperation = union(enum) { pub const Source = enum(u16) { @@ -208,7 +211,6 @@ pub const Command = union(enum) { }; pub const ProgressReport = struct { - // sync with ghostty_terminal_osc_command_progressreport_state_e in include/ghostty.h pub const State = enum(c_int) { remove, set, @@ -220,7 +222,7 @@ pub const Command = union(enum) { state: State, progress: ?u8 = null, - // sync with ghostty_terminal_osc_command_progressreport_s in include/ghostty.h + // sync with ghostty_action_progress_report_s pub const C = extern struct { state: c_int, progress: i8, @@ -229,7 +231,11 @@ pub const Command = union(enum) { pub fn cval(self: ProgressReport) C { return .{ .state = @intFromEnum(self.state), - .progress = if (self.progress) |progress| @intCast(std.math.clamp(progress, 0, 100)) else -1, + .progress = if (self.progress) |progress| @intCast(std.math.clamp( + progress, + 0, + 100, + )) else -1, }; } }; @@ -431,6 +437,7 @@ pub const Parser = struct { conemu_progress_state, conemu_progress_prevalue, conemu_progress_value, + conemu_guimacro, }; pub fn init() Parser { @@ -957,107 +964,147 @@ pub const Parser = struct { .osc_9 => switch (c) { '1' => { self.state = .conemu_sleep; + // This will end up being either a ConEmu sleep OSC 9;1, + // or a desktop notification OSC 9 that begins with '1', so + // mark as complete. + self.complete = true; }, '2' => { self.state = .conemu_message_box; + // This will end up being either a ConEmu message box OSC 9;2, + // or a desktop notification OSC 9 that begins with '2', so + // mark as complete. + self.complete = true; }, '3' => { self.state = .conemu_tab; + // This will end up being either a ConEmu message box OSC 9;3, + // or a desktop notification OSC 9 that begins with '3', so + // mark as complete. + self.complete = true; }, '4' => { self.state = .conemu_progress_prestate; + // This will end up being either a ConEmu progress report + // OSC 9;4, or a desktop notification OSC 9 that begins with + // '4', so mark as complete. + self.complete = true; }, '5' => { + // Note that sending an OSC 9 desktop notification that + // starts with 5 is impossible due to this. self.state = .swallow; - self.command = .{ .wait_input = {} }; + self.command = .conemu_wait_input; + self.complete = true; + }, + '6' => { + self.state = .conemu_guimacro; + // This will end up being either a ConEmu GUI macro OSC 9;6, + // or a desktop notification OSC 9 that begins with '6', so + // mark as complete. self.complete = true; }, - // Todo: parse out other ConEmu operating system commands. - // Even if we don't support them we probably don't want - // them showing up as desktop notifications. + // Todo: parse out other ConEmu operating system commands. Even + // if we don't support them we probably don't want them showing + // up as desktop notifications. else => self.showDesktopNotification(), }, .conemu_sleep => switch (c) { ';' => { - self.command = .{ .sleep = .{ .duration_ms = 100 } }; + self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } }; self.buf_start = self.buf_idx; self.complete = true; self.state = .conemu_sleep_value; }, - else => self.state = .invalid, - }, - .conemu_message_box => switch (c) { - ';' => { - self.command = .{ .show_message_box = undefined }; - self.temp_state = .{ .str = &self.command.show_message_box }; - self.buf_start = self.buf_idx; - self.complete = true; - self.prepAllocableString(); - }, - else => self.state = .invalid, + // OSC 9;1 is a desktop + // notification. + else => self.showDesktopNotification(), }, .conemu_sleep_value => switch (c) { else => self.complete = true, }, + .conemu_message_box => switch (c) { + ';' => { + self.command = .{ .conemu_show_message_box = undefined }; + self.temp_state = .{ .str = &self.command.conemu_show_message_box }; + self.buf_start = self.buf_idx; + self.complete = true; + self.prepAllocableString(); + }, + + // OSC 9;2 is a desktop + // notification. + else => self.showDesktopNotification(), + }, + .conemu_tab => switch (c) { ';' => { self.state = .conemu_tab_txt; - self.command = .{ .change_conemu_tab_title = .{ .reset = {} } }; + self.command = .{ .conemu_change_tab_title = .reset }; self.buf_start = self.buf_idx; self.complete = true; }, - else => self.state = .invalid, + + // OSC 9;3 is a desktop + // notification. + else => self.showDesktopNotification(), }, .conemu_tab_txt => { - self.command = .{ .change_conemu_tab_title = .{ .value = undefined } }; - self.temp_state = .{ .str = &self.command.change_conemu_tab_title.value }; + self.command = .{ .conemu_change_tab_title = .{ .value = undefined } }; + self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value }; self.complete = true; self.prepAllocableString(); }, .conemu_progress_prestate => switch (c) { ';' => { - self.command = .{ .progress_report = .{ + self.command = .{ .conemu_progress_report = .{ .state = undefined, } }; self.state = .conemu_progress_state; }, + + // OSC 9;4 is a desktop + // notification. else => self.showDesktopNotification(), }, .conemu_progress_state => switch (c) { '0' => { - self.command.progress_report.state = .remove; + self.command.conemu_progress_report.state = .remove; self.state = .swallow; self.complete = true; }, '1' => { - self.command.progress_report.state = .set; - self.command.progress_report.progress = 0; + self.command.conemu_progress_report.state = .set; + self.command.conemu_progress_report.progress = 0; self.state = .conemu_progress_prevalue; }, '2' => { - self.command.progress_report.state = .@"error"; + self.command.conemu_progress_report.state = .@"error"; self.complete = true; self.state = .conemu_progress_prevalue; }, '3' => { - self.command.progress_report.state = .indeterminate; + self.command.conemu_progress_report.state = .indeterminate; self.complete = true; self.state = .swallow; }, '4' => { - self.command.progress_report.state = .pause; + self.command.conemu_progress_report.state = .pause; self.complete = true; self.state = .conemu_progress_prevalue; }, + + // OSC 9;4; is a desktop + // notification. else => self.showDesktopNotification(), }, @@ -1066,6 +1113,8 @@ pub const Parser = struct { self.state = .conemu_progress_value; }, + // OSC 9;4;<0-4> is a desktop + // notification. else => self.showDesktopNotification(), }, @@ -1077,8 +1126,16 @@ pub const Parser = struct { // If we aren't a set substate, then we don't care // about the value. - const p = &self.command.progress_report; - if (p.state != .set and p.state != .@"error" and p.state != .pause) break :value; + const p = &self.command.conemu_progress_report; + switch (p.state) { + .remove, + .indeterminate, + => break :value, + .set, + .@"error", + .pause, + => {}, + } if (p.state == .set) assert(p.progress != null) @@ -1104,6 +1161,20 @@ pub const Parser = struct { }, }, + .conemu_guimacro => switch (c) { + ';' => { + self.command = .{ .conemu_guimacro = undefined }; + self.temp_state = .{ .str = &self.command.conemu_guimacro }; + self.buf_start = self.buf_idx; + self.state = .string; + self.complete = true; + }, + + // OSC 9;6 is a desktop + // notification. + else => self.showDesktopNotification(), + }, + .semantic_prompt => switch (c) { 'A' => { self.state = .semantic_option_start; @@ -1212,6 +1283,11 @@ pub const Parser = struct { self.temp_state = .{ .str = &self.command.show_desktop_notification.body }; self.state = .string; + // Set as complete as we've already seen one character that should be + // part of the notification. If we wait for another character to set + // `complete` when the state is `.string` we won't be able to send any + // single character notifications. + self.complete = true; } fn prepAllocableString(self: *Parser) void { @@ -1332,7 +1408,7 @@ pub const Parser = struct { fn endConEmuSleepValue(self: *Parser) void { switch (self.command) { - .sleep => |*v| v.duration_ms = value: { + .conemu_sleep => |*v| v.duration_ms = value: { const str = self.buf[self.buf_start..self.buf_idx]; if (str.len == 0) break :value 100; @@ -1595,6 +1671,26 @@ pub const Parser = struct { .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .conemu_sleep_value => self.endConEmuSleepValue(), + + // We received OSC 9;X ST, but nothing else, finish off as a + // desktop notification with "X" as the body. + .conemu_sleep, + .conemu_message_box, + .conemu_tab, + .conemu_progress_prestate, + .conemu_progress_state, + .conemu_guimacro, + => { + self.showDesktopNotification(); + self.endString(); + }, + + // A ConEmu progress report that has reached these states is + // complete, don't do anything to them. + .conemu_progress_prevalue, + .conemu_progress_value, + => {}, + .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), @@ -2770,7 +2866,7 @@ test "OSC: OSC104: empty palette index" { try std.testing.expect(it.next() == null); } -test "OSC: conemu sleep" { +test "OSC: OSC 9;1 ConEmu sleep" { const testing = std.testing; var p: Parser = .init(); @@ -2780,11 +2876,11 @@ test "OSC: conemu sleep" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(420, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(420, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep with no value default to 100ms" { +test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" { const testing = std.testing; var p: Parser = .init(); @@ -2794,11 +2890,11 @@ test "OSC: conemu sleep with no value default to 100ms" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(100, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep cannot exceed 10000ms" { +test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" { const testing = std.testing; var p: Parser = .init(); @@ -2808,11 +2904,11 @@ test "OSC: conemu sleep cannot exceed 10000ms" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(10000, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms); } -test "OSC: conemu sleep invalid input" { +test "OSC: OSC 9;1 conemu sleep invalid input" { const testing = std.testing; var p: Parser = .init(); @@ -2822,11 +2918,39 @@ test "OSC: conemu sleep invalid input" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .sleep); - try testing.expectEqual(100, cmd.sleep.duration_ms); + try testing.expect(cmd == .conemu_sleep); + try testing.expectEqual(100, cmd.conemu_sleep.duration_ms); } -test "OSC: show desktop notification" { +test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;1a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9 show desktop notification" { const testing = std.testing; var p: Parser = .init(); @@ -2836,11 +2960,25 @@ test "OSC: show desktop notification" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .show_desktop_notification); - try testing.expectEqualStrings(cmd.show_desktop_notification.title, ""); - try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world"); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body); } -test "OSC: show desktop notification with title" { +test "OSC: OSC 9 show single character desktop notification" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;H"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("", cmd.show_desktop_notification.title); + try testing.expectEqualStrings("H", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 777 show desktop notification with title" { const testing = std.testing; var p: Parser = .init(); @@ -2854,7 +2992,7 @@ test "OSC: show desktop notification with title" { try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body"); } -test "OSC: conemu message box" { +test "OSC: OSC 9;2 ConEmu message box" { const testing = std.testing; var p: Parser = .init(); @@ -2863,11 +3001,11 @@ test "OSC: conemu message box" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings("hello world", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box); } -test "OSC: conemu message box invalid input" { +test "OSC: 9;2 ConEmu message box invalid input" { const testing = std.testing; var p: Parser = .init(); @@ -2875,11 +3013,12 @@ test "OSC: conemu message box invalid input" { const input = "9;2"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); } -test "OSC: conemu message box empty message" { +test "OSC: 9;2 ConEmu message box empty message" { const testing = std.testing; var p: Parser = .init(); @@ -2888,11 +3027,11 @@ test "OSC: conemu message box empty message" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings("", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings("", cmd.conemu_show_message_box); } -test "OSC: conemu message box spaces only message" { +test "OSC: 9;2 ConEmu message box spaces only message" { const testing = std.testing; var p: Parser = .init(); @@ -2901,11 +3040,39 @@ test "OSC: conemu message box spaces only message" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .show_message_box); - try testing.expectEqualStrings(" ", cmd.show_message_box); + try testing.expect(cmd == .conemu_show_message_box); + try testing.expectEqualStrings(" ", cmd.conemu_show_message_box); } -test "OSC: conemu change tab title" { +test "OSC: OSC 9;2 message box -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;2 message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;2a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body); +} + +test "OSC: 9;3 ConEmu change tab title" { const testing = std.testing; var p: Parser = .init(); @@ -2914,11 +3081,11 @@ test "OSC: conemu change tab title" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .change_conemu_tab_title); - try testing.expectEqualStrings("foo bar", cmd.change_conemu_tab_title.value); + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value); } -test "OSC: conemu change tab reset title" { +test "OSC: 9;3 ConEmu change tab title reset" { const testing = std.testing; var p: Parser = .init(); @@ -2928,11 +3095,11 @@ test "OSC: conemu change tab reset title" { const cmd = p.end('\x1b').?; - const expected_command: Command = .{ .change_conemu_tab_title = .{ .reset = {} } }; + const expected_command: Command = .{ .conemu_change_tab_title = .reset }; try testing.expectEqual(expected_command, cmd); } -test "OSC: conemu change tab spaces only title" { +test "OSC: 9;3 ConEmu change tab title spaces only" { const testing = std.testing; var p: Parser = .init(); @@ -2942,11 +3109,11 @@ test "OSC: conemu change tab spaces only title" { const cmd = p.end('\x1b').?; - try testing.expect(cmd == .change_conemu_tab_title); - try testing.expectEqualStrings(" ", cmd.change_conemu_tab_title.value); + try testing.expect(cmd == .conemu_change_tab_title); + try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value); } -test "OSC: conemu change tab invalid input" { +test "OSC: OSC 9;3 change tab title -> desktop notification 1" { const testing = std.testing; var p: Parser = .init(); @@ -2954,11 +3121,27 @@ test "OSC: conemu change tab invalid input" { const input = "9;3"; for (input) |ch| p.next(ch); - const cmd = p.end('\x1b'); - try testing.expect(cmd == null); + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3", cmd.show_desktop_notification.body); } -test "OSC: OSC9 progress set" { +test "OSC: OSC 9;3 message box -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;3a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 ConEmu progress set" { const testing = std.testing; var p: Parser = .init(); @@ -2967,12 +3150,12 @@ test "OSC: OSC9 progress set" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 100); } -test "OSC: OSC9 progress set overflow" { +test "OSC: OSC 9;4 ConEmu progress set overflow" { const testing = std.testing; var p: Parser = .init(); @@ -2981,12 +3164,12 @@ test "OSC: OSC9 progress set overflow" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); } -test "OSC: OSC9 progress set single digit" { +test "OSC: OSC 9;4 ConEmu progress set single digit" { const testing = std.testing; var p: Parser = .init(); @@ -2995,12 +3178,12 @@ test "OSC: OSC9 progress set single digit" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 9); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expect(cmd.conemu_progress_report.progress == 9); } -test "OSC: OSC9 progress set double digit" { +test "OSC: OSC 9;4 ConEmu progress set double digit" { const testing = std.testing; var p: Parser = .init(); @@ -3009,12 +3192,12 @@ test "OSC: OSC9 progress set double digit" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 94); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(94, cmd.conemu_progress_report.progress); } -test "OSC: OSC9 progress set extra semicolon ignored" { +test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" { const testing = std.testing; var p: Parser = .init(); @@ -3023,12 +3206,12 @@ test "OSC: OSC9 progress set extra semicolon ignored" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .set); - try testing.expect(cmd.progress_report.progress == 100); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .set); + try testing.expectEqual(100, cmd.conemu_progress_report.progress); } -test "OSC: OSC9 progress remove with no progress" { +test "OSC: OSC 9;4 ConEmu progress remove with no progress" { const testing = std.testing; var p: Parser = .init(); @@ -3037,12 +3220,12 @@ test "OSC: OSC9 progress remove with no progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); } -test "OSC: OSC9 progress remove with double semicolon" { +test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" { const testing = std.testing; var p: Parser = .init(); @@ -3051,12 +3234,12 @@ test "OSC: OSC9 progress remove with double semicolon" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); } -test "OSC: OSC9 progress remove ignores progress" { +test "OSC: OSC 9;4 ConEmu progress remove ignores progress" { const testing = std.testing; var p: Parser = .init(); @@ -3065,12 +3248,12 @@ test "OSC: OSC9 progress remove ignores progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); - try testing.expect(cmd.progress_report.progress == null); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); + try testing.expect(cmd.conemu_progress_report.progress == null); } -test "OSC: OSC9 progress remove extra semicolon" { +test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" { const testing = std.testing; var p: Parser = .init(); @@ -3079,11 +3262,11 @@ test "OSC: OSC9 progress remove extra semicolon" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .remove); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .remove); } -test "OSC: OSC9 progress error" { +test "OSC: OSC 9;4 ConEmu progress error" { const testing = std.testing; var p: Parser = .init(); @@ -3092,12 +3275,12 @@ test "OSC: OSC9 progress error" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .@"error"); - try testing.expect(cmd.progress_report.progress == null); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == null); } -test "OSC: OSC9 progress error with progress" { +test "OSC: OSC 9;4 ConEmu progress error with progress" { const testing = std.testing; var p: Parser = .init(); @@ -3106,12 +3289,12 @@ test "OSC: OSC9 progress error with progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .@"error"); - try testing.expect(cmd.progress_report.progress == 100); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .@"error"); + try testing.expect(cmd.conemu_progress_report.progress == 100); } -test "OSC: OSC9 progress pause" { +test "OSC: OSC 9;4 progress pause" { const testing = std.testing; var p: Parser = .init(); @@ -3120,12 +3303,12 @@ test "OSC: OSC9 progress pause" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .pause); - try testing.expect(cmd.progress_report.progress == null); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == null); } -test "OSC: OSC9 progress pause with progress" { +test "OSC: OSC 9;4 ConEmu progress pause with progress" { const testing = std.testing; var p: Parser = .init(); @@ -3134,12 +3317,68 @@ test "OSC: OSC9 progress pause with progress" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .progress_report); - try testing.expect(cmd.progress_report.state == .pause); - try testing.expect(cmd.progress_report.progress == 100); + try testing.expect(cmd == .conemu_progress_report); + try testing.expect(cmd.conemu_progress_report.state == .pause); + try testing.expect(cmd.conemu_progress_report.progress == 100); } -test "OSC: OSC9 conemu wait input" { +test "OSC: OSC 9;4 progress -> desktop notification 1" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 2" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 3" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;4 progress -> desktop notification 4" { + const testing = std.testing; + + var p: Parser = .init(); + + const input = "9;4;5a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body); +} + +test "OSC: OSC 9;5 ConEmu wait input" { const testing = std.testing; var p: Parser = .init(); @@ -3148,10 +3387,10 @@ test "OSC: OSC9 conemu wait input" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .wait_input); + try testing.expect(cmd == .conemu_wait_input); } -test "OSC: OSC9 conemu wait ignores trailing characters" { +test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" { const testing = std.testing; var p: Parser = .init(); @@ -3160,7 +3399,7 @@ test "OSC: OSC9 conemu wait ignores trailing characters" { for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .wait_input); + try testing.expect(cmd == .conemu_wait_input); } test "OSC: empty param" { @@ -3415,3 +3654,45 @@ test "OSC: kitty color protocol no key" { try testing.expect(cmd == .kitty_color_protocol); try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len); } + +test "OSC: 9;6: ConEmu guimacro 1" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;a"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("a", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 2" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6;ab"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .conemu_guimacro); + try testing.expectEqualStrings("ab", cmd.conemu_guimacro); +} + +test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" { + const testing = std.testing; + + var p: Parser = .initAlloc(testing.allocator); + defer p.deinit(); + + const input = "9;6"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .show_desktop_notification); + try testing.expectEqualStrings("6", cmd.show_desktop_notification.body); +} diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index e4b85fbdd..d589172ad 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -134,7 +134,7 @@ pub const Parser = struct { self.idx += 1; return .{ .unknown = .{ .full = self.params, - .partial = slice[0 .. self.idx - start + 1], + .partial = slice[0..@min(self.idx - start + 1, slice.len)], } }; }, }; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index f40fc4c94..3009935ec 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -249,7 +249,7 @@ pub fn Stream(comptime Handler: type) type { // the parser state to ground. 0x18, 0x1A => self.parser.state = .ground, // A parameter digit: - '0'...'9' => if (self.parser.params_idx < 16) { + '0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.param_acc *|= 10; self.parser.param_acc +|= c - '0'; // The parser's CSI param action uses param_acc_idx @@ -259,7 +259,7 @@ pub fn Stream(comptime Handler: type) type { self.parser.param_acc_idx |= 1; }, // A parameter separator: - ':', ';' => if (self.parser.params_idx < 16) { + ':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) { self.parser.params[self.parser.params_idx] = self.parser.param_acc; if (c == ':') self.parser.params_sep.set(self.parser.params_idx); self.parser.params_idx += 1; @@ -1598,14 +1598,19 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .progress_report => |v| { + .conemu_progress_report => |v| { if (@hasDecl(T, "handleProgressReport")) { try self.handler.handleProgressReport(v); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .sleep, .show_message_box, .change_conemu_tab_title, .wait_input => { + .conemu_sleep, + .conemu_show_message_box, + .conemu_change_tab_title, + .conemu_wait_input, + .conemu_guimacro, + => { log.warn("unimplemented OSC callback: {}", .{cmd}); }, @@ -2596,3 +2601,22 @@ test "stream CSI ? W reset tab stops" { try s.nextSlice("\x1b[?1;2;3W"); try testing.expect(s.handler.reset); } + +test "stream: SGR with 17+ parameters for underline color" { + const H = struct { + attrs: ?sgr.Attribute = null, + called: bool = false, + + pub fn setAttribute(self: *@This(), attr: sgr.Attribute) !void { + self.attrs = attr; + self.called = true; + } + }; + + var s: Stream(H) = .init(.{}); + + // Kakoune-style SGR with underline color as 17th parameter + // This tests the fix where param 17 was being dropped + try s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m"); + try testing.expect(s.handler.called); +} diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 15b6b8cd4..5a15392b4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1513,7 +1513,8 @@ fn execCommand( } return switch (command) { - .direct => |v| v, + // We need to clone the command since there's no guarantee the config remains valid. + .direct => |_| (try command.clone(alloc)).direct, .shell => |v| shell: { var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); @@ -1688,3 +1689,35 @@ test "execCommand: direct command, error passwd" { try testing.expectEqualStrings(result[0], "foo"); try testing.expectEqualStrings(result[1], "bar baz"); } + +test "execCommand: direct command, config freed" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var command_arena = ArenaAllocator.init(alloc); + const command_alloc = command_arena.allocator(); + const command = try (configpkg.Command{ + .direct = &.{ + "foo", + "bar baz", + }, + }).clone(command_alloc); + + const result = try execCommand(alloc, command, struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }); + + command_arena.deinit(); + + try testing.expectEqual(2, result.len); + try testing.expectEqualStrings(result[0], "foo"); + try testing.expectEqualStrings(result[1], "bar baz"); +}