libghostty: add selection adjustment api

This commit is contained in:
Mitchell Hashimoto
2026-05-23 15:08:38 -07:00
parent 545a5aef66
commit 15d8963681
6 changed files with 168 additions and 12 deletions

View File

@@ -77,6 +77,61 @@ typedef struct {
bool rectangle;
} GhosttySelection;
/**
* Operation used to adjust a selection endpoint.
*
* Adjustment mutates the selection's logical end endpoint, not whichever
* endpoint is visually bottom/right. This preserves keyboard and drag
* behavior for both forward and reversed selections.
*
* @ingroup selection
*/
typedef enum GHOSTTY_ENUM_TYPED {
/** Move left to the previous non-empty cell, wrapping upward. */
GHOSTTY_SELECTION_ADJUST_LEFT = 0,
/** Move right to the next non-empty cell, wrapping downward. */
GHOSTTY_SELECTION_ADJUST_RIGHT = 1,
/**
* Move up one row at the current column, or to the beginning of the
* line if already at the top.
*/
GHOSTTY_SELECTION_ADJUST_UP = 2,
/**
* Move down to the next non-blank row at the current column, or to the
* end of the line if none exists.
*/
GHOSTTY_SELECTION_ADJUST_DOWN = 3,
/** Move to the top-left cell of the screen. */
GHOSTTY_SELECTION_ADJUST_HOME = 4,
/** Move to the right edge of the last non-blank row on the screen. */
GHOSTTY_SELECTION_ADJUST_END = 5,
/**
* Move up by one terminal page height, or to home if that would move
* past the top.
*/
GHOSTTY_SELECTION_ADJUST_PAGE_UP = 6,
/**
* Move down by one terminal page height, or to end if that would move
* past the bottom.
*/
GHOSTTY_SELECTION_ADJUST_PAGE_DOWN = 7,
/** Move to the left edge of the current line. */
GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE = 8,
/** Move to the right edge of the current line. */
GHOSTTY_SELECTION_ADJUST_END_OF_LINE = 9,
GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
} GhosttySelectionAdjust;
/** @} */
#ifdef __cplusplus

View File

@@ -1123,6 +1123,35 @@ 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);
/**
* Resolve a point in the terminal grid to a grid reference.
*

View File

@@ -239,6 +239,7 @@ comptime {
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" });
@export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" });
@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

@@ -4,6 +4,7 @@ const Selection = @This();
const std = @import("std");
const assert = @import("../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;
const lib = @import("lib.zig");
const page = @import("page.zig");
const point = @import("point.zig");
const PageList = @import("PageList.zig");
@@ -389,18 +390,18 @@ pub fn containedRowCached(
}
/// Possible adjustments to the selection.
pub const Adjustment = enum {
left,
right,
up,
down,
home,
end,
page_up,
page_down,
beginning_of_line,
end_of_line,
};
pub const Adjustment = lib.Enum(lib.target, &.{
"left",
"right",
"up",
"down",
"home",
"end",
"page_up",
"page_down",
"beginning_of_line",
"end_of_line",
});
/// Adjust the selection by some given adjustment. An adjustment allows
/// a selection to be expanded slightly left, right, up, down, etc.

View File

@@ -170,6 +170,7 @@ 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_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

@@ -7,6 +7,7 @@ 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");
@@ -664,6 +665,26 @@ 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;
}
fn getTyped(
terminal_: Terminal,
comptime data: TerminalData,
@@ -1389,6 +1410,54 @@ test "set and get selection" {
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
}
test "selection_adjust mutates snapshot end" {
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", 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 sel: selection_c.CSelection = .{
.start = start_ref,
.end = end_ref,
};
try testing.expectEqual(Result.success, selection_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(@as(u16, 0), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
sel = .{
.start = end_ref,
.end = start_ref,
};
try testing.expectEqual(Result.success, selection_adjust(t, &sel, .right));
try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x);
}
test "grid_ref" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(