diff --git a/example/c-vt-search/README.md b/example/c-vt-search/README.md new file mode 100644 index 000000000..3d11a3153 --- /dev/null +++ b/example/c-vt-search/README.md @@ -0,0 +1,19 @@ +# Example: `ghostty-vt` Terminal Search + +This contains a simple example of how to use the `ghostty-vt` search +API from C. It writes content into a terminal, searches it for a +string, navigates between the matches like a find bar, and reads the +viewport matches an embedder would use to draw highlights. + +This uses a `build.zig` and `Zig` to build the C program so that we +can reuse a lot of our build logic and depend directly on our source +tree, but Ghostty emits a standard C library that can be used with any +C tooling. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-search/build.zig b/example/c-vt-search/build.zig new file mode 100644 index 000000000..51ddac608 --- /dev/null +++ b/example/c-vt-search/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_search", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-search/build.zig.zon b/example/c-vt-search/build.zig.zon new file mode 100644 index 000000000..2882f49f3 --- /dev/null +++ b/example/c-vt-search/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_search, + .version = "0.0.0", + .fingerprint = 0x91a25a7f6cd3ff58, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-search/src/main.c b/example/c-vt-search/src/main.c new file mode 100644 index 000000000..9d15c9f8d --- /dev/null +++ b/example/c-vt-search/src/main.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include + +//! [search-main] +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 typed a query into the find bar, so create a search bound + // to the terminal. Matching is byte-exact except ASCII letters, which + // compare case-insensitively, so "error" also finds "ERROR". The + // needle can't be changed later. Retyping means free and create. + GhosttySearchOptions opts = GHOSTTY_INIT_SIZED(GhosttySearchOptions); + opts.needle = (GhosttyString){ (const uint8_t *)"error", 5 }; + + GhosttySearch search; + result = ghostty_search_new(NULL, &search, terminal, &opts); + 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; + result = ghostty_search_get(search, GHOSTTY_SEARCH_DATA_TOTAL_MATCHES, + &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_set(search, GHOSTTY_SEARCH_OPT_SELECT_NEXT, NULL); + 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 = GHOSTTY_INIT_SIZED(GhosttySelection); + const GhosttySearchData keys[] = { + GHOSTTY_SEARCH_DATA_SELECTED_INDEX, + GHOSTTY_SEARCH_DATA_SELECTED_MATCH, + }; + void *values[] = { &idx, &match }; + result = ghostty_search_get_multi( + 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]; + GhosttySelectionBuffer viewport = { + .ptr = viewport_storage, + .cap = sizeof(viewport_storage) / sizeof(viewport_storage[0]), + }; + result = ghostty_search_get(search, GHOSTTY_SEARCH_DATA_VIEWPORT_MATCHES, + &viewport); + assert(result == GHOSTTY_SUCCESS); + for (size_t i = 0; i < viewport.len; i++) { + GhosttyPointCoordinate start, end; + if (ghostty_terminal_point_from_grid_ref( + terminal, &viewport_storage[i].start, GHOSTTY_POINT_TAG_VIEWPORT, + &start) != GHOSTTY_SUCCESS) continue; + if (ghostty_terminal_point_from_grid_ref( + 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 (or retyping the needle). The search borrows + // the terminal, so it must always be freed before the terminal. + ghostty_search_free(search); + ghostty_terminal_free(terminal); + return 0; +} +//! [search-main] diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 976901c49..cfa1017e7 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -60,6 +60,7 @@ * - @ref c-vt-grid-traverse/src/main.c - Grid traversal example using grid refs * - @ref c-vt-grid-ref-tracked/src/main.c - Tracked grid ref example * - @ref c-vt-compression/src/main.c - Idle scrollback compression example + * - @ref c-vt-search/src/main.c - Terminal search example * */ @@ -126,6 +127,12 @@ * PNG decoder callback and send a Kitty Graphics Protocol image. */ +/** @example c-vt-search/src/main.c + * This example demonstrates how to search terminal contents for a + * string, navigate between the matches like a find bar, and read the + * viewport matches used to draw highlights. + */ + #ifndef GHOSTTY_VT_H #define GHOSTTY_VT_H