diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 57f0a0e65..976901c49 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -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 #include #include +#include #include #include #include diff --git a/include/ghostty/vt/search.h b/include/ghostty/vt/search.h new file mode 100644 index 000000000..c3b22c9d6 --- /dev/null +++ b/include/ghostty/vt/search.h @@ -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 +#include +#include +#include + +#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 */ diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index 3b926aab6..27d3c47b2 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -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. * diff --git a/include/ghostty/vt/types.h b/include/ghostty/vt/types.h index 2184ec95c..8c5f35f21 100644 --- a/include/ghostty/vt/types.h +++ b/include/ghostty/vt/types.h @@ -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. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 325f0e138..effa464d7 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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) { diff --git a/src/terminal/build_options.zig b/src/terminal/build_options.zig index 725fccfd0..42fd3edc1 100644 --- a/src/terminal/build_options.zig +++ b/src/terminal/build_options.zig @@ -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 diff --git a/src/terminal/c/types.zig b/src/terminal/c/types.zig index c1f524a86..690b70293 100644 --- a/src/terminal/c/types.zig +++ b/src/terminal/c/types.zig @@ -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"),