libghostty: add ghostty_search_* terminal search C API

This commit is contained in:
Mitchell Hashimoto
2026-08-31 13:49:25 -07:00
parent f0c918fc4b
commit f9202919f7
7 changed files with 563 additions and 0 deletions

View File

@@ -32,6 +32,7 @@
* - @ref render "Render State" - Incremental render state updates for custom renderers
* - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML
* - @ref snapshot "Terminal Snapshot" - Encode and incrementally restore terminal state
* - @ref search "Search" - Search terminal contents, including scrollback
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences
* - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences
* - @ref paste "Paste" - Paste into a terminal, validate and encode paste data
@@ -156,6 +157,7 @@ extern "C" {
#include <ghostty/vt/paste.h>
#include <ghostty/vt/point.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/search.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/size_report.h>
#include <ghostty/vt/snapshot.h>

498
include/ghostty/vt/search.h Normal file
View File

@@ -0,0 +1,498 @@
/**
* @file search.h
*
* Search terminal contents, including scrollback, for a string.
*/
#ifndef GHOSTTY_VT_SEARCH_H
#define GHOSTTY_VT_SEARCH_H
#include <stddef.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/selection.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup search Search
*
* Search a terminal for a string, covering the active area and
* scrollback of both the primary and alternate screens.
*
* A GhosttySearch searches one terminal for one needle. It is bound to
* the terminal it was created with and handles the hard parts of
* terminal search internally: results stay in sync with the live
* screens, survive primary/alternate screen switches (entering and
* leaving a fullscreen app such as vim does not restart a scrollback
* search), and recover from resize, reflow, resets, and scrollback
* pruning.
*
* ## Driving a search
*
* Searching a large scrollback takes time, so the work is split into
* small steps the caller drives so that the caller can control
* performance more directly:
*
* - ghostty_search_tick() makes a bounded amount of progress on data
* the search has already copied. It never touches the terminal,
* meaning it can be safely called from a thread.
* - ghostty_search_feed() reads the terminal to copy in more data and
* pick up terminal changes. Feeding is the only way the search
* learns that the terminal changed, so keep feeding periodically
* while the search is in use. This requires exclusive terminal access.
* - ghostty_search_run() is a blocking convenience that feeds and
* ticks until the search is caught up.
*
* GHOSTTY_SEARCH_STATUS_COMPLETE means the search is caught up with
* the terminal as of the last feed. It never means finished forever,
* since later terminal writes require another feed to be seen.
*
* ## Matches are selections
*
* Every match is returned as a GhosttySelection snapshot with
* rectangle set to false, so the existing selection APIs all work on
* matches: ghostty_terminal_selection_format_buf() to copy the
* matched text, ghostty_terminal_point_from_grid_ref() with
* GHOSTTY_POINT_TAG_VIEWPORT to position highlight rectangles,
* ghostty_terminal_selection_contains() for hit testing, and
* ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to make
* a match the terminal's selection.
*
* Returned matches follow the usual snapshot lifetime rules: they are
* only valid until the next operation that modifies the terminal,
* including ghostty_terminal_vt_write(), resize, reset, and free.
* Read matches after a feed, use them before the terminal changes
* again, and re-read them rather than caching them. The selected
* match is kept accurate internally across terminal changes, so the
* safe way to follow a match is to re-read
* GHOSTTY_SEARCH_DATA_SELECTED_MATCH after each feed.
*
* ## Lifetime
*
* The search borrows the terminal it was created with and never frees
* it. Any number of searches, alongside other terminal readers such
* as formatters and render states, may share one terminal.
*
* The search and its terminal can be freed in either order. Freeing
* the search first releases tracked state it holds within the
* terminal. If the terminal is freed first, the search detects this:
* calls that need the terminal return GHOSTTY_INVALID_VALUE, reads
* return whatever the search last saw, and ghostty_search_free()
* releases only search-owned memory. A search cannot be rebound, so
* searching another terminal means creating a new search.
*
* ## Threading
*
* The library creates no threads. Calls on one GhosttySearch are not
* safe to make concurrently with each other, so the caller must
* serialize them.
*
* Functions that touch the terminal (ghostty_search_new(),
* ghostty_search_feed(), ghostty_search_run(), ghostty_search_set()
* with the select options, and ghostty_search_free()) must also be
* serialized with all other access to the same terminal.
*
* Everything else (ghostty_search_tick(), ghostty_search_get(), and
* ghostty_search_get_multi()) only touches memory owned by the search
* and is safe to call while another thread modifies the terminal.
* This split is how Ghostty runs search on a background thread: tick
* freely, and take the terminal lock only to feed. Reading returned
* match values is always safe, but passing them to APIs that take the
* terminal follows the terminal serialization rule above.
*
* ## Example
*
* @snippet c-vt-search/src/main.c search-main
*
* @{
*/
/**
* Progress state of a search.
*
* @ingroup search
*/
typedef enum GHOSTTY_ENUM_TYPED {
/**
* ghostty_search_tick() can make progress without terminal access.
*/
GHOSTTY_SEARCH_STATUS_RUNNING = 0,
/**
* Blocked until ghostty_search_feed(). This is also the initial
* state, since a search that has never been fed has never seen the
* terminal.
*/
GHOSTTY_SEARCH_STATUS_FEED_REQUIRED = 1,
/**
* Caught up with the terminal state as of the last feed. This never
* means finished forever, since later terminal writes require
* another feed to be seen.
*/
GHOSTTY_SEARCH_STATUS_COMPLETE = 2,
GHOSTTY_SEARCH_STATUS_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySearchStatus;
/**
* Scroll policy applied when a match becomes selected via
* GHOSTTY_SEARCH_OPT_SELECT_NEXT or GHOSTTY_SEARCH_OPT_SELECT_PREV.
*
* @ingroup search
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Scroll the viewport so the match is visible, only if it is not
* already visible. This is the default. */
GHOSTTY_SEARCH_SCROLL_IF_NEEDED = 0,
/** Never scroll the viewport. */
GHOSTTY_SEARCH_SCROLL_NONE = 1,
GHOSTTY_SEARCH_SCROLL_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySearchScroll;
/**
* Data fields readable with ghostty_search_get(). The output value
* type is documented per field.
*
* All reads reflect the terminal's active screen as of the last feed.
* When the running application switches to the alternate screen, the
* next feed switches counts, matches, and selection to that screen's
* results. Primary screen results, including completed scrollback
* searches, are retained and restored on the way back.
*
* @ingroup search
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Current search status: GhosttySearchStatus*. */
GHOSTTY_SEARCH_DATA_STATUS = 0,
/**
* The needle this search is looking for: GhosttyString*. The bytes
* are borrowed from the search and remain valid until
* ghostty_search_free().
*/
GHOSTTY_SEARCH_DATA_NEEDLE = 1,
/**
* Total matches found so far on the active screen: size_t*. Zero
* until the first feed.
*/
GHOSTTY_SEARCH_DATA_TOTAL_MATCHES = 2,
/**
* Index of the selected match: size_t*. This indexes the newest to
* oldest ordering of GHOSTTY_SEARCH_DATA_MATCHES, where 0 is the
* newest match, so a "k of n" find bar renders index + 1 of
* GHOSTTY_SEARCH_DATA_TOTAL_MATCHES. Returns GHOSTTY_NO_VALUE when
* nothing is selected.
*/
GHOSTTY_SEARCH_DATA_SELECTED_INDEX = 3,
/**
* The selected match: GhosttySelection*. This is an untracked
* snapshot with standard GhosttySelection lifetime rules. Returns
* GHOSTTY_NO_VALUE when nothing is selected.
*/
GHOSTTY_SEARCH_DATA_SELECTED_MATCH = 4,
/**
* All matches on the active screen, ordered newest to oldest, from
* the bottom of the active area up through scrollback:
* GhosttySelectionBuffer*. Set ptr to NULL with cap 0 to query the
* required capacity. An undersized buffer returns
* GHOSTTY_OUT_OF_SPACE with the required capacity in len.
*/
GHOSTTY_SEARCH_DATA_MATCHES = 5,
/**
* Matches on the pages covering the viewport, for drawing highlight
* rectangles: GhosttySelectionBuffer*. The list is computed during
* feeds and cached, so it reflects the viewport as of the last
* feed.
*
* Matches are found a page at a time, so the list can include
* matches slightly outside the visible viewport when they share a
* page with it. Ghostty's own renderer behaves the same way.
* Converting each match to viewport coordinates with
* ghostty_terminal_point_from_grid_ref() clips this naturally: skip
* matches that fail the conversion or whose row is beyond the
* visible row count.
*/
GHOSTTY_SEARCH_DATA_VIEWPORT_MATCHES = 6,
/** Current scroll policy: GhosttySearchScroll*. */
GHOSTTY_SEARCH_DATA_SELECT_SCROLL = 7,
GHOSTTY_SEARCH_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySearchData;
/**
* Options writable with ghostty_search_set(). The value type, and
* what a NULL value means, is documented per option.
*
* @ingroup search
*/
typedef enum GHOSTTY_ENUM_TYPED {
/**
* Select the next match, moving toward older content: from the
* bottom of the screen upward into history, the direction a search
* from the prompt usually wants. Wraps around past the oldest
* match.
*
* The value must be NULL. It is reserved for future use.
*
* This catches up with the terminal first, so it is safe to call at
* any time relative to feeds. The viewport scrolls to the newly
* selected match according to GHOSTTY_SEARCH_OPT_SELECT_SCROLL.
* This reads the terminal, so the caller must serialize it with all
* other access to the same terminal. Returns GHOSTTY_NO_VALUE when
* there are no matches.
*/
GHOSTTY_SEARCH_OPT_SELECT_NEXT = 0,
/**
* Select the previous match, moving toward newer content, wrapping
* around past the newest match. Otherwise identical to
* GHOSTTY_SEARCH_OPT_SELECT_NEXT.
*/
GHOSTTY_SEARCH_OPT_SELECT_PREV = 1,
/**
* Set the scroll policy applied by the select options: const
* GhosttySearchScroll*. The policy persists until changed. A NULL
* value resets it to GHOSTTY_SEARCH_SCROLL_IF_NEEDED. This only
* modifies search-owned state and never reads the terminal.
*/
GHOSTTY_SEARCH_OPT_SELECT_SCROLL = 2,
GHOSTTY_SEARCH_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySearchOption;
/**
* Options for creating a search.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup search
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttySearchOptions). */
size_t size;
/**
* The bytes to search for. This is copied, so the caller's memory
* does not need to outlive the create call. Must not be empty.
*
* Matching is byte-exact except ASCII letters, which compare
* case-insensitively.
*
* The needle cannot be changed after the search is created. To
* change the query, free the search and create a new one. This is
* also how Ghostty's own find bar handles retyping.
*/
GhosttyString needle;
} GhosttySearchOptions;
/**
* Create a search bound to a terminal.
*
* The search borrows the terminal and never frees it. The search and
* the terminal can be freed in either order; see ghostty_search_free().
*
* Creation is cheap and does not read terminal contents (the first
* feed does), but it registers the search with the terminal so the
* two can be freed in any order. The caller must serialize this call
* with all other access to the same terminal.
*
* @param allocator Allocator, or NULL for the default allocator
* @param out_search Receives the created search handle
* @param terminal Terminal to bind the search to
* @param options Search options (the needle is copied)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if
* out_search, terminal, or options are invalid or the needle
* is empty, or GHOSTTY_OUT_OF_MEMORY if allocation fails
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_new(
const GhosttyAllocator* allocator,
GhosttySearch* out_search,
GhosttyTerminal terminal,
const GhosttySearchOptions* options);
/**
* Free a search.
*
* If the bound terminal is still alive, this releases tracked state
* the search holds within it, so the caller must serialize this call
* with all other access to the same terminal. If the terminal was
* already freed, the search has been detached and this releases only
* search-owned memory. Passing NULL is allowed and is a no-op.
*
* @param search Search handle to free
*
* @ingroup search
*/
GHOSTTY_API void ghostty_search_free(GhosttySearch search);
/**
* Make a bounded amount of search progress.
*
* This only works on data the search has already copied and never
* reads the terminal, so it is safe to call while another thread
* modifies the terminal. Call it in a loop while the status is
* GHOSTTY_SEARCH_STATUS_RUNNING. When the status becomes
* GHOSTTY_SEARCH_STATUS_FEED_REQUIRED, call ghostty_search_feed() to
* unblock it.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param[out] out_status Receives the status after the tick (may be NULL)
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
* search is NULL
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_tick(
GhosttySearch search,
GhosttySearchStatus* out_status);
/**
* Read the terminal to update the search.
*
* Each feed catches the search up with the terminal: it reconciles
* the tracked screens against the live ones, re-scans the active
* area, refreshes the viewport match list, gives the scrollback
* searcher its next chunk of data, and prunes results that scrollback
* eviction invalidated. Feeding is also the only way the search
* learns about terminal changes, so keep feeding periodically while
* the search is in use, even after it reports complete.
*
* This reads the terminal, so the caller must serialize it with all
* other access to the same terminal. Each call does a bounded amount
* of work so that any caller-held terminal lock is held only briefly.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
* search is NULL or the terminal was freed
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_feed(GhosttySearch search);
/**
* Feed and tick until the search is caught up with the terminal.
*
* This is a blocking convenience for one-shot and single-threaded
* embedders. It always performs at least one feed, so it also picks
* up any terminal changes since the last feed, then loops until the
* status is GHOSTTY_SEARCH_STATUS_COMPLETE. Searching a large
* scrollback can take a while, so interactive embedders should drive
* ghostty_search_tick() and ghostty_search_feed() themselves.
*
* This reads the terminal for the entire call, so the caller must
* serialize it with all other access to the same terminal.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @return GHOSTTY_SUCCESS on success, or GHOSTTY_INVALID_VALUE if
* search is NULL or the terminal was freed
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_run(GhosttySearch search);
/**
* Write an option to a search.
*
* The value type, and what a NULL value means, depends on the option
* and is documented by GhosttySearchOption. The select options read
* the terminal, so the caller must serialize those calls with all
* other access to the same terminal.
* GHOSTTY_SEARCH_OPT_SELECT_SCROLL only modifies search-owned state.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param option Option to write
* @param value Pointer to the input value for the option. The meaning
* of NULL is documented per option.
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if a select
* option found no matches, GHOSTTY_OUT_OF_MEMORY if selection
* tracking fails, or GHOSTTY_INVALID_VALUE if search, option,
* or value is invalid or a select option is used after the
* terminal was freed
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_set(
GhosttySearch search,
GhosttySearchOption option,
const void* value);
/**
* Read a data field from a search.
*
* The output value type depends on data and is documented by
* GhosttySearchData. This never reads the terminal, so it is safe to
* call while another thread modifies the terminal. Returned
* selections are untracked snapshots with standard GhosttySelection
* lifetime rules.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param data Data field to read
* @param value Output pointer whose type depends on data
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the
* requested data has no value, GHOSTTY_OUT_OF_SPACE if a
* provided GhosttySelectionBuffer is too small (required
* capacity in its len), GHOSTTY_OUT_OF_MEMORY if collecting
* viewport matches fails, or GHOSTTY_INVALID_VALUE if search,
* data, or value is invalid
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_get(
GhosttySearch search,
GhosttySearchData data,
void* value);
/**
* Read multiple data fields from a search in a single call.
*
* This is an optimization over calling ghostty_search_get() multiple
* times. Each entry in values must point to storage of the type
* documented by the corresponding GhosttySearchData key.
*
* If any individual read fails, the function returns that error and
* writes the index of the failing key to out_written when out_written
* is non-NULL. Earlier keys have already been written. On success,
* out_written receives count when non-NULL. A too-small
* GhosttySelectionBuffer stops the batch with GHOSTTY_OUT_OF_SPACE at
* that key's index with the required capacity in its len, so order
* buffer-valued keys after scalar keys.
*
* @param search Search handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param count Number of data fields to read
* @param keys Data fields to read (must not be NULL)
* @param values Output pointers corresponding to keys (must not be NULL)
* @param out_written Optional number of fields read, or failing index
* on error
* @return GHOSTTY_SUCCESS on success, or the first failing read's
* result
*
* @ingroup search
*/
GHOSTTY_API GhosttyResult ghostty_search_get_multi(
GhosttySearch search,
size_t count,
const GhosttySearchData* keys,
void** values,
size_t* out_written);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_SEARCH_H */

View File

@@ -116,6 +116,29 @@ typedef struct {
bool rectangle;
} GhosttySelection;
/**
* A caller-provided buffer of selections.
*
* This follows the same conventions as GhosttyBuffer: ptr may be NULL with
* cap 0 to query the required capacity. APIs that fill this type set len to
* the number of entries written on GHOSTTY_SUCCESS, or to the required entry
* capacity on GHOSTTY_OUT_OF_SPACE.
*
* @ingroup selection
*/
typedef struct {
/** Destination buffer for selections. May be NULL when cap is 0 to query
* the required capacity. */
GhosttySelection* ptr;
/** Capacity of ptr in entries. */
size_t cap;
/** Entries written on success, or required entry capacity on
* GHOSTTY_OUT_OF_SPACE. */
size_t len;
} GhosttySelectionBuffer;
/**
* Options for deriving a word selection from a terminal grid reference.
*

View File

@@ -188,6 +188,19 @@ typedef struct GhosttyRenderStateRowIteratorImpl* GhosttyRenderStateRowIterator;
*/
typedef struct GhosttyRenderStateRowCellsImpl* GhosttyRenderStateRowCells;
/**
* Opaque handle to a terminal search.
*
* A search is bound to the terminal it was created with. It borrows the
* terminal, so it never frees it, and the search must be freed with
* ghostty_search_free(). If the terminal is freed first, the search
* detects this: calls that need the terminal fail cleanly and the
* search can still be freed.
*
* @ingroup search
*/
typedef struct GhosttySearchImpl* GhosttySearch;
/**
* Opaque handle to an SGR parser instance.
*

View File

@@ -349,6 +349,16 @@ comptime {
@export(&c.selection_gesture_event_free, .{ .name = "ghostty_selection_gesture_event_free" });
@export(&c.selection_gesture_event_set, .{ .name = "ghostty_selection_gesture_event_set" });
}
if (features.search) {
@export(&c.search_new, .{ .name = "ghostty_search_new" });
@export(&c.search_free, .{ .name = "ghostty_search_free" });
@export(&c.search_tick, .{ .name = "ghostty_search_tick" });
@export(&c.search_feed, .{ .name = "ghostty_search_feed" });
@export(&c.search_run, .{ .name = "ghostty_search_run" });
@export(&c.search_set, .{ .name = "ghostty_search_set" });
@export(&c.search_get, .{ .name = "ghostty_search_get" });
@export(&c.search_get_multi, .{ .name = "ghostty_search_get_multi" });
}
// Selections are expressed in grid references, so the untracked
// reference constructors are required by both features.
if (features.grid_introspection or features.selection) {

View File

@@ -88,6 +88,15 @@ pub const Options = struct {
/// `ghostty_terminal_selection_*`, `ghostty_selection_gesture_*`.
selection: bool = true,
/// Terminal search: find matches for a string in the active
/// area and scrollback (ASCII case-insensitive), with results
/// that survive primary/alternate screen switches, resize, and
/// scrollback pruning, plus match selection with wrap-around
/// and viewport scrolling. Used to implement find bars.
///
/// C API: `ghostty_search_*`.
search: bool = true,
/// The render state API: a coherent, update-in-place view of
/// the visible screen (rows, cells, styles, cursor, colors,
/// palette) designed to drive a renderer at frame rates. This

View File

@@ -41,6 +41,7 @@ const paste = @import("paste.zig");
const render = @import("render.zig");
const result = @import("result.zig");
const row = @import("row.zig");
const search = @import("search.zig");
const selection = @import("selection.zig");
const selection_gesture = @import("selection_gesture.zig");
const size_report = @import("size_report.zig");
@@ -203,7 +204,9 @@ const type_decls = [_]TypeDecl{
.initStruct("GhosttyRenderStateColors", render.Colors),
.initStruct("GhosttyRenderStateCursor", render.Cursor),
.initStruct("GhosttyRenderStateRowSelection", render.RowSelection),
.initStruct("GhosttySearchOptions", search.Options),
.initStruct("GhosttySelection", selection.CSelection),
.initStruct("GhosttySelectionBuffer", selection.CSelectionBuffer),
.initStruct("GhosttySelectionGestureBehaviors", selection_gesture.Behaviors),
.initStruct("GhosttySelectionGestureGeometry", selection_gesture.Geometry),
.initTaggedStruct("GhosttySgrAttribute", sgr.Attribute.C, "tag", "value", .generated),
@@ -287,6 +290,10 @@ const type_decls = [_]TypeDecl{
.initEnum("GhosttyRenderStateRowOption", render.RowOption, "GHOSTTY_RENDER_STATE_ROW_OPTION_"),
.initEnum("GhosttyRowData", row.RowData, "GHOSTTY_ROW_DATA_"),
.initEnum("GhosttyRowSemanticPrompt", row.SemanticPrompt, "GHOSTTY_ROW_SEMANTIC_"),
.initEnum("GhosttySearchData", search.Data, "GHOSTTY_SEARCH_DATA_"),
.initEnum("GhosttySearchOption", search.Option, "GHOSTTY_SEARCH_OPT_"),
.initEnum("GhosttySearchScroll", search.Scroll, "GHOSTTY_SEARCH_SCROLL_"),
.initEnum("GhosttySearchStatus", search.Status, "GHOSTTY_SEARCH_STATUS_"),
.initEnum("GhosttySelectionAdjust", Selection.Adjustment, "GHOSTTY_SELECTION_ADJUST_"),
.initEnum("GhosttySelectionGestureAutoscroll", selection_gesture.Autoscroll, "GHOSTTY_SELECTION_GESTURE_AUTOSCROLL_"),
.initEnum("GhosttySelectionGestureBehavior", selection_gesture.Behavior, "GHOSTTY_SELECTION_GESTURE_BEHAVIOR_"),
@@ -333,6 +340,7 @@ const type_decls = [_]TypeDecl{
.initOpaque("GhosttyRenderState"),
.initOpaque("GhosttyRenderStateRowCells"),
.initOpaque("GhosttyRenderStateRowIterator"),
.initOpaque("GhosttySearch"),
.initOpaque("GhosttySelectionGesture"),
.initOpaque("GhosttySelectionGestureEvent"),
.initOpaque("GhosttySgrParser"),