Skip to content

libghostty: introduce optional "effects" to handle queries and side effects for terminals#11787

Merged
mitchellh merged 15 commits intomainfrom
libvt-effects
Mar 23, 2026
Merged

libghostty: introduce optional "effects" to handle queries and side effects for terminals#11787
mitchellh merged 15 commits intomainfrom
libvt-effects

Conversation

@mitchellh
Copy link
Copy Markdown
Contributor

Renames ReadonlyStream to TerminalStream and introduces an effects-based callback system so that the stream handler can optionally respond to queries and side effects (bell, title changes, device attributes, device status, size reports, XTVERSION, ENQ, DECRQM, kitty keyboard queries).

The default behavior is still read-only, callers have to opt-in to setting callbacks to function pointers.

This doesn't handle every possible side effect yet, e.g. this doesn't include clipboards, pwd reporting, and others. But this covers the important ones.

This PR is Zig only, the C version of this will come later.

Add an Effects struct to the readonly stream Handler that allows
callers to provide optional callbacks for side effects like bell.
Previously, the bell action was silently ignored along with other
query/response actions. Now it is handled separately and dispatched
through the effects callback if one is provided.

Add a test that verifies bell with a null callback (default readonly
behavior) does not crash, and that a provided callback is invoked
the correct number of times.
Rename stream_readonly.zig to stream_terminal.zig and its exported
types from ReadonlyStream/ReadonlyHandler to TerminalStream. The
"readonly" name is now wrong since the handler now supports
settable effects callbacks. The new name better reflects that this
is a stream handler for updating terminal state.
Add a generic write_pty effect callback to the stream terminal
handler, allowing callers to receive pty response data. Use it to
implement request_mode and request_mode_unknown (DECRQM), which
encode the mode state as a DECRPM response and write it back
through the callback. Previously these were silently ignored.

The write_pty data is stack-allocated and only valid for the
duration of the call.
Previously the window_title action was silently ignored in the
readonly stream handler. Add a set_window_title callback to the
Effects struct so callers can be notified when a window title is
set via OSC 2. Follows the same pattern as bell and write_pty
where the callback is optional and defaults to null in readonly
mode.
Add a title field to Terminal, mirroring the existing pwd field.
The title is set via setTitle/getTitle and tracks the most recent
value written by OSC 0/2 sequences. The stream handler now persists
the title in terminal state in addition to forwarding it to the
surface. The field is cleared on full reset.
The effect callback no longer receives the title string directly.
Instead, the handler stores the title in terminal state via setTitle
before invoking the callback, so consumers query it through
handler.terminal.getTitle(). This removes the redundant parameter
and keeps the effect signature consistent with the new terminal
title field. Tests now verify terminal state directly rather than
tracking the title through the callback.
Previously kitty_keyboard_query was listed as a no-op in the
readonly stream handler. This implements it using the write_pty
effect callback so that the current kitty keyboard flags are
reported back via the pty, matching the behavior in the full
stream handler.
Add an xtversion callback to the Effects struct so that
stream_terminal can respond to XTVERSION queries. The callback
returns the version string to embed in the DCS response. If the
callback is unset or returns an empty string, the response defaults
to "libghostty". The response is formatted and written back via the
existing write_pty effect.
Add a `size` callback to the stream_terminal Effects struct that
returns a size_report.Size geometry snapshot for XTWINOPS size
queries (CSI 14/16/18 t). The handler owns all protocol encoding
using the existing size_report.encode, keeping VT knowledge out
of effect consumers. This follows the same pattern as the xtversion
effect: the callback supplies data, the handler formats the reply
and calls write_pty.

CSI 21 t (title report) is handled internally from terminal state
since the title is already available via terminal.getTitle() and
does not require an external callback.
ReadonlyStream was removed from the public API. Update the stream
fuzzer to use TerminalStream, which is the type now returned by
Terminal.vtStream().
Previously the ENQ (0x05) action was ignored in stream_terminal,
listed in the no-op group alongside other unhandled queries. The
real implementation in termio/stream_handler writes a configurable
response string back to the pty.

Add an enquiry callback to Effects following the same query-style
pattern as xtversion: the callback returns the raw response bytes
and the handler owns writing them to the pty via writePty. When no
callback is set (readonly mode), ENQ is silently ignored. Empty
responses are also ignored. The response is capped at 256 bytes
using a stack buffer with sentinel conversion for writePty.
Previously device_status was in the ignored "no terminal-modifying
effect" group in stream_terminal.zig. This ports it to use the
Effects pattern, handling all three DSR request types.

Operating status and cursor position are handled entirely within
stream_terminal since they only need terminal state and write_pty.
Cursor position respects origin mode and scrolling region offsets.

Color scheme adds a new color_scheme effect callback that returns
a ColorScheme enum (light/dark). The handler encodes the response
internally, keeping protocol knowledge in the terminal layer. A
new ColorScheme type is added to device_status.zig so the terminal
layer does not depend on apprt.
Introduce a dedicated device_attributes.zig module that consolidates
all device attribute types and encoding logic. This moves
DeviceAttributeReq out of ansi.zig and adds structured response
types for DA1 (primary), DA2 (secondary), and DA3 (tertiary) with
self-encoding methods.

Primary DA uses a ConformanceLevel enum covering VT100-series
per-model values and VT200+ conformance levels, plus a Feature
enum with all known xterm DA1 attribute codes (132-col, printer,
sixel, color, clipboard, etc.) as a simple slice. Secondary DA
uses a DeviceType enum matching the xterm decTerminalID values.
Tertiary DA encodes the DECRPTUI unit ID as a u32 formatted to
8 hex digits.

This is preparatory work for exposing device attributes through
the stream_terminal Effects callback system.
Add a device_attributes effect callback to the stream_terminal
Handler. The callback returns a device_attributes.Attributes
struct which the handler encodes and writes back to the pty.

Add Attributes.encode which dispatches to the correct sub-type
encoder based on the request type (primary, secondary, tertiary).

In readonly mode the callback is null so all DA queries are
silently ignored, matching the previous behavior where
device_attributes was in the ignored actions list.

Tests cover all three DA types with default attributes, custom
attributes, and readonly mode.
@mitchellh mitchellh added this to the 1.4.0 milestone Mar 23, 2026
The default firmware_version for Secondary device attributes is 0,
but the test expected a value of 10. Update the test expectation to
match the actual default.
@mitchellh mitchellh requested a review from a team as a code owner March 23, 2026 22:02
@mitchellh mitchellh merged commit 69104fb into main Mar 23, 2026
162 checks passed
@mitchellh mitchellh deleted the libvt-effects branch March 23, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant