mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-09-14 18:01:58 +00:00
example: add c-vt-search demonstrating the terminal search C API
This commit is contained in:
19
example/c-vt-search/README.md
Normal file
19
example/c-vt-search/README.md
Normal file
@@ -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
|
||||
```
|
||||
42
example/c-vt-search/build.zig
Normal file
42
example/c-vt-search/build.zig
Normal file
@@ -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);
|
||||
}
|
||||
24
example/c-vt-search/build.zig.zon
Normal file
24
example/c-vt-search/build.zig.zon
Normal file
@@ -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",
|
||||
},
|
||||
}
|
||||
114
example/c-vt-search/src/main.c
Normal file
114
example/c-vt-search/src/main.c
Normal file
@@ -0,0 +1,114 @@
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <ghostty/vt.h>
|
||||
|
||||
//! [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]
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user