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");
+}