libghostty-vt 0.0.0
Loading...
Searching...
No Matches
Search

A GhosttySearch finds a string in a terminal. It covers the active area and scrollback of both the primary and alternate screens.

Terminal search is harder than searching a string, because the text won't hold still. Scrollback can be huge, and a program can keep writing while you search. We handle that part. Results stay in sync with the live screens. They survive primary and alternate screen switches, so opening and closing a fullscreen app such as vim doesn't restart a scrollback search. And they recover from resize, reflow, resets and scrollback pruning.

What's left to you is deciding when the work happens. If you want a one-shot search, ghostty_search_run() does all of it, and you can go straight to the example. If you're building a find bar for a terminal with a lot of scrollback, read the first section.

Driving a search

A search starts idle. Setting a needle with GHOSTTY_SEARCH_OPT_NEEDLE starts it, changing the needle restarts it from scratch, and clearing it returns the search to idle. Setting the needle it already has keeps the results, so a find bar can resubmit freely.

Matching is byte-exact, except for ASCII letters, which compare case-insensitively.

Searching a large scrollback takes time, so the work is split into small steps you drive yourself. That gives you direct control over the performance:

Function What it does Needs the terminal
ghostty_search_tick() Makes a bounded amount of progress on data the search has already copied. No
ghostty_search_feed() Reads the terminal to copy in more data and pick up changes. Yes
ghostty_search_run() Blocks, feeding and ticking until the search is caught up. Interactive programs should use the other two instead. Yes

Tick while the status is GHOSTTY_SEARCH_STATUS_RUNNING, and feed when it becomes GHOSTTY_SEARCH_STATUS_FEED_REQUIRED. That's also the status right after a needle is set, because the search hasn't seen the terminal yet.

Feeding is the only way a search learns that the terminal changed. So keep feeding periodically while the search is in use, even after it reports complete.

GHOSTTY_SEARCH_STATUS_COMPLETE means the search is caught up with the terminal as of the last feed. It never means finished forever, because later writes need another feed to be seen.

Matches are selections

Every match is a GhosttySelection snapshot with rectangle set to false. So everything in Selection works on matches:

Matches follow the usual snapshot rule. They're only valid until the next operation that changes the terminal, including ghostty_terminal_vt_write(), resize, reset and free. Read matches after a feed, use them before the terminal changes again, and read them again rather than caching them.

We keep the selected match accurate across terminal changes. So the safe way to follow a match is to read GHOSTTY_SEARCH_DATA_SELECTED_MATCH again after each feed.

Lifetime

A search borrows the terminal it was created with and never frees it. Any number of searches can share one terminal, alongside other readers such as formatters and render states.

You can free the search and its terminal in either order. Freeing the search first releases the tracked state it holds in the terminal. If the terminal goes first, the search detects that:

A search can't be rebound. To search another terminal, create a new search.

Threads

The library creates no threads. Calls on one GhosttySearch aren't safe to make concurrently with each other, so you have to serialize them.

The functions that touch the terminal also have to be serialized with all other access to that terminal:

Everything else, meaning ghostty_search_tick(), ghostty_search_get() and ghostty_search_get_multi(), only touches memory the search owns. Those are safe to call while another thread changes the terminal.

That split is how Ghostty runs search on a background thread: tick freely, and take the terminal lock only to feed.

Reading a returned match value is always safe. Passing one to a function that takes the terminal follows the terminal's serialization rule above.

Example

This creates a search, runs it, steps through the matches like a find bar, and reads the viewport matches to draw highlights.

int main() {
// Create a terminal and fill it with some content to search.
GhosttyTerminal terminal;
GhosttyResult result = ghostty_terminal_new(NULL, &terminal, 80, 24);
assert(result == GHOSTTY_SUCCESS);
const char *lines[] = {
"$ make test\r\n",
"compiling module A... ok\r\n",
"compiling module B... error: missing semicolon\r\n",
"linking... error: undefined symbol\r\n",
"$ grep -n ERROR build.log\r\n",
};
for (size_t i = 0; i < sizeof(lines) / sizeof(lines[0]); i++) {
ghostty_terminal_vt_write(terminal, (const uint8_t *)lines[i],
strlen(lines[i]));
}
// The user opened the find bar, so create a search bound to the
// terminal. It starts idle until it has a needle.
GhosttySearch search;
result = ghostty_search_new(NULL, &search, terminal);
assert(result == GHOSTTY_SUCCESS);
// The user typed a query. Matching is byte-exact except ASCII
// letters, which compare case-insensitively, so "error" also finds
// "ERROR". Retyping just sets the needle again: a changed needle
// restarts the search and an unchanged one keeps its results.
GhosttyString needle = { (const uint8_t *)"error", 5 };
result = ghostty_search_set(search, GHOSTTY_SEARCH_OPT_NEEDLE, &needle);
assert(result == GHOSTTY_SUCCESS);
// Drive the search. Interactive embedders interleave
// ghostty_search_tick() and ghostty_search_feed() with their event
// loop, but for a one-shot search we can just run it to completion.
result = ghostty_search_run(search);
assert(result == GHOSTTY_SUCCESS);
// The total match count, for find bar text like "1 of 3".
size_t total = 0;
&total);
assert(result == GHOSTTY_SUCCESS);
printf("%zu matches for \"error\"\n", total);
// The user pressed Enter, so select the next match. Selection starts
// at the newest match, moves toward older content, and wraps around.
// This scrolls the viewport to the match if it isn't visible, per
// the GHOSTTY_SEARCH_OPT_SELECT_SCROLL policy.
while (true) {
result = ghostty_search_select_next(search);
if (result != GHOSTTY_SUCCESS) break;
// Read the selection state in one call. Index 0 is the newest
// match, so a "k of n" find bar renders index + 1.
size_t idx = 0;
GhosttySelection match = {0};
const GhosttySearchData keys[] = {
};
void *values[] = { &idx, &match };
search, sizeof(keys) / sizeof(keys[0]), keys, values, NULL);
assert(result == GHOSTTY_SUCCESS);
printf("selected %zu of %zu\n", idx + 1, total);
// Wrapped back around to the first match: stop.
if (idx + 1 == total) break;
}
// Each frame while the find bar is open, feed to catch up with any
// terminal changes and then read the viewport matches to draw
// highlights. The list can include matches just past the viewport
// when they share a page with it, so convert each endpoint to
// viewport coordinates and skip matches outside the visible rows.
result = ghostty_search_feed(search);
assert(result == GHOSTTY_SUCCESS);
GhosttySelection viewport_storage[64];
.ptr = viewport_storage,
.cap = sizeof(viewport_storage) / sizeof(viewport_storage[0]),
};
&viewport);
assert(result == GHOSTTY_SUCCESS);
for (size_t i = 0; i < viewport.len; i++) {
terminal, &viewport_storage[i].start, GHOSTTY_POINT_TAG_VIEWPORT,
&start) != GHOSTTY_SUCCESS) continue;
terminal, &viewport_storage[i].end, GHOSTTY_POINT_TAG_VIEWPORT,
&end) != GHOSTTY_SUCCESS) continue;
if (start.y >= 24 || end.y >= 24) continue;
// A real embedder draws a highlight rect from start to end here.
printf("highlight rows %u-%u, cols %u-%u\n",
(unsigned)start.y, (unsigned)end.y,
(unsigned)start.x, (unsigned)end.x);
}
// Closing the find bar. The search borrows the terminal, but the
// two can be freed in either order.
return 0;
}

Full program: c-vt-search/src/main.c