libghostty: selectWordBetween in C

This commit is contained in:
Mitchell Hashimoto
2026-05-24 13:51:43 -07:00
parent e8f5353912
commit eb777b8036
7 changed files with 158 additions and 0 deletions

View File

@@ -76,6 +76,40 @@ int main() {
assert(result == GHOSTTY_SUCCESS);
print_selection(terminal, "word", &selection);
//! [selection-word-between]
// Double-click-and-drag style selection. Suppose the user double-clicks
// "git" and drags to "status". The pointer may pass over whitespace, so
// select the nearest word between the original click and current drag point
// in both directions, then combine the outer word bounds.
GhosttyGridRef click_ref = ref_at(terminal, 2, 0); // the "git" in "git status"
GhosttyGridRef drag_ref = ref_at(terminal, 6, 0); // the "status" in "git status"
GhosttyTerminalSelectWordBetweenOptions start_word_opts =
GHOSTTY_INIT_SIZED(GhosttyTerminalSelectWordBetweenOptions);
start_word_opts.start = click_ref;
start_word_opts.end = drag_ref;
GhosttySelection start_word = GHOSTTY_INIT_SIZED(GhosttySelection);
result = ghostty_terminal_select_word_between(
terminal, &start_word_opts, &start_word);
assert(result == GHOSTTY_SUCCESS);
GhosttyTerminalSelectWordBetweenOptions end_word_opts =
GHOSTTY_INIT_SIZED(GhosttyTerminalSelectWordBetweenOptions);
end_word_opts.start = drag_ref;
end_word_opts.end = click_ref;
GhosttySelection end_word = GHOSTTY_INIT_SIZED(GhosttySelection);
result = ghostty_terminal_select_word_between(
terminal, &end_word_opts, &end_word);
assert(result == GHOSTTY_SUCCESS);
GhosttySelection drag_selection = GHOSTTY_INIT_SIZED(GhosttySelection);
drag_selection.start = start_word.start;
drag_selection.end = end_word.end;
print_selection(terminal, "double-click drag", &drag_selection);
//! [selection-word-between]
// Triple-click style line selection. With semantic prompt boundaries enabled,
// this selects only the input area rather than the leading "$ " prompt.
GhosttyTerminalSelectLineOptions line = GHOSTTY_INIT_SIZED(GhosttyTerminalSelectLineOptions);

View File

@@ -107,6 +107,33 @@ typedef struct {
size_t boundary_codepoints_len;
} GhosttyTerminalSelectWordOptions;
/**
* Options for deriving the nearest word selection between two grid references.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
* If boundary_codepoints is NULL and boundary_codepoints_len is 0, Ghostty's
* default word-boundary codepoints are used. If boundary_codepoints_len is
* non-zero, boundary_codepoints must not be NULL.
*
* @ingroup selection
*/
typedef struct {
/** Size of this struct in bytes. Must be set to sizeof(GhosttyTerminalSelectWordBetweenOptions). */
size_t size;
/** Starting grid reference for the inclusive search range. */
GhosttyGridRef start;
/** Ending grid reference for the inclusive search range. */
GhosttyGridRef end;
/** Optional word-boundary codepoints as uint32_t scalar values. */
const uint32_t* boundary_codepoints;
/** Number of entries in boundary_codepoints. */
size_t boundary_codepoints_len;
} GhosttyTerminalSelectWordBetweenOptions;
/**
* Options for deriving a line selection from a terminal grid reference.
*
@@ -235,6 +262,40 @@ GHOSTTY_API GhosttyResult ghostty_terminal_select_word(
const GhosttyTerminalSelectWordOptions* options,
GhosttySelection* out_selection);
/**
* Derive the nearest word selection snapshot between two terminal grid refs.
*
* Starting at options->start, this searches toward options->end (inclusive)
* and returns the first selectable word found using Ghostty's word-selection
* rules.
*
* This is useful for implementing double-click-and-drag selection in a UI. If
* a user double-clicks one word and drags across spaces or punctuation toward
* another word, selecting only the word directly under the current pointer can
* flicker or collapse when the pointer is between words. Instead, ask for the
* nearest word between the original click and the drag point, ask again in the
* reverse direction, and combine the two word bounds into the drag selection.
*
* @snippet c-vt-selection/src/main.c selection-word-between
*
* The returned selection is not installed as the terminal's current
* selection. It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param options Word-between-selection options
* @param[out] out_selection On success, receives the derived selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if there is no
* selectable word content between the valid refs, or
* GHOSTTY_INVALID_VALUE if the terminal, options, refs, codepoint
* pointer, or output pointer are invalid.
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_select_word_between(
GhosttyTerminal terminal,
const GhosttyTerminalSelectWordBetweenOptions* options,
GhosttySelection* out_selection);
/**
* Derive a line selection snapshot from a terminal grid reference.
*

View File

@@ -240,6 +240,7 @@ comptime {
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
@export(&c.terminal_select_word, .{ .name = "ghostty_terminal_select_word" });
@export(&c.terminal_select_word_between, .{ .name = "ghostty_terminal_select_word_between" });
@export(&c.terminal_select_line, .{ .name = "ghostty_terminal_select_line" });
@export(&c.terminal_select_all, .{ .name = "ghostty_terminal_select_all" });
@export(&c.terminal_select_output, .{ .name = "ghostty_terminal_select_output" });

View File

@@ -171,6 +171,7 @@ pub const terminal_mode_set = terminal.mode_set;
pub const terminal_get = terminal.get;
pub const terminal_get_multi = terminal.get_multi;
pub const terminal_select_word = selection.word;
pub const terminal_select_word_between = selection.word_between;
pub const terminal_select_line = selection.line;
pub const terminal_select_all = selection.all;
pub const terminal_select_output = selection.output;

View File

@@ -43,6 +43,15 @@ pub const SelectWordOptions = extern struct {
boundary_codepoints_len: usize = 0,
};
/// C: GhosttyTerminalSelectWordBetweenOptions
pub const SelectWordBetweenOptions = extern struct {
size: usize = @sizeOf(SelectWordBetweenOptions),
start: grid_ref.CGridRef,
end: grid_ref.CGridRef,
boundary_codepoints: ?[*]const u32 = null,
boundary_codepoints_len: usize = 0,
};
/// C: GhosttyTerminalSelectLineOptions
pub const SelectLineOptions = extern struct {
size: usize = @sizeOf(SelectLineOptions),
@@ -77,6 +86,33 @@ pub fn word(
return .success;
}
pub fn word_between(
terminal: terminal_c.Terminal,
options: ?*const SelectWordBetweenOptions,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const opts = options orelse return .invalid_value;
if (opts.size < @sizeOf(SelectWordBetweenOptions)) return .invalid_value;
const out = out_selection orelse return .invalid_value;
const boundary_codepoints = codepointSlice(
opts.boundary_codepoints,
opts.boundary_codepoints_len,
) catch return .invalid_value;
const screen = t.screens.active;
const start = opts.start.toPin() orelse return .invalid_value;
const end = opts.end.toPin() orelse return .invalid_value;
out.* = .fromZig(screen.selectWordBetween(
start,
end,
boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
) orelse
return .no_value);
return .success;
}
pub fn line(
terminal: terminal_c.Terminal,
options: ?*const SelectLineOptions,

View File

@@ -1438,6 +1438,28 @@ test "selection derivation helpers" {
word_opts.ref = empty_ref;
try testing.expectEqual(Result.no_value, selection_c.word(t, &word_opts, &out));
var between_start_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 20, .y = 1 } },
}, &between_start_ref));
var between_end_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 1 } },
}, &between_end_ref));
var word_between_opts: selection_c.SelectWordBetweenOptions = .{
.start = between_start_ref,
.end = between_end_ref,
};
try testing.expectEqual(Result.success, selection_c.word_between(t, &word_between_opts, &out));
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
var line_opts: selection_c.SelectLineOptions = .{
.ref = line_ref,
};
@@ -1457,6 +1479,8 @@ test "selection derivation helpers" {
try testing.expectEqual(Result.invalid_value, selection_c.line(t, &line_opts, &out));
try testing.expectEqual(Result.invalid_value, selection_c.word(t, null, &out));
try testing.expectEqual(Result.invalid_value, selection_c.word(t, &word_opts, null));
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, null, &out));
try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, &word_between_opts, null));
}
test "selection_adjust mutates snapshot end" {

View File

@@ -31,6 +31,7 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: {
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
.{ "GhosttySelection", StructInfo.init(selection.CSelection) },
.{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
.{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) },
.{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },