From 3e0477a14a1a6a0a8f4a5256b95528aa8145351a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 27 May 2026 11:00:51 -0700 Subject: [PATCH] example/c-vt-selection-gesture --- example/c-vt-selection-gesture/README.md | 18 +++ example/c-vt-selection-gesture/build.zig | 42 +++++ example/c-vt-selection-gesture/build.zig.zon | 24 +++ example/c-vt-selection-gesture/src/main.c | 162 +++++++++++++++++++ include/ghostty/vt.h | 5 + include/ghostty/vt/selection.h | 10 ++ 6 files changed, 261 insertions(+) create mode 100644 example/c-vt-selection-gesture/README.md create mode 100644 example/c-vt-selection-gesture/build.zig create mode 100644 example/c-vt-selection-gesture/build.zig.zon create mode 100644 example/c-vt-selection-gesture/src/main.c diff --git a/example/c-vt-selection-gesture/README.md b/example/c-vt-selection-gesture/README.md new file mode 100644 index 000000000..a64df0e53 --- /dev/null +++ b/example/c-vt-selection-gesture/README.md @@ -0,0 +1,18 @@ +# Example: `ghostty-vt` Selection Gestures + +This contains a simple example of how to use the `ghostty-vt` selection +gesture API from C. It creates synthetic press, drag, release, and deep-press +events and formats the resulting selection snapshots. + +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-selection-gesture/build.zig b/example/c-vt-selection-gesture/build.zig new file mode 100644 index 000000000..05f8d1bbc --- /dev/null +++ b/example/c-vt-selection-gesture/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_selection_gesture", + .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-selection-gesture/build.zig.zon b/example/c-vt-selection-gesture/build.zig.zon new file mode 100644 index 000000000..08db85223 --- /dev/null +++ b/example/c-vt-selection-gesture/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt_selection_gesture, + .version = "0.0.0", + .fingerprint = 0x5a4e72d27b582404, + .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-selection-gesture/src/main.c b/example/c-vt-selection-gesture/src/main.c new file mode 100644 index 000000000..050e9a3b1 --- /dev/null +++ b/example/c-vt-selection-gesture/src/main.c @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include + +//! [selection-gesture-main] +static void vt_write(GhosttyTerminal terminal, const char *s) { + ghostty_terminal_vt_write(terminal, (const uint8_t *)s, strlen(s)); +} + +static GhosttyGridRef ref_at(GhosttyTerminal terminal, uint16_t x, uint16_t y) { + GhosttyGridRef ref = GHOSTTY_INIT_SIZED(GhosttyGridRef); + GhosttyPoint point = { + .tag = GHOSTTY_POINT_TAG_ACTIVE, + .value = { .coordinate = { .x = x, .y = y } }, + }; + + GhosttyResult result = ghostty_terminal_grid_ref(terminal, point, &ref); + assert(result == GHOSTTY_SUCCESS); + return ref; +} + +static void print_selection( + GhosttyTerminal terminal, + const char *label, + const GhosttySelection *selection) { + GhosttyTerminalSelectionFormatOptions opts = + GHOSTTY_INIT_SIZED(GhosttyTerminalSelectionFormatOptions); + opts.emit = GHOSTTY_FORMATTER_FORMAT_PLAIN; + opts.trim = true; + opts.selection = selection; + + uint8_t *buf = NULL; + size_t len = 0; + GhosttyResult result = ghostty_terminal_selection_format_alloc( + terminal, NULL, opts, &buf, &len); + assert(result == GHOSTTY_SUCCESS); + + printf("%s: ", label); + fwrite(buf, 1, len, stdout); + printf("\n"); + + ghostty_free(NULL, buf, len); +} + +static GhosttySelectionGestureEvent new_event( + GhosttySelectionGestureEventType type) { + GhosttySelectionGestureEvent event = NULL; + GhosttyResult result = ghostty_selection_gesture_event_new(NULL, &event, type); + assert(result == GHOSTTY_SUCCESS); + return event; +} + +int main() { + GhosttyTerminal terminal; + GhosttyTerminalOptions opts = { + .cols = 20, + .rows = 4, + .max_scrollback = 100, + }; + GhosttyResult result = ghostty_terminal_new(NULL, &terminal, opts); + assert(result == GHOSTTY_SUCCESS); + + vt_write(terminal, "hello world\r\nsecond line"); + + GhosttySelectionGesture gesture = NULL; + result = ghostty_selection_gesture_new(NULL, &gesture); + assert(result == GHOSTTY_SUCCESS); + + GhosttySelectionGestureEvent press = + new_event(GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS); + GhosttySelectionGestureEvent drag = + new_event(GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DRAG); + GhosttySelectionGestureEvent release = + new_event(GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_RELEASE); + GhosttySelectionGestureEvent deep_press = + new_event(GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_DEEP_PRESS); + + GhosttySelectionGestureGeometry geometry = { + .columns = 20, + .cell_width = 10, + .padding_left = 0, + .screen_height = 40, + }; + + // Press in the first cell. A normal single press records the click anchor but + // doesn't produce a selection yet, so we discard the optional output. + GhosttyGridRef press_ref = ref_at(terminal, 0, 0); + result = ghostty_selection_gesture_event_set( + press, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF, &press_ref); + assert(result == GHOSTTY_SUCCESS); + + GhosttySurfacePosition press_pos = { .x = 2, .y = 8 }; + result = ghostty_selection_gesture_event_set( + press, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION, &press_pos); + assert(result == GHOSTTY_SUCCESS); + + result = ghostty_selection_gesture_event( + gesture, terminal, press, NULL); + assert(result == GHOSTTY_NO_VALUE); + + // Drag across "hello". The drag event returns a selection snapshot that the + // embedder can apply to its UI, copy, or format immediately. + GhosttyGridRef drag_ref = ref_at(terminal, 4, 0); + result = ghostty_selection_gesture_event_set( + drag, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF, &drag_ref); + assert(result == GHOSTTY_SUCCESS); + + GhosttySurfacePosition drag_pos = { .x = 46, .y = 8 }; + result = ghostty_selection_gesture_event_set( + drag, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_POSITION, &drag_pos); + assert(result == GHOSTTY_SUCCESS); + + result = ghostty_selection_gesture_event_set( + drag, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_GEOMETRY, &geometry); + assert(result == GHOSTTY_SUCCESS); + + GhosttySelection selection = GHOSTTY_INIT_SIZED(GhosttySelection); + result = ghostty_selection_gesture_event( + gesture, terminal, drag, &selection); + assert(result == GHOSTTY_SUCCESS); + print_selection(terminal, "drag", &selection); + + // Release updates gesture state but never produces a selection. + result = ghostty_selection_gesture_event_set( + release, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF, &drag_ref); + assert(result == GHOSTTY_SUCCESS); + result = ghostty_selection_gesture_event( + gesture, terminal, release, NULL); + assert(result == GHOSTTY_NO_VALUE); + + bool dragged = false; + result = ghostty_selection_gesture_get( + gesture, terminal, GHOSTTY_SELECTION_GESTURE_DATA_DRAGGED, &dragged); + assert(result == GHOSTTY_SUCCESS); + printf("dragged: %s\n", dragged ? "true" : "false"); + + // Deep press uses the active click anchor to select the surrounding word. + ghostty_selection_gesture_reset(gesture, terminal); + GhosttyGridRef world_ref = ref_at(terminal, 6, 0); + result = ghostty_selection_gesture_event_set( + press, GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF, &world_ref); + assert(result == GHOSTTY_SUCCESS); + result = ghostty_selection_gesture_event( + gesture, terminal, press, NULL); + assert(result == GHOSTTY_NO_VALUE); + + result = ghostty_selection_gesture_event( + gesture, terminal, deep_press, &selection); + assert(result == GHOSTTY_SUCCESS); + print_selection(terminal, "deep press", &selection); + + ghostty_selection_gesture_event_free(deep_press); + ghostty_selection_gesture_event_free(release); + ghostty_selection_gesture_event_free(drag); + ghostty_selection_gesture_event_free(press); + ghostty_selection_gesture_free(gesture, terminal); + ghostty_terminal_free(terminal); + return 0; +} +//! [selection-gesture-main] diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 7a6a9758a..94a850334 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -104,6 +104,11 @@ * detect when it loses its value, and move it to a new point. */ +/** @example c-vt-selection-gesture/src/main.c + * This example demonstrates how to use synthetic selection gesture events to + * derive drag and deep-press selection snapshots. + */ + /** @example c-vt-kitty-graphics/src/main.c * This example demonstrates how to use the system interface to install a * PNG decoder callback and send a Kitty Graphics Protocol image. diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index d42fa3c0e..3b926aab6 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -32,9 +32,19 @@ extern "C" { * for the endpoints and reconstruct a GhosttySelection from fresh snapshots * when needed. * + * Selection gestures provide a reusable state machine for turning UI pointer + * interactions into selection snapshots. A caller creates one + * GhosttySelectionGesture per active gesture stream, reuses typed + * GhosttySelectionGestureEvent objects for synthetic press, drag, release, + * autoscroll tick, and deep-press events, and applies each event with + * ghostty_selection_gesture_event(). The returned GhosttySelection is a + * snapshot; the embedder decides whether to render it, format/copy it, or + * install it as the terminal's active selection. + * * ## Examples * * @snippet c-vt-selection/src/main.c selection-main + * @snippet c-vt-selection-gesture/src/main.c selection-gesture-main * * @{ */