vt: cover c row iterator new/free

Add a C ABI row-iterator handle for render state with
ghostty_render_state_row_iterator_new and
ghostty_render_state_row_iterator_free, and wire them through
src/terminal/c/main.zig, src/lib_vt.zig, and
include/ghostty/vt/render.h. The header now documents only the
currently exported iterator API.
This commit is contained in:
Mitchell Hashimoto
2026-03-19 09:28:26 -07:00
parent b35f8ed16e
commit ad0e47ebac
4 changed files with 170 additions and 0 deletions

View File

@@ -7,6 +7,9 @@
#ifndef GHOSTTY_VT_RENDER_H
#define GHOSTTY_VT_RENDER_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/terminal.h>
@@ -50,6 +53,13 @@ extern "C" {
*/
typedef struct GhosttyRenderState* GhosttyRenderState;
/**
* Opaque handle to a render-state row iterator.
*
* @ingroup render
*/
typedef struct GhosttyRenderStateRowIterator* GhosttyRenderStateRowIterator;
/**
* Dirty state of a render state after update.
*
@@ -199,6 +209,33 @@ GhosttyResult ghostty_render_state_dirty_get(GhosttyRenderState state,
GhosttyResult ghostty_render_state_dirty_set(GhosttyRenderState state,
GhosttyRenderStateDirty dirty);
/**
* Create a row iterator for a render state.
*
* The iterator borrows from `state`; `state` must outlive the iterator.
*
* @param allocator Pointer to allocator, or NULL to use the default allocator
* @param state The render state handle to iterate (NULL returns GHOSTTY_INVALID_VALUE)
* @param[out] out_iterator On success, receives the created iterator handle
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if `state` is
* NULL, GHOSTTY_OUT_OF_MEMORY on allocation failure
*
* @ingroup render
*/
GhosttyResult ghostty_render_state_row_iterator_new(
const GhosttyAllocator* allocator,
GhosttyRenderState state,
GhosttyRenderStateRowIterator* out_iterator);
/**
* Free a render-state row iterator.
*
* @param iterator The iterator handle to free (may be NULL)
*
* @ingroup render
*/
void ghostty_render_state_row_iterator_free(GhosttyRenderStateRowIterator iterator);
/**
* Free a render state instance.
*

View File

@@ -193,6 +193,8 @@ comptime {
@export(&c.render_state_colors_get, .{ .name = "ghostty_render_state_colors_get" });
@export(&c.render_state_dirty_get, .{ .name = "ghostty_render_state_dirty_get" });
@export(&c.render_state_dirty_set, .{ .name = "ghostty_render_state_dirty_set" });
@export(&c.render_state_row_iterator_new, .{ .name = "ghostty_render_state_row_iterator_new" });
@export(&c.render_state_row_iterator_free, .{ .name = "ghostty_render_state_row_iterator_free" });
@export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
@export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
@export(&c.terminal_free, .{ .name = "ghostty_terminal_free" });

View File

@@ -43,6 +43,8 @@ pub const render_state_size_get = render.size_get;
pub const render_state_colors_get = render.colors_get;
pub const render_state_dirty_get = render.dirty_get;
pub const render_state_dirty_set = render.dirty_set;
pub const render_state_row_iterator_new = render.row_iterator_new;
pub const render_state_row_iterator_free = render.row_iterator_free;
pub const sgr_new = sgr.new;
pub const sgr_free = sgr.free;

View File

@@ -1,9 +1,11 @@
const std = @import("std");
const testing = std.testing;
const Allocator = std.mem.Allocator;
const lib = @import("../../lib/main.zig");
const lib_alloc = @import("../../lib/allocator.zig");
const CAllocator = lib_alloc.Allocator;
const colorpkg = @import("../color.zig");
const page = @import("../page.zig");
const size = @import("../size.zig");
const terminal_c = @import("terminal.zig");
const renderpkg = @import("../render.zig");
@@ -14,9 +16,24 @@ const RenderStateWrapper = struct {
state: renderpkg.RenderState = .empty,
};
const RowIteratorWrapper = struct {
alloc: std.mem.Allocator,
/// The current index (also y value) into the row list.
y: size.CellCountInt,
/// These are the raw pointers into the render state data.
raws: []const page.Row,
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
dirty: []const bool,
};
/// C: GhosttyRenderState
pub const RenderState = ?*RenderStateWrapper;
/// C: GhosttyRenderStateRowIterator
pub const RowIterator = ?*RowIteratorWrapper;
/// C: GhosttyRenderStateDirty
pub const Dirty = renderpkg.RenderState.Dirty;
@@ -159,6 +176,53 @@ pub fn dirty_set(
return .success;
}
pub fn row_iterator_new(
alloc_: ?*const CAllocator,
state_: RenderState,
out_iterator_: ?*RowIterator,
) callconv(.c) Result {
const state = state_ orelse return .invalid_value;
const out_iterator = out_iterator_ orelse return .invalid_value;
const alloc = lib_alloc.default(alloc_);
out_iterator.* = row_iterator_new_(
alloc,
state,
) catch |err| {
out_iterator.* = null;
switch (err) {
error.OutOfMemory => return .out_of_memory,
}
};
return .success;
}
fn row_iterator_new_(
alloc: Allocator,
state: *RenderStateWrapper,
) !*RowIteratorWrapper {
const it = try alloc.create(RowIteratorWrapper);
errdefer alloc.destroy(it);
const row_data = state.state.row_data.slice();
it.* = .{
.alloc = alloc,
.y = 0,
.raws = row_data.items(.raw),
.cells = row_data.items(.cells),
.dirty = row_data.items(.dirty),
};
return it;
}
pub fn row_iterator_free(iterator_: RowIterator) callconv(.c) void {
const iterator = iterator_ orelse return;
const alloc = iterator.alloc;
alloc.destroy(iterator);
}
pub fn free(state_: RenderState) callconv(.c) void {
const state = state_ orelse return;
const alloc = state.alloc;
@@ -291,6 +355,71 @@ test "render: dirty set invalid enum value" {
try testing.expectEqual(Result.invalid_value, dirty_set(state, 99));
}
test "render: row iterator new invalid value" {
var state: RenderState = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
&state,
));
defer free(state);
var iterator: RowIterator = null;
try testing.expectEqual(Result.invalid_value, row_iterator_new(
&lib_alloc.test_allocator,
null,
&iterator,
));
try testing.expectEqual(Result.invalid_value, row_iterator_new(
&lib_alloc.test_allocator,
state,
null,
));
}
test "render: row iterator new/free" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib_alloc.test_allocator,
&terminal,
.{
.cols = 80,
.rows = 24,
.max_scrollback = 10_000,
},
));
defer terminal_c.free(terminal);
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 iterator: RowIterator = null;
try testing.expectEqual(Result.success, row_iterator_new(
&lib_alloc.test_allocator,
state,
&iterator,
));
defer row_iterator_free(iterator);
try testing.expect(iterator != null);
const iterator_ptr = iterator.?;
const row_data = state.?.state.row_data.slice();
try testing.expectEqual(@as(size.CellCountInt, 0), 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(.dirty).len, iterator_ptr.dirty.len);
}
test "render: row iterator free null" {
row_iterator_free(null);
}
test "render: update" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(