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

pull/8757/head
Jacob Sandlund 2025-09-01 01:34:03 -04:00
commit c67f51f3ee
74 changed files with 3060 additions and 1182 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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#<package>`.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#<vmtype>`. `<vmtype>` 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#<package name>`.
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.

329
HACKING.md Normal file
View File

@ -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=<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#<package>`.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#<vmtype>`. `<vmtype>` 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#<package name>`.
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.

128
README.md
View File

@ -13,7 +13,9 @@
·
<a href="https://ghostty.org/docs">Documentation</a>
·
<a href="#developing-ghostty">Developing</a>
<a href="CONTRIBUTING.md">Contributing</a>
·
<a href="HACKING.md">Developing</a>
</p>
</p>
@ -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=<filter>` for running a specific subset of those unit tests
- `zig build run -Dconformance=<name>` 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.

View File

@ -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,
},
},

6
build.zig.zon.json generated
View File

@ -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",

7
build.zig.zon.nix generated
View File

@ -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=";
};
}
{

2
build.zig.zon.txt generated
View File

@ -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

View File

@ -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"
}
}

View File

@ -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";
};
};

View File

@ -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

View File

@ -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",

View File

@ -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 {

View File

@ -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 = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = "<group>"; };
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = "<group>"; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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
// `<filename>; 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
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
)
}
}
}
}

View File

@ -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 }

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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")

View File

@ -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

View File

@ -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) {

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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 <damyan.bogoev@gmail.com>, 2025.
# reo101 <pavel.atanasov2001@gmail.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-05-19 11:34+0300\n"
"Last-Translator: Damyan Bogoev <damyan.bogoev@gmail.com>\n"
"PO-Revision-Date: 2025-08-22 14:52+0300\n"
"Last-Translator: reo101 <pavel.atanasov2001@gmail.com>\n"
"Language-Team: Bulgarian <dict@ludost.net>\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"

View File

@ -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 <francesc.arpi@gmail.com>, 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 <francesc.arpi@gmail.com>\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 lopció 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 ""

View File

@ -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 <r@rpfaeffle.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\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 ""

View File

@ -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 <miguelp@quientienemail.com>\n"
"Language-Team: Spanish <es@tp.org.es>\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 ""

View File

@ -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 <swiip__@hotmail.com>\n"
"Language-Team: French <traduc@traduc.org>\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 ""

View File

@ -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 <aindriu80@gmail.com>\n"
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\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"

View File

@ -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 <ghostty@slsrepo.com>, 2025.
# CraziestOwl <craziestowl@proton.me>, 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 <ghostty@slsrepo."
"com>\n"
"PO-Revision-Date: 2025-08-23 08:00+0300\n"
"Last-Translator: CraziestOwl <craziestowl@proton.me>\n"
"Language-Team: Hebrew <he_IL@lists.sourceforge.net>\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"

320
po/hu_HU.UTF-8.po Normal file
View File

@ -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 <bszucs1209@gmail.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-08-23 17:14+0200\n"
"Last-Translator: Balázs Szücs <bszucs1209@gmail.com>\n"
"Language-Team: Hungarian <translation-team-hu@lists.sourceforge.net>\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ó"

View File

@ -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 <andrej.daskalov@gmail.com>, 2025.
# Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.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-23 14:17+0100\n"
"Last-Translator: Andrej Daskalov <andrej.daskalov@gmail.com>\n"
"PO-Revision-Date: 2025-08-25 22:17+0200\n"
"Last-Translator: Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.com>\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 ""

View File

@ -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 <hanna@hanna.lol>, 2025.
# Hanna Rose <me@hanna.lol>, 2025.
# Uzair Aftab <uzaaft@outlook.com>, 2025.
# Christoffer Tønnessen <christoffer@cto.gg>, 2025.
# cryptocode <cryptocode@zolo.io>, 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 <cryptocode@zolo.io>\n"
"PO-Revision-Date: 2025-08-23 12:52+0000\n"
"Last-Translator: Hanna Rose <me@hanna.lol>\n"
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\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"

View File

@ -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 <gsodevel@gmail.com>, 2025.
# Guilherme Tiscoski <github@guilhermetiscoski.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-06-20 10:19-0300\n"
"Last-Translator: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>\n"
"PO-Revision-Date: 2025-08-25 11:46-0500\n"
"Last-Translator: Guilherme Tiscoski <github@guihermetiscoski.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
"net>\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"

View File

@ -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 <emir_sari@icloud.com>\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"

View File

@ -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 <danilmail0110@gmail.com>, 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 <danilmail0110@gmail.com>\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 <trans-uk@lists.fedoraproject.org>\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"

View File

@ -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,

View File

@ -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,
};
}
};

View File

@ -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,
};

View File

@ -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(

View File

@ -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);
}

View File

@ -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();

View File

@ -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,

View File

@ -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(

View File

@ -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);

View File

@ -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.

View File

@ -7,4 +7,6 @@ template $GhosttyCloseConfirmationDialog: $GhosttyDialog {
cancel: _("Cancel"),
close: _("Close") destructive,
]
close-response: "cancel";
}

View File

@ -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 <bool>;
transition-type: crossfade;
transition-duration: 500;
Box bell_overlay {
styles [
"bell-overlay",
]
halign: fill;
valign: fill;
}
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;
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 <bool>;
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 <string>;
}
}
@ -228,7 +254,8 @@ menu context_menu_model {
item {
label: _("Close Tab");
action: "win.close-tab";
action: "tab.close";
target: "this";
}
}

View File

@ -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";
}
}

View File

@ -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 });

View File

@ -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(

View File

@ -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();

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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,

View File

@ -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",
};

View File

@ -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.
{

View File

@ -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);

View File

@ -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 <something other than semicolon> 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 <something other than semicolon> 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 <something other than semicolon> 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 <something other than semicolon> 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; <something other than 0-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> <something other than semicolon> 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 <something other than semicolon> 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);
}

View File

@ -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)],
} };
},
};

View File

@ -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);
}

View File

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