Previously libghostty-vt had no way for C consumers to query, set, or
report on terminal modes. Callers that needed to respond to DECRPM
requests or inspect mode state had no public interface to do so.
This adds three layers of mode support to the C API:
- `GhosttyMode` — a 16-bit packed type with inline helpers to construct
and inspect mode tags, plus `GHOSTTY_MODE_*` macros for all supported
ANSI and DEC private modes.
- `ghostty_terminal_mode_get` / `ghostty_terminal_mode_set` — query and
set mode values on a terminal handle.
- `ghostty_mode_report_encode` — encode a DECRPM response sequence (`CSI
[?] Ps1 ; Ps2 $ y`) into a caller-provided buffer.
## Example
```c
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
char buf[32];
size_t written = 0;
// Query a terminal's cursor visibility and encode a DECRPM report
GhosttyMode mode = GHOSTTY_MODE_CURSOR_VISIBLE;
bool value = false;
ghostty_terminal_mode_get(terminal, mode, &value);
GhosttyModeReportState state = value
? GHOSTTY_MODE_REPORT_SET
: GHOSTTY_MODE_REPORT_RESET;
if (ghostty_mode_report_encode(mode, state, buf, sizeof(buf), &written)
== GHOSTTY_SUCCESS) {
// writes ESC[?25;1$y or ESC[?25;2$y
fwrite(buf, 1, written, stdout);
}
}
```
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.
Add modes.h with GhosttyModeTag, a uint16_t typedef matching the
Zig ModeTag packed struct layout (bits 0-14 for the mode value,
bit 15 for the ANSI flag). Three inline helper functions provide
construction and inspection: ghostty_mode_tag_new,
ghostty_mode_tag_value, and ghostty_mode_tag_ansi.
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).
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).
The milestone action currently runs for all merged pull_request_target
close events, including PRs opened by bots such as dependabot and
ghostty-vouch. That causes milestone binding to run on automated PRs
that should be ignored.
Gate the update-milestone job so pull request events only run when the
author is not a bot, while still allowing closed-issue events to run.
This preserves existing issue milestone behavior and prevents bot PRs
from triggering the workflow.
The milestone action currently runs for all merged pull_request_target
close events, including PRs opened by bots such as dependabot and
ghostty-vouch. That causes milestone binding to run on automated PRs
that should be ignored.
Gate the update-milestone job so pull request events only run when the
author is not a bot, while still allowing closed-issue events to run.
This preserves existing issue milestone behavior and prevents bot PRs
from triggering the workflow.
## 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)
This adds a Zig and C API for mouse event encoding.
With these APIs in place, users can now create mouse events, configure a
mouse encoder with tracking mode, output format, and terminal size, and
encode those events into terminal escape sequences. All standard mouse
protocols are supported: X10, UTF-8, SGR, URxvt, and SGR-Pixels.
## Example
```c
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <ghostty/vt.h>
int main() {
GhosttyMouseEncoder encoder;
GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder);
assert(result == GHOSTTY_SUCCESS);
// Set tracking mode to normal (button press/release)
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_EVENT,
&(GhosttyMouseTrackingMode){GHOSTTY_MOUSE_TRACKING_NORMAL});
// Set output format to SGR
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_FORMAT,
&(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR});
// Set terminal geometry so the encoder can map pixel positions to cells
ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE,
&(GhosttyMouseEncoderSize){
.size = sizeof(GhosttyMouseEncoderSize),
.screen_width = 800, .screen_height = 600,
.cell_width = 10, .cell_height = 20,
});
// Create mouse event: left button press at pixel position (50, 40)
GhosttyMouseEvent event;
result = ghostty_mouse_event_new(NULL, &event);
assert(result == GHOSTTY_SUCCESS);
ghostty_mouse_event_set_action(event, GHOSTTY_MOUSE_ACTION_PRESS);
ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT);
ghostty_mouse_event_set_position(event, (GhosttyMousePosition){.x = 50.0f, .y = 40.0f});
// Encode the mouse event
char buf[128];
size_t written = 0;
result = ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written);
assert(result == GHOSTTY_SUCCESS);
fwrite(buf, 1, written, stdout);
ghostty_mouse_event_free(event);
ghostty_mouse_encoder_free(encoder);
return 0;
}
```
## New APIs
| Function | Description |
|----------|-------------|
| `ghostty_mouse_event_new` | Create a new mouse event instance |
| `ghostty_mouse_event_free` | Free a mouse event instance |
| `ghostty_mouse_event_set_action` | Set the event action (press,
release, motion) |
| `ghostty_mouse_event_get_action` | Get the event action |
| `ghostty_mouse_event_set_button` | Set the event button |
| `ghostty_mouse_event_clear_button` | Clear the event button (for
motion events) |
| `ghostty_mouse_event_get_button` | Get the event button (returns
whether one is set) |
| `ghostty_mouse_event_set_mods` | Set keyboard modifiers held during
the event |
| `ghostty_mouse_event_get_mods` | Get keyboard modifiers held during
the event |
| `ghostty_mouse_event_set_position` | Set position in surface-space
pixels |
| `ghostty_mouse_event_get_position` | Get position in surface-space
pixels |
| `ghostty_mouse_encoder_new` | Create a new mouse encoder instance |
| `ghostty_mouse_encoder_free` | Free a mouse encoder instance |
| `ghostty_mouse_encoder_setopt` | Set an encoder option (tracking mode,
format, size, etc.) |
| `ghostty_mouse_encoder_setopt_from_terminal` | Sync encoder options
from a terminal's current state |
| `ghostty_mouse_encoder_reset` | Reset internal encoder state (motion
deduplication) |
| `ghostty_mouse_encoder_encode` | Encode a mouse event into a terminal
escape sequence |
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.
Add a new c-vt-mouse-encode example that demonstrates how to use the
mouse encoder C API. The example creates a mouse encoder configured
with SGR format and normal tracking mode, sets up terminal geometry
for pixel-to-cell coordinate mapping, and encodes a left button press
event into a terminal escape sequence.
Mirrors the structure of the existing c-vt-key-encode example. Also
adds the corresponding @example doxygen reference in vt.h.
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).
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from
4.0.0 to 4.0.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fbd0ab8f3e"><code>fbd0ab8</code></a>
feat: add merge_group event support</li>
<li><a
href="efb1da7ce8"><code>efb1da7</code></a>
feat: add dist/ freshness check to PR workflow</li>
<li><a
href="d8f7b061b2"><code>d8f7b06</code></a>
Merge pull request <a
href="https://redirect.github.com/dorny/paths-filter/issues/302">#302</a>
from dorny/issue-299</li>
<li><a
href="addbc147a9"><code>addbc14</code></a>
Update README for v4</li>
<li>See full diff in <a
href="9d7afb8d21...fbd0ab8f3e">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
</details>
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.