libghostty: add selection adjustment api
parent
545a5aef66
commit
15d8963681
|
|
@ -77,6 +77,61 @@ typedef struct {
|
||||||
bool rectangle;
|
bool rectangle;
|
||||||
} GhosttySelection;
|
} 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
|
#ifdef __cplusplus
|
||||||
|
|
|
||||||
|
|
@ -1123,6 +1123,35 @@ GHOSTTY_API GhosttyResult ghostty_terminal_get_multi(GhosttyTerminal terminal,
|
||||||
void** values,
|
void** values,
|
||||||
size_t* out_written);
|
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.
|
* Resolve a point in the terminal grid to a grid reference.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,7 @@ comptime {
|
||||||
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
|
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
|
||||||
@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_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" });
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ const Selection = @This();
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const assert = @import("../quirks.zig").inlineAssert;
|
const assert = @import("../quirks.zig").inlineAssert;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
const lib = @import("lib.zig");
|
||||||
const page = @import("page.zig");
|
const page = @import("page.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const PageList = @import("PageList.zig");
|
const PageList = @import("PageList.zig");
|
||||||
|
|
@ -389,18 +390,18 @@ pub fn containedRowCached(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Possible adjustments to the selection.
|
/// Possible adjustments to the selection.
|
||||||
pub const Adjustment = enum {
|
pub const Adjustment = lib.Enum(lib.target, &.{
|
||||||
left,
|
"left",
|
||||||
right,
|
"right",
|
||||||
up,
|
"up",
|
||||||
down,
|
"down",
|
||||||
home,
|
"home",
|
||||||
end,
|
"end",
|
||||||
page_up,
|
"page_up",
|
||||||
page_down,
|
"page_down",
|
||||||
beginning_of_line,
|
"beginning_of_line",
|
||||||
end_of_line,
|
"end_of_line",
|
||||||
};
|
});
|
||||||
|
|
||||||
/// Adjust the selection by some given adjustment. An adjustment allows
|
/// Adjust the selection by some given adjustment. An adjustment allows
|
||||||
/// a selection to be expanded slightly left, right, up, down, etc.
|
/// a selection to be expanded slightly left, right, up, down, etc.
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,7 @@ pub const terminal_mode_get = terminal.mode_get;
|
||||||
pub const terminal_mode_set = terminal.mode_set;
|
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_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;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const ZigTerminal = @import("../Terminal.zig");
|
||||||
const Stream = @import("../stream_terminal.zig").Stream;
|
const Stream = @import("../stream_terminal.zig").Stream;
|
||||||
const ScreenSet = @import("../ScreenSet.zig");
|
const ScreenSet = @import("../ScreenSet.zig");
|
||||||
const PageList = @import("../PageList.zig");
|
const PageList = @import("../PageList.zig");
|
||||||
|
const Selection = @import("../Selection.zig");
|
||||||
const apc = @import("../apc.zig");
|
const apc = @import("../apc.zig");
|
||||||
const kitty = @import("../kitty/key.zig");
|
const kitty = @import("../kitty/key.zig");
|
||||||
const kitty_gfx_c = @import("kitty_graphics.zig");
|
const kitty_gfx_c = @import("kitty_graphics.zig");
|
||||||
|
|
@ -664,6 +665,26 @@ pub fn get_multi(
|
||||||
return .success;
|
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(
|
fn getTyped(
|
||||||
terminal_: Terminal,
|
terminal_: Terminal,
|
||||||
comptime data: TerminalData,
|
comptime data: TerminalData,
|
||||||
|
|
@ -1389,6 +1410,54 @@ test "set and get selection" {
|
||||||
try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out)));
|
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" {
|
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