## Summary
Fix `+new-window -e <command>` inheriting forced shell integration from
the running GTK application.
Arguments after `-e` are now marked as an explicit command that requires
shell detection. That marker is propagated with the existing command
override to the new surface for both the current `+new-window` and
`+new-tab` GTK paths. When the override is applied, forced shell
integration becomes `detect`; an explicit `none` remains disabled.
This is scoped to the GTK `-e` path. It does not change the generic
forced shell-integration behavior, so configured integration continues
to support shell executables with non-standard names.
Fixes#12378.
## Root cause
The GTK IPC path replaced `config.command` for the new surface but
retained the running application's `shell-integration` value. `termio`
therefore treated an arbitrary explicit command such as Vim as the
forced shell and appended shell-specific arguments.
## AI usage
I used Hermes Agent (with ChatGPT 5.6) to help inspect the codebase,
trace the GTK command path, and draft parts of the implementation and
test. I manually reviewed and edited the changes, validated the behavior
and test results, and understand the affected code paths.
Arguments after -e are now marked as an explicit command that requires shell detection. That marker is propagated with the existing command override to the new surface for both the current +new-window and +new-tab GTK paths. When the override is applied, forced shell integration becomes detect; an explicit none remains disabled.
This is scoped to the GTK -e path. It does not change the generic forced shell-integration behavior, so configured integration continues to support shell executables with non-standard names.
Fixes#12378.
## Summary
- Add `GHOSTTY_TERMINAL_DATA_CURSOR_AT_PROMPT` to expose semantic prompt
state through `libghostty-vt`.
- Reuse `Terminal.cursorIsAtPrompt()` rather than introducing separate
prompt tracking.
## Motivation
Ghostty already tracks whether the cursor is at a semantic prompt
through `Terminal.cursorIsAtPrompt()`. Exposing this query through
`libghostty-vt` lets downstream terminal consumers use Ghostty's
existing semantic prompt state without introducing separate tracking.
The query returns false when semantic prompt information is unavailable
or the alternate screen is active.
Discussed in https://github.com/ghostty-org/ghostty/discussions/13755.
## Testing
- `zig build test-lib-vt`
- `zig build test-lib-vt -Dtest-filter="get cursor_at_prompt"`
- `zig build -Demit-lib-vt`
## AI disclosure
Codex assisted with adding the `libghostty-vt` query and test,
addressing review feedback, running validation, and drafting this
description. I reviewed and edited the final code and description,
reviewed the validation results, and understand how the change interacts
with Ghostty's semantic prompt state.
This adds new functions to both C and Zig to write VT data until the VT
parser reaches a "ground" state. The ground state is when the
parser/stream is stateless: between all partial UTF-8, OSC, CSI, etc.
This lets embedders safely interleave custom VT sequences from multiple
sources. A practical example is a standard terminal reading from a pty
that is then doing custom APC or something mid-stream for their emulator
client.
The new function is anywhere from 1% to 5% slower than normal VT write,
but that should be acceptable due to its special case. Normal VT writes
are unchanged.
Swift explicitly [marked UnsafeMutablePointer as non sendable](0568dbf903). Moving from `@unchecked @retroactive` to `nonisolated(unsafe)` is safe for us as per the previous comments
This adds new functions to both C and Zig to write VT data until the
VT parser reaches a "ground" state. The ground state is when the
parser/stream is stateless: between all partial UTF-8, OSC, CSI, etc.
This lets embedders safely interleave custom VT sequences from multiple
sources. A practical example is a standard terminal reading from a pty
that is then doing custom APC or something mid-stream for their emulator
client.
As discussed in Discord: Ghostty internal support for iOS is not
important and we verify iOS compatibility for `libghostty-vt` through
compilation.
Diff is **BIG**, but it contains mostly cleanup and didn't touch macOS's
implementation (except for some renaming).
### AI Disclosure
Claude did batch removal for me, I manually reviewed them and ran
locally for macOS.
Swap out the hash map backed by an arena with a fixed array.
Measurements from poop:
CPU Cycles: 173M → 171M (−1.2%)
Instructions: 519M → 517M (−0.5%)
Peak RSS: 11.9 → 11.8 MB
Cache misses: 356K → 312K (−12.4%)
Graphics commands prev. stored parsed control fields in a hash map
backed that used an arena.
This added hashing and allocation to every command even though protocol
keys are single ASCII letters.
Store letter keys in a fixed array with a presence bitmap and remove the
now-unnecessary arena. Unknown non-letter keys remain ignored and are
covered by a regression test.
#13319#13748
Normalize command-line file arguments as file URLs internally while
keeping the AppKit and FileManager string boundaries unchanged.
This handles relative paths, URL-sensitive characters, and trailing
directory separators consistently when matching duplicate open-file
events.
Fixes#13319
AppKit treats existing positional arguments as documents, causing paths
passed to a child command after -e to open an extra terminal surface.
We now process args ourselves during openFile callbacks to ignore file
paths after `-e`. There isn't a way to avoid this I can find because
AppKit processes argc/argv from the main entrypoint and that can't be
overridden.
Fixes#13319
AppKit treats existing positional arguments as documents, causing paths
passed to a child command after -e to open an extra terminal surface.
We now process args ourselves during openFile callbacks to ignore
file paths after `-e`. There isn't a way to avoid this I can find
because AppKit processes argc/argv from the main entrypoint and that
can't be overridden.
Fixes#10077
Clipboard read confirmations would immediately show a sheet which
grabbed focus. This could be used for a bunch of dumb reasons, including
DoS attacks. But, it also caused focus/sheet loops for programs that did
OSC52 on focus changes (which was seen via some Neovim configs!).
Now, if a surface is unfocused, we bell the surface and show the
confirmation request on next focus. If the surface is not focused or
another request comes in, we cancel the prior one.
This also fixes some memory management issues around clipboard requests
that were likely small leaks (didn't verify the old bug, but verified
the new code, and eyeballed the old).
To implement this, I decided to reorient the whole clipboard
confirmation thing around state on SurfaceView (which simplifies memory
management) and using Combine on BaseTerminalController to get notified.
Fixes#10077
Clipboard read confirmations would immediately show a sheet which
grabbed focus. This could be used for a bunch of dumb reasons, including
DoS attacks. But, it also caused focus/sheet loops for programs that did
OSC52 on focus changes (which was seen via some Neovim configs!).
Now, if a surface is unfocused, we bell the surface and show the confirmation
request on next focus. If the surface is not focused or another request
comes in, we cancel the prior one.
This also fixes some memory management issues around clipboard requests
that were likely small leaks (didn't verify the old bug, but verified
the new code, and eyeballed the old).
`needleSelection` was introduced in #12712 to select all texts when
syncing pasteboard, the crash happens most on macOS 15 in
`readPasteboardNeedle`. It seems that `objectWillChange` fires
differently there, and it's hard to reproduce on macOS 26/27. I think
guaranteeing from ourside is enough, I believe SwiftUI already as its
own when updating the binding.
**Confirmed with a simple example on macOS 15, it seems a SwiftUI
issue. So I changed the minimal macOS version for text selection to
macOS 26. I don't see an elegant way to fix it.**
<img width="1352" height="849" alt="image"
src="https://github.com/user-attachments/assets/1dfef3f5-ceaa-41dd-bb91-c23dbc5e4ad3"
/>
```swift
struct ContentView: View {
@State private var text = ""
@State private var selection: TextSelection?
var body: some View {
TextField("Search", text: $text, selection: $selection)
}
}
```
This adds some better handling of existing paths when editing
configuration files:
* If we've found an existing file we just skip any attempts to create
files/dirs, and just return the path.
* If the path (including file) does not exist, we check to see if the
directory exists first (possibly following symlinks). Directory creation
happens normally after this (note that any intermediary symlinks in this
process will still cause the process to fail, this is to prevent
infinite loops, as per the comments in
std.Io.Threaded.dirCreateDirPath).
This adds some better handling of existing paths when editing
configuration files:
* If we've found an existing file we just skip any attempts to create
files/dirs, and just return the path.
* If the path (including file) does not exist, we check to see if the
directory exists first (possibly following symlinks). Directory
creation happens normally after this (note that any intermediary
symlinks in this process will still cause the process to fail, this is
to prevent infinite loops, as per the comments in
std.Io.Threaded.dirCreateDirPath).
Fixes#13713.
`WeakRef(T)` offers `set` and `get`, so releasing one is spelled
`set(null)` — indistinguishable from an ordinary assignment. The
requirement that it *must* happen before the owning memory is freed
lives in a comment in `class/inspector_window.zig`, which is not where
somebody using the type is looking.
This adds `deinit`, forwarding to `g_weak_ref_clear` — the call GLib
documents for a `GWeakRef` that is going away — and switches the
dispose-time clears to it.
### What changed
- `weak_ref.zig`: new `deinit`, with the reasoning in its doc comment.
- `window.zig`, `split_tree.zig`, `application.zig`,
`command_palette.zig`: the four dispose-time clears now call `deinit`.
`set(null)` is unchanged and still valid. The clear in
`Application.handleReloadConfig` deliberately stays a `set(null)`: the
object is alive there and the reference is reused, so it is a logical
clear rather than teardown — which is the distinction the new name is
meant to make visible.
### Why it is worth a method
Zig has no destructors, so this enforces nothing; it is documentation
that happens to be executable. The concrete case is in #13713: I added a
`WeakRef(Window)` in a downstream branch, did not clear it, and closing
a window that had shown that dialog deadlocked the GTK main loop inside
`weak_ref_data_clear_list` locking freed memory. Every upstream call
site already gets this right — the point is only to put the rule where
the next person will see it.
### Testing
`zig build test` passes. `zig fmt --check` clean. Built and used on
Linux/GTK; the change is behaviourally identical to what was there,
since `g_weak_ref_clear` and `g_weak_ref_set(NULL)` both unregister.
---
**AI disclosure per `AI_POLICY.md`:** I investigated the underlying
incident with Claude Code and it drafted this change; I reviewed it.