Merge remote-tracking branch 'origin/main' into shaping-positions
commit
6b1d1d7f9d
|
|
@ -47,7 +47,7 @@ jobs:
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ jobs:
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
# Important so that build number generation works
|
# Important so that build number generation works
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -166,7 +166,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -127,7 +127,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -160,7 +160,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -194,7 +194,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -237,7 +237,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -273,7 +273,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -302,7 +302,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -335,7 +335,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -381,7 +381,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -600,7 +600,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -642,7 +642,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -690,7 +690,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -725,7 +725,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -789,7 +789,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -816,7 +816,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -844,7 +844,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -871,7 +871,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -898,7 +898,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -925,7 +925,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -952,7 +952,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -986,7 +986,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1013,7 +1013,7 @@ jobs:
|
||||||
path: |
|
path: |
|
||||||
/nix
|
/nix
|
||||||
/zig
|
/zig
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1050,7 +1050,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
@ -1138,7 +1138,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ jobs:
|
||||||
/zig
|
/zig
|
||||||
|
|
||||||
- name: Setup Nix
|
- name: Setup Nix
|
||||||
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||||
|
|
|
||||||
|
|
@ -584,6 +584,12 @@ typedef struct {
|
||||||
const char* title;
|
const char* title;
|
||||||
} ghostty_action_set_title_s;
|
} ghostty_action_set_title_s;
|
||||||
|
|
||||||
|
// apprt.action.PromptTitle
|
||||||
|
typedef enum {
|
||||||
|
GHOSTTY_PROMPT_TITLE_SURFACE,
|
||||||
|
GHOSTTY_PROMPT_TITLE_TAB,
|
||||||
|
} ghostty_action_prompt_title_e;
|
||||||
|
|
||||||
// apprt.action.Pwd.C
|
// apprt.action.Pwd.C
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char* pwd;
|
const char* pwd;
|
||||||
|
|
@ -831,7 +837,7 @@ typedef enum {
|
||||||
GHOSTTY_ACTION_END_SEARCH,
|
GHOSTTY_ACTION_END_SEARCH,
|
||||||
GHOSTTY_ACTION_SEARCH_TOTAL,
|
GHOSTTY_ACTION_SEARCH_TOTAL,
|
||||||
GHOSTTY_ACTION_SEARCH_SELECTED,
|
GHOSTTY_ACTION_SEARCH_SELECTED,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
ghostty_action_split_direction_e new_split;
|
ghostty_action_split_direction_e new_split;
|
||||||
|
|
@ -847,6 +853,7 @@ typedef union {
|
||||||
ghostty_action_inspector_e inspector;
|
ghostty_action_inspector_e inspector;
|
||||||
ghostty_action_desktop_notification_s desktop_notification;
|
ghostty_action_desktop_notification_s desktop_notification;
|
||||||
ghostty_action_set_title_s set_title;
|
ghostty_action_set_title_s set_title;
|
||||||
|
ghostty_action_prompt_title_e prompt_title;
|
||||||
ghostty_action_pwd_s pwd;
|
ghostty_action_pwd_s pwd;
|
||||||
ghostty_action_mouse_shape_e mouse_shape;
|
ghostty_action_mouse_shape_e mouse_shape;
|
||||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,7 @@
|
||||||
Features/Terminal/ErrorView.swift,
|
Features/Terminal/ErrorView.swift,
|
||||||
Features/Terminal/TerminalController.swift,
|
Features/Terminal/TerminalController.swift,
|
||||||
Features/Terminal/TerminalRestorable.swift,
|
Features/Terminal/TerminalRestorable.swift,
|
||||||
|
Features/Terminal/TerminalTabColor.swift,
|
||||||
Features/Terminal/TerminalView.swift,
|
Features/Terminal/TerminalView.swift,
|
||||||
"Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift",
|
"Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift",
|
||||||
"Features/Terminal/Window Styles/Terminal.xib",
|
"Features/Terminal/Window Styles/Terminal.xib",
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ class AppDelegate: NSObject,
|
||||||
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
||||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||||
@IBOutlet private var menuChangeTitle: NSMenuItem?
|
@IBOutlet private var menuChangeTitle: NSMenuItem?
|
||||||
|
@IBOutlet private var menuChangeTabTitle: NSMenuItem?
|
||||||
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
||||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||||
@IBOutlet private var menuCommandPalette: NSMenuItem?
|
@IBOutlet private var menuCommandPalette: NSMenuItem?
|
||||||
|
|
@ -541,7 +542,7 @@ class AppDelegate: NSObject,
|
||||||
self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller")
|
self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller")
|
||||||
self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection")
|
self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection")
|
||||||
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
|
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
|
||||||
self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||||
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
|
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
|
||||||
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
|
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
|
||||||
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
|
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
|
||||||
|
|
@ -609,6 +610,7 @@ class AppDelegate: NSObject,
|
||||||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||||
syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle)
|
syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle)
|
||||||
|
syncMenuShortcut(config, action: "prompt_tab_title", menuItem: self.menuChangeTabTitle)
|
||||||
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal)
|
||||||
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility)
|
||||||
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
|
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
|
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
|
||||||
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
|
<outlet property="menuBringAllToFront" destination="LE2-aR-0XJ" id="AP9-oK-60V"/>
|
||||||
|
<outlet property="menuChangeTabTitle" destination="iac-lh-Cl7" id="tId-v0-a3E"/>
|
||||||
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
|
<outlet property="menuChangeTitle" destination="24I-xg-qIq" id="kg6-kT-jNL"/>
|
||||||
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||||
|
|
@ -315,7 +316,13 @@
|
||||||
<action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/>
|
<action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem title="Change Title..." id="24I-xg-qIq">
|
<menuItem title="Change Tab Title..." id="iac-lh-Cl7">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="changeTabTitle:" target="-1" id="Jhl-9P-bMj"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Change Terminal Title..." id="24I-xg-qIq">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
|
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,15 @@ class BaseTerminalController: NSWindowController,
|
||||||
/// The cancellables related to our focused surface.
|
/// The cancellables related to our focused surface.
|
||||||
private var focusedSurfaceCancellables: Set<AnyCancellable> = []
|
private var focusedSurfaceCancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
/// An override title for the tab/window set by the user via prompt_tab_title.
|
||||||
|
/// When set, this takes precedence over the computed title from the terminal.
|
||||||
|
var titleOverride: String? = nil {
|
||||||
|
didSet { applyTitleToWindow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The last computed title from the focused surface (without the override).
|
||||||
|
private var lastComputedTitle: String = "👻"
|
||||||
|
|
||||||
/// The time that undo/redo operations that contain running ptys are valid for.
|
/// The time that undo/redo operations that contain running ptys are valid for.
|
||||||
var undoExpiration: Duration {
|
var undoExpiration: Duration {
|
||||||
ghostty.config.undoTimeout
|
ghostty.config.undoTimeout
|
||||||
|
|
@ -325,6 +334,37 @@ class BaseTerminalController: NSWindowController,
|
||||||
self.alert = alert
|
self.alert = alert
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prompt the user to change the tab/window title.
|
||||||
|
func promptTabTitle() {
|
||||||
|
guard let window else { return }
|
||||||
|
|
||||||
|
let alert = NSAlert()
|
||||||
|
alert.messageText = "Change Tab Title"
|
||||||
|
alert.informativeText = "Leave blank to restore the default."
|
||||||
|
alert.alertStyle = .informational
|
||||||
|
|
||||||
|
let textField = NSTextField(frame: NSRect(x: 0, y: 0, width: 250, height: 24))
|
||||||
|
textField.stringValue = titleOverride ?? window.title
|
||||||
|
alert.accessoryView = textField
|
||||||
|
|
||||||
|
alert.addButton(withTitle: "OK")
|
||||||
|
alert.addButton(withTitle: "Cancel")
|
||||||
|
|
||||||
|
alert.window.initialFirstResponder = textField
|
||||||
|
|
||||||
|
alert.beginSheetModal(for: window) { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
guard response == .alertFirstButtonReturn else { return }
|
||||||
|
|
||||||
|
let newTitle = textField.stringValue
|
||||||
|
if newTitle.isEmpty {
|
||||||
|
self.titleOverride = nil
|
||||||
|
} else {
|
||||||
|
self.titleOverride = newTitle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Close a surface from a view.
|
/// Close a surface from a view.
|
||||||
func closeSurface(
|
func closeSurface(
|
||||||
_ view: Ghostty.SurfaceView,
|
_ view: Ghostty.SurfaceView,
|
||||||
|
|
@ -718,10 +758,13 @@ class BaseTerminalController: NSWindowController,
|
||||||
}
|
}
|
||||||
|
|
||||||
private func titleDidChange(to: String) {
|
private func titleDidChange(to: String) {
|
||||||
|
lastComputedTitle = to
|
||||||
|
applyTitleToWindow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTitleToWindow() {
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
window.title = titleOverride ?? lastComputedTitle
|
||||||
// Set the main window title
|
|
||||||
window.title = to
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func pwdDidChange(to: URL?) {
|
func pwdDidChange(to: URL?) {
|
||||||
|
|
@ -1017,6 +1060,10 @@ class BaseTerminalController: NSWindowController,
|
||||||
window.performClose(sender)
|
window.performClose(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction func changeTabTitle(_ sender: Any) {
|
||||||
|
promptTabTitle()
|
||||||
|
}
|
||||||
|
|
||||||
@IBAction func splitRight(_ sender: Any) {
|
@IBAction func splitRight(_ sender: Any) {
|
||||||
guard let surface = focusedSurface?.surface else { return }
|
guard let surface = focusedSurface?.surface else { return }
|
||||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private(set) var derivedConfig: DerivedConfig
|
private(set) var derivedConfig: DerivedConfig
|
||||||
|
|
||||||
|
|
||||||
/// The notification cancellable for focused surface property changes.
|
/// The notification cancellable for focused surface property changes.
|
||||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
|
@ -148,7 +149,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
|
|
||||||
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
override func surfaceTreeDidChange(from: SplitTree<Ghostty.SurfaceView>, to: SplitTree<Ghostty.SurfaceView>) {
|
||||||
super.surfaceTreeDidChange(from: from, to: to)
|
super.surfaceTreeDidChange(from: from, to: to)
|
||||||
|
|
||||||
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
// Whenever our surface tree changes in any way (new split, close split, etc.)
|
||||||
// we want to invalidate our state.
|
// we want to invalidate our state.
|
||||||
invalidateRestorableState()
|
invalidateRestorableState()
|
||||||
|
|
@ -195,7 +196,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
$0.window?.isMainWindow ?? false
|
$0.window?.isMainWindow ?? false
|
||||||
} ?? lastMain ?? all.last
|
} ?? lastMain ?? all.last
|
||||||
}
|
}
|
||||||
|
|
||||||
// The last controller to be main. We use this when paired with "preferredParent"
|
// The last controller to be main. We use this when paired with "preferredParent"
|
||||||
// to find the preferred window to attach new tabs, perform actions, etc. We
|
// to find the preferred window to attach new tabs, perform actions, etc. We
|
||||||
// always prefer the main window but if there isn't any (because we're triggered
|
// always prefer the main window but if there isn't any (because we're triggered
|
||||||
|
|
@ -517,13 +518,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
fromTopLeftOffsetX: CGFloat(x),
|
fromTopLeftOffsetX: CGFloat(x),
|
||||||
offsetY: CGFloat(y),
|
offsetY: CGFloat(y),
|
||||||
windowSize: frame.size)
|
windowSize: frame.size)
|
||||||
|
|
||||||
// Clamp the origin to ensure the window stays fully visible on screen
|
// Clamp the origin to ensure the window stays fully visible on screen
|
||||||
var safeOrigin = origin
|
var safeOrigin = origin
|
||||||
let vf = screen.visibleFrame
|
let vf = screen.visibleFrame
|
||||||
safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width)
|
safeOrigin.x = min(max(safeOrigin.x, vf.minX), vf.maxX - frame.width)
|
||||||
safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height)
|
safeOrigin.y = min(max(safeOrigin.y, vf.minY), vf.maxY - frame.height)
|
||||||
|
|
||||||
// Return our new origin
|
// Return our new origin
|
||||||
var result = frame
|
var result = frame
|
||||||
result.origin = safeOrigin
|
result.origin = safeOrigin
|
||||||
|
|
@ -558,7 +559,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
closeWindowImmediately()
|
closeWindowImmediately()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo
|
// Undo
|
||||||
if let undoManager, let undoState {
|
if let undoManager, let undoState {
|
||||||
// Register undo action to restore the tab
|
// Register undo action to restore the tab
|
||||||
|
|
@ -579,15 +580,15 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.close()
|
window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func closeOtherTabsImmediately() {
|
private func closeOtherTabsImmediately() {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
guard let tabGroup = window.tabGroup else { return }
|
guard let tabGroup = window.tabGroup else { return }
|
||||||
guard tabGroup.windows.count > 1 else { return }
|
guard tabGroup.windows.count > 1 else { return }
|
||||||
|
|
||||||
// Start an undo grouping
|
// Start an undo grouping
|
||||||
if let undoManager {
|
if let undoManager {
|
||||||
undoManager.beginUndoGrouping()
|
undoManager.beginUndoGrouping()
|
||||||
|
|
@ -595,7 +596,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
defer {
|
defer {
|
||||||
undoManager?.endUndoGrouping()
|
undoManager?.endUndoGrouping()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate through all tabs except the current one.
|
// Iterate through all tabs except the current one.
|
||||||
for window in tabGroup.windows where window != self.window {
|
for window in tabGroup.windows where window != self.window {
|
||||||
// We ignore any non-terminal tabs. They don't currently exist and we can't
|
// We ignore any non-terminal tabs. They don't currently exist and we can't
|
||||||
|
|
@ -607,10 +608,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
controller.closeTabImmediately(registerRedo: false)
|
controller.closeTabImmediately(registerRedo: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let undoManager {
|
if let undoManager {
|
||||||
undoManager.setActionName("Close Other Tabs")
|
undoManager.setActionName("Close Other Tabs")
|
||||||
|
|
||||||
// We need to register an undo that refocuses this window. Otherwise, the
|
// We need to register an undo that refocuses this window. Otherwise, the
|
||||||
// undo operation above for each tab will steal focus.
|
// undo operation above for each tab will steal focus.
|
||||||
undoManager.registerUndo(
|
undoManager.registerUndo(
|
||||||
|
|
@ -620,7 +621,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
target.window?.makeKeyAndOrderFront(nil)
|
target.window?.makeKeyAndOrderFront(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register redo action
|
// Register redo action
|
||||||
undoManager.registerUndo(
|
undoManager.registerUndo(
|
||||||
withTarget: target,
|
withTarget: target,
|
||||||
|
|
@ -746,7 +747,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
case (nil, nil): return true
|
case (nil, nil): return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the index of the key window in our sorted states. This is a bit verbose
|
// Find the index of the key window in our sorted states. This is a bit verbose
|
||||||
// but we only need this for this style of undo so we don't want to add it to
|
// but we only need this for this style of undo so we don't want to add it to
|
||||||
// UndoState.
|
// UndoState.
|
||||||
|
|
@ -772,12 +773,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
let controllers = undoStates.map { undoState in
|
let controllers = undoStates.map { undoState in
|
||||||
TerminalController(ghostty, with: undoState)
|
TerminalController(ghostty, with: undoState)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The first controller becomes the parent window for all tabs.
|
// The first controller becomes the parent window for all tabs.
|
||||||
// If we don't have a first controller (shouldn't be possible?)
|
// If we don't have a first controller (shouldn't be possible?)
|
||||||
// then we can't restore tabs.
|
// then we can't restore tabs.
|
||||||
guard let firstController = controllers.first else { return }
|
guard let firstController = controllers.first else { return }
|
||||||
|
|
||||||
// Add all subsequent controllers as tabs to the first window
|
// Add all subsequent controllers as tabs to the first window
|
||||||
for controller in controllers.dropFirst() {
|
for controller in controllers.dropFirst() {
|
||||||
controller.showWindow(nil)
|
controller.showWindow(nil)
|
||||||
|
|
@ -786,7 +787,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
firstWindow.addTabbedWindow(newWindow, ordered: .above)
|
firstWindow.addTabbedWindow(newWindow, ordered: .above)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the appropriate window key. If we had a key window, restore it.
|
// Make the appropriate window key. If we had a key window, restore it.
|
||||||
// Otherwise, make the last window key.
|
// Otherwise, make the last window key.
|
||||||
if let keyWindowIndex, keyWindowIndex < controllers.count {
|
if let keyWindowIndex, keyWindowIndex < controllers.count {
|
||||||
|
|
@ -852,6 +853,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
let focusedSurface: UUID?
|
let focusedSurface: UUID?
|
||||||
let tabIndex: Int?
|
let tabIndex: Int?
|
||||||
weak var tabGroup: NSWindowTabGroup?
|
weak var tabGroup: NSWindowTabGroup?
|
||||||
|
let tabColor: TerminalTabColor
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(_ ghostty: Ghostty.App,
|
convenience init(_ ghostty: Ghostty.App,
|
||||||
|
|
@ -863,6 +865,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
showWindow(nil)
|
showWindow(nil)
|
||||||
if let window {
|
if let window {
|
||||||
window.setFrame(undoState.frame, display: true)
|
window.setFrame(undoState.frame, display: true)
|
||||||
|
if let terminalWindow = window as? TerminalWindow {
|
||||||
|
terminalWindow.tabColor = undoState.tabColor
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a tab group and index, restore the tab to its original position
|
// If we have a tab group and index, restore the tab to its original position
|
||||||
if let tabGroup = undoState.tabGroup,
|
if let tabGroup = undoState.tabGroup,
|
||||||
|
|
@ -898,7 +903,8 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
surfaceTree: surfaceTree,
|
surfaceTree: surfaceTree,
|
||||||
focusedSurface: focusedSurface?.id,
|
focusedSurface: focusedSurface?.id,
|
||||||
tabIndex: window.tabGroup?.windows.firstIndex(of: window),
|
tabIndex: window.tabGroup?.windows.firstIndex(of: window),
|
||||||
tabGroup: window.tabGroup)
|
tabGroup: window.tabGroup,
|
||||||
|
tabColor: (window as? TerminalWindow)?.tabColor ?? .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - NSWindowController
|
//MARK: - NSWindowController
|
||||||
|
|
@ -939,14 +945,14 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
viewModel: self,
|
viewModel: self,
|
||||||
delegate: self,
|
delegate: self,
|
||||||
))
|
))
|
||||||
|
|
||||||
// If we have a default size, we want to apply it.
|
// If we have a default size, we want to apply it.
|
||||||
if let defaultSize {
|
if let defaultSize {
|
||||||
switch (defaultSize) {
|
switch (defaultSize) {
|
||||||
case .frame:
|
case .frame:
|
||||||
// Frames can be applied immediately
|
// Frames can be applied immediately
|
||||||
defaultSize.apply(to: window)
|
defaultSize.apply(to: window)
|
||||||
|
|
||||||
case .contentIntrinsicSize:
|
case .contentIntrinsicSize:
|
||||||
// Content intrinsic size requires a short delay so that AppKit
|
// Content intrinsic size requires a short delay so that AppKit
|
||||||
// can layout our SwiftUI views.
|
// can layout our SwiftUI views.
|
||||||
|
|
@ -956,13 +962,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store our initial frame so we can know our default later. This MUST
|
// Store our initial frame so we can know our default later. This MUST
|
||||||
// be after the defaultSize call above so that we don't re-apply our frame.
|
// be after the defaultSize call above so that we don't re-apply our frame.
|
||||||
// Note: we probably want to set this on the first frame change or something
|
// Note: we probably want to set this on the first frame change or something
|
||||||
// so it respects cascade.
|
// so it respects cascade.
|
||||||
initialFrame = window.frame
|
initialFrame = window.frame
|
||||||
|
|
||||||
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
// In various situations, macOS automatically tabs new windows. Ghostty handles
|
||||||
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
|
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
|
||||||
// it.
|
// it.
|
||||||
|
|
@ -1073,7 +1079,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
if let window {
|
if let window {
|
||||||
LastWindowPosition.shared.save(window)
|
LastWindowPosition.shared.save(window)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember our last main
|
// Remember our last main
|
||||||
Self.lastMain = self
|
Self.lastMain = self
|
||||||
}
|
}
|
||||||
|
|
@ -1120,7 +1126,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
@IBAction func closeOtherTabs(_ sender: Any?) {
|
@IBAction func closeOtherTabs(_ sender: Any?) {
|
||||||
guard let window = window else { return }
|
guard let window = window else { return }
|
||||||
guard let tabGroup = window.tabGroup else { return }
|
guard let tabGroup = window.tabGroup else { return }
|
||||||
|
|
||||||
// If we only have one window then we have no other tabs to close
|
// If we only have one window then we have no other tabs to close
|
||||||
guard tabGroup.windows.count > 1 else { return }
|
guard tabGroup.windows.count > 1 else { return }
|
||||||
|
|
||||||
|
|
@ -1219,7 +1225,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
}
|
}
|
||||||
|
|
||||||
//MARK: - TerminalViewDelegate
|
//MARK: - TerminalViewDelegate
|
||||||
|
|
||||||
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) {
|
||||||
super.focusedSurfaceDidChange(to: to)
|
super.focusedSurfaceDidChange(to: to)
|
||||||
|
|
||||||
|
|
@ -1283,7 +1289,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
|
|
||||||
// Get our target window
|
// Get our target window
|
||||||
let targetWindow = tabbedWindows[finalIndex]
|
let targetWindow = tabbedWindows[finalIndex]
|
||||||
|
|
||||||
// Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs.
|
// Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs.
|
||||||
// I believe this is due to messed up constraints for our hacky tab bar. I'd like to
|
// I believe this is due to messed up constraints for our hacky tab bar. I'd like to
|
||||||
// find a better workaround. For now, this improves things dramatically.
|
// find a better workaround. For now, this improves things dramatically.
|
||||||
|
|
@ -1296,7 +1302,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
selectedWindow.makeKey()
|
selectedWindow.makeKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1451,24 +1457,24 @@ extension TerminalController {
|
||||||
guard let window, let tabGroup = window.tabGroup else { return false }
|
guard let window, let tabGroup = window.tabGroup else { return false }
|
||||||
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false }
|
guard let currentIndex = tabGroup.windows.firstIndex(of: window) else { return false }
|
||||||
return tabGroup.windows.enumerated().contains { $0.offset > currentIndex }
|
return tabGroup.windows.enumerated().contains { $0.offset > currentIndex }
|
||||||
|
|
||||||
case #selector(returnToDefaultSize):
|
case #selector(returnToDefaultSize):
|
||||||
guard let window else { return false }
|
guard let window else { return false }
|
||||||
|
|
||||||
// Native fullscreen windows can't revert to default size.
|
// Native fullscreen windows can't revert to default size.
|
||||||
if window.styleMask.contains(.fullScreen) {
|
if window.styleMask.contains(.fullScreen) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're fullscreen at all then we can't change size
|
// If we're fullscreen at all then we can't change size
|
||||||
if fullscreenStyle?.isFullscreen ?? false {
|
if fullscreenStyle?.isFullscreen ?? false {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If our window is already the default size or we don't have a
|
// If our window is already the default size or we don't have a
|
||||||
// default size, then disable.
|
// default size, then disable.
|
||||||
return defaultSize?.isChanged(for: window) ?? false
|
return defaultSize?.isChanged(for: window) ?? false
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.validateMenuItem(item)
|
return super.validateMenuItem(item)
|
||||||
}
|
}
|
||||||
|
|
@ -1484,10 +1490,10 @@ extension TerminalController {
|
||||||
enum DefaultSize {
|
enum DefaultSize {
|
||||||
/// A frame, set with `window.setFrame`
|
/// A frame, set with `window.setFrame`
|
||||||
case frame(NSRect)
|
case frame(NSRect)
|
||||||
|
|
||||||
/// A content size, set with `window.setContentSize`
|
/// A content size, set with `window.setContentSize`
|
||||||
case contentIntrinsicSize
|
case contentIntrinsicSize
|
||||||
|
|
||||||
func isChanged(for window: NSWindow) -> Bool {
|
func isChanged(for window: NSWindow) -> Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .frame(let rect):
|
case .frame(let rect):
|
||||||
|
|
@ -1496,11 +1502,11 @@ extension TerminalController {
|
||||||
guard let view = window.contentView else {
|
guard let view = window.contentView else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return view.frame.size != view.intrinsicContentSize
|
return view.frame.size != view.intrinsicContentSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apply(to window: NSWindow) {
|
func apply(to window: NSWindow) {
|
||||||
switch self {
|
switch self {
|
||||||
case .frame(let rect):
|
case .frame(let rect):
|
||||||
|
|
@ -1509,13 +1515,13 @@ extension TerminalController {
|
||||||
guard let size = window.contentView?.intrinsicContentSize else {
|
guard let size = window.contentView?.intrinsicContentSize else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setContentSize(size)
|
window.setContentSize(size)
|
||||||
window.constrainToScreen()
|
window.constrainToScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var defaultSize: DefaultSize? {
|
private var defaultSize: DefaultSize? {
|
||||||
if derivedConfig.maximize, let screen = window?.screen ?? NSScreen.main {
|
if derivedConfig.maximize, let screen = window?.screen ?? NSScreen.main {
|
||||||
// Maximize takes priority, we take up the full screen we're on.
|
// Maximize takes priority, we take up the full screen we're on.
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,20 @@ import Cocoa
|
||||||
class TerminalRestorableState: Codable {
|
class TerminalRestorableState: Codable {
|
||||||
static let selfKey = "state"
|
static let selfKey = "state"
|
||||||
static let versionKey = "version"
|
static let versionKey = "version"
|
||||||
static let version: Int = 5
|
static let version: Int = 7
|
||||||
|
|
||||||
let focusedSurface: String?
|
let focusedSurface: String?
|
||||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||||
let effectiveFullscreenMode: FullscreenMode?
|
let effectiveFullscreenMode: FullscreenMode?
|
||||||
|
let tabColor: TerminalTabColor
|
||||||
|
let titleOverride: String?
|
||||||
|
|
||||||
init(from controller: TerminalController) {
|
init(from controller: TerminalController) {
|
||||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||||
self.surfaceTree = controller.surfaceTree
|
self.surfaceTree = controller.surfaceTree
|
||||||
self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode
|
self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode
|
||||||
|
self.tabColor = (controller.window as? TerminalWindow)?.tabColor ?? .none
|
||||||
|
self.titleOverride = controller.titleOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(coder aDecoder: NSCoder) {
|
init?(coder aDecoder: NSCoder) {
|
||||||
|
|
@ -31,6 +35,8 @@ class TerminalRestorableState: Codable {
|
||||||
self.surfaceTree = v.value.surfaceTree
|
self.surfaceTree = v.value.surfaceTree
|
||||||
self.focusedSurface = v.value.focusedSurface
|
self.focusedSurface = v.value.focusedSurface
|
||||||
self.effectiveFullscreenMode = v.value.effectiveFullscreenMode
|
self.effectiveFullscreenMode = v.value.effectiveFullscreenMode
|
||||||
|
self.tabColor = v.value.tabColor
|
||||||
|
self.titleOverride = v.value.titleOverride
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(with coder: NSCoder) {
|
func encode(with coder: NSCoder) {
|
||||||
|
|
@ -94,6 +100,12 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore our tab color
|
||||||
|
(window as? TerminalWindow)?.tabColor = state.tabColor
|
||||||
|
|
||||||
|
// Restore the tab title override
|
||||||
|
c.titleOverride = state.titleOverride
|
||||||
|
|
||||||
// Setup our restored state on the controller
|
// Setup our restored state on the controller
|
||||||
// Find the focused surface in surfaceTree
|
// Find the focused surface in surfaceTree
|
||||||
if let focusedStr = state.focusedSurface {
|
if let focusedStr = state.focusedSurface {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum TerminalTabColor: Int, CaseIterable, Codable {
|
||||||
|
case none
|
||||||
|
case blue
|
||||||
|
case purple
|
||||||
|
case pink
|
||||||
|
case red
|
||||||
|
case orange
|
||||||
|
case yellow
|
||||||
|
case green
|
||||||
|
case teal
|
||||||
|
case graphite
|
||||||
|
|
||||||
|
var localizedName: String {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return "None"
|
||||||
|
case .blue:
|
||||||
|
return "Blue"
|
||||||
|
case .purple:
|
||||||
|
return "Purple"
|
||||||
|
case .pink:
|
||||||
|
return "Pink"
|
||||||
|
case .red:
|
||||||
|
return "Red"
|
||||||
|
case .orange:
|
||||||
|
return "Orange"
|
||||||
|
case .yellow:
|
||||||
|
return "Yellow"
|
||||||
|
case .green:
|
||||||
|
return "Green"
|
||||||
|
case .teal:
|
||||||
|
return "Teal"
|
||||||
|
case .graphite:
|
||||||
|
return "Graphite"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayColor: NSColor? {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
case .blue:
|
||||||
|
return .systemBlue
|
||||||
|
case .purple:
|
||||||
|
return .systemPurple
|
||||||
|
case .pink:
|
||||||
|
return .systemPink
|
||||||
|
case .red:
|
||||||
|
return .systemRed
|
||||||
|
case .orange:
|
||||||
|
return .systemOrange
|
||||||
|
case .yellow:
|
||||||
|
return .systemYellow
|
||||||
|
case .green:
|
||||||
|
return .systemGreen
|
||||||
|
case .teal:
|
||||||
|
if #available(macOS 13.0, *) {
|
||||||
|
return .systemMint
|
||||||
|
} else {
|
||||||
|
return .systemTeal
|
||||||
|
}
|
||||||
|
case .graphite:
|
||||||
|
return .systemGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func swatchImage(selected: Bool) -> NSImage {
|
||||||
|
let size = NSSize(width: 18, height: 18)
|
||||||
|
return NSImage(size: size, flipped: false) { rect in
|
||||||
|
let circleRect = rect.insetBy(dx: 1, dy: 1)
|
||||||
|
let circlePath = NSBezierPath(ovalIn: circleRect)
|
||||||
|
|
||||||
|
if let fillColor = self.displayColor {
|
||||||
|
fillColor.setFill()
|
||||||
|
circlePath.fill()
|
||||||
|
} else {
|
||||||
|
NSColor.clear.setFill()
|
||||||
|
circlePath.fill()
|
||||||
|
NSColor.quaternaryLabelColor.setStroke()
|
||||||
|
circlePath.lineWidth = 1
|
||||||
|
circlePath.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
if self == .none {
|
||||||
|
let slash = NSBezierPath()
|
||||||
|
slash.move(to: NSPoint(x: circleRect.minX + 2, y: circleRect.minY + 2))
|
||||||
|
slash.line(to: NSPoint(x: circleRect.maxX - 2, y: circleRect.maxY - 2))
|
||||||
|
slash.lineWidth = 1.5
|
||||||
|
NSColor.secondaryLabelColor.setStroke()
|
||||||
|
slash.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
if selected {
|
||||||
|
let highlight = NSBezierPath(ovalIn: rect.insetBy(dx: 0.5, dy: 0.5))
|
||||||
|
highlight.lineWidth = 2
|
||||||
|
NSColor.controlAccentColor.setStroke()
|
||||||
|
highlight.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Menu View
|
||||||
|
|
||||||
|
/// A SwiftUI view displaying a color palette for tab color selection.
|
||||||
|
/// Used as a custom view inside an NSMenuItem in the tab context menu.
|
||||||
|
struct TabColorMenuView: View {
|
||||||
|
@State private var currentSelection: TerminalTabColor
|
||||||
|
let onSelect: (TerminalTabColor) -> Void
|
||||||
|
|
||||||
|
init(selectedColor: TerminalTabColor, onSelect: @escaping (TerminalTabColor) -> Void) {
|
||||||
|
self._currentSelection = State(initialValue: selectedColor)
|
||||||
|
self.onSelect = onSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text("Tab Color")
|
||||||
|
.padding(.bottom, 2)
|
||||||
|
|
||||||
|
ForEach(Self.paletteRows, id: \.self) { row in
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ForEach(row, id: \.self) { color in
|
||||||
|
TabColorSwatch(
|
||||||
|
color: color,
|
||||||
|
isSelected: color == currentSelection
|
||||||
|
) {
|
||||||
|
currentSelection = color
|
||||||
|
onSelect(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, Self.leadingPadding)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
.padding(.top, 4)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let paletteRows: [[TerminalTabColor]] = [
|
||||||
|
[.none, .blue, .purple, .pink, .red],
|
||||||
|
[.orange, .yellow, .green, .teal, .graphite],
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Leading padding to align with the menu's icon gutter.
|
||||||
|
/// macOS 26 introduced icons in menus, requiring additional padding.
|
||||||
|
private static var leadingPadding: CGFloat {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
return 40
|
||||||
|
} else {
|
||||||
|
return 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single color swatch button in the tab color palette.
|
||||||
|
private struct TabColorSwatch: View {
|
||||||
|
let color: TerminalTabColor
|
||||||
|
let isSelected: Bool
|
||||||
|
let action: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: action) {
|
||||||
|
Group {
|
||||||
|
if color == .none {
|
||||||
|
Image(systemName: isSelected ? "circle.slash" : "circle")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else if let displayColor = color.displayColor {
|
||||||
|
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle.fill")
|
||||||
|
.foregroundStyle(Color(nsColor: displayColor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.help(color.localizedName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,10 +7,10 @@ import GhosttyKit
|
||||||
class TerminalWindow: NSWindow {
|
class TerminalWindow: NSWindow {
|
||||||
/// Posted when a terminal window awakes from nib.
|
/// Posted when a terminal window awakes from nib.
|
||||||
static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake")
|
static let terminalDidAwake = Notification.Name("TerminalWindowDidAwake")
|
||||||
|
|
||||||
/// Posted when a terminal window will close
|
/// Posted when a terminal window will close
|
||||||
static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose")
|
static let terminalWillCloseNotification = Notification.Name("TerminalWindowWillClose")
|
||||||
|
|
||||||
/// This is the key in UserDefaults to use for the default `level` value. This is
|
/// This is the key in UserDefaults to use for the default `level` value. This is
|
||||||
/// used by the manual float on top menu item feature.
|
/// used by the manual float on top menu item feature.
|
||||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||||
|
|
@ -20,15 +20,23 @@ class TerminalWindow: NSWindow {
|
||||||
|
|
||||||
/// Reset split zoom button in titlebar
|
/// Reset split zoom button in titlebar
|
||||||
private let resetZoomAccessory = NSTitlebarAccessoryViewController()
|
private let resetZoomAccessory = NSTitlebarAccessoryViewController()
|
||||||
|
|
||||||
/// Update notification UI in titlebar
|
/// Update notification UI in titlebar
|
||||||
private let updateAccessory = NSTitlebarAccessoryViewController()
|
private let updateAccessory = NSTitlebarAccessoryViewController()
|
||||||
|
|
||||||
|
/// Visual indicator that mirrors the selected tab color.
|
||||||
|
private lazy var tabColorIndicator: NSHostingView<TabColorIndicatorView> = {
|
||||||
|
let view = NSHostingView(rootView: TabColorIndicatorView(tabColor: tabColor))
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||||
private(set) var derivedConfig: DerivedConfig = .init()
|
private(set) var derivedConfig: DerivedConfig = .init()
|
||||||
|
|
||||||
private var tabMenuObserver: NSObjectProtocol? = nil
|
|
||||||
|
|
||||||
|
/// Sets up our tab context menu
|
||||||
|
private var tabMenuObserver: NSObjectProtocol? = nil
|
||||||
|
|
||||||
/// Whether this window supports the update accessory. If this is false, then views within this
|
/// Whether this window supports the update accessory. If this is false, then views within this
|
||||||
/// window should determine how to show update notifications.
|
/// window should determine how to show update notifications.
|
||||||
var supportsUpdateAccessory: Bool {
|
var supportsUpdateAccessory: Bool {
|
||||||
|
|
@ -40,7 +48,17 @@ class TerminalWindow: NSWindow {
|
||||||
var terminalController: TerminalController? {
|
var terminalController: TerminalController? {
|
||||||
windowController as? TerminalController
|
windowController as? TerminalController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The color assigned to this window's tab. Setting this updates the tab color indicator
|
||||||
|
/// and marks the window's restorable state as dirty.
|
||||||
|
var tabColor: TerminalTabColor = .none {
|
||||||
|
didSet {
|
||||||
|
guard tabColor != oldValue else { return }
|
||||||
|
tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor)
|
||||||
|
invalidateRestorableState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: NSWindow Overrides
|
// MARK: NSWindow Overrides
|
||||||
|
|
||||||
override var toolbar: NSToolbar? {
|
override var toolbar: NSToolbar? {
|
||||||
|
|
@ -66,7 +84,7 @@ class TerminalWindow: NSWindow {
|
||||||
guard let self, let menu = n.object as? NSMenu else { return }
|
guard let self, let menu = n.object as? NSMenu else { return }
|
||||||
self.configureTabContextMenuIfNeeded(menu)
|
self.configureTabContextMenuIfNeeded(menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is required so that window restoration properly creates our tabs
|
// This is required so that window restoration properly creates our tabs
|
||||||
// again. I'm not sure why this is required. If you don't do this, then
|
// again. I'm not sure why this is required. If you don't do this, then
|
||||||
// tabs restore as separate windows.
|
// tabs restore as separate windows.
|
||||||
|
|
@ -74,14 +92,14 @@ class TerminalWindow: NSWindow {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.tabbingMode = .automatic
|
self.tabbingMode = .automatic
|
||||||
}
|
}
|
||||||
|
|
||||||
// All new windows are based on the app config at the time of creation.
|
// All new windows are based on the app config at the time of creation.
|
||||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return }
|
guard let appDelegate = NSApp.delegate as? AppDelegate else { return }
|
||||||
let config = appDelegate.ghostty.config
|
let config = appDelegate.ghostty.config
|
||||||
|
|
||||||
// Setup our initial config
|
// Setup our initial config
|
||||||
derivedConfig = .init(config)
|
derivedConfig = .init(config)
|
||||||
|
|
||||||
// If there is a hardcoded title in the configuration, we set that
|
// If there is a hardcoded title in the configuration, we set that
|
||||||
// immediately. Future `set_title` apprt actions will override this
|
// immediately. Future `set_title` apprt actions will override this
|
||||||
// if necessary but this ensures our window loads with the proper
|
// if necessary but this ensures our window loads with the proper
|
||||||
|
|
@ -116,7 +134,7 @@ class TerminalWindow: NSWindow {
|
||||||
}))
|
}))
|
||||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||||
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
// Create update notification accessory
|
// Create update notification accessory
|
||||||
if supportsUpdateAccessory {
|
if supportsUpdateAccessory {
|
||||||
updateAccessory.layoutAttribute = .right
|
updateAccessory.layoutAttribute = .right
|
||||||
|
|
@ -132,9 +150,16 @@ class TerminalWindow: NSWindow {
|
||||||
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||||
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
||||||
// where buttons were not clickable.
|
// where buttons were not clickable.
|
||||||
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton])
|
tabColorIndicator.rootView = TabColorIndicatorView(tabColor: tabColor)
|
||||||
|
|
||||||
|
let stackView = NSStackView()
|
||||||
|
stackView.orientation = .horizontal
|
||||||
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
||||||
stackView.spacing = 3
|
stackView.spacing = 4
|
||||||
|
stackView.alignment = .centerY
|
||||||
|
stackView.addArrangedSubview(tabColorIndicator)
|
||||||
|
stackView.addArrangedSubview(keyEquivalentLabel)
|
||||||
|
stackView.addArrangedSubview(resetZoomTabButton)
|
||||||
tab.accessoryView = stackView
|
tab.accessoryView = stackView
|
||||||
|
|
||||||
// Get our saved level
|
// Get our saved level
|
||||||
|
|
@ -145,7 +170,7 @@ class TerminalWindow: NSWindow {
|
||||||
// still become key/main and receive events.
|
// still become key/main and receive events.
|
||||||
override var canBecomeKey: Bool { return true }
|
override var canBecomeKey: Bool { return true }
|
||||||
override var canBecomeMain: Bool { return true }
|
override var canBecomeMain: Bool { return true }
|
||||||
|
|
||||||
override func close() {
|
override func close() {
|
||||||
NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
|
NotificationCenter.default.post(name: Self.terminalWillCloseNotification, object: self)
|
||||||
super.close()
|
super.close()
|
||||||
|
|
@ -215,8 +240,6 @@ class TerminalWindow: NSWindow {
|
||||||
/// added.
|
/// added.
|
||||||
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar")
|
||||||
|
|
||||||
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
|
|
||||||
|
|
||||||
func findTitlebarView() -> NSView? {
|
func findTitlebarView() -> NSView? {
|
||||||
// Find our tab bar. If it doesn't exist we don't do anything.
|
// Find our tab bar. If it doesn't exist we don't do anything.
|
||||||
//
|
//
|
||||||
|
|
@ -279,7 +302,7 @@ class TerminalWindow: NSWindow {
|
||||||
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) {
|
||||||
removeTitlebarAccessoryViewController(at: idx)
|
removeTitlebarAccessoryViewController(at: idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't need to do this with the update accessory. I don't know why but
|
// We don't need to do this with the update accessory. I don't know why but
|
||||||
// everything works fine.
|
// everything works fine.
|
||||||
}
|
}
|
||||||
|
|
@ -292,52 +315,6 @@ class TerminalWindow: NSWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
|
|
||||||
guard isTabContextMenu(menu) else { return }
|
|
||||||
|
|
||||||
// Get the target from an existing menu item. The native tab context menu items
|
|
||||||
// target the specific window/controller that was right-clicked, not the focused one.
|
|
||||||
// We need to use that same target so validation and action use the correct tab.
|
|
||||||
let targetController = menu.items
|
|
||||||
.first { $0.action == NSSelectorFromString("performClose:") }
|
|
||||||
.flatMap { $0.target as? NSWindow }
|
|
||||||
.flatMap { $0.windowController as? TerminalController }
|
|
||||||
|
|
||||||
// Close tabs to the right
|
|
||||||
let item = NSMenuItem(title: "Close Tabs to the Right", action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "")
|
|
||||||
item.identifier = Self.closeTabsOnRightMenuItemIdentifier
|
|
||||||
item.target = targetController
|
|
||||||
item.setImageIfDesired(systemSymbolName: "xmark")
|
|
||||||
if !menu.insertItem(item, after: NSSelectorFromString("performCloseOtherTabs:")) &&
|
|
||||||
!menu.insertItem(item, after: NSSelectorFromString("performClose:")) {
|
|
||||||
menu.addItem(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other close items should have the xmark to match Safari on macOS 26
|
|
||||||
for menuItem in menu.items {
|
|
||||||
if menuItem.action == NSSelectorFromString("performClose:") ||
|
|
||||||
menuItem.action == NSSelectorFromString("performCloseOtherTabs:") {
|
|
||||||
menuItem.setImageIfDesired(systemSymbolName: "xmark")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
|
|
||||||
guard NSApp.keyWindow === self else { return false }
|
|
||||||
|
|
||||||
// These are the target selectors, at least for macOS 26.
|
|
||||||
let tabContextSelectors: Set<String> = [
|
|
||||||
"performClose:",
|
|
||||||
"performCloseOtherTabs:",
|
|
||||||
"moveTabToNewWindow:",
|
|
||||||
"toggleTabOverview:"
|
|
||||||
]
|
|
||||||
|
|
||||||
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
|
|
||||||
return !selectorNames.isDisjoint(with: tabContextSelectors)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: Tab Key Equivalents
|
// MARK: Tab Key Equivalents
|
||||||
|
|
||||||
var keyEquivalent: String? = nil {
|
var keyEquivalent: String? = nil {
|
||||||
|
|
@ -549,7 +526,7 @@ class TerminalWindow: NSWindow {
|
||||||
|
|
||||||
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
|
private func setInitialWindowPosition(x: Int16?, y: Int16?) {
|
||||||
// If we don't have an X/Y then we try to use the previously saved window pos.
|
// If we don't have an X/Y then we try to use the previously saved window pos.
|
||||||
guard let x, let y else {
|
guard x != nil, y != nil else {
|
||||||
if (!LastWindowPosition.shared.restore(self)) {
|
if (!LastWindowPosition.shared.restore(self)) {
|
||||||
center()
|
center()
|
||||||
}
|
}
|
||||||
|
|
@ -568,7 +545,7 @@ class TerminalWindow: NSWindow {
|
||||||
center()
|
center()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen)
|
let frame = terminalController.adjustForWindowPosition(frame: frame, on: screen)
|
||||||
setFrameOrigin(frame.origin)
|
setFrameOrigin(frame.origin)
|
||||||
}
|
}
|
||||||
|
|
@ -584,7 +561,7 @@ class TerminalWindow: NSWindow {
|
||||||
NotificationCenter.default.removeObserver(observer)
|
NotificationCenter.default.removeObserver(observer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Config
|
// MARK: Config
|
||||||
|
|
||||||
struct DerivedConfig {
|
struct DerivedConfig {
|
||||||
|
|
@ -651,12 +628,12 @@ extension TerminalWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A pill-shaped button that displays update status and provides access to update actions.
|
/// A pill-shaped button that displays update status and provides access to update actions.
|
||||||
struct UpdateAccessoryView: View {
|
struct UpdateAccessoryView: View {
|
||||||
@ObservedObject var viewModel: ViewModel
|
@ObservedObject var viewModel: ViewModel
|
||||||
@ObservedObject var model: UpdateViewModel
|
@ObservedObject var model: UpdateViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// We use the same top/trailing padding so that it hugs the same.
|
// We use the same top/trailing padding so that it hugs the same.
|
||||||
UpdatePill(model: model)
|
UpdatePill(model: model)
|
||||||
|
|
@ -666,3 +643,120 @@ extension TerminalWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A small circle indicator displayed in the tab accessory view that shows
|
||||||
|
/// the user-assigned tab color. When no color is set, the view is hidden.
|
||||||
|
private struct TabColorIndicatorView: View {
|
||||||
|
/// The tab color to display.
|
||||||
|
let tabColor: TerminalTabColor
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let color = tabColor.displayColor {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(color))
|
||||||
|
.frame(width: 6, height: 6)
|
||||||
|
} else {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.clear)
|
||||||
|
.frame(width: 6, height: 6)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Tab Context Menu
|
||||||
|
|
||||||
|
extension TerminalWindow {
|
||||||
|
private static let closeTabsOnRightMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.closeTabsOnTheRightMenuItem")
|
||||||
|
private static let changeTitleMenuItemIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.changeTitleMenuItem")
|
||||||
|
private static let tabColorSeparatorIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorSeparator")
|
||||||
|
|
||||||
|
private static let tabColorPaletteIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorPalette")
|
||||||
|
|
||||||
|
func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
|
||||||
|
guard isTabContextMenu(menu) else { return }
|
||||||
|
|
||||||
|
// Get the target from an existing menu item. The native tab context menu items
|
||||||
|
// target the specific window/controller that was right-clicked, not the focused one.
|
||||||
|
// We need to use that same target so validation and action use the correct tab.
|
||||||
|
let targetController = menu.items
|
||||||
|
.first { $0.action == NSSelectorFromString("performClose:") }
|
||||||
|
.flatMap { $0.target as? NSWindow }
|
||||||
|
.flatMap { $0.windowController as? TerminalController }
|
||||||
|
|
||||||
|
// Close tabs to the right
|
||||||
|
let item = NSMenuItem(title: "Close Tabs to the Right", action: #selector(TerminalController.closeTabsOnTheRight(_:)), keyEquivalent: "")
|
||||||
|
item.identifier = Self.closeTabsOnRightMenuItemIdentifier
|
||||||
|
item.target = targetController
|
||||||
|
item.setImageIfDesired(systemSymbolName: "xmark")
|
||||||
|
if menu.insertItem(item, after: NSSelectorFromString("performCloseOtherTabs:")) == nil,
|
||||||
|
menu.insertItem(item, after: NSSelectorFromString("performClose:")) == nil {
|
||||||
|
menu.addItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other close items should have the xmark to match Safari on macOS 26
|
||||||
|
for menuItem in menu.items {
|
||||||
|
if menuItem.action == NSSelectorFromString("performClose:") ||
|
||||||
|
menuItem.action == NSSelectorFromString("performCloseOtherTabs:") {
|
||||||
|
menuItem.setImageIfDesired(systemSymbolName: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTabModifierSection(to: menu, target: targetController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTabContextMenu(_ menu: NSMenu) -> Bool {
|
||||||
|
guard NSApp.keyWindow === self else { return false }
|
||||||
|
|
||||||
|
// These are the target selectors, at least for macOS 26.
|
||||||
|
let tabContextSelectors: Set<String> = [
|
||||||
|
"performClose:",
|
||||||
|
"performCloseOtherTabs:",
|
||||||
|
"moveTabToNewWindow:",
|
||||||
|
"toggleTabOverview:"
|
||||||
|
]
|
||||||
|
|
||||||
|
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
|
||||||
|
return !selectorNames.isDisjoint(with: tabContextSelectors)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func appendTabModifierSection(to menu: NSMenu, target: TerminalController?) {
|
||||||
|
menu.removeItems(withIdentifiers: [
|
||||||
|
Self.tabColorSeparatorIdentifier,
|
||||||
|
Self.changeTitleMenuItemIdentifier,
|
||||||
|
Self.tabColorPaletteIdentifier
|
||||||
|
])
|
||||||
|
|
||||||
|
let separator = NSMenuItem.separator()
|
||||||
|
separator.identifier = Self.tabColorSeparatorIdentifier
|
||||||
|
menu.addItem(separator)
|
||||||
|
|
||||||
|
// Change Title...
|
||||||
|
let changeTitleItem = NSMenuItem(title: "Change Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "")
|
||||||
|
changeTitleItem.identifier = Self.changeTitleMenuItemIdentifier
|
||||||
|
changeTitleItem.target = target
|
||||||
|
changeTitleItem.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||||
|
menu.addItem(changeTitleItem)
|
||||||
|
|
||||||
|
let paletteItem = NSMenuItem()
|
||||||
|
paletteItem.identifier = Self.tabColorPaletteIdentifier
|
||||||
|
paletteItem.view = makeTabColorPaletteView(
|
||||||
|
selectedColor: (target?.window as? TerminalWindow)?.tabColor ?? .none
|
||||||
|
) { [weak target] color in
|
||||||
|
(target?.window as? TerminalWindow)?.tabColor = color
|
||||||
|
}
|
||||||
|
menu.addItem(paletteItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTabColorPaletteView(
|
||||||
|
selectedColor: TerminalTabColor,
|
||||||
|
selectionHandler: @escaping (TerminalTabColor) -> Void
|
||||||
|
) -> NSView {
|
||||||
|
let hostingView = NSHostingView(rootView: TabColorMenuView(
|
||||||
|
selectedColor: selectedColor,
|
||||||
|
onSelect: selectionHandler
|
||||||
|
))
|
||||||
|
hostingView.frame.size = hostingView.intrinsicContentSize
|
||||||
|
return hostingView
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,20 @@ extension Ghostty.Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PromptTitle {
|
||||||
|
case surface
|
||||||
|
case tab
|
||||||
|
|
||||||
|
init(_ c: ghostty_action_prompt_title_e) {
|
||||||
|
switch c {
|
||||||
|
case GHOSTTY_PROMPT_TITLE_TAB:
|
||||||
|
self = .tab
|
||||||
|
default:
|
||||||
|
self = .surface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Putting the initializer in an extension preserves the automatic one.
|
// Putting the initializer in an extension preserves the automatic one.
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,7 @@ extension Ghostty {
|
||||||
setTitle(app, target: target, v: action.action.set_title)
|
setTitle(app, target: target, v: action.action.set_title)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_PROMPT_TITLE:
|
case GHOSTTY_ACTION_PROMPT_TITLE:
|
||||||
return promptTitle(app, target: target)
|
return promptTitle(app, target: target, v: action.action.prompt_title)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_PWD:
|
case GHOSTTY_ACTION_PWD:
|
||||||
pwdChanged(app, target: target, v: action.action.pwd)
|
pwdChanged(app, target: target, v: action.action.pwd)
|
||||||
|
|
@ -1350,22 +1350,50 @@ extension Ghostty {
|
||||||
|
|
||||||
private static func promptTitle(
|
private static func promptTitle(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
target: ghostty_target_s) -> Bool {
|
target: ghostty_target_s,
|
||||||
switch (target.tag) {
|
v: ghostty_action_prompt_title_e) -> Bool {
|
||||||
case GHOSTTY_TARGET_APP:
|
let promptTitle = Action.PromptTitle(v)
|
||||||
Ghostty.logger.warning("set title prompt does nothing with an app target")
|
switch promptTitle {
|
||||||
return false
|
case .surface:
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
Ghostty.logger.warning("set title prompt does nothing with an app target")
|
||||||
|
return false
|
||||||
|
|
||||||
case GHOSTTY_TARGET_SURFACE:
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
guard let surface = target.target.surface else { return false }
|
guard let surface = target.target.surface else { return false }
|
||||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||||
surfaceView.promptTitle()
|
surfaceView.promptTitle()
|
||||||
|
return true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case .tab:
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
guard let window = NSApp.mainWindow ?? NSApp.keyWindow,
|
||||||
|
let controller = window.windowController as? BaseTerminalController
|
||||||
|
else { return false }
|
||||||
|
controller.promptTabTitle()
|
||||||
|
return true
|
||||||
|
|
||||||
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
guard let surface = target.target.surface else { return false }
|
||||||
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||||
|
guard let window = surfaceView.window,
|
||||||
|
let controller = window.windowController as? BaseTerminalController
|
||||||
|
else { return false }
|
||||||
|
controller.promptTabTitle()
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func pwdChanged(
|
private static func pwdChanged(
|
||||||
|
|
|
||||||
|
|
@ -1417,8 +1417,9 @@ extension Ghostty {
|
||||||
item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
|
item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
|
||||||
item.setImageIfDesired(systemSymbolName: "scope")
|
item.setImageIfDesired(systemSymbolName: "scope")
|
||||||
menu.addItem(.separator())
|
menu.addItem(.separator())
|
||||||
item = menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "")
|
item = menu.addItem(withTitle: "Change Tab Title...", action: #selector(BaseTerminalController.changeTabTitle(_:)), keyEquivalent: "")
|
||||||
item.setImageIfDesired(systemSymbolName: "pencil.line")
|
item.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||||
|
item = menu.addItem(withTitle: "Change Terminal Title...", action: #selector(changeTitle(_:)), keyEquivalent: "")
|
||||||
|
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,20 +10,33 @@ extension NSMenu {
|
||||||
/// - item: The menu item to insert.
|
/// - item: The menu item to insert.
|
||||||
/// - action: The action selector to search for. The new item will be inserted after the first
|
/// - action: The action selector to search for. The new item will be inserted after the first
|
||||||
/// item with this action.
|
/// item with this action.
|
||||||
/// - Returns: `true` if the item was inserted after the specified action, `false` if the action
|
/// - Returns: The index where the item was inserted, or `nil` if the action was not found
|
||||||
/// was not found and the item was not inserted.
|
/// and the item was not inserted.
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func insertItem(_ item: NSMenuItem, after action: Selector) -> Bool {
|
func insertItem(_ item: NSMenuItem, after action: Selector) -> UInt? {
|
||||||
if let identifier = item.identifier,
|
if let identifier = item.identifier,
|
||||||
let existing = items.first(where: { $0.identifier == identifier }) {
|
let existing = items.first(where: { $0.identifier == identifier }) {
|
||||||
removeItem(existing)
|
removeItem(existing)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let idx = items.firstIndex(where: { $0.action == action }) else {
|
guard let idx = items.firstIndex(where: { $0.action == action }) else {
|
||||||
return false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
insertItem(item, at: idx + 1)
|
let insertionIndex = idx + 1
|
||||||
return true
|
insertItem(item, at: insertionIndex)
|
||||||
|
return UInt(insertionIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all menu items whose identifier is in the given set.
|
||||||
|
///
|
||||||
|
/// - Parameter identifiers: The set of identifiers to match for removal.
|
||||||
|
func removeItems(withIdentifiers identifiers: Set<NSUserInterfaceItemIdentifier>) {
|
||||||
|
for (index, item) in items.enumerated().reversed() {
|
||||||
|
guard let identifier = item.identifier else { continue }
|
||||||
|
if identifiers.contains(identifier) {
|
||||||
|
removeItem(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ intended to be regenerated by code contributors. If there is a problem with
|
||||||
the template file, please reach out to a code contributor.
|
the template file, please reach out to a code contributor.
|
||||||
|
|
||||||
Instead, only edit the translation file corresponding to your language/locale,
|
Instead, only edit the translation file corresponding to your language/locale,
|
||||||
identified via the its _locale name_: for example, `de_DE.UTF-8.po` would be
|
identified via its _locale name_: for example, `de_DE.UTF-8.po` would be the
|
||||||
the translation file for German (language code `de`) as spoken in Germany
|
translation file for German (language code `de`) as spoken in Germany (country
|
||||||
(country code `DE`). The GNU `gettext` manual contains
|
code `DE`). The GNU `gettext` manual contains
|
||||||
[further information about locale names](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1),
|
[further information about locale names](https://www.gnu.org/software/gettext/manual/gettext.html#Locale-Names-1),
|
||||||
including a list of language and country codes.
|
including a list of language and country codes.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5183,7 +5183,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||||
.prompt_surface_title => return try self.rt_app.performAction(
|
.prompt_surface_title => return try self.rt_app.performAction(
|
||||||
.{ .surface = self },
|
.{ .surface = self },
|
||||||
.prompt_title,
|
.prompt_title,
|
||||||
{},
|
.surface,
|
||||||
|
),
|
||||||
|
|
||||||
|
.prompt_tab_title => return try self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.prompt_title,
|
||||||
|
.tab,
|
||||||
),
|
),
|
||||||
|
|
||||||
.clear_screen => {
|
.clear_screen => {
|
||||||
|
|
|
||||||
|
|
@ -189,8 +189,9 @@ pub const Action = union(Key) {
|
||||||
set_title: SetTitle,
|
set_title: SetTitle,
|
||||||
|
|
||||||
/// Set the title of the target to a prompted value. It is up to
|
/// Set the title of the target to a prompted value. It is up to
|
||||||
/// the apprt to prompt.
|
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||||
prompt_title,
|
/// surface title or the tab title.
|
||||||
|
prompt_title: PromptTitle,
|
||||||
|
|
||||||
/// The current working directory has changed for the target terminal.
|
/// The current working directory has changed for the target terminal.
|
||||||
pwd: Pwd,
|
pwd: Pwd,
|
||||||
|
|
@ -536,6 +537,12 @@ pub const MouseVisibility = enum(c_int) {
|
||||||
hidden,
|
hidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Whether to prompt for the surface title or tab title.
|
||||||
|
pub const PromptTitle = enum(c_int) {
|
||||||
|
surface,
|
||||||
|
tab,
|
||||||
|
};
|
||||||
|
|
||||||
pub const MouseOverLink = struct {
|
pub const MouseOverLink = struct {
|
||||||
url: [:0]const u8,
|
url: [:0]const u8,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -693,7 +693,7 @@ pub const Application = extern struct {
|
||||||
|
|
||||||
.progress_report => return Action.progressReport(target, value),
|
.progress_report => return Action.progressReport(target, value),
|
||||||
|
|
||||||
.prompt_title => return Action.promptTitle(target),
|
.prompt_title => return Action.promptTitle(target, value),
|
||||||
|
|
||||||
.quit => self.quit(),
|
.quit => self.quit(),
|
||||||
|
|
||||||
|
|
@ -2250,12 +2250,18 @@ const Action = struct {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn promptTitle(target: apprt.Target) bool {
|
pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool {
|
||||||
switch (target) {
|
switch (value) {
|
||||||
.app => return false,
|
.surface => switch (target) {
|
||||||
.surface => |v| {
|
.app => return false,
|
||||||
v.rt_surface.surface.promptTitle();
|
.surface => |v| {
|
||||||
return true;
|
v.rt_surface.surface.promptTitle();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.tab => {
|
||||||
|
// GTK does not yet support tab title prompting
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -519,6 +519,11 @@ pub const Action = union(enum) {
|
||||||
/// version can be found by running `ghostty +version`.
|
/// version can be found by running `ghostty +version`.
|
||||||
prompt_surface_title,
|
prompt_surface_title,
|
||||||
|
|
||||||
|
/// Change the title of the current tab/window via a pop-up prompt. The
|
||||||
|
/// title set via this prompt overrides any title set by the terminal
|
||||||
|
/// and persists across focus changes within the tab.
|
||||||
|
prompt_tab_title,
|
||||||
|
|
||||||
/// Create a new split in the specified direction.
|
/// Create a new split in the specified direction.
|
||||||
///
|
///
|
||||||
/// Valid arguments:
|
/// Valid arguments:
|
||||||
|
|
@ -1191,6 +1196,7 @@ pub const Action = union(enum) {
|
||||||
.reset_font_size,
|
.reset_font_size,
|
||||||
.set_font_size,
|
.set_font_size,
|
||||||
.prompt_surface_title,
|
.prompt_surface_title,
|
||||||
|
.prompt_tab_title,
|
||||||
.clear_screen,
|
.clear_screen,
|
||||||
.select_all,
|
.select_all,
|
||||||
.scroll_to_top,
|
.scroll_to_top,
|
||||||
|
|
|
||||||
|
|
@ -413,10 +413,16 @@ fn actionCommands(action: Action.Key) []const Command {
|
||||||
|
|
||||||
.prompt_surface_title => comptime &.{.{
|
.prompt_surface_title => comptime &.{.{
|
||||||
.action = .prompt_surface_title,
|
.action = .prompt_surface_title,
|
||||||
.title = "Change Title...",
|
.title = "Change Terminal Title...",
|
||||||
.description = "Prompt for a new title for the current terminal.",
|
.description = "Prompt for a new title for the current terminal.",
|
||||||
}},
|
}},
|
||||||
|
|
||||||
|
.prompt_tab_title => comptime &.{.{
|
||||||
|
.action = .prompt_tab_title,
|
||||||
|
.title = "Change Tab Title...",
|
||||||
|
.description = "Prompt for a new title for the current tab.",
|
||||||
|
}},
|
||||||
|
|
||||||
.new_split => comptime &.{
|
.new_split => comptime &.{
|
||||||
.{
|
.{
|
||||||
.action = .{ .new_split = .left },
|
.action = .{ .new_split = .left },
|
||||||
|
|
|
||||||
|
|
@ -1220,7 +1220,7 @@ const ReflowCursor = struct {
|
||||||
// with graphemes then we increase capacity.
|
// with graphemes then we increase capacity.
|
||||||
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
|
if (self.page.graphemeCount() >= self.page.graphemeCapacity()) {
|
||||||
try self.adjustCapacity(list, .{
|
try self.adjustCapacity(list, .{
|
||||||
.hyperlink_bytes = cap.grapheme_bytes * 2,
|
.grapheme_bytes = cap.grapheme_bytes * 2,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10758,3 +10758,86 @@ test "PageList clears history" {
|
||||||
.x = 0,
|
.x = 0,
|
||||||
}, s.getTopLeft(.active));
|
}, s.getTopLeft(.active));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "PageList resize reflow grapheme map capacity exceeded" {
|
||||||
|
// This test verifies that when reflowing content with many graphemes,
|
||||||
|
// the grapheme map capacity is correctly increased when needed.
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var s = try init(alloc, 4, 10, 0);
|
||||||
|
defer s.deinit();
|
||||||
|
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||||
|
|
||||||
|
// Get the grapheme capacity from the page. We need more than this many
|
||||||
|
// graphemes in a single destination page to trigger capacity increase
|
||||||
|
// during reflow. Since each source page can only hold this many graphemes,
|
||||||
|
// we create two source pages with graphemes that will merge into one
|
||||||
|
// destination page.
|
||||||
|
const grapheme_capacity = s.pages.first.?.data.graphemeCapacity();
|
||||||
|
// Use slightly more than half the capacity per page, so combined they
|
||||||
|
// exceed the capacity of a single destination page.
|
||||||
|
const graphemes_per_page = grapheme_capacity / 2 + grapheme_capacity / 4;
|
||||||
|
|
||||||
|
// Grow to the capacity of the first page and add more rows
|
||||||
|
// so that we have two pages total.
|
||||||
|
{
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
page.pauseIntegrityChecks(true);
|
||||||
|
for (page.size.rows..page.capacity.rows) |_| {
|
||||||
|
_ = try s.grow();
|
||||||
|
}
|
||||||
|
page.pauseIntegrityChecks(false);
|
||||||
|
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||||
|
try s.growRows(graphemes_per_page);
|
||||||
|
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||||
|
|
||||||
|
// We now have two pages.
|
||||||
|
try testing.expect(s.pages.first.? != s.pages.last.?);
|
||||||
|
try testing.expectEqual(s.pages.last.?, s.pages.first.?.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add graphemes to both pages. We add graphemes to rows at the END of the
|
||||||
|
// first page, and graphemes to rows at the START of the second page.
|
||||||
|
// When reflowing to 2 columns, these rows will wrap and stay together
|
||||||
|
// on the same destination page, requiring capacity increase.
|
||||||
|
|
||||||
|
// Add graphemes to the end of the first page (last rows)
|
||||||
|
{
|
||||||
|
const page = &s.pages.first.?.data;
|
||||||
|
const start_row = page.size.rows - graphemes_per_page;
|
||||||
|
for (0..graphemes_per_page) |i| {
|
||||||
|
const y = start_row + i;
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = 'A' },
|
||||||
|
};
|
||||||
|
try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0301)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add graphemes to the beginning of the second page
|
||||||
|
{
|
||||||
|
const page = &s.pages.last.?.data;
|
||||||
|
const count = @min(graphemes_per_page, page.size.rows);
|
||||||
|
for (0..count) |y| {
|
||||||
|
const rac = page.getRowAndCell(0, y);
|
||||||
|
rac.cell.* = .{
|
||||||
|
.content_tag = .codepoint,
|
||||||
|
.content = .{ .codepoint = 'B' },
|
||||||
|
};
|
||||||
|
try page.appendGrapheme(rac.row, rac.cell, @as(u21, @intCast(0x0302)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize to fewer columns to trigger reflow.
|
||||||
|
// The graphemes from both pages will be copied to destination pages.
|
||||||
|
// They will all end up in a contiguous region of the destination.
|
||||||
|
// If the bug exists (hyperlink_bytes increased instead of grapheme_bytes),
|
||||||
|
// this will fail with GraphemeMapOutOfMemory when we exceed capacity.
|
||||||
|
try s.resize(.{ .cols = 2, .reflow = true });
|
||||||
|
|
||||||
|
// Verify the resize succeeded
|
||||||
|
try testing.expectEqual(@as(usize, 2), s.cols);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -855,13 +855,17 @@ fn HashMapUnmanaged(
|
||||||
pub fn layoutForCapacity(new_capacity: Size) Layout {
|
pub fn layoutForCapacity(new_capacity: Size) Layout {
|
||||||
assert(new_capacity == 0 or std.math.isPowerOfTwo(new_capacity));
|
assert(new_capacity == 0 or std.math.isPowerOfTwo(new_capacity));
|
||||||
|
|
||||||
|
// Cast to usize to prevent overflow in size calculations.
|
||||||
|
// See: https://github.com/ziglang/zig/pull/19048
|
||||||
|
const cap: usize = new_capacity;
|
||||||
|
|
||||||
// Pack our metadata, keys, and values.
|
// Pack our metadata, keys, and values.
|
||||||
const meta_start = @sizeOf(Header);
|
const meta_start = @sizeOf(Header);
|
||||||
const meta_end = @sizeOf(Header) + new_capacity * @sizeOf(Metadata);
|
const meta_end = @sizeOf(Header) + cap * @sizeOf(Metadata);
|
||||||
const keys_start = std.mem.alignForward(usize, meta_end, key_align);
|
const keys_start = std.mem.alignForward(usize, meta_end, key_align);
|
||||||
const keys_end = keys_start + new_capacity * @sizeOf(K);
|
const keys_end = keys_start + cap * @sizeOf(K);
|
||||||
const vals_start = std.mem.alignForward(usize, keys_end, val_align);
|
const vals_start = std.mem.alignForward(usize, keys_end, val_align);
|
||||||
const vals_end = vals_start + new_capacity * @sizeOf(V);
|
const vals_end = vals_start + cap * @sizeOf(V);
|
||||||
|
|
||||||
// Our total memory size required is the end of our values
|
// Our total memory size required is the end of our values
|
||||||
// aligned to the base required alignment.
|
// aligned to the base required alignment.
|
||||||
|
|
@ -1511,3 +1515,26 @@ test "OffsetHashMap remake map" {
|
||||||
try expectEqual(5, map.get(5).?);
|
try expectEqual(5, map.get(5).?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "layoutForCapacity no overflow for large capacity" {
|
||||||
|
// Test that layoutForCapacity correctly handles large capacities without overflow.
|
||||||
|
// Prior to the fix, new_capacity (u32) was multiplied before widening to usize,
|
||||||
|
// causing overflow when new_capacity * @sizeOf(K) exceeded 2^32.
|
||||||
|
// See: https://github.com/ghostty-org/ghostty/issues/9862
|
||||||
|
const Map = AutoHashMapUnmanaged(u64, u64);
|
||||||
|
|
||||||
|
// Use 2^30 capacity - this would overflow in u32 when multiplied by @sizeOf(u64)=8
|
||||||
|
// 0x40000000 * 8 = 0x2_0000_0000 which wraps to 0 in u32
|
||||||
|
const large_cap: Map.Size = 1 << 30;
|
||||||
|
const layout = Map.layoutForCapacity(large_cap);
|
||||||
|
|
||||||
|
// With the fix, total_size should be at least cap * (sizeof(K) + sizeof(V))
|
||||||
|
// = 2^30 * 16 = 2^34 bytes = 16 GiB
|
||||||
|
// Without the fix, this would wrap and produce a much smaller value.
|
||||||
|
const min_expected: usize = @as(usize, large_cap) * (@sizeOf(u64) + @sizeOf(u64));
|
||||||
|
try expect(layout.total_size >= min_expected);
|
||||||
|
|
||||||
|
// Also verify the individual offsets don't wrap
|
||||||
|
try expect(layout.keys_start > 0);
|
||||||
|
try expect(layout.vals_start > layout.keys_start);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ fn setupBash(
|
||||||
resource_dir: []const u8,
|
resource_dir: []const u8,
|
||||||
env: *EnvMap,
|
env: *EnvMap,
|
||||||
) !?config.Command {
|
) !?config.Command {
|
||||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3);
|
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 2);
|
||||||
defer args.deinit(alloc);
|
defer args.deinit(alloc);
|
||||||
|
|
||||||
// Iterator that yields each argument in the original command line.
|
// Iterator that yields each argument in the original command line.
|
||||||
|
|
@ -273,11 +273,6 @@ fn setupBash(
|
||||||
} else return null;
|
} else return null;
|
||||||
try args.append(alloc, "--posix");
|
try args.append(alloc, "--posix");
|
||||||
|
|
||||||
// On macOS, we request a login shell to match that platform's norms.
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try args.append(alloc, "--login");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stores the list of intercepted command line flags that will be passed
|
// Stores the list of intercepted command line flags that will be passed
|
||||||
// to our shell integration script: --norc --noprofile
|
// to our shell integration script: --norc --noprofile
|
||||||
// We always include at least "1" so the script can differentiate between
|
// We always include at least "1" so the script can differentiate between
|
||||||
|
|
@ -357,9 +352,8 @@ fn setupBash(
|
||||||
);
|
);
|
||||||
try env.put("ENV", integ_dir);
|
try env.put("ENV", integ_dir);
|
||||||
|
|
||||||
// Since we built up a command line, we don't need to wrap it in
|
// Join the accumulated arguments to form the final command string.
|
||||||
// ANOTHER shell anymore and can do a direct command.
|
return .{ .shell = try std.mem.joinZ(alloc, " ", args.items) };
|
||||||
return .{ .direct = try args.toOwnedSlice(alloc) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "bash" {
|
test "bash" {
|
||||||
|
|
@ -373,12 +367,7 @@ test "bash" {
|
||||||
|
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
|
||||||
|
|
||||||
try testing.expect(command.?.direct.len >= 2);
|
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
|
try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
|
||||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
|
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
|
||||||
}
|
}
|
||||||
|
|
@ -421,12 +410,7 @@ test "bash: inject flags" {
|
||||||
|
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
|
||||||
|
|
||||||
try testing.expect(command.?.direct.len >= 2);
|
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
|
try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -437,12 +421,7 @@ test "bash: inject flags" {
|
||||||
|
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
|
||||||
|
|
||||||
try testing.expect(command.?.direct.len >= 2);
|
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
|
try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -459,24 +438,14 @@ test "bash: rcfile" {
|
||||||
// bash --rcfile
|
// bash --rcfile
|
||||||
{
|
{
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
|
||||||
try testing.expect(command.?.direct.len >= 2);
|
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// bash --init-file
|
// bash --init-file
|
||||||
{
|
{
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
|
||||||
try testing.expect(command.?.direct.len >= 2);
|
try testing.expectEqualStrings("bash --posix", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -538,35 +507,13 @@ test "bash: additional arguments" {
|
||||||
// "-" argument separator
|
// "-" argument separator
|
||||||
{
|
{
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
|
||||||
try testing.expect(command.?.direct.len >= 6);
|
try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
|
|
||||||
try testing.expectEqualStrings("-", command.?.direct[offset + 0]);
|
|
||||||
try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
|
|
||||||
try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
|
|
||||||
try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "--" argument separator
|
// "--" argument separator
|
||||||
{
|
{
|
||||||
const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
|
const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
|
||||||
try testing.expect(command.?.direct.len >= 6);
|
try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?.shell);
|
||||||
try testing.expectEqualStrings("bash", command.?.direct[0]);
|
|
||||||
try testing.expectEqualStrings("--posix", command.?.direct[1]);
|
|
||||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
|
||||||
try testing.expectEqualStrings("--login", command.?.direct[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2;
|
|
||||||
try testing.expectEqualStrings("--", command.?.direct[offset + 0]);
|
|
||||||
try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]);
|
|
||||||
try testing.expectEqualStrings("file1", command.?.direct[offset + 2]);
|
|
||||||
try testing.expectEqualStrings("file2", command.?.direct[offset + 3]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue