Embedders that render theme editors, palette pickers, or custom settings
UI need to use the same color semantics as Ghostty.
This moves the shared parsing paths into terminal/color and exposes them
through libghostty-vt. Config color and palette parsing now delegate to
the same helpers, so CLI/config behavior and the C ABI stay in lockstep.
From C:
GhosttyColorRgb rgb;
ghostty_color_parse("ForestGreen", 11, &rgb);
uint8_t index;
ghostty_color_parse_palette_entry(
"0x10=#282c34", 12, &index, &rgb);
const GhosttyColorX11Entry* names =
ghostty_color_x11_names();
The exported color API is:
ghostty_color_parse
ghostty_color_parse_x11
ghostty_color_parse_palette_entry
ghostty_color_palette_default
ghostty_color_palette_generate
ghostty_color_luminance
ghostty_color_perceived_luminance
ghostty_color_contrast
ghostty_color_x11_names
ghostty_color_x11_name_count
The X11 name table is parsed once at comptime into null-terminated
entries in rgb.txt order. The existing case-insensitive map keeps the
same behavior for RGB.parse and +list-colors, while bindings can walk a
static table without allocations.
This doesn't add any more binary size since all of this was already used
by terminal internals.
Embedders that render theme editors, palette pickers, or custom
settings UI need to use the same color semantics as Ghostty.
This moves the shared parsing paths into terminal/color and exposes them
through libghostty-vt. Config color and palette parsing now delegate to
the same helpers, so CLI/config behavior and the C ABI stay in lockstep.
From C:
GhosttyColorRgb rgb;
ghostty_color_parse("ForestGreen", 11, &rgb);
uint8_t index;
ghostty_color_parse_palette_entry(
"0x10=#282c34", 12, &index, &rgb);
const GhosttyColorX11Entry* names =
ghostty_color_x11_names();
The exported color API is:
ghostty_color_parse
ghostty_color_parse_x11
ghostty_color_parse_palette_entry
ghostty_color_palette_default
ghostty_color_palette_generate
ghostty_color_luminance
ghostty_color_perceived_luminance
ghostty_color_contrast
ghostty_color_x11_names
ghostty_color_x11_name_count
The X11 name table is parsed once at comptime into null-terminated
entries in rgb.txt order. The existing case-insensitive map keeps the
same behavior for RGB.parse and +list-colors, while bindings can walk a
static table without allocations.
Add a shared encoder for CSI ? 997 ; Ps n color scheme reports and use
it for both CSI ? 996 n replies and unsolicited Termio reports. Export the
same encoder through the libghostty-vt C API with docs and an example.
This is a really light API, arguably easy for consumers to hardcode,
but it didn't match the rest of our style in the libghostty API so we
should expose it.
Example: GHOSTTY_COLOR_SCHEME_DARK encodes to ESC [ ? 997 ; 1 n,
while GHOSTTY_COLOR_SCHEME_LIGHT encodes to ESC [ ? 997 ; 2 n.
Embedders that render text outside the terminal grid need to predict
how many cells text will occupy once it is written to the terminal.
The existing codepoint width API exposes the table used by print, but
that is not enough for mode 2027 grapheme clustering: VS15/VS16, ZWJ
sequences, skin tone modifiers, and other continuation codepoints can
change the width of the whole cluster.
This exposes a single segment-and-measure API so callers use Ghostty
segmentation and width folding together:
uint8_t width;
size_t n = ghostty_unicode_grapheme_width(cps, len, &width);
From the Zig module:
const vt = @import("ghostty-vt");
const result = vt.unicode.graphemeWidth(u21, cps);
Callers loop until their string is consumed. The API is intentionally
not streaming: input must contain a complete first cluster or the
logical string end, so chunked readers should keep buffering when the
function consumes all available codepoints and more may arrive.
The terminal hot path now shares the width-decision func with the
API, the helper is inline and preserves the old branch structure. So
this doesn't change codegen at all.
This adds a GHOSTTY_SCROLL_VIEWPORT_ROW tag with a `size_t row` member
in the value union. The row is an absolute offset from the top of the
scrollable area, clamped to the active area, in the same row space as
the scrollbar offset so thumb positions round-trip cleanly:
ghostty_terminal_scroll_viewport(term,
(GhosttyTerminalScrollViewport){
.tag = GHOSTTY_SCROLL_VIEWPORT_ROW,
.value = {.row = 42},
});
The tag is appended to the existing enum and the union fits within the
reserved padding, so this is ABI compatible.
This also corrects the docs on GHOSTTY_TERMINAL_DATA_SCROLLBAR: the
getter is amortized O(1) (total is maintained incrementally, the offset
is cached), not "expensive". Since there is intentionally no change
callback, the docs now bless polling per frame or per write batch and
diffing, which is what Ghostty's own renderer does.
Motivation: Embedders building native scrollbars can already read scroll
state via GHOSTTY_TERMINAL_DATA_SCROLLBAR, but the write side only
exposed top/bottom/delta scrolling. Mapping a scrollbar thumb drag to an
absolute position required reading the current offset and computing a
delta, which is two calls that must be sequenced atomically by the
caller.
The core already supports absolute positioning and the macOS app uses it
for scroller drags via the scroll_to_row keybinding; this exposes the
same operation through the libghostty C API.
This adds a GHOSTTY_SCROLL_VIEWPORT_ROW tag with a `size_t row` member
in the value union. The row is an absolute offset from the top of the
scrollable area, clamped to the active area, in the same row space as
the scrollbar offset so thumb positions round-trip cleanly:
ghostty_terminal_scroll_viewport(term,
(GhosttyTerminalScrollViewport){
.tag = GHOSTTY_SCROLL_VIEWPORT_ROW,
.value = {.row = 42},
});
The tag is appended to the existing enum and the union fits within the
reserved padding, so this is ABI compatible.
This also corrects the docs on GHOSTTY_TERMINAL_DATA_SCROLLBAR: the
getter is amortized O(1) (total is maintained incrementally, the offset
is cached), not "expensive". Since there is intentionally no change
callback, the docs now bless polling per frame or per write batch and
diffing, which is what Ghostty's own renderer does.
Motivation: Embedders building native scrollbars can already read scroll state via
GHOSTTY_TERMINAL_DATA_SCROLLBAR, but the write side only exposed
top/bottom/delta scrolling. Mapping a scrollbar thumb drag to an
absolute position required reading the current offset and computing a
delta, which is two calls that must be sequenced atomically by the
caller.
The core already supports absolute positioning and the macOS
app uses it for scroller drags via the scroll_to_row keybinding; this
exposes the same operation through the libghostty C API.
Embedders that render text outside the terminal grid need to predict
how many cells a codepoint will occupy once it is written to the
terminal. The immediate motivation is IME preedit overlay rendering:
measuring preedit text with font APIs (e.g. CoreText advances) can
disagree with the terminal's unicode table on ambiguous-width CJK and
emoji, causing the overlay to visibly jump when the composed text
commits and reflows through the real grid layout.
This exposes the exact width table the terminal print path already
uses, so overlays are column-accurate by construction. From C:
uint8_t w = ghostty_unicode_codepoint_width(0x4E00); // 2
And from the Zig module:
const vt = @import("ghostty-vt");
const w = vt.unicode.codepointWidth(0x4E00); // 2
The function is total over its input: 0 for zero-width codepoints
(controls, combining marks, default-ignorables, surrogates), 2 for
wide codepoints (East Asian Wide/Fullwidth, regional indicators,
clamped at 2), and 1 for everything else, including invalid values
beyond U+10FFFF.
Perf: uses the LUT lookup we use for the main core terminal
Binary size: the width table was already linked into libghostty-vt
via the print path, so this adds only the exported wrapper.
Add a generation counter to the kitty graphics image storage. Every
content mutation (image transmit/replace, placement add, delete)
assigns the storage a fresh stamp, and every image is stamped when
it is added or replaced.
This solves two problems:
First, a retransmission of the same image ID with identical dimensions
was previously undetectable by anything comparing width, height, format,
and data length; the per-image stamp changes on every add/replace, so caches
keyed on it always see the change. Second, the dirty flag was the only
storage-wide signal, and it is also set by scrolling and resizing, which move
placements without changing contents. The generation is only bumped by content
mutations, so an unchanged value means the placement set and all
image data are identical and consumers can skip re-reading them,
recomputing only placement geometry.
The generation replaces Image.transmit_time entirely: newest-image
lookup by number, eviction ordering, and the renderer's texture
staleness checks all key on it now. A monotonic counter strictly
orders transmissions where Instant-based times could collide within
clock resolution (the renderer previously assumed equal timestamps
meant identical images), and this removes a syscall and an error
path per transmission.
Delete commands now only mark a mutation (dirty flag and
generation) when they actually remove something. A delete-all runs
on every screen clear, so previously every ESC [ 2 J dirtied the
image state even with no images stored. Eviction via setLimit also
now marks the state dirty, which it previously did not.
Both generations are exposed through libghostty-vt as
GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION and
GHOSTTY_KITTY_IMAGE_DATA_GENERATION (uint64_t), and the headers now
document that stored image data is always post-inflate/post-decode:
COMPRESSION always reports NONE, FORMAT is never PNG, and DATA_PTR
is raw pixels ready for GPU upload.
uint64_t gen = 0;
ghostty_kitty_graphics_get(
graphics, GHOSTTY_KITTY_GRAPHICS_DATA_GENERATION, &gen);
if (gen == last_gen) return; // nothing changed, skip re-reads
Previously the libghostty-vt stream handler dropped .report_pwd as a
no-op, so embedders never saw shell-reported cwd changes and the
terminal's pwd field was never populated from escape sequences.
Wire the action to setPwd and expose a pwd_changed callback analogous
to title_changed via GHOSTTY_TERMINAL_OPT_PWD_CHANGED. The payload is
passed through unparsed; embedders read it with ghostty_terminal_get
and decode any URI scheme themselves.
This hooks up the glyph protocol glossary to the terminal state. This
effectively makes us handle the APC protocol for it both in Ghostty GUI
and libghostty, although we didn't implement the renderer yet.
The Zig/C libghostty API also has a way to disable the protocol but it is
enabled by default. The memory usage is bound by the specification.
For dirty tracking for the renderer, we're going with the simple route that
any glyph change marks a coarse grained dirty flag and we'll [in the future]
rebuild the entire state in the renderer. I think this will be fine for
realistic workloads, but we can reassess in the future when we have
real workloads.
This PR adds 2 options to `libghostty-vt` to configure the style and
blink status of the default cursor. They control how the terminal
renders the cursor when a program doesn't request any explicit style or
when it resets it to the terminal's default state by sending a DECSCUSR
reset sequence (`CSI 0 q`).
The core had no signal to the apprt when the active selection changed,
so a consumer (e.g. a screen reader) kept reading a stale selection
until some unrelated query refreshed it.
This change adds a payload-less selection_changed action that's fired on
a selection state transition. The apprt reads the current selection
through the normal read path.
This consolidates selection state changes so the notification fires
consistently: all sites route through setSelection rather than calling
screen.select directly, including the mouse paths that previously
bypassed it for clipboard timing.
The new setSelectionAndCopy extends setSelection with the additional
'copy_on_select' behavior.
On macOS, this posts .ghosttySelectionDidChange, which is debounced
before posting a NSAccessibility .selectedTextChanged notification.
GTK has no consumer yet and no-ops the action.
Adds an option to `libghostty-vt` to configure the default cursor style
that should be displayed when an app sends a DECSCUSR reset sequence
(`CSI 0 q`).
Add a render-state row-cells getter that encodes the current cell's
full grapheme cluster directly as UTF-8 into a caller-provided
GhosttyBuffer. The getter writes the base codepoint first, followed by
any extra grapheme codepoints, and follows the existing buffer-writer
convention where len is bytes written on success or required capacity
on GHOSTTY_OUT_OF_SPACE.
Previously C consumers could query grapheme codepoints, but bindings
that needed UTF-8 text had to reconstruct and encode the cluster
themselves. That duplicated terminal internals in downstream bindings
and made users pay for awkward cross-language struct handling. By
owning the UTF-8/grapheme behavior in libghostty, bindings can use one
stable C API and optionally wrap it with small binding-local helpers.
Add a render row-cells data key for querying whether the current cell has
explicit styling. This lets consumers avoid fetching a raw cell or full style
snapshot when all they need is the cell's HasStyling bit.
The new key is appended to the existing enum for ABI safety and is served by
the existing row-cells getter path. Existing data keys and function exports are
unchanged.
Expose whether the terminal viewport is currently pinned to the active
area through the libghostty-vt terminal data API. Previously embedders
could only infer this from scrollbar geometry, which was indirect and
could require the more expensive scrollbar calculation.
The new GHOSTTY_TERMINAL_DATA_VIEWPORT_ACTIVE value returns the exact
PageList viewport state as a bool. The scroll viewport test now verifies
the value while moving between the active area and scrollback.
Render-state rows already expose their selected range, but
cell-oriented C API consumers had to fetch that row range separately
and duplicate the containment check while rendering.
Add a SELECTED row-cells data kind that carries the row selection into
the row-cells wrapper and returns whether the current cell column is in
that inclusive range. The field remains separate from cell colors and
style so selection stays an explicit render overlay policy.
For performance reasons, the span-based row getter is recommended still
but this is a convenient thing to do for cell-oriented folks.
Tracked grid references previously held a raw terminal wrapper pointer
and were required to be freed before the terminal. If callers kept one
past terminal destruction, later tracked-ref calls could dereference
freed terminal or page-list memory before detecting that the reference
was no longer meaningful.
Track live C tracked-grid-ref handles from the terminal wrapper and
detach them before tearing down terminal storage. Detached refs now
report no value through the tracked-ref APIs and can still be freed by
the caller. Update the C API docs to describe this lifetime behavior and
add a regression test for using a tracked ref after terminal free.
This introduces some overhead but tracked pins shouldn't be numerous and
this dramatically improves safety.
No API changes due to this (just more safety).
Tracked grid references previously held a raw terminal wrapper pointer and
were required to be freed before the terminal. If callers kept one past
terminal destruction, later tracked-ref calls could dereference freed
terminal or page-list memory before detecting that the reference was no
longer meaningful.
Track live C tracked-grid-ref handles from the terminal wrapper and detach
them before tearing down terminal storage. Detached refs now report no
value through the tracked-ref APIs and can still be freed by the caller.
Update the C API docs to describe this lifetime behavior and add a
regression test for using a tracked ref after terminal free.
This introduces some overhead but tracked pins shouldn't be numerous
and this dramatically improves safety.
Expose a C API for checking whether a GhosttyPoint is inside a
GhosttySelection. The new terminal helper validates the selection snapshot
against the active screen, resolves the point to a grid pin, and delegates
to the internal Selection.contains implementation so C consumers get the
same linear and rectangular selection semantics as Ghostty.
Wire the symbol through the C API exports and public headers, and add a
focused test covering linear containment and rectangular selection behavior.
Expose selection endpoint ordering through the libghostty-vt C API so
embedders can safely normalize selections whose start and end refs may be
reversed. The new APIs report the current order and return a fresh
untracked selection with forward or reverse bounds.
Selection.Order now uses lib.Enum, matching the existing adjustment enum
pattern and keeping the C ABI enum generated from the same Zig source of
truth. The new functions are wired through the C API re-export and lib-vt
export paths, with coverage for mirrored rectangular selection ordering.
Clarify that GhosttySelection is a snapshot type whose endpoints are
untracked GhosttyGridRef values. The previous documentation described the
range shape but did not repeat the grid reference lifetime caveat, which
made it easy to keep selections across terminal mutations incorrectly.
Render state already tracks the selected cell range for each viewport row,
but C renderers could only get the full terminal selection. That required
consumers to map global selection pins back into row-local spans themselves.
Add row selection data to the render-state row API. Querying the new row
data returns GHOSTTY_NO_VALUE for unselected rows and writes the inclusive
start and end columns for selected rows. The render example now demonstrates
setting a selection and reading the row-local range while iterating rows.
Add terminal set/get support for the active screen selection through the
existing option and data APIs. Setting a selection copies the C snapshot
into terminal-owned tracked state, while passing NULL clears the current
selection.
Getting the selection now returns an untracked GhosttySelection snapshot
or GHOSTTY_NO_VALUE when there is no selection. The C header documents
the different lifetimes for set and get so embedders know when input and
returned grid references remain valid.
Add a C API for tracked pins, known as a tracked grid ref in C.
The new API can create tracked refs from terminal points, snapshot them
back to regular grid refs for cell access, convert them to coordinates,
move them to a new point, report when their semantic location was lost,
and free the tracked pin bookkeeping. This is backed by PageList tracked
pins and exposed through the libghostty-vt export layer and headers.
Expose toggle-quick-terminal as a proper IPC action so it can be
triggered via 'ghostty +toggle-quick-terminal' from the command line,
instead of calling the raw D-Bus org.gtk.Actions.Activate interface.
This follows the same pattern as the existing +new-window IPC command:
- Add toggle_quick_terminal to apprt.ipc.Action enum (Zig + C ABI)
- Create apprt/gtk/ipc/toggle_quick_terminal.zig (GTK D-Bus handler)
- Route .toggle_quick_terminal in apprt/gtk/App.zig performIpc
- Register toggle-quick-terminal GAction in application.zig
- Add +toggle-quick-terminal CLI handler in cli/
- Register in cli/ghostty.zig Action enum, runMain, and options
- Add stub in apprt/embedded.zig
- Update include/ghostty.h C header enum
Usage:
ghostty +toggle-quick-terminal
Closes: #12618
Previously `ghostty_app_key_is_binding` (unlike Surface) is just using `config.keybind` to check whether a KeyEvent is in the set or not.
After this, I can add unit tests for keybinding more easily, with dummy configs.
Expose the foreground process PID and TTY device path as read-only properties on the AppleScript terminal class and App Intents TerminalEntity. This enables reliable process-to-terminal mapping for automation tools when multiple terminals share the same CWD.
Closes#11592Closes#10756
Session: 019d341c-a165-7843-a2f7-2f426114cf17
This mode allows programs to modify the code that the `backspace`
key (backarrow key in DEC parlance) sends. If this mode is
`off`/`false`/`reset` (the default, the same as before this PR), we
send the byte `0x7f`. If this mode is `on`/`true`/`set` we send the
byte `0x08`.