libghostty: introduce optional "effects" to handle queries and side effects for terminals#11787
Merged
libghostty: introduce optional "effects" to handle queries and side effects for terminals#11787
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Renames
ReadonlyStreamtoTerminalStreamand 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.