Merge branch 'main' into feat/fullscreen-non-native-config
commit
ad1c6f4fbb
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ jobs:
|
|||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
with:
|
||||
# Important so that build number generation works
|
||||
fetch-depth: 0
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -166,7 +166,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -127,7 +127,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -160,7 +160,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -194,7 +194,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -237,7 +237,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -273,7 +273,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -302,7 +302,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -335,7 +335,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -381,7 +381,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -600,7 +600,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -642,7 +642,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -690,7 +690,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -725,7 +725,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -789,7 +789,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -816,7 +816,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -844,7 +844,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -871,7 +871,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -898,7 +898,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -925,7 +925,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -952,7 +952,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -986,7 +986,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1013,7 +1013,7 @@ jobs:
|
|||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
- uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1050,7 +1050,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
@ -1138,7 +1138,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
# 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:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31.8.4
|
||||
uses: cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
|
|||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> If you are using **any kind of AI assistance** to contribute to Ghostty,
|
||||
> it must be disclosed in the pull request.
|
||||
> The Ghostty project allows AI-**assisted** _code contributions_, which
|
||||
> must be properly disclosed in the pull request.
|
||||
|
||||
If you are using any kind of AI assistance while contributing to Ghostty,
|
||||
**this must be disclosed in the pull request**, along with the extent to
|
||||
which AI assistance was used (e.g. docs only vs. code generation).
|
||||
As a small exception, trivial tab-completion doesn't need to be disclosed,
|
||||
so long as it is limited to single keywords or short phrases.
|
||||
|
||||
The submitter must have also tested the pull request on all impacted
|
||||
platforms, and it's **highly discouraged** to code for an unfamiliar platform
|
||||
|
|
@ -32,10 +30,49 @@ with AI assistance alone: if you only have a macOS machine, do **not** ask AI
|
|||
to write the equivalent GTK code, and vice versa — someone else with more
|
||||
expertise will eventually get to it and do it for you.
|
||||
|
||||
Even though using AI to generate responses on a PR is allowed when properly
|
||||
disclosed, **we do not encourage you to do so**. Often, the positive impact
|
||||
of genuine, responsive human interaction more than makes up for any language
|
||||
barrier. ❤️
|
||||
> [!WARNING]
|
||||
> **Note that AI _assistance_ does not equal AI _generation_**. We require
|
||||
> a significant amount of human accountability, involvement and interaction
|
||||
> even within AI-assisted contributions. Contributors are required to be able
|
||||
> to understand the AI-assisted output, reason with it and answer critical
|
||||
> questions about it. Should a PR see no visible human accountability and
|
||||
> involvement, or it is so broken that it requires significant rework to be
|
||||
> acceptable, **we reserve the right to close it without hesitation**.
|
||||
|
||||
**In addition, we currently restrict AI assistance to code changes only.**
|
||||
No AI-generated media, e.g. artwork, icons, videos and other assets is
|
||||
allowed, as it goes against the methodology and ethos behind Ghostty.
|
||||
While AI-assisted code can help with productive prototyping, creative
|
||||
inspiration and even automated bugfinding, we have currently found zero
|
||||
benefit to AI-generated assets. Instead, we are far more interested and
|
||||
invested in funding professional work done by human designers and artists.
|
||||
If you intend to submit AI-generated assets to Ghostty, sorry,
|
||||
we are not interested.
|
||||
|
||||
Likewise, all community interactions, including all comments on issues and
|
||||
discussions and all PR titles and descriptions **must be composed by a human**.
|
||||
Community moderators and Ghostty maintainers reserve the right to mark
|
||||
AI-generated responses as spam or disruptive content, and ban users who have
|
||||
been repeatedly caught relying entirely on LLMs during interactions.
|
||||
|
||||
> [!NOTE]
|
||||
> If your English isn't the best and you are currently relying on an LLM to
|
||||
> translate your responses, don't fret — usually we maintainers will be able
|
||||
> to understand your messages well enough. We'd like to encourage real humans
|
||||
> to interact with each other more, and the positive impact of genuine,
|
||||
> responsive yet imperfect human interaction more than makes up for any
|
||||
> language barrier.
|
||||
>
|
||||
> Please write your responses yourself, to the best of your ability.
|
||||
> If you do feel the need to polish your sentences, however, please use
|
||||
> dedicated translation software rather than an LLM.
|
||||
>
|
||||
> We greatly appreciate it. Thank you. ❤️
|
||||
|
||||
Minor exceptions to this policy include trivial AI-generated tab completion
|
||||
functionality, as it usually does not impact the quality of the code and
|
||||
do not need to be disclosed, and commit titles and messages, which are often
|
||||
generated by AI coding agents.
|
||||
|
||||
An example disclosure:
|
||||
|
||||
|
|
@ -60,13 +97,6 @@ work than any human. That isn't the world we live in today, and in most cases
|
|||
it's generating slop. I say this despite being a fan of and using them
|
||||
successfully myself (with heavy supervision)!
|
||||
|
||||
When using AI assistance, we expect a fairly high level of accountability
|
||||
and responsibility from contributors, and expect them to understand the code
|
||||
that is produced and be able to answer critical questions about it. It
|
||||
isn't a maintainers job to review a PR so broken that it requires
|
||||
significant rework to be acceptable, and we **reserve the right to close
|
||||
these PRs without hesitation**.
|
||||
|
||||
Please be respectful to maintainers and disclose AI assistance.
|
||||
|
||||
## Quick Guide
|
||||
|
|
|
|||
|
|
@ -582,6 +582,12 @@ typedef enum {
|
|||
GHOSTTY_QUIT_TIMER_STOP,
|
||||
} ghostty_action_quit_timer_e;
|
||||
|
||||
// apprt.action.Readonly
|
||||
typedef enum {
|
||||
GHOSTTY_READONLY_OFF,
|
||||
GHOSTTY_READONLY_ON,
|
||||
} ghostty_action_readonly_e;
|
||||
|
||||
// apprt.action.DesktopNotification.C
|
||||
typedef struct {
|
||||
const char* title;
|
||||
|
|
@ -593,6 +599,12 @@ typedef struct {
|
|||
const char* title;
|
||||
} 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
|
||||
typedef struct {
|
||||
const char* pwd;
|
||||
|
|
@ -840,7 +852,8 @@ typedef enum {
|
|||
GHOSTTY_ACTION_END_SEARCH,
|
||||
GHOSTTY_ACTION_SEARCH_TOTAL,
|
||||
GHOSTTY_ACTION_SEARCH_SELECTED,
|
||||
} ghostty_action_tag_e;
|
||||
GHOSTTY_ACTION_READONLY,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
ghostty_action_split_direction_e new_split;
|
||||
|
|
@ -856,6 +869,7 @@ typedef union {
|
|||
ghostty_action_inspector_e inspector;
|
||||
ghostty_action_desktop_notification_s desktop_notification;
|
||||
ghostty_action_set_title_s set_title;
|
||||
ghostty_action_prompt_title_e prompt_title;
|
||||
ghostty_action_pwd_s pwd;
|
||||
ghostty_action_mouse_shape_e mouse_shape;
|
||||
ghostty_action_mouse_visibility_e mouse_visibility;
|
||||
|
|
@ -876,6 +890,7 @@ typedef union {
|
|||
ghostty_action_start_search_s start_search;
|
||||
ghostty_action_search_total_s search_total;
|
||||
ghostty_action_search_selected_s search_selected;
|
||||
ghostty_action_readonly_e readonly;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,8 @@ class AppDelegate: NSObject,
|
|||
@IBOutlet private var menuDecreaseFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuResetFontSize: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTitle: NSMenuItem?
|
||||
@IBOutlet private var menuChangeTabTitle: NSMenuItem?
|
||||
@IBOutlet private var menuReadonly: NSMenuItem?
|
||||
@IBOutlet private var menuQuickTerminal: NSMenuItem?
|
||||
@IBOutlet private var menuTerminalInspector: NSMenuItem?
|
||||
@IBOutlet private var menuCommandPalette: NSMenuItem?
|
||||
|
|
@ -541,8 +543,9 @@ class AppDelegate: NSObject,
|
|||
self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller")
|
||||
self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection")
|
||||
self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal")
|
||||
self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
self.menuChangeTabTitle?.setImageIfDesired(systemSymbolName: "pencil.line")
|
||||
self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope")
|
||||
self.menuReadonly?.setImageIfDesired(systemSymbolName: "eye.fill")
|
||||
self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward")
|
||||
self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye")
|
||||
self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right")
|
||||
|
|
@ -609,6 +612,7 @@ class AppDelegate: NSObject,
|
|||
syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize)
|
||||
syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize)
|
||||
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_visibility", menuItem: self.menuToggleVisibility)
|
||||
syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
<connections>
|
||||
<outlet property="menuAbout" destination="5kV-Vb-QxS" id="Y5y-UO-NK6"/>
|
||||
<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="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||
|
|
@ -46,6 +47,7 @@
|
|||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
<outlet property="menuReadonly" destination="xpe-ia-Yjw" id="MMT-Sl-AfD"/>
|
||||
<outlet property="menuRedo" destination="EX8-lB-4s7" id="wON-2J-yT1"/>
|
||||
<outlet property="menuReloadConfig" destination="KKH-XX-5py" id="Wvp-7J-wqX"/>
|
||||
<outlet property="menuResetFontSize" destination="Jah-MY-aLX" id="ger-qM-wrm"/>
|
||||
|
|
@ -315,12 +317,24 @@
|
|||
<action selector="toggleCommandPalette:" target="-1" id="FcT-XD-gM1"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<connections>
|
||||
<action selector="changeTitle:" target="-1" id="XuL-QB-Q9l"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Terminal Read-only" id="xpe-ia-Yjw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="toggleReadonly:" target="-1" id="Gqx-wT-K9v"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="Vkj-tP-dMZ"/>
|
||||
<menuItem title="Quick Terminal" id="1pv-LF-NBJ">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
|
|
|
|||
|
|
@ -81,6 +81,15 @@ class BaseTerminalController: NSWindowController,
|
|||
/// The cancellables related to our focused surface.
|
||||
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.
|
||||
var undoExpiration: Duration {
|
||||
ghostty.config.undoTimeout
|
||||
|
|
@ -325,6 +334,37 @@ class BaseTerminalController: NSWindowController,
|
|||
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.
|
||||
func closeSurface(
|
||||
_ view: Ghostty.SurfaceView,
|
||||
|
|
@ -718,10 +758,13 @@ class BaseTerminalController: NSWindowController,
|
|||
}
|
||||
|
||||
private func titleDidChange(to: String) {
|
||||
lastComputedTitle = to
|
||||
applyTitleToWindow()
|
||||
}
|
||||
|
||||
private func applyTitleToWindow() {
|
||||
guard let window else { return }
|
||||
|
||||
// Set the main window title
|
||||
window.title = to
|
||||
window.title = titleOverride ?? lastComputedTitle
|
||||
}
|
||||
|
||||
func pwdDidChange(to: URL?) {
|
||||
|
|
@ -1017,6 +1060,10 @@ class BaseTerminalController: NSWindowController,
|
|||
window.performClose(sender)
|
||||
}
|
||||
|
||||
@IBAction func changeTabTitle(_ sender: Any) {
|
||||
promptTabTitle()
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ import Cocoa
|
|||
class TerminalRestorableState: Codable {
|
||||
static let selfKey = "state"
|
||||
static let versionKey = "version"
|
||||
static let version: Int = 6
|
||||
static let version: Int = 7
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
let effectiveFullscreenMode: FullscreenMode?
|
||||
let tabColor: TerminalTabColor
|
||||
let titleOverride: String?
|
||||
|
||||
init(from controller: TerminalController) {
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
self.surfaceTree = controller.surfaceTree
|
||||
self.effectiveFullscreenMode = controller.fullscreenStyle?.fullscreenMode
|
||||
self.tabColor = (controller.window as? TerminalWindow)?.tabColor ?? .none
|
||||
self.titleOverride = controller.titleOverride
|
||||
}
|
||||
|
||||
init?(coder aDecoder: NSCoder) {
|
||||
|
|
@ -34,6 +36,7 @@ class TerminalRestorableState: Codable {
|
|||
self.focusedSurface = v.value.focusedSurface
|
||||
self.effectiveFullscreenMode = v.value.effectiveFullscreenMode
|
||||
self.tabColor = v.value.tabColor
|
||||
self.titleOverride = v.value.titleOverride
|
||||
}
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
|
|
@ -100,6 +103,9 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
|||
// 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
|
||||
// Find the focused surface in surfaceTree
|
||||
if let focusedStr = state.focusedSurface {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ struct TabColorMenuView: View {
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -668,8 +668,9 @@ private struct TabColorIndicatorView: View {
|
|||
|
||||
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 tabColorHeaderIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorHeader")
|
||||
|
||||
private static let tabColorPaletteIdentifier = NSUserInterfaceItemIdentifier("com.mitchellh.ghostty.tabColorPalette")
|
||||
|
||||
func configureTabContextMenuIfNeeded(_ menu: NSMenu) {
|
||||
|
|
@ -701,14 +702,14 @@ extension TerminalWindow {
|
|||
}
|
||||
}
|
||||
|
||||
appendTabColorSection(to: menu, target: targetController)
|
||||
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> = [
|
||||
// These selectors must all exist for it to be a tab context menu.
|
||||
let requiredSelectors: Set<String> = [
|
||||
"performClose:",
|
||||
"performCloseOtherTabs:",
|
||||
"moveTabToNewWindow:",
|
||||
|
|
@ -716,13 +717,13 @@ extension TerminalWindow {
|
|||
]
|
||||
|
||||
let selectorNames = Set(menu.items.compactMap { $0.action }.map { NSStringFromSelector($0) })
|
||||
return !selectorNames.isDisjoint(with: tabContextSelectors)
|
||||
return requiredSelectors.isSubset(of: selectorNames)
|
||||
}
|
||||
|
||||
private func appendTabColorSection(to menu: NSMenu, target: TerminalController?) {
|
||||
private func appendTabModifierSection(to menu: NSMenu, target: TerminalController?) {
|
||||
menu.removeItems(withIdentifiers: [
|
||||
Self.tabColorSeparatorIdentifier,
|
||||
Self.tabColorHeaderIdentifier,
|
||||
Self.changeTitleMenuItemIdentifier,
|
||||
Self.tabColorPaletteIdentifier
|
||||
])
|
||||
|
||||
|
|
@ -730,17 +731,17 @@ extension TerminalWindow {
|
|||
separator.identifier = Self.tabColorSeparatorIdentifier
|
||||
menu.addItem(separator)
|
||||
|
||||
let headerItem = NSMenuItem()
|
||||
headerItem.identifier = Self.tabColorHeaderIdentifier
|
||||
headerItem.title = "Tab Color"
|
||||
headerItem.isEnabled = false
|
||||
headerItem.setImageIfDesired(systemSymbolName: "eyedropper")
|
||||
menu.addItem(headerItem)
|
||||
// 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: tabColor
|
||||
selectedColor: (target?.window as? TerminalWindow)?.tabColor ?? .none
|
||||
) { [weak target] color in
|
||||
(target?.window as? TerminalWindow)?.tabColor = color
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ extension Ghostty {
|
|||
setTitle(app, target: target, v: action.action.set_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:
|
||||
pwdChanged(app, target: target, v: action.action.pwd)
|
||||
|
|
@ -588,6 +588,9 @@ extension Ghostty {
|
|||
case GHOSTTY_ACTION_RING_BELL:
|
||||
ringBell(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_READONLY:
|
||||
setReadonly(app, target: target, v: action.action.readonly)
|
||||
|
||||
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
||||
checkForUpdates(app)
|
||||
|
||||
|
|
@ -1010,6 +1013,31 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
private static func setReadonly(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_readonly_e) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set readonly does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyDidChangeReadonly,
|
||||
object: surfaceView,
|
||||
userInfo: [
|
||||
SwiftUI.Notification.Name.ReadonlyKey: v == GHOSTTY_READONLY_ON,
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func moveTab(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
|
@ -1350,22 +1378,50 @@ extension Ghostty {
|
|||
|
||||
private static func promptTitle(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s) -> Bool {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("set title prompt does nothing with an app target")
|
||||
return false
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_prompt_title_e) -> Bool {
|
||||
let promptTitle = Action.PromptTitle(v)
|
||||
switch promptTitle {
|
||||
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:
|
||||
guard let surface = target.target.surface else { return false }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
surfaceView.promptTitle()
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return false }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||
surfaceView.promptTitle()
|
||||
return true
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
default:
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -391,6 +391,10 @@ extension Notification.Name {
|
|||
|
||||
/// Ring the bell
|
||||
static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing")
|
||||
|
||||
/// Readonly mode changed
|
||||
static let ghosttyDidChangeReadonly = Notification.Name("com.mitchellh.ghostty.didChangeReadonly")
|
||||
static let ReadonlyKey = ghosttyDidChangeReadonly.rawValue + ".readonly"
|
||||
static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle")
|
||||
|
||||
/// Toggle maximize of current window
|
||||
|
|
|
|||
|
|
@ -116,6 +116,13 @@ extension Ghostty {
|
|||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// Readonly indicator badge
|
||||
if surfaceView.readonly {
|
||||
ReadonlyBadge {
|
||||
surfaceView.toggleReadonly(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in the middle of a key sequence, then we show a visual element. We only
|
||||
// support this on macOS currently although in theory we can support mobile with keyboards!
|
||||
if !surfaceView.keySequence.isEmpty {
|
||||
|
|
@ -757,6 +764,96 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Readonly Badge
|
||||
|
||||
/// A badge overlay that indicates a surface is in readonly mode.
|
||||
/// Positioned in the top-right corner and styled to be noticeable but unobtrusive.
|
||||
struct ReadonlyBadge: View {
|
||||
let onDisable: () -> Void
|
||||
|
||||
@State private var showingPopover = false
|
||||
|
||||
private let badgeColor = Color(hue: 0.08, saturation: 0.5, brightness: 0.8)
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "eye.fill")
|
||||
.font(.system(size: 12))
|
||||
Text("Read-only")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(badgeBackground)
|
||||
.foregroundStyle(badgeColor)
|
||||
.onTapGesture {
|
||||
showingPopover = true
|
||||
}
|
||||
.backport.pointerStyle(.link)
|
||||
.popover(isPresented: $showingPopover, arrowEdge: .bottom) {
|
||||
ReadonlyPopoverView(onDisable: onDisable, isPresented: $showingPopover)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Read-only terminal")
|
||||
}
|
||||
|
||||
private var badgeBackground: some View {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(.regularMaterial)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.strokeBorder(Color.orange.opacity(0.6), lineWidth: 1.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ReadonlyPopoverView: View {
|
||||
let onDisable: () -> Void
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "eye.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 13))
|
||||
Text("Read-Only Mode")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
}
|
||||
|
||||
Text("This terminal is in read-only mode. You can still view, select, and scroll through the content, but no input events will be sent to the running application.")
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button("Disable") {
|
||||
onDisable()
|
||||
isPresented = false
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.frame(width: 280)
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
/// When changing the split state, or going full screen (native or non), the terminal view
|
||||
/// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@ extension Ghostty {
|
|||
/// True when the bell is active. This is set inactive on focus or event.
|
||||
@Published private(set) var bell: Bool = false
|
||||
|
||||
/// True when the surface is in readonly mode.
|
||||
@Published private(set) var readonly: Bool = false
|
||||
|
||||
// An initial size to request for a window. This will only affect
|
||||
// then the view is moved to a new window.
|
||||
var initialSize: NSSize? = nil
|
||||
|
|
@ -333,6 +336,11 @@ extension Ghostty {
|
|||
selector: #selector(ghosttyBellDidRing(_:)),
|
||||
name: .ghosttyBellDidRing,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyDidChangeReadonly(_:)),
|
||||
name: .ghosttyDidChangeReadonly,
|
||||
object: self)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidChangeScreen),
|
||||
|
|
@ -703,6 +711,11 @@ extension Ghostty {
|
|||
bell = true
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidChangeReadonly(_ notification: SwiftUI.Notification) {
|
||||
guard let value = notification.userInfo?[SwiftUI.Notification.Name.ReadonlyKey] as? Bool else { return }
|
||||
readonly = value
|
||||
}
|
||||
|
||||
@objc private func windowDidChangeScreen(notification: SwiftUI.Notification) {
|
||||
guard let window = self.window else { return }
|
||||
guard let object = notification.object as? NSWindow, window == object else { return }
|
||||
|
|
@ -1416,9 +1429,13 @@ extension Ghostty {
|
|||
item.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise")
|
||||
item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "")
|
||||
item.setImageIfDesired(systemSymbolName: "scope")
|
||||
item = menu.addItem(withTitle: "Terminal Read-only", action: #selector(toggleReadonly(_:)), keyEquivalent: "")
|
||||
item.setImageIfDesired(systemSymbolName: "eye.fill")
|
||||
item.state = readonly ? .on : .off
|
||||
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 = menu.addItem(withTitle: "Change Terminal Title...", action: #selector(changeTitle(_:)), keyEquivalent: "")
|
||||
|
||||
return menu
|
||||
}
|
||||
|
|
@ -1498,6 +1515,14 @@ extension Ghostty {
|
|||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleReadonly(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "toggle_readonly"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.lengthOfBytes(using: .utf8)))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func splitRight(_ sender: Any) {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_split(surface, GHOSTTY_SPLIT_DIRECTION_RIGHT)
|
||||
|
|
@ -1974,6 +1999,10 @@ extension Ghostty.SurfaceView: NSMenuItemValidation {
|
|||
case #selector(findHide):
|
||||
return searchState != nil
|
||||
|
||||
case #selector(toggleReadonly):
|
||||
item.state = readonly ? .on : .off
|
||||
return true
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ extension Ghostty {
|
|||
|
||||
// The current search state. When non-nil, the search overlay should be shown.
|
||||
@Published var searchState: SearchState? = nil
|
||||
|
||||
/// True when the surface is in readonly mode.
|
||||
@Published private(set) var readonly: Bool = false
|
||||
|
||||
// Returns sizing information for the surface. This is the raw C
|
||||
// structure because I'm lazy.
|
||||
|
|
|
|||
127
src/Surface.zig
127
src/Surface.zig
|
|
@ -145,6 +145,12 @@ focused: bool = true,
|
|||
/// Used to determine whether to continuously scroll.
|
||||
selection_scroll_active: bool = false,
|
||||
|
||||
/// True if the surface is in read-only mode. When read-only, no input
|
||||
/// is sent to the PTY but terminal-level operations like selections,
|
||||
/// (native) scrolling, and copy keybinds still work. Warn before quit is
|
||||
/// always enabled in this state.
|
||||
readonly: bool = false,
|
||||
|
||||
/// Used to send notifications that long running commands have finished.
|
||||
/// Requires that shell integration be active. Should represent a nanosecond
|
||||
/// precision timestamp. It does not necessarily need to correspond to the
|
||||
|
|
@ -814,6 +820,30 @@ inline fn surfaceMailbox(self: *Surface) Mailbox {
|
|||
};
|
||||
}
|
||||
|
||||
/// Queue a message for the IO thread.
|
||||
///
|
||||
/// We centralize all our logic into this spot so we can intercept
|
||||
/// messages for example in readonly mode.
|
||||
fn queueIo(
|
||||
self: *Surface,
|
||||
msg: termio.Message,
|
||||
mutex: termio.Termio.MutexState,
|
||||
) void {
|
||||
// In readonly mode, we don't allow any writes through to the pty.
|
||||
if (self.readonly) {
|
||||
switch (msg) {
|
||||
.write_small,
|
||||
.write_stable,
|
||||
.write_alloc,
|
||||
=> return,
|
||||
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.io.queueMessage(msg, mutex);
|
||||
}
|
||||
|
||||
/// Forces the surface to render. This is useful for when the surface
|
||||
/// is in the middle of animation (such as a resize, etc.) or when
|
||||
/// the render timer is managed manually by the apprt.
|
||||
|
|
@ -845,7 +875,7 @@ pub fn activateInspector(self: *Surface) !void {
|
|||
|
||||
// Notify our components we have an inspector active
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = true }, .{ .forever = {} });
|
||||
self.io.queueMessage(.{ .inspector = true }, .unlocked);
|
||||
self.queueIo(.{ .inspector = true }, .unlocked);
|
||||
}
|
||||
|
||||
/// Deactivate the inspector and stop collecting any information.
|
||||
|
|
@ -862,7 +892,7 @@ pub fn deactivateInspector(self: *Surface) void {
|
|||
|
||||
// Notify our components we have deactivated inspector
|
||||
_ = self.renderer_thread.mailbox.push(.{ .inspector = false }, .{ .forever = {} });
|
||||
self.io.queueMessage(.{ .inspector = false }, .unlocked);
|
||||
self.queueIo(.{ .inspector = false }, .unlocked);
|
||||
|
||||
// Deinit the inspector
|
||||
insp.deinit();
|
||||
|
|
@ -873,6 +903,9 @@ pub fn deactivateInspector(self: *Surface) void {
|
|||
/// True if the surface requires confirmation to quit. This should be called
|
||||
/// by apprt to determine if the surface should confirm before quitting.
|
||||
pub fn needsConfirmQuit(self: *Surface) bool {
|
||||
// If the surface is in read-only mode, always require confirmation
|
||||
if (self.readonly) return true;
|
||||
|
||||
// If the child has exited, then our process is certainly not alive.
|
||||
// We check this first to avoid the locking overhead below.
|
||||
if (self.child_exited) return false;
|
||||
|
|
@ -931,7 +964,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||
// We always use an allocating message because we don't know
|
||||
// the length of the title and this isn't a performance critical
|
||||
// path.
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.write_alloc = .{
|
||||
.alloc = self.alloc,
|
||||
.data = data,
|
||||
|
|
@ -1123,7 +1156,7 @@ fn selectionScrollTick(self: *Surface) !void {
|
|||
// If our screen changed while this is happening, we stop our
|
||||
// selection scroll.
|
||||
if (self.mouse.left_click_screen != t.screens.active_key) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -1355,7 +1388,7 @@ fn reportColorScheme(self: *Surface, force: bool) void {
|
|||
.dark => "\x1B[?997;1n",
|
||||
};
|
||||
|
||||
self.io.queueMessage(.{ .write_stable = output }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = output }, .unlocked);
|
||||
}
|
||||
|
||||
fn searchCallback(event: terminal.search.Thread.Event, ud: ?*anyopaque) void {
|
||||
|
|
@ -1728,7 +1761,7 @@ pub fn updateConfig(
|
|||
errdefer termio_config_ptr.deinit();
|
||||
|
||||
_ = self.renderer_thread.mailbox.push(renderer_message, .{ .forever = {} });
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.change_config = .{
|
||||
.alloc = self.alloc,
|
||||
.ptr = termio_config_ptr,
|
||||
|
|
@ -2294,7 +2327,7 @@ fn setCellSize(self: *Surface, size: rendererpkg.CellSize) !void {
|
|||
self.balancePaddingIfNeeded();
|
||||
|
||||
// Notify the terminal
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
self.queueIo(.{ .resize = self.size }, .unlocked);
|
||||
|
||||
// Update our terminal default size if necessary.
|
||||
self.recomputeInitialSize() catch |err| {
|
||||
|
|
@ -2397,7 +2430,7 @@ fn resize(self: *Surface, size: rendererpkg.ScreenSize) !void {
|
|||
}
|
||||
|
||||
// Mail the IO thread
|
||||
self.io.queueMessage(.{ .resize = self.size }, .unlocked);
|
||||
self.queueIo(.{ .resize = self.size }, .unlocked);
|
||||
}
|
||||
|
||||
/// Recalculate the balanced padding if needed.
|
||||
|
|
@ -2673,7 +2706,7 @@ pub fn keyCallback(
|
|||
}
|
||||
|
||||
errdefer write_req.deinit();
|
||||
self.io.queueMessage(switch (write_req) {
|
||||
self.queueIo(switch (write_req) {
|
||||
.small => |v| .{ .write_small = v },
|
||||
.stable => |v| .{ .write_stable = v },
|
||||
.alloc => |v| .{ .write_alloc = v },
|
||||
|
|
@ -2902,7 +2935,7 @@ fn endKeySequence(
|
|||
if (self.keyboard.queued.items.len > 0) {
|
||||
switch (action) {
|
||||
.flush => for (self.keyboard.queued.items) |write_req| {
|
||||
self.io.queueMessage(switch (write_req) {
|
||||
self.queueIo(switch (write_req) {
|
||||
.small => |v| .{ .write_small = v },
|
||||
.stable => |v| .{ .write_stable = v },
|
||||
.alloc => |v| .{ .write_alloc = v },
|
||||
|
|
@ -3128,7 +3161,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
|||
self.renderer_state.mutex.lock();
|
||||
self.io.terminal.flags.focused = focused;
|
||||
self.renderer_state.mutex.unlock();
|
||||
self.io.queueMessage(.{ .focused = focused }, .unlocked);
|
||||
self.queueIo(.{ .focused = focused }, .unlocked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3292,7 +3325,7 @@ pub fn scrollCallback(
|
|||
};
|
||||
};
|
||||
for (0..y.magnitude()) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = seq }, .locked);
|
||||
self.queueIo(.{ .write_stable = seq }, .locked);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3513,7 +3546,7 @@ fn mouseReport(
|
|||
data[5] = 32 + @as(u8, @intCast(viewport_point.y)) + 1;
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = 6,
|
||||
} }, .locked);
|
||||
|
|
@ -3536,7 +3569,7 @@ fn mouseReport(
|
|||
i += try std.unicode.utf8Encode(@intCast(32 + viewport_point.y + 1), data[i..]);
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(i),
|
||||
} }, .locked);
|
||||
|
|
@ -3557,7 +3590,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3574,7 +3607,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3603,7 +3636,7 @@ fn mouseReport(
|
|||
});
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
self.io.queueMessage(.{ .write_small = .{
|
||||
self.queueIo(.{ .write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(resp.len),
|
||||
} }, .locked);
|
||||
|
|
@ -3755,7 +3788,7 @@ pub fn mouseButtonCallback(
|
|||
// Stop selection scrolling when releasing the left mouse button
|
||||
// but only when selection scrolling is active.
|
||||
if (self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.unlocked,
|
||||
);
|
||||
|
|
@ -4112,7 +4145,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
|||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B";
|
||||
};
|
||||
for (0..@abs(path.y)) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = arrow }, .locked);
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
}
|
||||
if (path.x != 0) {
|
||||
|
|
@ -4122,7 +4155,7 @@ fn clickMoveCursor(self: *Surface, to: terminal.Pin) !void {
|
|||
break :arrow if (t.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C";
|
||||
};
|
||||
for (0..@abs(path.x)) |_| {
|
||||
self.io.queueMessage(.{ .write_stable = arrow }, .locked);
|
||||
self.queueIo(.{ .write_stable = arrow }, .locked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4395,7 +4428,7 @@ pub fn cursorPosCallback(
|
|||
// Stop selection scrolling when inside the viewport within a 1px buffer
|
||||
// for fullscreen windows, but only when selection scrolling is active.
|
||||
if (pos.y >= 1 and self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -4495,7 +4528,7 @@ pub fn cursorPosCallback(
|
|||
if ((pos.y <= 1 or pos.y > max_y - 1) and
|
||||
!self.selection_scroll_active)
|
||||
{
|
||||
self.io.queueMessage(
|
||||
self.queueIo(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
|
|
@ -4871,7 +4904,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
.esc => try std.fmt.bufPrint(&buf, "\x1b{s}", .{data}),
|
||||
else => unreachable,
|
||||
};
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
full_data,
|
||||
), .unlocked);
|
||||
|
|
@ -4898,7 +4931,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
);
|
||||
return true;
|
||||
};
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
text,
|
||||
), .unlocked);
|
||||
|
|
@ -4931,9 +4964,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
};
|
||||
|
||||
if (normal) {
|
||||
self.io.queueMessage(.{ .write_stable = ck.normal }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = ck.normal }, .unlocked);
|
||||
} else {
|
||||
self.io.queueMessage(.{ .write_stable = ck.application }, .unlocked);
|
||||
self.queueIo(.{ .write_stable = ck.application }, .unlocked);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -5185,7 +5218,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
.prompt_surface_title => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.prompt_title,
|
||||
{},
|
||||
.surface,
|
||||
),
|
||||
|
||||
.prompt_tab_title => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.prompt_title,
|
||||
.tab,
|
||||
),
|
||||
|
||||
.clear_screen => {
|
||||
|
|
@ -5200,19 +5239,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
if (self.io.terminal.screens.active_key == .alternate) return false;
|
||||
}
|
||||
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.clear_screen = .{ .history = true },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_to_top => {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .top = {} },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_to_bottom => {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .bottom = {} },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5242,14 +5281,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
|
||||
.scroll_page_up => {
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = -1 * rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_page_down => {
|
||||
const rows: isize = @intCast(self.size.grid().rows);
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = rows },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5257,19 +5296,19 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
.scroll_page_fractional => |fraction| {
|
||||
const rows: f32 = @floatFromInt(self.size.grid().rows);
|
||||
const delta: isize = @intFromFloat(@trunc(fraction * rows));
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = delta },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.scroll_page_lines => |lines| {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.scroll_viewport = .{ .delta = lines },
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.jump_to_prompt => |delta| {
|
||||
self.io.queueMessage(.{
|
||||
self.queueIo(.{
|
||||
.jump_to_prompt = @intCast(delta),
|
||||
}, .unlocked);
|
||||
},
|
||||
|
|
@ -5379,6 +5418,16 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
{},
|
||||
),
|
||||
|
||||
.toggle_readonly => {
|
||||
self.readonly = !self.readonly;
|
||||
_ = try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.readonly,
|
||||
if (self.readonly) .on else .off,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
||||
.reset_window_size => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.reset_window_size,
|
||||
|
|
@ -5484,7 +5533,7 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||
};
|
||||
},
|
||||
|
||||
.io => self.io.queueMessage(.{ .crash = {} }, .unlocked),
|
||||
.io => self.queueIo(.{ .crash = {} }, .unlocked),
|
||||
},
|
||||
|
||||
.adjust_selection => |direction| {
|
||||
|
|
@ -5682,7 +5731,7 @@ fn writeScreenFile(
|
|||
},
|
||||
.url = path,
|
||||
}),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
.paste => self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
path,
|
||||
), .unlocked),
|
||||
|
|
@ -5822,7 +5871,7 @@ fn completeClipboardPaste(
|
|||
};
|
||||
|
||||
for (vecs) |vec| if (vec.len > 0) {
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
vec,
|
||||
), .unlocked);
|
||||
|
|
@ -5868,7 +5917,7 @@ fn completeClipboardReadOSC52(
|
|||
const encoded = enc.encode(buf[prefix.len..], data);
|
||||
assert(encoded.len == size);
|
||||
|
||||
self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.queueIo(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
buf,
|
||||
), .unlocked);
|
||||
|
|
|
|||
|
|
@ -189,8 +189,9 @@ pub const Action = union(Key) {
|
|||
set_title: SetTitle,
|
||||
|
||||
/// Set the title of the target to a prompted value. It is up to
|
||||
/// the apprt to prompt.
|
||||
prompt_title,
|
||||
/// the apprt to prompt. The value specifies whether to prompt for the
|
||||
/// surface title or the tab title.
|
||||
prompt_title: PromptTitle,
|
||||
|
||||
/// The current working directory has changed for the target terminal.
|
||||
pwd: Pwd,
|
||||
|
|
@ -313,6 +314,9 @@ pub const Action = union(Key) {
|
|||
/// The currently selected search match index (1-based).
|
||||
search_selected: SearchSelected,
|
||||
|
||||
/// The readonly state of the surface has changed.
|
||||
readonly: Readonly,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
|
|
@ -374,6 +378,7 @@ pub const Action = union(Key) {
|
|||
end_search,
|
||||
search_total,
|
||||
search_selected,
|
||||
readonly,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
|
|
@ -531,11 +536,22 @@ pub const QuitTimer = enum(c_int) {
|
|||
stop,
|
||||
};
|
||||
|
||||
pub const Readonly = enum(c_int) {
|
||||
off,
|
||||
on,
|
||||
};
|
||||
|
||||
pub const MouseVisibility = enum(c_int) {
|
||||
visible,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// Whether to prompt for the surface title or tab title.
|
||||
pub const PromptTitle = enum(c_int) {
|
||||
surface,
|
||||
tab,
|
||||
};
|
||||
|
||||
pub const MouseOverLink = struct {
|
||||
url: [:0]const u8,
|
||||
|
||||
|
|
|
|||
|
|
@ -693,7 +693,7 @@ pub const Application = extern struct {
|
|||
|
||||
.progress_report => return Action.progressReport(target, value),
|
||||
|
||||
.prompt_title => return Action.promptTitle(target),
|
||||
.prompt_title => return Action.promptTitle(target, value),
|
||||
|
||||
.quit => self.quit(),
|
||||
|
||||
|
|
@ -746,6 +746,7 @@ pub const Application = extern struct {
|
|||
.check_for_updates,
|
||||
.undo,
|
||||
.redo,
|
||||
.readonly,
|
||||
=> {
|
||||
log.warn("unimplemented action={}", .{action});
|
||||
return false;
|
||||
|
|
@ -2250,12 +2251,18 @@ const Action = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn promptTitle(target: apprt.Target) bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
pub fn promptTitle(target: apprt.Target, value: apprt.action.PromptTitle) bool {
|
||||
switch (value) {
|
||||
.surface => switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
v.rt_surface.surface.promptTitle();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
.tab => {
|
||||
// GTK does not yet support tab title prompting
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2842,7 +2842,7 @@ keybind: Keybinds = .{},
|
|||
/// also known as the traffic lights, that allow you to close, miniaturize, and
|
||||
/// zoom the window.
|
||||
///
|
||||
/// This setting has no effect when `window-decoration = false` or
|
||||
/// This setting has no effect when `window-decoration = none` or
|
||||
/// `macos-titlebar-style = hidden`, as the window buttons are always hidden in
|
||||
/// these modes.
|
||||
///
|
||||
|
|
@ -2883,7 +2883,7 @@ keybind: Keybinds = .{},
|
|||
/// macOS 14 does not have this issue and any other macOS version has not
|
||||
/// been tested.
|
||||
///
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = false`,
|
||||
/// The "hidden" style hides the titlebar. Unlike `window-decoration = none`,
|
||||
/// however, it does not remove the frame from the window or cause it to have
|
||||
/// squared corners. Changing to or from this option at run-time may affect
|
||||
/// existing windows in buggy ways.
|
||||
|
|
@ -3222,7 +3222,7 @@ else
|
|||
/// manager's simple titlebar. The behavior of this option will vary with your
|
||||
/// window manager.
|
||||
///
|
||||
/// This option does nothing when `window-decoration` is false or when running
|
||||
/// This option does nothing when `window-decoration` is none or when running
|
||||
/// under macOS.
|
||||
@"gtk-titlebar": bool = true,
|
||||
|
||||
|
|
|
|||
|
|
@ -519,6 +519,11 @@ pub const Action = union(enum) {
|
|||
/// version can be found by running `ghostty +version`.
|
||||
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.
|
||||
///
|
||||
/// Valid arguments:
|
||||
|
|
@ -547,6 +552,16 @@ pub const Action = union(enum) {
|
|||
/// reflect this by displaying an icon indicating the zoomed state.
|
||||
toggle_split_zoom,
|
||||
|
||||
/// Toggle read-only mode for the current surface.
|
||||
///
|
||||
/// When a surface is in read-only mode:
|
||||
/// - No input is sent to the PTY (mouse events, key encoding)
|
||||
/// - Input can still be used at the terminal level to make selections,
|
||||
/// copy/paste (keybinds), scroll, etc.
|
||||
/// - Warn before quit is always enabled in this state even if an active
|
||||
/// process is not running
|
||||
toggle_readonly,
|
||||
|
||||
/// Resize the current split in the specified direction and amount in
|
||||
/// pixels. The two arguments should be joined with a comma (`,`),
|
||||
/// like in `resize_split:up,10`.
|
||||
|
|
@ -1191,6 +1206,7 @@ pub const Action = union(enum) {
|
|||
.reset_font_size,
|
||||
.set_font_size,
|
||||
.prompt_surface_title,
|
||||
.prompt_tab_title,
|
||||
.clear_screen,
|
||||
.select_all,
|
||||
.scroll_to_top,
|
||||
|
|
@ -1235,6 +1251,7 @@ pub const Action = union(enum) {
|
|||
.new_split,
|
||||
.goto_split,
|
||||
.toggle_split_zoom,
|
||||
.toggle_readonly,
|
||||
.resize_split,
|
||||
.equalize_splits,
|
||||
.inspector,
|
||||
|
|
|
|||
|
|
@ -413,10 +413,16 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
|
||||
.prompt_surface_title => comptime &.{.{
|
||||
.action = .prompt_surface_title,
|
||||
.title = "Change Title...",
|
||||
.title = "Change Terminal Title...",
|
||||
.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 &.{
|
||||
.{
|
||||
.action = .{ .new_split = .left },
|
||||
|
|
@ -479,6 +485,12 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||
.description = "Toggle the zoom state of the current split.",
|
||||
}},
|
||||
|
||||
.toggle_readonly => comptime &.{.{
|
||||
.action = .toggle_readonly,
|
||||
.title = "Toggle Read-Only Mode",
|
||||
.description = "Toggle read-only mode for the current surface.",
|
||||
}},
|
||||
|
||||
.equalize_splits => comptime &.{.{
|
||||
.action = .equalize_splits,
|
||||
.title = "Equalize Splits",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,84 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Writer = std.Io.Writer;
|
||||
|
||||
/// Builder for constructing space-separated shell command strings.
|
||||
/// Uses a caller-provided allocator (typically with stackFallback).
|
||||
pub const ShellCommandBuilder = struct {
|
||||
buffer: std.Io.Writer.Allocating,
|
||||
|
||||
pub fn init(allocator: Allocator) ShellCommandBuilder {
|
||||
return .{ .buffer = .init(allocator) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ShellCommandBuilder) void {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
/// Append an argument to the command with automatic space separation.
|
||||
pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void {
|
||||
if (arg.len == 0) return;
|
||||
if (self.buffer.written().len > 0) {
|
||||
try self.buffer.writer.writeByte(' ');
|
||||
}
|
||||
try self.buffer.writer.writeAll(arg);
|
||||
}
|
||||
|
||||
/// Get the final null-terminated command string, transferring ownership to caller.
|
||||
/// Calling deinit() after this is safe but unnecessary.
|
||||
pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 {
|
||||
return try self.buffer.toOwnedSliceSentinel(0);
|
||||
}
|
||||
};
|
||||
|
||||
test ShellCommandBuilder {
|
||||
// Empty command
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try testing.expectEqualStrings("", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Single arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Multiple args
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
try cmd.appendArg("-l");
|
||||
try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Empty arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// toOwnedSlice
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
const result = try cmd.toOwnedSlice();
|
||||
defer testing.allocator.free(result);
|
||||
try testing.expectEqualStrings("bash --posix", result);
|
||||
try testing.expectEqual(@as(u8, 0), result[result.len]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer that escapes characters that shells treat specially to reduce the
|
||||
/// risk of injection attacks or other such weirdness. Specifically excludes
|
||||
/// linefeeds so that they can be used to delineate lists of file paths.
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ const configpkg = @import("../config.zig");
|
|||
|
||||
const log = std.log.scoped(.io_exec);
|
||||
|
||||
/// Mutex state argument for queueMessage.
|
||||
pub const MutexState = enum { locked, unlocked };
|
||||
|
||||
/// Allocator
|
||||
alloc: Allocator,
|
||||
|
||||
|
|
@ -380,7 +383,7 @@ pub fn threadExit(self: *Termio, data: *ThreadData) void {
|
|||
pub fn queueMessage(
|
||||
self: *Termio,
|
||||
msg: termio.Message,
|
||||
mutex: enum { locked, unlocked },
|
||||
mutex: MutexState,
|
||||
) void {
|
||||
self.mailbox.send(msg, switch (mutex) {
|
||||
.locked => self.renderer_state.mutex,
|
||||
|
|
|
|||
|
|
@ -259,8 +259,9 @@ fn setupBash(
|
|||
resource_dir: []const u8,
|
||||
env: *EnvMap,
|
||||
) !?config.Command {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 3);
|
||||
defer args.deinit(alloc);
|
||||
var stack_fallback = std.heap.stackFallback(4096, alloc);
|
||||
var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get());
|
||||
defer cmd.deinit();
|
||||
|
||||
// Iterator that yields each argument in the original command line.
|
||||
// This will allocate once proportionate to the command line length.
|
||||
|
|
@ -269,14 +270,9 @@ fn setupBash(
|
|||
|
||||
// Start accumulating arguments with the executable and initial flags.
|
||||
if (iter.next()) |exe| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, exe));
|
||||
try cmd.appendArg(exe);
|
||||
} else return null;
|
||||
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");
|
||||
}
|
||||
try cmd.appendArg("--posix");
|
||||
|
||||
// Stores the list of intercepted command line flags that will be passed
|
||||
// to our shell integration script: --norc --noprofile
|
||||
|
|
@ -309,17 +305,17 @@ fn setupBash(
|
|||
if (std.mem.indexOfScalar(u8, arg, 'c') != null) {
|
||||
return null;
|
||||
}
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
} else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
|
||||
// All remaining arguments should be passed directly to the shell
|
||||
// command. We shouldn't perform any further option processing.
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
while (iter.next()) |remaining_arg| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, remaining_arg));
|
||||
try cmd.appendArg(remaining_arg);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
}
|
||||
}
|
||||
try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]);
|
||||
|
|
@ -357,9 +353,11 @@ fn setupBash(
|
|||
);
|
||||
try env.put("ENV", integ_dir);
|
||||
|
||||
// Since we built up a command line, we don't need to wrap it in
|
||||
// ANOTHER shell anymore and can do a direct command.
|
||||
return .{ .direct = try args.toOwnedSlice(alloc) };
|
||||
// Get the command string from the builder, then copy it to the arena
|
||||
// allocator. The stackFallback allocator's memory becomes invalid after
|
||||
// this function returns, so we must copy to the arena.
|
||||
const cmd_str = try cmd.toOwnedSlice();
|
||||
return .{ .shell = try alloc.dupeZ(u8, cmd_str) };
|
||||
}
|
||||
|
||||
test "bash" {
|
||||
|
|
@ -373,12 +371,7 @@ test "bash" {
|
|||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
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("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?);
|
||||
try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
|
|
@ -421,12 +414,7 @@ test "bash: inject flags" {
|
|||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
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("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
|
||||
|
|
@ -437,12 +425,7 @@ test "bash: inject flags" {
|
|||
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env);
|
||||
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
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("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?);
|
||||
}
|
||||
}
|
||||
|
|
@ -459,24 +442,14 @@ test "bash: rcfile" {
|
|||
// bash --rcfile
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
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("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||
}
|
||||
|
||||
// bash --init-file
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 2);
|
||||
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("bash --posix", command.?.shell);
|
||||
try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?);
|
||||
}
|
||||
}
|
||||
|
|
@ -538,35 +511,13 @@ test "bash: additional arguments" {
|
|||
// "-" argument separator
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 6);
|
||||
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]);
|
||||
try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?.shell);
|
||||
}
|
||||
|
||||
// "--" argument separator
|
||||
{
|
||||
const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env);
|
||||
try testing.expect(command.?.direct.len >= 6);
|
||||
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]);
|
||||
try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?.shell);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue