diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/terminal/c/render.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c/render.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/render.zig | 1382 |
1 files changed, 1382 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/render.zig b/vendor/libghostty-vt/src/terminal/c/render.zig new file mode 100644 index 00000000..b95bedd5 --- /dev/null +++ b/vendor/libghostty-vt/src/terminal/c/render.zig @@ -0,0 +1,1382 @@ +const std = @import("std"); +const testing = std.testing; +const Allocator = std.mem.Allocator; +const lib = @import("../lib.zig"); +const CAllocator = lib.alloc.Allocator; +const colorpkg = @import("../color.zig"); +const cursorpkg = @import("../cursor.zig"); +const page = @import("../page.zig"); +const size = @import("../size.zig"); +const Style = @import("../style.zig").Style; +const terminal_c = @import("terminal.zig"); +const ZigTerminal = @import("../Terminal.zig"); +const renderpkg = @import("../render.zig"); +const Result = @import("result.zig").Result; +const row = @import("row.zig"); +const style_c = @import("style.zig"); + +const log = std.log.scoped(.render_state_c); + +const RenderStateWrapper = struct { + alloc: std.mem.Allocator, + 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: []bool, + + /// The color palette from the render state, needed to resolve + /// palette-indexed background colors on cells. + palette: *const colorpkg.Palette, +}; + +const RowCellsWrapper = struct { + alloc: std.mem.Allocator, + x: ?size.CellCountInt, + raws: []const page.Cell, + graphemes: []const []const u21, + styles: []const Style, + + /// The color palette, needed to resolve palette-indexed background colors. + palette: *const colorpkg.Palette, +}; + +/// C: GhosttyRenderState +pub const RenderState = ?*RenderStateWrapper; + +/// C: GhosttyRenderStateRowIterator +pub const RowIterator = ?*RowIteratorWrapper; + +/// C: GhosttyRenderStateRowCells +pub const RowCells = ?*RowCellsWrapper; + +/// C: GhosttyRenderStateDirty +pub const Dirty = renderpkg.RenderState.Dirty; + +/// C: GhosttyRenderStateCursorVisualStyle +pub const CursorVisualStyle = enum(c_int) { + bar = 0, + block = 1, + underline = 2, + block_hollow = 3, + + pub fn fromCursorStyle(s: cursorpkg.Style) CursorVisualStyle { + return switch (s) { + .bar => .bar, + .block => .block, + .underline => .underline, + .block_hollow => .block_hollow, + }; + } +}; + +/// C: GhosttyRenderStateData +pub const Data = enum(c_int) { + invalid = 0, + cols = 1, + rows = 2, + dirty = 3, + row_iterator = 4, + color_background = 5, + color_foreground = 6, + color_cursor = 7, + color_cursor_has_value = 8, + color_palette = 9, + cursor_visual_style = 10, + cursor_visible = 11, + cursor_blinking = 12, + cursor_password_input = 13, + cursor_viewport_has_value = 14, + cursor_viewport_x = 15, + cursor_viewport_y = 16, + cursor_viewport_wide_tail = 17, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: Data) type { + return switch (self) { + .invalid => void, + .cols, .rows => size.CellCountInt, + .dirty => Dirty, + .row_iterator => RowIterator, + .color_background, .color_foreground, .color_cursor => colorpkg.RGB.C, + .color_cursor_has_value => bool, + .color_palette => colorpkg.PaletteC, + .cursor_visual_style => CursorVisualStyle, + .cursor_visible, .cursor_blinking, .cursor_password_input => bool, + .cursor_viewport_has_value, .cursor_viewport_wide_tail => bool, + .cursor_viewport_x, .cursor_viewport_y => size.CellCountInt, + }; + } +}; + +/// C: GhosttyRenderStateOption +pub const SetOption = enum(c_int) { + dirty = 0, + + /// Input type expected for setting the option. + pub fn InType(comptime self: SetOption) type { + return switch (self) { + .dirty => Dirty, + }; + } +}; + +/// C: GhosttyRenderStateColors +pub const Colors = extern struct { + size: usize = @sizeOf(Colors), + background: colorpkg.RGB.C, + foreground: colorpkg.RGB.C, + cursor: colorpkg.RGB.C, + cursor_has_value: bool, + palette: colorpkg.PaletteC, +}; + +pub fn new( + alloc_: ?*const CAllocator, + result: *RenderState, +) callconv(lib.calling_conv) Result { + result.* = new_(alloc_) catch |err| { + result.* = null; + return switch (err) { + error.OutOfMemory => .out_of_memory, + }; + }; + + return .success; +} + +fn new_(alloc_: ?*const CAllocator) error{OutOfMemory}!*RenderStateWrapper { + const alloc = lib.alloc.default(alloc_); + const ptr = alloc.create(RenderStateWrapper) catch + return error.OutOfMemory; + ptr.* = .{ .alloc = alloc }; + return ptr; +} + +pub fn free(state_: RenderState) callconv(lib.calling_conv) void { + const state = state_ orelse return; + const alloc = state.alloc; + state.state.deinit(alloc); + alloc.destroy(state); +} + +pub fn update( + state_: RenderState, + terminal_: terminal_c.Terminal, +) callconv(lib.calling_conv) Result { + const state = state_ orelse return .invalid_value; + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + + state.state.update(state.alloc, t) catch return .out_of_memory; + return .success; +} + +pub fn get( + state_: RenderState, + data: Data, + out: ?*anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Data, @intFromEnum(data)) catch { + log.warn("render_state_get invalid data value={d}", .{@intFromEnum(data)}); + return .invalid_value; + }; + } + + return switch (data) { + .invalid => .invalid_value, + inline else => |comptime_data| getTyped( + state_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn getTyped( + state_: RenderState, + comptime data: Data, + out: *data.OutType(), +) Result { + const state = state_ orelse return .invalid_value; + switch (data) { + .invalid => return .invalid_value, + .cols => out.* = state.state.cols, + .rows => out.* = state.state.rows, + .dirty => out.* = state.state.dirty, + .row_iterator => { + const it = out.* orelse return .invalid_value; + const row_data = state.state.row_data.slice(); + it.* = .{ + .alloc = it.alloc, + .y = null, + .raws = row_data.items(.raw), + .cells = row_data.items(.cells), + .dirty = row_data.items(.dirty), + .palette = &state.state.colors.palette, + }; + }, + .color_background => out.* = state.state.colors.background.cval(), + .color_foreground => out.* = state.state.colors.foreground.cval(), + .color_cursor => { + const cursor = state.state.colors.cursor orelse return .invalid_value; + out.* = cursor.cval(); + }, + .color_cursor_has_value => out.* = state.state.colors.cursor != null, + .color_palette => out.* = colorpkg.paletteCval(&state.state.colors.palette), + .cursor_visual_style => out.* = CursorVisualStyle.fromCursorStyle(state.state.cursor.visual_style), + .cursor_visible => out.* = state.state.cursor.visible, + .cursor_blinking => out.* = state.state.cursor.blinking, + .cursor_password_input => out.* = state.state.cursor.password_input, + .cursor_viewport_has_value => out.* = state.state.cursor.viewport != null, + .cursor_viewport_x => { + const vp = state.state.cursor.viewport orelse return .invalid_value; + out.* = vp.x; + }, + .cursor_viewport_y => { + const vp = state.state.cursor.viewport orelse return .invalid_value; + out.* = vp.y; + }, + .cursor_viewport_wide_tail => { + const vp = state.state.cursor.viewport orelse return .invalid_value; + out.* = vp.wide_tail; + }, + } + + return .success; +} + +pub fn set( + state_: RenderState, + option: SetOption, + value: ?*const anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(SetOption, @intFromEnum(option)) catch { + log.warn("render_state_set invalid option value={d}", .{@intFromEnum(option)}); + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| setTyped( + state_, + comptime_option, + @ptrCast(@alignCast(value orelse return .invalid_value)), + ), + }; +} + +fn setTyped( + state_: RenderState, + comptime option: SetOption, + value: *const option.InType(), +) Result { + const state = state_ orelse return .invalid_value; + switch (option) { + .dirty => state.state.dirty = value.*, + } + + return .success; +} + +pub fn colors_get( + state_: RenderState, + out_colors_: ?*Colors, +) callconv(lib.calling_conv) Result { + const state = state_ orelse return .invalid_value; + const out_colors = out_colors_ orelse return .invalid_value; + const out_size = out_colors.size; + if (out_size < @sizeOf(usize)) return .invalid_value; + + const colors = state.state.colors; + if (lib.structSizedFieldFits( + Colors, + out_size, + "background", + )) { + out_colors.background = colors.background.cval(); + } + + if (lib.structSizedFieldFits( + Colors, + out_size, + "foreground", + )) { + out_colors.foreground = colors.foreground.cval(); + } + + if (colors.cursor) |cursor| { + if (lib.structSizedFieldFits( + Colors, + out_size, + "cursor", + )) { + out_colors.cursor = cursor.cval(); + } + } + + if (lib.structSizedFieldFits( + Colors, + out_size, + "cursor_has_value", + )) { + out_colors.cursor_has_value = colors.cursor != null; + } + + { + const palette_offset = @offsetOf(Colors, "palette"); + if (out_size > palette_offset) { + const available = out_size - palette_offset; + const max_entries = @min(colors.palette.len, available / @sizeOf(colorpkg.RGB.C)); + for (0..max_entries) |i| { + out_colors.palette[i] = colors.palette[i].cval(); + } + } + } + + return .success; +} + +pub fn row_iterator_new( + alloc_: ?*const CAllocator, + result: *RowIterator, +) callconv(lib.calling_conv) Result { + const alloc = lib.alloc.default(alloc_); + const ptr = alloc.create(RowIteratorWrapper) catch { + result.* = null; + return .out_of_memory; + }; + ptr.* = .{ + .alloc = alloc, + .y = undefined, + .raws = undefined, + .cells = undefined, + .dirty = undefined, + .palette = undefined, + }; + result.* = ptr; + return .success; +} + +pub fn row_iterator_free(iterator_: RowIterator) callconv(lib.calling_conv) void { + const iterator = iterator_ orelse return; + const alloc = iterator.alloc; + alloc.destroy(iterator); +} + +pub fn row_iterator_next(iterator_: RowIterator) callconv(lib.calling_conv) bool { + const it = iterator_ orelse return false; + const next_y: size.CellCountInt = if (it.y) |y| y + 1 else 0; + if (next_y >= it.raws.len) return false; + it.y = next_y; + return true; +} + +pub fn row_cells_new( + alloc_: ?*const CAllocator, + result: *RowCells, +) callconv(lib.calling_conv) Result { + const alloc = lib.alloc.default(alloc_); + const ptr = alloc.create(RowCellsWrapper) catch { + result.* = null; + return .out_of_memory; + }; + ptr.* = .{ + .alloc = alloc, + .x = undefined, + .raws = undefined, + .graphemes = undefined, + .styles = undefined, + .palette = undefined, + }; + result.* = ptr; + return .success; +} + +pub fn row_cells_next(cells_: RowCells) callconv(lib.calling_conv) bool { + const cells = cells_ orelse return false; + const next_x: size.CellCountInt = if (cells.x) |x| x + 1 else 0; + if (next_x >= cells.raws.len) return false; + cells.x = next_x; + return true; +} + +pub fn row_cells_select(cells_: RowCells, x: size.CellCountInt) callconv(lib.calling_conv) Result { + const cells = cells_ orelse return .invalid_value; + if (x >= cells.raws.len) return .invalid_value; + cells.x = x; + return .success; +} + +pub fn row_cells_free(cells_: RowCells) callconv(lib.calling_conv) void { + const cells = cells_ orelse return; + const alloc = cells.alloc; + alloc.destroy(cells); +} + +/// C: GhosttyRenderStateRowCellsData +pub const RowCellsData = enum(c_int) { + invalid = 0, + raw = 1, + style = 2, + graphemes_len = 3, + graphemes_buf = 4, + bg_color = 5, + fg_color = 6, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: RowCellsData) type { + return switch (self) { + .invalid => void, + .raw => page.Cell.C, + .style => style_c.Style, + .graphemes_len => u32, + .graphemes_buf => u32, + .bg_color, .fg_color => colorpkg.RGB.C, + }; + } +}; + +pub fn row_cells_get( + cells_: RowCells, + data: RowCellsData, + out: ?*anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(RowCellsData, @intFromEnum(data)) catch { + log.warn("render_state_row_cells_get invalid data value={d}", .{@intFromEnum(data)}); + return .invalid_value; + }; + } + + return switch (data) { + .invalid => .invalid_value, + inline else => |comptime_data| rowCellsGetTyped( + cells_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn rowCellsGetTyped( + cells_: RowCells, + comptime data: RowCellsData, + out: *data.OutType(), +) Result { + const cells = cells_ orelse return .invalid_value; + const x = cells.x orelse return .invalid_value; + const cell = cells.raws[x]; + switch (data) { + .invalid => return .invalid_value, + .raw => out.* = cell.cval(), + .style => out.* = if (cell.hasStyling()) + style_c.Style.fromStyle(cells.styles[x]) + else + style_c.Style.fromStyle(.{}), + .graphemes_len => { + if (!cell.hasText()) { + out.* = 0; + return .success; + } + const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{}; + out.* = @intCast(1 + extra.len); + }, + .graphemes_buf => { + if (!cell.hasText()) return .success; + const extra = if (cell.hasGrapheme()) cells.graphemes[x] else &[_]u21{}; + const buf: [*]u32 = @ptrCast(out); + buf[0] = cell.codepoint(); + for (extra, 1..) |cp, i| { + buf[i] = cp; + } + }, + .bg_color => { + const s: Style = if (cell.hasStyling()) cells.styles[x] else .{}; + const bg = s.bg(&cell, cells.palette) orelse return .invalid_value; + out.* = bg.cval(); + }, + .fg_color => { + const s: Style = if (cell.hasStyling()) cells.styles[x] else .{}; + if (s.fg_color == .none) return .invalid_value; + const fg = s.fg(.{ .default = .{}, .palette = cells.palette }); + out.* = fg.cval(); + }, + } + + return .success; +} + +/// C: GhosttyRenderStateRowData +pub const RowData = enum(c_int) { + invalid = 0, + dirty = 1, + raw = 2, + cells = 3, + + /// Output type expected for querying the data of the given kind. + pub fn OutType(comptime self: RowData) type { + return switch (self) { + .invalid => void, + .dirty => bool, + .raw => row.CRow, + .cells => RowCells, + }; + } +}; + +/// C: GhosttyRenderStateRowOption +pub const RowOption = enum(c_int) { + dirty = 0, + + /// Input type expected for setting the option. + pub fn InType(comptime self: RowOption) type { + return switch (self) { + .dirty => bool, + }; + } +}; + +pub fn row_get( + iterator_: RowIterator, + data: RowData, + out: ?*anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(RowData, @intFromEnum(data)) catch { + log.warn("render_state_row_get invalid data value={d}", .{@intFromEnum(data)}); + return .invalid_value; + }; + } + + return switch (data) { + .invalid => .invalid_value, + inline else => |comptime_data| rowGetTyped( + iterator_, + comptime_data, + @ptrCast(@alignCast(out)), + ), + }; +} + +fn rowGetTyped( + iterator_: RowIterator, + comptime data: RowData, + out: *data.OutType(), +) Result { + const it = iterator_ orelse return .invalid_value; + const y = it.y orelse return .invalid_value; + switch (data) { + .invalid => return .invalid_value, + .dirty => out.* = it.dirty[y], + .raw => out.* = it.raws[y].cval(), + .cells => { + const cells = out.* orelse return .invalid_value; + const cell_data = it.cells[y].slice(); + cells.* = .{ + .alloc = cells.alloc, + .x = null, + .raws = cell_data.items(.raw), + .graphemes = cell_data.items(.grapheme), + .styles = cell_data.items(.style), + .palette = it.palette, + }; + }, + } + + return .success; +} + +pub fn row_set( + iterator_: RowIterator, + option: RowOption, + value: ?*const anyopaque, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(RowOption, @intFromEnum(option)) catch { + log.warn("render_state_row_set invalid option value={d}", .{@intFromEnum(option)}); + return .invalid_value; + }; + } + + return switch (option) { + inline else => |comptime_option| rowSetTyped( + iterator_, + comptime_option, + @ptrCast(@alignCast(value orelse return .invalid_value)), + ), + }; +} + +fn rowSetTyped( + iterator_: RowIterator, + comptime option: RowOption, + value: *const option.InType(), +) Result { + const it = iterator_ orelse return .invalid_value; + const y = it.y orelse return .invalid_value; + switch (option) { + .dirty => it.dirty[y] = value.*, + } + + return .success; +} + +test "render: new/free" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + try testing.expect(state != null); + free(state); +} + +test "render: free null" { + free(null); +} + +test "render: update invalid value" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + try testing.expectEqual(Result.invalid_value, update(null, null)); + try testing.expectEqual(Result.invalid_value, update(state, null)); +} + +test "render: get invalid value" { + var cols: size.CellCountInt = 0; + try testing.expectEqual(Result.invalid_value, get(null, .cols, @ptrCast(&cols))); +} + +test "render: get invalid data" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + try testing.expectEqual(Result.invalid_value, get(state, .invalid, null)); +} + +test "render: colors get invalid value" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + var colors: Colors = std.mem.zeroes(Colors); + colors.size = @sizeOf(Colors); + + try testing.expectEqual(Result.invalid_value, colors_get(null, &colors)); + try testing.expectEqual(Result.invalid_value, colors_get(state, null)); + + colors.size = @sizeOf(usize) - 1; + try testing.expectEqual(Result.invalid_value, colors_get(state, &colors)); +} + +test "render: get/set dirty invalid value" { + var dirty: Dirty = .false; + try testing.expectEqual(Result.invalid_value, get(null, .dirty, @ptrCast(&dirty))); + const dirty_full: Dirty = .full; + try testing.expectEqual(Result.invalid_value, set(null, .dirty, @ptrCast(&dirty_full))); +} + +test "render: get/set dirty" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + var dirty: Dirty = undefined; + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); + try testing.expectEqual(Dirty.false, dirty); + + const dirty_partial: Dirty = .partial; + try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_partial))); + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); + try testing.expectEqual(Dirty.partial, dirty); + + const dirty_full: Dirty = .full; + try testing.expectEqual(Result.success, set(state, .dirty, @ptrCast(&dirty_full))); + try testing.expectEqual(Result.success, get(state, .dirty, @ptrCast(&dirty))); + try testing.expectEqual(Dirty.full, dirty); +} + +test "render: set null value" { + var state: RenderState = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &state, + )); + defer free(state); + + try testing.expectEqual(Result.invalid_value, set(state, .dirty, null)); +} + +test "render: row iterator get invalid value" { + var iterator: RowIterator = null; + try testing.expectEqual(Result.success, row_iterator_new( + &lib.alloc.test_allocator, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expectEqual(Result.invalid_value, get(null, .row_iterator, @ptrCast(&iterator))); +} + +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, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expect(iterator != null); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator))); + + const iterator_ptr = iterator.?; + const row_data = state.?.state.row_data.slice(); + + 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(.dirty).len, iterator_ptr.dirty.len); +} + +test "render: row iterator free null" { + row_iterator_free(null); +} + +test "render: row iterator next null" { + try testing.expect(!row_iterator_next(null)); +} + +test "render: row get null" { + var dirty: bool = undefined; + try testing.expectEqual(Result.invalid_value, row_get(null, .dirty, @ptrCast(&dirty))); +} + +test "render: row get invalid data" { + 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, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator))); + try testing.expect(row_iterator_next(iterator)); + try testing.expectEqual(Result.invalid_value, row_get(iterator, .invalid, null)); +} + +test "render: row set null" { + const dirty = false; + try testing.expectEqual(Result.invalid_value, row_set(null, .dirty, @ptrCast(&dirty))); +} + +test "render: row set before iteration" { + 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, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator))); + const dirty = false; + try testing.expectEqual(Result.invalid_value, row_set(iterator, .dirty, @ptrCast(&dirty))); +} + +test "render: row get before iteration" { + 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, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator))); + var dirty: bool = undefined; + try testing.expectEqual(Result.invalid_value, row_get(iterator, .dirty, @ptrCast(&dirty))); +} + +test "render: row get/set dirty" { + 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)); + + // Dirty the first row so the iterator has at least one dirty row to observe. + terminal_c.vt_write(terminal, "hello", 5); + try testing.expectEqual(Result.success, update(state, terminal)); + + // Create an iterator and verify it is dirty. + 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))); + try testing.expect(row_iterator_next(it)); + var dirty: bool = undefined; + try testing.expectEqual(Result.success, row_get(it, .dirty, @ptrCast(&dirty))); + try testing.expect(dirty); + + // Clear dirty on this row. + const dirty_false = false; + try testing.expectEqual(Result.success, row_set(it, .dirty, @ptrCast(&dirty_false))); + + // It should not be dirty anymore. + var it2: RowIterator = null; + try testing.expectEqual(Result.success, row_iterator_new( + &lib.alloc.test_allocator, + &it2, + )); + defer row_iterator_free(it2); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it2))); + try testing.expect(row_iterator_next(it2)); + try testing.expectEqual(Result.success, row_get(it2, .dirty, @ptrCast(&dirty))); + try testing.expect(!dirty); +} + +test "render: row iterator next" { + 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, + &iterator, + )); + defer row_iterator_free(iterator); + + try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&iterator))); + + const rows = state.?.state.rows; + if (rows == 0) { + try testing.expect(!row_iterator_next(iterator)); + return; + } + + try testing.expect(row_iterator_next(iterator)); + try testing.expectEqual(@as(?size.CellCountInt, 0), iterator.?.y); + + var i: size.CellCountInt = 1; + while (i < rows) : (i += 1) { + try testing.expect(row_iterator_next(iterator)); + try testing.expectEqual(@as(?size.CellCountInt, i), iterator.?.y); + } + + try testing.expect(!row_iterator_next(iterator)); + try testing.expectEqual(@as(?size.CellCountInt, rows - 1), iterator.?.y); +} + +test "render: update" { + 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)); + + terminal_c.vt_write(terminal, "hello", 5); + try testing.expectEqual(Result.success, update(state, terminal)); + + var cols: size.CellCountInt = 0; + var rows_val: size.CellCountInt = 0; + try testing.expectEqual(Result.success, get(state, .cols, @ptrCast(&cols))); + try testing.expectEqual(Result.success, get(state, .rows, @ptrCast(&rows_val))); + try testing.expectEqual(@as(size.CellCountInt, 80), cols); + try testing.expectEqual(@as(size.CellCountInt, 24), rows_val); +} + +test "render: colors get" { + 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 colors: Colors = std.mem.zeroes(Colors); + colors.size = @sizeOf(Colors); + try testing.expectEqual(Result.success, colors_get(state, &colors)); + + const state_colors = &state.?.state.colors; + try testing.expectEqual(state_colors.background.cval(), colors.background); + try testing.expectEqual(state_colors.foreground.cval(), colors.foreground); + + if (state_colors.cursor) |cursor| { + try testing.expect(colors.cursor_has_value); + try testing.expectEqual(cursor.cval(), colors.cursor); + } else { + try testing.expect(!colors.cursor_has_value); + } + + for (state_colors.palette, colors.palette) |expected, actual| { + try testing.expectEqual(expected.cval(), actual); + } +} + +test "render: row cells bg_color no background" { + 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); + + // Write plain text (no background color set). + terminal_c.vt_write(terminal, "hello", 5); + + 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))); + try testing.expect(row_iterator_next(it)); + + var cells: RowCells = null; + try testing.expectEqual(Result.success, row_cells_new( + &lib.alloc.test_allocator, + &cells, + )); + defer row_cells_free(cells); + + try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells))); + try testing.expect(row_cells_next(cells)); + + // No background set, should return invalid_value. + var bg: colorpkg.RGB.C = undefined; + try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .bg_color, @ptrCast(&bg))); +} + +test "render: row cells bg_color from style" { + 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); + + // Set an RGB background via SGR 48;2;R;G;B and write text. + terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30mA", 18); + + 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))); + try testing.expect(row_iterator_next(it)); + + var cells: RowCells = null; + try testing.expectEqual(Result.success, row_cells_new( + &lib.alloc.test_allocator, + &cells, + )); + defer row_cells_free(cells); + + try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells))); + try testing.expect(row_cells_next(cells)); + + var bg: colorpkg.RGB.C = undefined; + try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg))); + try testing.expectEqual(@as(u8, 10), bg.r); + try testing.expectEqual(@as(u8, 20), bg.g); + try testing.expectEqual(@as(u8, 30), bg.b); +} + +test "render: row cells bg_color from content tag" { + 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); + + // Set an RGB background and then erase the line. The erased cells + // should carry the background color via the content tag (bg_color_rgb) + // rather than through the style. + terminal_c.vt_write(terminal, "\x1b[48;2;10;20;30m\x1b[2K", 21); + + 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))); + try testing.expect(row_iterator_next(it)); + + var cells: RowCells = null; + try testing.expectEqual(Result.success, row_cells_new( + &lib.alloc.test_allocator, + &cells, + )); + defer row_cells_free(cells); + + try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells))); + try testing.expect(row_cells_next(cells)); + + var bg: colorpkg.RGB.C = undefined; + try testing.expectEqual(Result.success, row_cells_get(cells, .bg_color, @ptrCast(&bg))); + try testing.expectEqual(@as(u8, 10), bg.r); + try testing.expectEqual(@as(u8, 20), bg.g); + try testing.expectEqual(@as(u8, 30), bg.b); +} + +test "render: row cells fg_color no foreground" { + 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); + + // Write plain text (no foreground color set). + terminal_c.vt_write(terminal, "hello", 5); + + 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))); + try testing.expect(row_iterator_next(it)); + + var cells: RowCells = null; + try testing.expectEqual(Result.success, row_cells_new( + &lib.alloc.test_allocator, + &cells, + )); + defer row_cells_free(cells); + + try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells))); + try testing.expect(row_cells_next(cells)); + + // No foreground set, should return invalid_value. + var fg: colorpkg.RGB.C = undefined; + try testing.expectEqual(Result.invalid_value, row_cells_get(cells, .fg_color, @ptrCast(&fg))); +} + +test "render: row cells fg_color from style" { + 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); + + // Set an RGB foreground via SGR 38;2;R;G;B and write text. + terminal_c.vt_write(terminal, "\x1b[38;2;10;20;30mA", 18); + + 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))); + try testing.expect(row_iterator_next(it)); + + var cells: RowCells = null; + try testing.expectEqual(Result.success, row_cells_new( + &lib.alloc.test_allocator, + &cells, + )); + defer row_cells_free(cells); + + try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells))); + try testing.expect(row_cells_next(cells)); + + var fg: colorpkg.RGB.C = undefined; + try testing.expectEqual(Result.success, row_cells_get(cells, .fg_color, @ptrCast(&fg))); + try testing.expectEqual(@as(u8, 10), fg.r); + try testing.expectEqual(@as(u8, 20), fg.g); + try testing.expectEqual(@as(u8, 30), fg.b); +} + +test "render: colors get supports truncated sized struct" { + 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 colors: Colors = std.mem.zeroes(Colors); + const sentinel: colorpkg.RGB.C = .{ .r = 0xAA, .g = 0xBB, .b = 0xCC }; + for (&colors.palette) |*entry| entry.* = sentinel; + + colors.size = @offsetOf(Colors, "palette") + @sizeOf(colorpkg.RGB.C) * 2; + try testing.expectEqual(Result.success, colors_get(state, &colors)); + + const state_colors = &state.?.state.colors; + try testing.expectEqual(state_colors.palette[0].cval(), colors.palette[0]); + try testing.expectEqual(state_colors.palette[1].cval(), colors.palette[1]); + try testing.expectEqual(sentinel, colors.palette[2]); +} |
