libghostty: add selection ordering APIs
Expose selection endpoint ordering through the libghostty-vt C API so embedders can safely normalize selections whose start and end refs may be reversed. The new APIs report the current order and return a fresh untracked selection with forward or reverse bounds. Selection.Order now uses lib.Enum, matching the existing adjustment enum pattern and keeping the C ABI enum generated from the same Zig source of truth. The new functions are wired through the C API re-export and lib-vt export paths, with coverage for mirrored rectangular selection ordering.pull/12794/head
parent
15d8963681
commit
4a77e81967
|
|
@ -77,6 +77,31 @@ typedef struct {
|
||||||
bool rectangle;
|
bool rectangle;
|
||||||
} GhosttySelection;
|
} GhosttySelection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordering of a selection's endpoints in terminal coordinates.
|
||||||
|
*
|
||||||
|
* Mirrored orders are only produced by rectangular selections whose start
|
||||||
|
* and end endpoints are on opposite diagonal corners that are not simple
|
||||||
|
* top-left-to-bottom-right or bottom-right-to-top-left orderings.
|
||||||
|
*
|
||||||
|
* @ingroup selection
|
||||||
|
*/
|
||||||
|
typedef enum GHOSTTY_ENUM_TYPED {
|
||||||
|
/** Start is before end in top-left to bottom-right order. */
|
||||||
|
GHOSTTY_SELECTION_ORDER_FORWARD = 0,
|
||||||
|
|
||||||
|
/** End is before start in top-left to bottom-right order. */
|
||||||
|
GHOSTTY_SELECTION_ORDER_REVERSE = 1,
|
||||||
|
|
||||||
|
/** Rectangular selection from top-right to bottom-left. */
|
||||||
|
GHOSTTY_SELECTION_ORDER_MIRRORED_FORWARD = 2,
|
||||||
|
|
||||||
|
/** Rectangular selection from bottom-left to top-right. */
|
||||||
|
GHOSTTY_SELECTION_ORDER_MIRRORED_REVERSE = 3,
|
||||||
|
|
||||||
|
GHOSTTY_SELECTION_ORDER_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||||
|
} GhosttySelectionOrder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operation used to adjust a selection endpoint.
|
* Operation used to adjust a selection endpoint.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1152,6 +1152,61 @@ GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust(
|
||||||
GhosttySelection* selection,
|
GhosttySelection* selection,
|
||||||
GhosttySelectionAdjust adjustment);
|
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);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a point in the terminal grid to a grid reference.
|
* Resolve a point in the terminal grid to a grid reference.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,8 @@ comptime {
|
||||||
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
|
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
|
||||||
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
|
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
|
||||||
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
|
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
|
||||||
|
@export(&c.terminal_selection_order, .{ .name = "ghostty_terminal_selection_order" });
|
||||||
|
@export(&c.terminal_selection_ordered, .{ .name = "ghostty_terminal_selection_ordered" });
|
||||||
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
|
@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_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" });
|
||||||
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
|
@export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" });
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,12 @@ pub fn bottomRight(self: Selection, s: *const Screen) Pin {
|
||||||
/// operations only flip the x or y axis, not both. Depending on the y axis
|
/// operations only flip the x or y axis, not both. Depending on the y axis
|
||||||
/// direction, this is either mirrored_forward or mirrored_reverse.
|
/// direction, this is either mirrored_forward or mirrored_reverse.
|
||||||
///
|
///
|
||||||
pub const Order = enum { forward, reverse, mirrored_forward, mirrored_reverse };
|
pub const Order = lib.Enum(lib.target, &.{
|
||||||
|
"forward",
|
||||||
|
"reverse",
|
||||||
|
"mirrored_forward",
|
||||||
|
"mirrored_reverse",
|
||||||
|
});
|
||||||
|
|
||||||
pub fn order(self: Selection, s: *const Screen) Order {
|
pub fn order(self: Selection, s: *const Screen) Order {
|
||||||
const start_pt = s.pages.pointFromPin(.screen, self.start()).?.screen;
|
const start_pt = s.pages.pointFromPin(.screen, self.start()).?.screen;
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,8 @@ pub const terminal_mode_set = terminal.mode_set;
|
||||||
pub const terminal_get = terminal.get;
|
pub const terminal_get = terminal.get;
|
||||||
pub const terminal_get_multi = terminal.get_multi;
|
pub const terminal_get_multi = terminal.get_multi;
|
||||||
pub const terminal_selection_adjust = terminal.selection_adjust;
|
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_grid_ref = terminal.grid_ref;
|
pub const terminal_grid_ref = terminal.grid_ref;
|
||||||
pub const terminal_grid_ref_track = terminal.grid_ref_track;
|
pub const terminal_grid_ref_track = terminal.grid_ref_track;
|
||||||
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
|
pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref;
|
||||||
|
|
|
||||||
|
|
@ -685,6 +685,50 @@ pub fn selection_adjust(
|
||||||
return .success;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
fn getTyped(
|
||||||
terminal_: Terminal,
|
terminal_: Terminal,
|
||||||
comptime data: TerminalData,
|
comptime data: TerminalData,
|
||||||
|
|
@ -1458,6 +1502,77 @@ test "selection_adjust mutates snapshot end" {
|
||||||
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "selection_order and selection_ordered" {
|
||||||
|
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);
|
||||||
|
|
||||||
|
vt_write(t, "Hello\r\nWorld", 12);
|
||||||
|
|
||||||
|
var start_ref: grid_ref_c.CGridRef = .{};
|
||||||
|
try testing.expectEqual(Result.success, grid_ref(t, .{
|
||||||
|
.tag = .active,
|
||||||
|
.value = .{ .active = .{ .x = 3, .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 = 1 } },
|
||||||
|
}, &end_ref));
|
||||||
|
|
||||||
|
const sel: selection_c.CSelection = .{
|
||||||
|
.start = start_ref,
|
||||||
|
.end = end_ref,
|
||||||
|
.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 out: selection_c.CSelection = undefined;
|
||||||
|
try testing.expectEqual(Result.success, selection_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(@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);
|
||||||
|
try testing.expectEqual(@as(u16, 0), out.end.toPin().?.y);
|
||||||
|
try testing.expect(out.rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "selection_order invalid values" {
|
||||||
|
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 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));
|
||||||
|
}
|
||||||
|
|
||||||
test "grid_ref" {
|
test "grid_ref" {
|
||||||
var t: Terminal = null;
|
var t: Terminal = null;
|
||||||
try testing.expectEqual(Result.success, new(
|
try testing.expectEqual(Result.success, new(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue