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.pull/12794/head
parent
ae03d3cae4
commit
24048ffd47
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
This contains an example of how to use the `ghostty-vt` render-state API
|
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
|
to create a render state, update it from terminal content, iterate rows
|
||||||
and cells, read styles and colors, inspect cursor state, and manage dirty
|
and cells, read styles and colors, inspect cursor and row-local selection
|
||||||
tracking.
|
state, and manage dirty tracking.
|
||||||
|
|
||||||
This uses a `build.zig` and `Zig` to build the C program so that we
|
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
|
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(
|
ghostty_terminal_vt_write(
|
||||||
terminal, (const uint8_t*)content, strlen(content));
|
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);
|
result = ghostty_render_state_update(render_state, terminal);
|
||||||
assert(result == GHOSTTY_SUCCESS);
|
assert(result == GHOSTTY_SUCCESS);
|
||||||
//! [render-state-update]
|
//! [render-state-update]
|
||||||
|
|
@ -154,6 +180,18 @@ int main(void) {
|
||||||
printf("Row %2d [%s]: ", row_index,
|
printf("Row %2d [%s]: ", row_index,
|
||||||
row_dirty ? "dirty" : "clean");
|
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).
|
// Get cells for this row (reuses the same cells handle).
|
||||||
result = ghostty_render_state_row_get(
|
result = ghostty_render_state_row_get(
|
||||||
row_iter, GHOSTTY_RENDER_STATE_ROW_DATA_CELLS, &cells);
|
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.
|
* valid as long as the underlying render state is not updated.
|
||||||
* It is unsafe to use cell data after updating the render state. */
|
* It is unsafe to use cell data after updating the render state. */
|
||||||
GHOSTTY_RENDER_STATE_ROW_DATA_CELLS = 3,
|
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,
|
GHOSTTY_RENDER_STATE_ROW_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||||
} GhosttyRenderStateRowData;
|
} GhosttyRenderStateRowData;
|
||||||
|
|
||||||
|
|
@ -235,6 +238,29 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||||
GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
GHOSTTY_RENDER_STATE_ROW_OPTION_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||||
} GhosttyRenderStateRowOption;
|
} 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.
|
* Render-state color information.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const RowIteratorWrapper = struct {
|
||||||
/// These are the raw pointers into the render state data.
|
/// These are the raw pointers into the render state data.
|
||||||
raws: []const page.Row,
|
raws: []const page.Row,
|
||||||
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
|
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
|
||||||
|
selection: []const ?[2]size.CellCountInt,
|
||||||
dirty: []bool,
|
dirty: []bool,
|
||||||
|
|
||||||
/// The color palette from the render state, needed to resolve
|
/// The color palette from the render state, needed to resolve
|
||||||
|
|
@ -61,6 +62,13 @@ pub const RowCells = ?*RowCellsWrapper;
|
||||||
/// C: GhosttyRenderStateDirty
|
/// C: GhosttyRenderStateDirty
|
||||||
pub const Dirty = renderpkg.RenderState.Dirty;
|
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
|
/// C: GhosttyRenderStateCursorVisualStyle
|
||||||
pub const CursorVisualStyle = enum(c_int) {
|
pub const CursorVisualStyle = enum(c_int) {
|
||||||
bar = 0,
|
bar = 0,
|
||||||
|
|
@ -241,6 +249,7 @@ fn getTyped(
|
||||||
.y = null,
|
.y = null,
|
||||||
.raws = row_data.items(.raw),
|
.raws = row_data.items(.raw),
|
||||||
.cells = row_data.items(.cells),
|
.cells = row_data.items(.cells),
|
||||||
|
.selection = row_data.items(.selection),
|
||||||
.dirty = row_data.items(.dirty),
|
.dirty = row_data.items(.dirty),
|
||||||
.palette = &state.state.colors.palette,
|
.palette = &state.state.colors.palette,
|
||||||
};
|
};
|
||||||
|
|
@ -381,6 +390,7 @@ pub fn row_iterator_new(
|
||||||
.y = undefined,
|
.y = undefined,
|
||||||
.raws = undefined,
|
.raws = undefined,
|
||||||
.cells = undefined,
|
.cells = undefined,
|
||||||
|
.selection = undefined,
|
||||||
.dirty = undefined,
|
.dirty = undefined,
|
||||||
.palette = undefined,
|
.palette = undefined,
|
||||||
};
|
};
|
||||||
|
|
@ -564,6 +574,7 @@ pub const RowData = enum(c_int) {
|
||||||
dirty = 1,
|
dirty = 1,
|
||||||
raw = 2,
|
raw = 2,
|
||||||
cells = 3,
|
cells = 3,
|
||||||
|
selection = 4,
|
||||||
|
|
||||||
/// Output type expected for querying the data of the given kind.
|
/// Output type expected for querying the data of the given kind.
|
||||||
pub fn OutType(comptime self: RowData) type {
|
pub fn OutType(comptime self: RowData) type {
|
||||||
|
|
@ -572,6 +583,7 @@ pub const RowData = enum(c_int) {
|
||||||
.dirty => bool,
|
.dirty => bool,
|
||||||
.raw => row.CRow,
|
.raw => row.CRow,
|
||||||
.cells => RowCells,
|
.cells => RowCells,
|
||||||
|
.selection => RowSelection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -654,6 +666,14 @@ fn rowGetTyped(
|
||||||
.palette = it.palette,
|
.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;
|
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(@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(.raw).len, iterator_ptr.raws.len);
|
||||||
try testing.expectEqual(row_data.items(.cells).len, iterator_ptr.cells.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);
|
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);
|
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" {
|
test "render: row iterator next" {
|
||||||
var terminal: terminal_c.Terminal = null;
|
var terminal: terminal_c.Terminal = null;
|
||||||
try testing.expectEqual(Result.success, terminal_c.new(
|
try testing.expectEqual(Result.success, terminal_c.new(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue