libghostty: add Selection equal and validate

This commit is contained in:
Mitchell Hashimoto
2026-05-23 15:21:44 -07:00
parent 2512fad940
commit ae839393d9
5 changed files with 172 additions and 0 deletions

View File

@@ -270,6 +270,53 @@ GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains(
GhosttyPoint point,
bool* out_contains);
/**
* Test whether two selection snapshots are equal.
*
* Equality uses the terminal's internal selection semantics: both endpoint
* pins must match and both selections must have the same rectangular/block
* state. This avoids requiring callers to compare raw GhosttyGridRef internals.
*
* Both selections' start and end grid refs must be valid untracked snapshots
* for the given terminal's currently active screen. In practice, they must
* come from that terminal and screen, and no mutating terminal call may have
* occurred since the refs were produced or reconstructed from tracked refs.
* Passing refs from another terminal, another screen, or stale refs returns
* GHOSTTY_INVALID_VALUE.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param a First selection snapshot to compare
* @param b Second selection snapshot to compare
* @param[out] out_equal On success, receives whether the selections are equal
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selections, selection references, or output pointer are invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_equal(
GhosttyTerminal terminal,
const GhosttySelection* a,
const GhosttySelection* b,
bool* out_equal);
/**
* Validate that a selection snapshot is representable for a terminal.
*
* A valid selection has both endpoint grid refs resolved in the terminal's
* currently active screen/page list. Malformed refs, stale refs, refs from
* another terminal, or refs from an inactive screen return GHOSTTY_INVALID_VALUE.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to validate
* @return GHOSTTY_SUCCESS if the selection is valid for the terminal's active
* screen/page list, otherwise GHOSTTY_INVALID_VALUE
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_validate(
GhosttyTerminal terminal,
const GhosttySelection* selection);
/** @} */
#ifdef __cplusplus

View File

@@ -243,6 +243,8 @@ comptime {
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });
@export(&c.terminal_selection_contains, .{ .name = "ghostty_terminal_selection_contains" });
@export(&c.terminal_selection_equal, .{ .name = "ghostty_terminal_selection_equal" });
@export(&c.terminal_selection_validate, .{ .name = "ghostty_terminal_selection_validate" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });

View File

@@ -174,6 +174,8 @@ pub const terminal_selection_adjust = selection.adjust;
pub const terminal_selection_order = selection.order;
pub const terminal_selection_ordered = selection.ordered;
pub const terminal_selection_contains = selection.contains;
pub const terminal_selection_equal = selection.equal;
pub const terminal_selection_validate = selection.validate;
pub const terminal_grid_ref = terminal.grid_ref;
pub const terminal_grid_ref_track = terminal.grid_ref_track;
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;

View File

@@ -110,6 +110,36 @@ pub fn contains(
return .success;
}
pub fn equal(
terminal: terminal_c.Terminal,
a: ?*const CSelection,
b: ?*const CSelection,
out_equal: ?*bool,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const sel_a = (a orelse return .invalid_value).toZig() orelse
return .invalid_value;
const sel_b = (b orelse return .invalid_value).toZig() orelse
return .invalid_value;
const out = out_equal orelse return .invalid_value;
if (!valid(t, sel_a) or !valid(t, sel_b)) return .invalid_value;
out.* = sel_a.eql(sel_b);
return .success;
}
pub fn validate(
terminal: terminal_c.Terminal,
selection: ?*const CSelection,
) callconv(lib.calling_conv) Result {
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const sel = (selection orelse return .invalid_value).toZig() orelse
return .invalid_value;
if (!valid(t, sel)) return .invalid_value;
return .success;
}
fn valid(t: *terminal_c.ZigTerminal, sel: Selection) bool {
const screen = t.screens.active;
return screen.pages.pointFromPin(.screen, sel.start()) != null and

View File

@@ -1552,6 +1552,97 @@ test "selection_contains" {
try testing.expect(contains);
}
test "selection_equal and selection_validate" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(t);
var other_t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&other_t,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 0,
},
));
defer free(other_t);
vt_write(t, "Hello", 5);
vt_write(other_t, "Hello", 5);
var start_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &start_ref));
var end_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 1, .y = 0 } },
}, &end_ref));
var other_end_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 2, .y = 0 } },
}, &other_end_ref));
var cross_terminal_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(other_t, .{
.tag = .active,
.value = .{ .active = .{ .x = 1, .y = 0 } },
}, &cross_terminal_ref));
const sel: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
};
const equal_sel: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
};
const different_endpoint: selection_c.CSelection = .{
.start = start_ref,
.end = other_end_ref,
};
const different_rectangle: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
.rectangle = true,
};
const cross_terminal: selection_c.CSelection = .{
.start = start_ref,
.end = cross_terminal_ref,
};
try testing.expectEqual(Result.success, selection_c.validate(t, &sel));
try testing.expectEqual(Result.invalid_value, selection_c.validate(t, &cross_terminal));
var equal: bool = undefined;
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &equal_sel, &equal));
try testing.expect(equal);
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_endpoint, &equal));
try testing.expect(!equal);
try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_rectangle, &equal));
try testing.expect(!equal);
try testing.expectEqual(Result.invalid_value, selection_c.equal(t, &sel, &cross_terminal, &equal));
try testing.expectEqual(Result.invalid_value, selection_c.equal(t, &sel, &equal_sel, null));
}
test "selection_order invalid values" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(