libghostty: move selection functions to selection doxygen group

This commit is contained in:
Mitchell Hashimoto
2026-05-23 15:20:12 -07:00
parent 671c12fad9
commit 2512fad940
5 changed files with 229 additions and 214 deletions

View File

@@ -10,6 +10,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/point.h>
#ifdef __cplusplus
extern "C" {
@@ -157,6 +158,118 @@ typedef enum GHOSTTY_ENUM_TYPED {
GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySelectionAdjust;
/**
* Adjust a selection snapshot using terminal selection semantics.
*
* This mutates the caller-provided GhosttySelection in place. The logical end
* endpoint is always moved, regardless of whether the selection is forward or
* reversed visually. The input selection remains a snapshot: after adjustment,
* call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it
* as the terminal-owned selection if desired.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to adjust in place
* @param adjustment The adjustment operation to apply
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, or adjustment are invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust(
GhosttyTerminal terminal,
GhosttySelection* selection,
GhosttySelectionAdjust adjustment);
/**
* Get the current endpoint ordering of a selection snapshot.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to inspect
* @param[out] out_order On success, receives the selection order
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, or output pointer are invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_order(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttySelectionOrder* out_order);
/**
* Return a selection snapshot with endpoints ordered as requested.
*
* Use GHOSTTY_SELECTION_ORDER_FORWARD to get top-left to bottom-right bounds,
* and GHOSTTY_SELECTION_ORDER_REVERSE to get bottom-right to top-left bounds.
* Mirrored desired orders are accepted but normalized the same as forward.
* The output selection is a fresh untracked snapshot and is not installed as
* the terminal's current selection.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to order
* @param desired Desired endpoint order
* @param[out] out_selection On success, receives the ordered selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, desired order, or output pointer
* are invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_ordered(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttySelectionOrder desired,
GhosttySelection* out_selection);
/**
* Test whether a terminal point is inside a selection snapshot.
*
* This uses the same selection semantics as the terminal, including
* rectangular/block selections and linear selections spanning multiple rows.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to inspect
* @param point Point to test for containment
* @param[out] out_contains On success, receives whether point is inside selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, point, or output pointer are invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttyPoint point,
bool* out_contains);
/** @} */
#ifdef __cplusplus

View File

@@ -1123,118 +1123,6 @@ GHOSTTY_API GhosttyResult ghostty_terminal_get_multi(GhosttyTerminal terminal,
void** values,
size_t* out_written);
/**
* Adjust a selection snapshot using terminal selection semantics.
*
* This mutates the caller-provided GhosttySelection in place. The logical end
* endpoint is always moved, regardless of whether the selection is forward or
* reversed visually. The input selection remains a snapshot: after adjustment,
* call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it
* as the terminal-owned selection if desired.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to adjust in place
* @param adjustment The adjustment operation to apply
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, or adjustment are invalid
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust(
GhosttyTerminal terminal,
GhosttySelection* selection,
GhosttySelectionAdjust adjustment);
/**
* Get the current endpoint ordering of a selection snapshot.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to inspect
* @param[out] out_order On success, receives the selection order
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, or output pointer are invalid
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_order(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttySelectionOrder* out_order);
/**
* Return a selection snapshot with endpoints ordered as requested.
*
* Use GHOSTTY_SELECTION_ORDER_FORWARD to get top-left to bottom-right bounds,
* and GHOSTTY_SELECTION_ORDER_REVERSE to get bottom-right to top-left bounds.
* Mirrored desired orders are accepted but normalized the same as forward.
* The output selection is a fresh untracked snapshot and is not installed as
* the terminal's current selection.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to order
* @param desired Desired endpoint order
* @param[out] out_selection On success, receives the ordered selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, desired order, or output pointer
* are invalid
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_ordered(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttySelectionOrder desired,
GhosttySelection* out_selection);
/**
* Test whether a terminal point is inside a selection snapshot.
*
* This uses the same selection semantics as the terminal, including
* rectangular/block selections and linear selections spanning multiple rows.
*
* The selection's start and end grid refs must both 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 violates this precondition.
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param selection Selection snapshot to inspect
* @param point Point to test for containment
* @param[out] out_contains On success, receives whether point is inside selection
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal,
* selection, selection references, point, or output pointer are invalid
*
* @ingroup terminal
*/
GHOSTTY_API GhosttyResult ghostty_terminal_selection_contains(
GhosttyTerminal terminal,
const GhosttySelection* selection,
GhosttyPoint point,
bool* out_contains);
/**
* Resolve a point in the terminal grid to a grid reference.
*

View File

@@ -170,10 +170,10 @@ pub const terminal_mode_get = terminal.mode_get;
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_selection_adjust = terminal.selection_adjust;
pub const terminal_selection_order = terminal.selection_order;
pub const terminal_selection_ordered = terminal.selection_ordered;
pub const terminal_selection_contains = terminal.selection_contains;
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_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

@@ -1,5 +1,16 @@
const std = @import("std");
const testing = std.testing;
const lib = @import("../lib.zig");
const grid_ref = @import("grid_ref.zig");
const point = @import("../point.zig");
const Selection = @import("../Selection.zig");
const Result = @import("result.zig").Result;
const terminal_c = @import("terminal.zig");
const log = std.log.scoped(.selection_c);
pub const Adjustment = Selection.Adjustment;
pub const Order = Selection.Order;
/// C: GhosttySelection
pub const CSelection = extern struct {
@@ -22,3 +33,85 @@ pub const CSelection = extern struct {
};
}
};
pub fn adjust(
terminal: terminal_c.Terminal,
selection: ?*CSelection,
adjustment: Selection.Adjustment,
) callconv(lib.calling_conv) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Selection.Adjustment, @intFromEnum(adjustment)) catch {
log.warn("terminal_selection_adjust invalid adjustment value={d}", .{@intFromEnum(adjustment)});
return .invalid_value;
};
}
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const sel_ptr = selection orelse return .invalid_value;
var sel = sel_ptr.toZig() orelse return .invalid_value;
sel.adjust(t.screens.active, adjustment);
sel_ptr.* = .fromZig(sel);
return .success;
}
pub fn order(
terminal: terminal_c.Terminal,
selection: ?*const CSelection,
out_order: ?*Selection.Order,
) 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;
const out = out_order orelse return .invalid_value;
if (!valid(t, sel)) return .invalid_value;
out.* = sel.order(t.screens.active);
return .success;
}
pub fn ordered(
terminal: terminal_c.Terminal,
selection: ?*const CSelection,
desired: Selection.Order,
out_selection: ?*CSelection,
) callconv(lib.calling_conv) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Selection.Order, @intFromEnum(desired)) catch {
log.warn("terminal_selection_ordered invalid desired value={d}", .{@intFromEnum(desired)});
return .invalid_value;
};
}
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const sel = (selection orelse return .invalid_value).toZig() orelse
return .invalid_value;
const out = out_selection orelse return .invalid_value;
if (!valid(t, sel)) return .invalid_value;
out.* = .fromZig(sel.ordered(t.screens.active, desired));
return .success;
}
pub fn contains(
terminal: terminal_c.Terminal,
selection: ?*const CSelection,
pt: point.Point.C,
out_contains: ?*bool,
) 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;
const out = out_contains orelse return .invalid_value;
if (!valid(t, sel)) return .invalid_value;
const screen = t.screens.active;
const pin = screen.pages.pin(.fromC(pt)) orelse return .invalid_value;
out.* = sel.contains(screen, pin);
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
screen.pages.pointFromPin(.screen, sel.end()) != null;
}

View File

@@ -3,11 +3,10 @@ const testing = std.testing;
const build_options = @import("terminal_options");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
const ZigTerminal = @import("../Terminal.zig");
pub const ZigTerminal = @import("../Terminal.zig");
const Stream = @import("../stream_terminal.zig").Stream;
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
const Selection = @import("../Selection.zig");
const apc = @import("../apc.zig");
const kitty = @import("../kitty/key.zig");
const kitty_gfx_c = @import("kitty_graphics.zig");
@@ -211,6 +210,10 @@ const Effects = struct {
/// C: GhosttyTerminal
pub const Terminal = ?*TerminalWrapper;
pub fn zigTerminal(terminal_: Terminal) ?*ZigTerminal {
return (terminal_ orelse return null).terminal;
}
/// C: GhosttyTerminalOptions
pub const Options = extern struct {
cols: size.CellCountInt,
@@ -665,88 +668,6 @@ pub fn get_multi(
return .success;
}
pub fn selection_adjust(
terminal_: Terminal,
selection: ?*selection_c.CSelection,
adjustment: Selection.Adjustment,
) callconv(lib.calling_conv) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Selection.Adjustment, @intFromEnum(adjustment)) catch {
log.warn("terminal_selection_adjust invalid adjustment value={d}", .{@intFromEnum(adjustment)});
return .invalid_value;
};
}
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const sel_ptr = selection orelse return .invalid_value;
var sel = sel_ptr.toZig() orelse return .invalid_value;
sel.adjust(t.screens.active, adjustment);
sel_ptr.* = .fromZig(sel);
return .success;
}
pub fn selection_order(
terminal_: Terminal,
selection: ?*const selection_c.CSelection,
out_order: ?*Selection.Order,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const sel = (selection orelse return .invalid_value).toZig() orelse
return .invalid_value;
const out = out_order orelse return .invalid_value;
if (!selectionValid(t, sel)) return .invalid_value;
out.* = sel.order(t.screens.active);
return .success;
}
pub fn selection_ordered(
terminal_: Terminal,
selection: ?*const selection_c.CSelection,
desired: Selection.Order,
out_selection: ?*selection_c.CSelection,
) callconv(lib.calling_conv) Result {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Selection.Order, @intFromEnum(desired)) catch {
log.warn("terminal_selection_ordered invalid desired value={d}", .{@intFromEnum(desired)});
return .invalid_value;
};
}
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const sel = (selection orelse return .invalid_value).toZig() orelse
return .invalid_value;
const out = out_selection orelse return .invalid_value;
if (!selectionValid(t, sel)) return .invalid_value;
out.* = .fromZig(sel.ordered(t.screens.active, desired));
return .success;
}
pub fn selection_contains(
terminal_: Terminal,
selection: ?*const selection_c.CSelection,
pt: point.Point.C,
out_contains: ?*bool,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
const sel = (selection orelse return .invalid_value).toZig() orelse
return .invalid_value;
const out = out_contains orelse return .invalid_value;
if (!selectionValid(t, sel)) return .invalid_value;
const screen = t.screens.active;
const pin = screen.pages.pin(.fromC(pt)) orelse return .invalid_value;
out.* = sel.contains(screen, pin);
return .success;
}
fn selectionValid(t: *ZigTerminal, sel: Selection) bool {
const screen = t.screens.active;
return screen.pages.pointFromPin(.screen, sel.start()) != null and
screen.pages.pointFromPin(.screen, sel.end()) != null;
}
fn getTyped(
terminal_: Terminal,
comptime data: TerminalData,
@@ -1503,11 +1424,11 @@ test "selection_adjust mutates snapshot end" {
.start = start_ref,
.end = end_ref,
};
try testing.expectEqual(Result.success, selection_adjust(t, &sel, .right));
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 2), sel.end.toPin().?.x);
try testing.expectEqual(Result.success, selection_adjust(t, &sel, .left));
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .left));
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
@@ -1515,7 +1436,7 @@ test "selection_adjust mutates snapshot end" {
.start = end_ref,
.end = start_ref,
};
try testing.expectEqual(Result.success, selection_adjust(t, &sel, .right));
try testing.expectEqual(Result.success, selection_c.adjust(t, &sel, .right));
try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
}
@@ -1553,19 +1474,19 @@ test "selection_order and selection_ordered" {
.rectangle = true,
};
var order: Selection.Order = undefined;
try testing.expectEqual(Result.success, selection_order(t, &sel, &order));
try testing.expectEqual(Selection.Order.mirrored_forward, order);
var order: selection_c.Order = undefined;
try testing.expectEqual(Result.success, selection_c.order(t, &sel, &order));
try testing.expectEqual(selection_c.Order.mirrored_forward, order);
var out: selection_c.CSelection = undefined;
try testing.expectEqual(Result.success, selection_ordered(t, &sel, .forward, &out));
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .forward, &out));
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
try testing.expectEqual(@as(u16, 3), out.end.toPin().?.x);
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
try testing.expect(out.rectangle);
try testing.expectEqual(Result.success, selection_ordered(t, &sel, .reverse, &out));
try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .reverse, &out));
try testing.expectEqual(@as(u16, 3), out.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
try testing.expectEqual(@as(u16, 1), out.end.toPin().?.x);
@@ -1606,13 +1527,13 @@ test "selection_contains" {
};
var contains: bool = undefined;
try testing.expectEqual(Result.success, selection_contains(t, &linear, .{
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
.tag = .active,
.value = .{ .active = .{ .x = 4, .y = 0 } },
}, &contains));
try testing.expect(contains);
try testing.expectEqual(Result.success, selection_contains(t, &linear, .{
try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
.tag = .active,
.value = .{ .active = .{ .x = 2, .y = 0 } },
}, &contains));
@@ -1624,7 +1545,7 @@ test "selection_contains" {
.rectangle = true,
};
try testing.expectEqual(Result.success, selection_contains(t, &rectangle, .{
try testing.expectEqual(Result.success, selection_c.contains(t, &rectangle, .{
.tag = .active,
.value = .{ .active = .{ .x = 2, .y = 0 } },
}, &contains));
@@ -1644,9 +1565,9 @@ test "selection_order invalid values" {
));
defer free(t);
var order: Selection.Order = undefined;
try testing.expectEqual(Result.invalid_value, selection_order(null, null, &order));
try testing.expectEqual(Result.invalid_value, selection_order(t, null, &order));
var order: selection_c.Order = undefined;
try testing.expectEqual(Result.invalid_value, selection_c.order(null, null, &order));
try testing.expectEqual(Result.invalid_value, selection_c.order(t, null, &order));
}
test "grid_ref" {