Add ghostty_mode_report_encode() which encodes a DECRPM response
sequence into a caller-provided buffer. The function takes a mode
tag, a report state integer, an output buffer, and writes the
appropriate CSI sequence (with ? prefix for DEC private modes).
The Zig-side ReportState is a non-exhaustive c_int enum that uses
std.meta.intToEnum for safe conversion to the internal type,
returning GHOSTTY_INVALID_VALUE on overflow. The C header exposes
a GhosttyModeReportState enum with named constants for the five
standard DECRPM state values.
Add modes.h with GhosttyModeTag (uint16_t) matching the Zig ModeTag
packed struct layout, along with inline helpers for constructing and
inspecting mode tags. Provide GHOSTTY_MODE_* macros for all 39
built-in modes (4 ANSI, 35 DEC), parenthesized for safety.
Add ghostty_terminal_mode_get and ghostty_terminal_mode_set to
terminal.h, both returning GhosttyResult so that null terminals
and unknown mode tags return GHOSTTY_INVALID_VALUE. The get function
writes its result through a bool out-parameter.
Add a note in the Zig mode entries reminding developers to update
modes.h when adding new modes.
This extracts our mode reporting from being hardcoded in termio
to being reusable in the existing `terminal.modes` namespace. The goal
is to expose this via the Zig API libghostty (done) and C API (to do
later).
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public
API, following the same patterns as key and mouse encoding.
The focus Event enum uses lib.Enum for C ABI compatibility. The C API
provides ghostty_focus_encode() which writes into a caller-provided
buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when the
buffer is too small.
Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE
instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors, reserving
OUT_OF_MEMORY for actual allocation failures. Update all corresponding
header documentation.
Add focus event encoding (CSI I / CSI O) to the libghostty-vt public
API, following the same patterns as key and mouse encoding.
The focus Event enum uses lib.Enum for C ABI compatibility. The C API
provides ghostty_focus_encode() which writes into a caller-provided
buffer and returns GHOSTTY_OUT_OF_SPACE with the required size when
the buffer is too small.
Also update key and mouse encoders to return GHOSTTY_OUT_OF_SPACE
instead of GHOSTTY_OUT_OF_MEMORY for buffer-too-small errors,
reserving OUT_OF_MEMORY for actual allocation failures. Update all
corresponding header documentation.
The way we originally handled globals gradually escalated into an unholy
mess of ad-hoc helper functions and special-case handlers, which proved
to be hard to scale. Using a type-erased EnumMap like this makes
everything *far* easier to work and reason with, I think.
Also nuked the `xdg_wm_dialog_v1` hack that was necessary to prevent
old versions of gtk4-layer-shell crashing. If by the time of 1.4's
release people are still using those versions, it's on them.
TL;DR: this description is (intentionally) nonsense but I ran
`\b(\w+)\s\1\b` over `src` and stole a singular typo fix from #11528.
Replacement of #11528 with 100% less slop and 99% less AI; I didn't feel
like saying no to free(ish) typo checking. Note that many of the fixes
there were outright incorrect (and clearly had no review from sentient
lifeforms, contrary to its—sorry, it's—description). A lot of extra
double words were caught with a handy `rg --pcre2 '\b(\w+)\s+\1\b' src`;
you could say this PR was “ripgrep-assisted” the way that one was
“AI-assisted”. Rather ironic since that PR also claims to have used grep
via Claude Code, but missed a lot of them.
The its → it's changes from that PR were elided; I decided to run a `rg
"\bit'?s\b" src`, but someone REALLY likes their its, so I reverted my
changes as there were an extremely large number of changes (probably a
hundred files with multiple hundred cases). The only other change was
“baout” → “about”.
# AI Usage
Claude Code was used by proxy for finding baout. Claude Code was used by
proxy for realizing that the correct spelling is about. Claude Code was
not used for fixing it. Oh my god it was so difficult to fix, the
original PR had it so easy. I had to type out the file name (fish's AI
sorry I mean autocomplete helped though) and like, type /baout, press R,
press ab, then save and exit. This is so difficult you know we should
use an AI for this, like this is so hard I don't know how people manage.
All changes were verified by me: I consulted the dictionary to delve
into double-checkment of “in existence; being in evidence; apparent.”
Uhhh insert assorted other AI impersonation here maybe? THE LLM IN ME
WANTS TO ESCAPE PLEASE HELP
This is a new CLI action that prints an option or keybind's help
documentation to stdout.
ghostty +explain-config font-size
ghostty +explain-config copy_to_clipboard
ghostty +explain-config --option=font-size
ghostty +explain-config --keybind=copy_to_clipboard
The --option and --keybind flags perform a specific lookup. A string
passed as a positional argument attempts to look up the name first as an
option and then as a keybind.
Our vim plugin uses this with &keywordprg, which allows you to look up
the documentation for the config option or keybind under the cursor (K).
## Problem
Ghostty's `ssh-env` shell integration uses `-o "SetEnv
COLORTERM=truecolor"` when wrapping SSH commands. OpenSSH treats
command-line `-o SetEnv` options as **replacements** for all `SetEnv`
entries in `~/.ssh/config`, not additions. This silently drops any
user-configured `SetEnv` variables.
For example, a user with this in their SSH config:
```
Host myserver
SetEnv MY_VAR=hello
```
...would find `MY_VAR` empty after SSHing through Ghostty with `ssh-env`
enabled.
Reference: https://github.com/ghostty-org/ghostty/discussions/10871
## Fix
Replace `-o "SetEnv COLORTERM=truecolor"` with the additive pattern: set
`COLORTERM=truecolor` locally before the SSH call and forward it via
`SendEnv`.
`SendEnv` is additive — it does not clobber `SetEnv` entries in
`~/.ssh/config`.
**Trade-off:** `SendEnv` requires `AcceptEnv COLORTERM` on the remote
server (unlike `SetEnv`). But this was already the case for
`TERM_PROGRAM`/`TERM_PROGRAM_VERSION`, so it's a consistent and
acceptable approach.
## Changes
All 5 shell integration files updated with the same pattern:
- `SetEnv COLORTERM=truecolor` option removed
- `COLORTERM` added to the existing `SendEnv` option
- `COLORTERM=truecolor` set as a local env var on the execute line (so
`SendEnv` has something to forward)
## Test plan
- [ ] Enable `ssh-env` in Ghostty config: `shell-integration-features =
ssh-env`
- [ ] Add `SetEnv MY_VAR=hello` under a host in `~/.ssh/config` and
`AcceptEnv MY_VAR` in `/etc/ssh/sshd_config` on the remote
- [ ] SSH to that host — `echo $MY_VAR` should return `hello` (was empty
before this fix)
- [ ] `echo $COLORTERM` returns `truecolor` (requires `AcceptEnv
COLORTERM`)
- [ ] `echo $TERM_PROGRAM` still propagates (same `AcceptEnv`
requirement as before)
Export mouse_encode types and functions through the lib_vt public
input API, mirroring the existing key encoding exports. This adds
MouseAction, MouseButton, MouseEncodeOptions, MouseEncodeEvent,
and encodeMouse so that consumers of the Zig module can encode
mouse events without reaching into internal packages.
Expose the internal mouse encoding functionality through the C API,
following the same pattern as the existing key encoding API. This
allows external consumers of libvt to encode mouse events into
terminal escape sequences (X10, UTF-8, SGR, URxvt, SGR-Pixels).
The API is split into two opaque handle types: GhosttyMouseEvent
for building normalized mouse events (action, button, modifiers,
position) and GhosttyMouseEncoder for converting those events into
escape sequences. The encoder is configured via a setopt interface
supporting tracking mode, output format, renderer geometry, button
state, and optional motion deduplication by last cell.
Encoder state can also be bulk-configured from a terminal handle
via ghostty_mouse_encoder_setopt_from_terminal. Failed encodes due
to insufficient buffer space report the required size without
mutating deduplication state.
Convert Coordinate in terminal/point.zig and CellSize, ScreenSize,
GridSize, and Padding in renderer/size.zig to extern structs. All
fields are already extern-compatible types, so this gives them a
guaranteed C ABI layout with no functional change.
Convert the Event and Format enums from fixed-size Zig enums to
lib.Enum so they are C ABI compatible when targeting C. The motion
method on Event becomes a free function eventIsMotion since lib.Enum
types cannot have declarations.
This is a new CLI action that prints an option or keybind's help
documentation to stdout.
ghostty +explain-config font-size
ghostty +explain-config copy_to_clipboard
ghostty +explain-config --option=font-size
ghostty +explain-config --keybind=copy_to_clipboard
The --option and --keybind flags perform a specific lookup. A string
passed as a positional argument attempts to look up the name first as an
option and then as a keybind.
Our vim plugin uses this with &keywordprg, which allows you to look up
the documentation for the config option or keybind under the cursor (K).
Move MouseEvent and MouseFormat out of Terminal.zig and MouseShape out
of mouse_shape.zig into a new mouse.zig file. The types are named
without the Mouse prefix inside the module (Event, Format, Shape) and
re-exported with the prefix from terminal/main.zig for external use.
Update all call sites (mouse_encode.zig, surface_mouse.zig, stream.zig)
to import through terminal/main.zig or directly from mouse.zig. Remove
the now-unused mouse_shape.zig.
Move mouse event encoding logic from Surface.zig into a new
input/mouse_encode.zig file.
The new file encapsulates event filtering (shouldReport),
button code computation, viewport bounds checking, motion
deduplication, and all five wire formats (X10, UTF-8, SGR,
urxvt, SGR-pixels). This makes the encoding independently
testable and adds unit tests covering each format and edge
case.
Additionally, Surface `mouseReport` can no longer fail, since the only
failure mode is no buffer space which should be impossible. Updated
the signature to remove the error set.
Expose the key encoder Options.fromTerminal function to the C API as
ghostty_key_encoder_setopt_from_terminal. This lets C callers sync all
terminal-derived encoding options (cursor key application mode, keypad
mode, alt escape prefix, modifyOtherKeys, and Kitty flags) in a single
call instead of setting each option individually.
Change `window-padding-balance` from `bool` to an enum with three
values:
- `false` - no balancing (default, unchanged)
- `true` - balance with vshift that caps top padding and shifts excess
to bottom (existing behavior, unchanged)
- `equal` - balance whitespace equally on all four sides
This gives users who prefer truly equal padding a way to opt in without
changing the default behavior.
The Discarding writer count field is u64, but several call sites
pass it where a usize is expected. On wasm32-freestanding, usize is
32-bit, so this caused compilation errors.
Use std.math.cast instead of a bare @intCast so that overflow is
handled gracefully, returning WriteFailed rather than triggering
safety-checked undefined behavior at runtime.
The Discarding writer count field is u64, but appendNTimes expects
usize which is u32 on 32-bit targets like arm-linux-androideabi.
Use std.math.cast instead of @intCast to safely handle the
conversion, returning WriteFailed on overflow rather than risking
undefined behavior.
Rename the existing format function to format_buf to clarify that it
writes into a caller-provided buffer. Add a new format_alloc variant
that allocates the output buffer internally using the provided
allocator (or the default if NULL). The caller receives the allocated
pointer and length and is responsible for freeing it.
This is useful for consumers that do not know the required buffer size
ahead of time and want to avoid the two-pass query-then-format pattern
needed with format_buf.
Add a size field as the first member of formatter option structs
(TerminalOptions, TerminalOptions.Extra, ScreenOptions.Extra) for ABI
compatibility. This allows adding new fields without breaking callers
compiled against older versions of the struct.
Introduce include/ghostty/vt/types.h as the foundational header
containing GhosttyResult and the GHOSTTY_INIT_SIZED macro for
zero-initializing sized structs. Remove the separate result.h header,
moving its contents into types.h.
The terminal.Stream next/nextSlice functions can now no longer fail.
All prior failure modes were fully isolated in the handler `vt`
callbacks. As such, vt callbacks are now required to not return an error
and handle their own errors somehow.
Allowing streams to be fallible before was an incorrect design. It
caused problematic scenarios like in `nextSlice` early terminating
processing due to handler errors. This should not be possible.
There is no safe way to bubble up vt errors through the stream because
if nextSlice is called and multiple errors are returned, we can't
coalesce them. We could modify that to return a partial result but its
just more work for stream that is unnecessary. The handler can do all of
this.
This work was discovered due to cleanups to prepare for more C APIs.
Less errors make C APIs easier to implement! And, it helps clean up our
Zig, too.
Adds progress-style config to control OSC 9;4 progress bar visibility.
Defaults to true, set false to hide.
Fixes#11241
AI Disclosure: Claude Code (Opus 4.6) used for codebase exploration,
code review, and testing assistance. All code written and reviewed by
hand.
Use OSC 133;P (prompt mark) instead of 133;A (fresh line + prompt mark)
inside PS1 and PS2. Readline redraws the prompt on vi mode switches,
Ctrl-L, and other events, and 133;A's fresh-line behavior would emit a
CR+LF whenever the cursor wasn't at column 0, causing visible extra
newlines.
The one-time 133;A is now emitted via printf in __ghostty_precmd, which
only runs once per prompt cycle via PROMPT_COMMAND. On SIGWINCH, bash
redraws PS1 (firing the 133;P marks) but doesn't re-run PROMPT_COMMAND,
so there's no unwanted fresh-line on resize either. The redraw=last flag
persists from the initial printf.
This is a little less optimal than our previous approach, in terms of
number of prompt marks we emit, but it produces an overall more correct
result, which is the important thing.
Because readline prints its output outside the scope of PS1, those
characters "inherit" the surrounded prompt scope. This is usually fine,
but it can sometimes get out of sync (especially during redraws). This
is inherently a limitation of the fact that it's a separate output
channel, so we just have to accept that can happen.
See: #11267
Fixes#11316
This mirrors the `prompt` actions (hence why there is no window action
here) and enables setting titles via keybind actions which importantly
lets this work via command palettes, App Intents, AppleScript, etc.
Only replace the \n prompt escape when inserting secondary prompt marks,
not literal newlines ($'\n'). Literal newlines may appear inside $(...)
or `...` command substitutions, and inserting escape sequences there
breaks the shell syntax. For example:
PS1='$(if [ $? -eq 0 ]; then echo -e "P";
else echo -e "F";
fi) $ '
The literal newlines between the if/else/fi are part of the shell syntax
inside the command substitution. The previous code replaced all literal
newlines in PS1 with newline + OSC 133 escape sequences, which injected
terminal escapes into the middle of the command substitution and caused
bash to report a syntax error when evaluating it.
The \n prompt escape is PS1-specific and safe to replace globally. This
means prompts using literal newlines for line breaks (rather than \n)
won't get per-line secondary marks, but this is the conventional form
and avoids the need for complex shell parsing.
Fixes: #11267
Replace the strip-in-preexec / re-add-in-precmd pattern for OSC 133
marks with a save/restore approach. Instead of pattern-matching marks
out of PS1 (which exposes PS1 in intermediate states to other hooks), we
save the original PS1/PS2 before adding marks and then restore them.
This also adds dynamic theme detection: if PS1 changed between cycles
(e.g., a theme rebuilt it), we skip injecting continuation marks into
newlines. This prevents breaking plugins like Pure that use pattern
matching to strip/rebuild the prompt.
Additionally, move _ghostty_precmd to the end of precmd_functions in
_ghostty_deferred_init (instead of substituting in-place) so that the
first prompt is properly marked even when other hooks were appended
after our auto-injection.
There's one scenario that we still don't complete cover:
precmd_functions+=(_test_overwrite_ps1)
_test_overwrite_ps1() {
PS1="test> "
}
... which results in the first prompt not printing its prompt marks
because _test_overwrite_ps1 becomes the last thing to run, overwriting
our marks, but this will be fixed for subsequent prompts when we move
our handler back to the last index.
Fixes: #11282
If the CLI argument `--working-directory` is not used with
`+new-window`, the current working directory that `ghostty +new-window`
is run from will be appended to the list of configuration data sent
to the main Ghostty process. If `-e` _was_ used on the CLI, the
`--working-directory` that was appended will be interpreted as part of
the command to be executed, likely causing it to fail.
Instead, insert `--working-directory` at the beginning of the list of
configuration that it sent to the main Ghostty process.
Fixes#11356