1--- a/src/terminal/c/render.zig
2+++ b/src/terminal/c/render.zig
3@@ -59,6 +59,12 @@
4
5 /// C: GhosttyRenderStateRowCells
6 pub const RowCells = ?*RowCellsWrapper;
7+
8+/// C: GhosttyRenderStateAsciiCell
9+pub const AsciiCell = extern struct {
10+ codepoint: u32,
11+ foreground: u32,
12+};
13
14 /// C: GhosttyRenderStateDirty
15 pub const Dirty = renderpkg.RenderState.Dirty;
16@@ -475,6 +481,67 @@
17 alloc.destroy(cells);
18 }
19
20+/// Export one row when every retained cell is narrow ASCII with only an
21+/// optional foreground color. Unsupported rows use the canonical cell API.
22+pub fn row_cells_ascii(
23+ cells_: RowCells,
24+ out_: ?[*]AsciiCell,
25+ capacity: usize,
26+ out_len_: ?*usize,
27+) callconv(lib.calling_conv) Result {
28+ const cells = cells_ orelse return .invalid_value;
29+ const out = out_ orelse return .invalid_value;
30+ const out_len = out_len_ orelse return .invalid_value;
31+ out_len.* = 0;
32+
33+ var end = cells.raws.len;
34+ while (end > 0 and @as(u64, @bitCast(cells.raws[end - 1])) == 0) end -= 1;
35+ if (capacity < end) {
36+ out_len.* = end;
37+ return .out_of_space;
38+ }
39+
40+ for (cells.raws[0..end], cells.styles[0..end], 0..) |cell, style_value, index| {
41+ if (@as(u64, @bitCast(cell)) == 0) {
42+ out[index] = .{ .codepoint = 0, .foreground = std.math.maxInt(u32) };
43+ continue;
44+ }
45+ if (cell.content_tag != .codepoint or
46+ cell.wide != .narrow or
47+ cell.protected or
48+ cell.semantic_content != .output or
49+ cell.hyperlink)
50+ {
51+ return .no_value;
52+ }
53+
54+ const codepoint = cell.codepoint();
55+ if (codepoint < 0x20 or codepoint > 0x7E) return .no_value;
56+ const s: Style = if (cell.hasStyling()) style_value else .{};
57+ if (!s.bg_color.eql(.none) or
58+ !s.underline_color.eql(.none) or
59+ s.flags != @TypeOf(s.flags){})
60+ {
61+ return .no_value;
62+ }
63+ const foreground: u32 = switch (s.fg_color) {
64+ .none => std.math.maxInt(u32),
65+ .palette => |palette_index| rgb: {
66+ const color = cells.palette[palette_index];
67+ break :rgb (@as(u32, color.r) << 16) |
68+ (@as(u32, color.g) << 8) |
69+ color.b;
70+ },
71+ .rgb => |color| (@as(u32, color.r) << 16) |
72+ (@as(u32, color.g) << 8) |
73+ color.b,
74+ };
75+ out[index] = .{ .codepoint = codepoint, .foreground = foreground };
76+ }
77+ out_len.* = end;
78+ return .success;
79+}
80+
81 /// C: GhosttyRenderStateRowCellsData
82 pub const RowCellsData = enum(c_int) {
83 invalid = 0,
84--- a/src/terminal/c/types.zig
85+++ b/src/terminal/c/types.zig
86@@ -60,6 +60,7 @@
87 .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
88 .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
89 .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
90+ .{ "GhosttyRenderStateAsciiCell", StructInfo.init(render.AsciiCell) },
91 .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
92 .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) },
93 .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) },
94--- a/src/terminal/c/main.zig
95+++ b/src/terminal/c/main.zig
96@@ -100,6 +100,7 @@
97 pub const render_state_row_cells_select = render.row_cells_select;
98 pub const render_state_row_cells_get = render.row_cells_get;
99 pub const render_state_row_cells_get_multi = render.row_cells_get_multi;
100+pub const render_state_row_cells_ascii = render.row_cells_ascii;
101 pub const render_state_row_cells_free = render.row_cells_free;
102
103 pub const sgr_new = sgr.new;
104--- a/src/lib_vt.zig
105+++ b/src/lib_vt.zig
106@@ -258,6 +258,7 @@
107 @export(&c.render_state_row_cells_select, .{ .name = "ghostty_render_state_row_cells_select" });
108 @export(&c.render_state_row_cells_get, .{ .name = "ghostty_render_state_row_cells_get" });
109 @export(&c.render_state_row_cells_get_multi, .{ .name = "ghostty_render_state_row_cells_get_multi" });
110+ @export(&c.render_state_row_cells_ascii, .{ .name = "ghostty_render_state_row_cells_ascii" });
111 @export(&c.render_state_row_cells_free, .{ .name = "ghostty_render_state_row_cells_free" });
112 @export(&c.render_state_free, .{ .name = "ghostty_render_state_free" });
113 @export(&c.terminal_new, .{ .name = "ghostty_terminal_new" });
114--- a/include/ghostty/vt/render.h
115+++ b/include/ghostty/vt/render.h
116@@ -272,6 +272,12 @@
117 /** End column of the row-local selection range, inclusive. */
118 uint16_t end_x;
119 } GhosttyRenderStateRowSelection;
120+
121+/** Narrow ASCII cell returned by the optional whole-row render fast path. */
122+typedef struct {
123+ uint32_t codepoint;
124+ uint32_t foreground;
125+} GhosttyRenderStateAsciiCell;
126
127 /**
128 * Render-state color information.
129@@ -776,6 +782,16 @@
130 const GhosttyRenderStateRowCellsData* keys,
131 void** values,
132 size_t* out_written);
133+
134+/**
135+ * Export the row when every retained cell is narrow printable ASCII with only
136+ * an optional foreground color. GHOSTTY_NO_VALUE selects the canonical API.
137+ */
138+GHOSTTY_API GhosttyResult ghostty_render_state_row_cells_ascii(
139+ GhosttyRenderStateRowCells cells,
140+ GhosttyRenderStateAsciiCell* out,
141+ size_t capacity,
142+ size_t* out_len);
143
144 /**
145 * Free a row cells instance.