mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-05-25 06:18:37 +00:00
libghostty: expose row-local render selections
Render state already tracks the selected cell range for each viewport row, but C renderers could only get the full terminal selection. That required consumers to map global selection pins back into row-local spans themselves. Add row selection data to the render-state row API. Querying the new row data returns GHOSTTY_NO_VALUE for unselected rows and writes the inclusive start and end columns for selected rows. The render example now demonstrates setting a selection and reading the row-local range while iterating rows.
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
This contains an example of how to use the `ghostty-vt` render-state API
|
||||
to create a render state, update it from terminal content, iterate rows
|
||||
and cells, read styles and colors, inspect cursor state, and manage dirty
|
||||
tracking.
|
||||
and cells, read styles and colors, inspect cursor and row-local selection
|
||||
state, and manage dirty tracking.
|
||||
|
||||
This uses a `build.zig` and `Zig` to build the C program so that we
|
||||
can reuse a lot of our build logic and depend directly on our source
|
||||
|
||||
@@ -46,6 +46,32 @@ int main(void) {
|
||||
ghostty_terminal_vt_write(
|
||||
terminal, (const uint8_t*)content, strlen(content));
|
||||
|
||||
// Select "underlined" on the second row. Render state exposes this
|
||||
// later as a row-local selected cell range.
|
||||
GhosttyGridRef selection_start = GHOSTTY_INIT_SIZED(GhosttyGridRef);
|
||||
GhosttyPoint selection_start_pt = {
|
||||
.tag = GHOSTTY_POINT_TAG_ACTIVE,
|
||||
.value = { .coordinate = { .x = 0, .y = 1 } },
|
||||
};
|
||||
result = ghostty_terminal_grid_ref(
|
||||
terminal, selection_start_pt, &selection_start);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttyGridRef selection_end = GHOSTTY_INIT_SIZED(GhosttyGridRef);
|
||||
GhosttyPoint selection_end_pt = {
|
||||
.tag = GHOSTTY_POINT_TAG_ACTIVE,
|
||||
.value = { .coordinate = { .x = 9, .y = 1 } },
|
||||
};
|
||||
result = ghostty_terminal_grid_ref(terminal, selection_end_pt, &selection_end);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
GhosttySelection selection = GHOSTTY_INIT_SIZED(GhosttySelection);
|
||||
selection.start = selection_start;
|
||||
selection.end = selection_end;
|
||||
result = ghostty_terminal_set(
|
||||
terminal, GHOSTTY_TERMINAL_OPT_SELECTION, &selection);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
|
||||
result = ghostty_render_state_update(render_state, terminal);
|
||||
assert(result == GHOSTTY_SUCCESS);
|
||||
//! [render-state-update]
|
||||
@@ -154,6 +180,18 @@ int main(void) {
|
||||
printf("Row %2d [%s]: ", row_index,
|
||||
row_dirty ? "dirty" : "clean");
|
||||
|
||||
// Query the row-local selection range. Rows without a selection return
|
||||
// GHOSTTY_NO_VALUE; selected rows return inclusive start/end columns.
|
||||
GhosttyRenderStateRowSelection row_selection =
|
||||
GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection);
|
||||
result = ghostty_render_state_row_get(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION, &row_selection);
|
||||
assert(result == GHOSTTY_SUCCESS || result == GHOSTTY_NO_VALUE);
|
||||
if (result == GHOSTTY_SUCCESS) {
|
||||
printf("selection=%u..%u ",
|
||||
row_selection.start_x, row_selection.end_x);
|
||||
}
|
||||
|
||||
// Get cells for this row (reuses the same cells handle).
|
||||
result = ghostty_render_state_row_get(
|
||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells);
|
||||
|
||||
@@ -221,6 +221,9 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
* valid as long as the underlying render state is not updated.
|
||||
* It is unsafe to use cell data after updating the render state. */
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
|
||||
|
||||
/** Row-local selected cell range (GhosttyRenderStateRowSelection). */
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION = 4,
|
||||
GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateRowData;
|
||||
|
||||
@@ -235,6 +238,29 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyRenderStateRowOption;
|
||||
|
||||
/**
|
||||
* Row-local selection range.
|
||||
*
|
||||
* This struct uses the sized-struct ABI pattern. Initialize with
|
||||
* GHOSTTY_INIT_SIZED(GhosttyRenderStateRowSelection) before querying
|
||||
* GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION.
|
||||
*
|
||||
* Querying GHOSTTY_RENDER_STATE_ROW_DATA_SELECTION returns GHOSTTY_NO_VALUE
|
||||
* if the current row does not intersect the current selection.
|
||||
*
|
||||
* @ingroup render
|
||||
*/
|
||||
typedef struct {
|
||||
/** Size of this struct in bytes. Must be set to sizeof(GhosttyRenderStateRowSelection). */
|
||||
size_t size;
|
||||
|
||||
/** Start column of the row-local selection range, inclusive. */
|
||||
uint16_t start_x;
|
||||
|
||||
/** End column of the row-local selection range, inclusive. */
|
||||
uint16_t end_x;
|
||||
} GhosttyRenderStateRowSelection;
|
||||
|
||||
/**
|
||||
* Render-state color information.
|
||||
*
|
||||
|
||||
@@ -31,6 +31,7 @@ const RowIteratorWrapper = struct {
|
||||
/// These are the raw pointers into the render state data.
|
||||
raws: []const page.Row,
|
||||
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
|
||||
selection: []const ?[2]size.CellCountInt,
|
||||
dirty: []bool,
|
||||
|
||||
/// The color palette from the render state, needed to resolve
|
||||
@@ -61,6 +62,13 @@ pub const RowCells = ?*RowCellsWrapper;
|
||||
/// C: GhosttyRenderStateDirty
|
||||
pub const Dirty = renderpkg.RenderState.Dirty;
|
||||
|
||||
/// C: GhosttyRenderStateRowSelection
|
||||
pub const RowSelection = extern struct {
|
||||
size: usize = @sizeOf(RowSelection),
|
||||
start_x: u16 = 0,
|
||||
end_x: u16 = 0,
|
||||
};
|
||||
|
||||
/// C: GhosttyRenderStateCursorVisualStyle
|
||||
pub const CursorVisualStyle = enum(c_int) {
|
||||
bar = 0,
|
||||
@@ -241,6 +249,7 @@ fn getTyped(
|
||||
.y = null,
|
||||
.raws = row_data.items(.raw),
|
||||
.cells = row_data.items(.cells),
|
||||
.selection = row_data.items(.selection),
|
||||
.dirty = row_data.items(.dirty),
|
||||
.palette = &state.state.colors.palette,
|
||||
};
|
||||
@@ -381,6 +390,7 @@ pub fn row_iterator_new(
|
||||
.y = undefined,
|
||||
.raws = undefined,
|
||||
.cells = undefined,
|
||||
.selection = undefined,
|
||||
.dirty = undefined,
|
||||
.palette = undefined,
|
||||
};
|
||||
@@ -564,6 +574,7 @@ pub const RowData = enum(c_int) {
|
||||
dirty = 1,
|
||||
raw = 2,
|
||||
cells = 3,
|
||||
selection = 4,
|
||||
|
||||
/// Output type expected for querying the data of the given kind.
|
||||
pub fn OutType(comptime self: RowData) type {
|
||||
@@ -572,6 +583,7 @@ pub const RowData = enum(c_int) {
|
||||
.dirty => bool,
|
||||
.raw => row.CRow,
|
||||
.cells => RowCells,
|
||||
.selection => RowSelection,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -654,6 +666,14 @@ fn rowGetTyped(
|
||||
.palette = it.palette,
|
||||
};
|
||||
},
|
||||
.selection => {
|
||||
const out_size = out.size;
|
||||
if (out_size < @sizeOf(RowSelection)) return .invalid_value;
|
||||
|
||||
const sel = it.selection[y] orelse return .no_value;
|
||||
out.start_x = sel[0];
|
||||
out.end_x = sel[1];
|
||||
},
|
||||
}
|
||||
|
||||
return .success;
|
||||
@@ -845,6 +865,7 @@ test "render: row iterator new/free" {
|
||||
try testing.expectEqual(@as(?size.CellCountInt, null), iterator_ptr.y);
|
||||
try testing.expectEqual(row_data.items(.raw).len, iterator_ptr.raws.len);
|
||||
try testing.expectEqual(row_data.items(.cells).len, iterator_ptr.cells.len);
|
||||
try testing.expectEqual(row_data.items(.selection).len, iterator_ptr.selection.len);
|
||||
try testing.expectEqual(row_data.items(.dirty).len, iterator_ptr.dirty.len);
|
||||
}
|
||||
|
||||
@@ -1026,6 +1047,60 @@ test "render: row get/set dirty" {
|
||||
try testing.expect(!dirty);
|
||||
}
|
||||
|
||||
test "render: row get selection" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib.alloc.test_allocator,
|
||||
&terminal,
|
||||
.{
|
||||
.cols = 10,
|
||||
.rows = 3,
|
||||
.max_scrollback = 10_000,
|
||||
},
|
||||
));
|
||||
defer terminal_c.free(terminal);
|
||||
|
||||
const t = terminal.?.terminal;
|
||||
const screen = t.screens.active;
|
||||
try screen.select(.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 2, .y = 1 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 4, .y = 1 } }).?,
|
||||
false,
|
||||
));
|
||||
|
||||
var state: RenderState = null;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib.alloc.test_allocator,
|
||||
&state,
|
||||
));
|
||||
defer free(state);
|
||||
|
||||
try testing.expectEqual(Result.success, update(state, terminal));
|
||||
|
||||
var it: RowIterator = null;
|
||||
try testing.expectEqual(Result.success, row_iterator_new(
|
||||
&lib.alloc.test_allocator,
|
||||
&it,
|
||||
));
|
||||
defer row_iterator_free(it);
|
||||
|
||||
try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
|
||||
|
||||
var sel: RowSelection = .{};
|
||||
try testing.expect(row_iterator_next(it));
|
||||
try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
|
||||
|
||||
try testing.expect(row_iterator_next(it));
|
||||
sel = .{};
|
||||
try testing.expectEqual(Result.success, row_get(it, .selection, @ptrCast(&sel)));
|
||||
try testing.expectEqual(@as(u16, 2), sel.start_x);
|
||||
try testing.expectEqual(@as(u16, 4), sel.end_x);
|
||||
|
||||
try testing.expect(row_iterator_next(it));
|
||||
sel = .{};
|
||||
try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
|
||||
}
|
||||
|
||||
test "render: row iterator next" {
|
||||
var terminal: terminal_c.Terminal = null;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
|
||||
Reference in New Issue
Block a user