- commit
- 0ca4141
- parent
- 6f881c7
- author
- Eric Bower
- date
- 2025-10-15 14:56:51 -0400 EDT
feat: rendering colors on reattach
2 files changed,
+56,
-39
+36,
-36
1@@ -47,9 +47,9 @@ pub fn emitStyleChange(
2 if (new_style.flags.faint != old_style.flags.faint) {
3 try addSep(&buf, allocator, &first);
4 if (new_style.flags.faint) {
5- try buf.append('2');
6+ try buf.append(allocator, '2');
7 } else {
8- try buf.appendSlice("22");
9+ try buf.appendSlice(allocator, "22");
10 }
11 }
12
13@@ -57,9 +57,9 @@ pub fn emitStyleChange(
14 if (new_style.flags.italic != old_style.flags.italic) {
15 try addSep(&buf, allocator, &first);
16 if (new_style.flags.italic) {
17- try buf.append('3');
18+ try buf.append(allocator, '3');
19 } else {
20- try buf.appendSlice("23");
21+ try buf.appendSlice(allocator, "23");
22 }
23 }
24
25@@ -67,12 +67,12 @@ pub fn emitStyleChange(
26 if (!std.meta.eql(new_style.flags.underline, old_style.flags.underline)) {
27 try addSep(&buf, allocator, &first);
28 switch (new_style.flags.underline) {
29- .none => try buf.appendSlice("24"),
30- .single => try buf.append('4'),
31- .double => try buf.appendSlice("21"),
32- .curly => try buf.appendSlice("4:3"),
33- .dotted => try buf.appendSlice("4:4"),
34- .dashed => try buf.appendSlice("4:5"),
35+ .none => try buf.appendSlice(allocator, "24"),
36+ .single => try buf.append(allocator, '4'),
37+ .double => try buf.appendSlice(allocator, "21"),
38+ .curly => try buf.appendSlice(allocator, "4:3"),
39+ .dotted => try buf.appendSlice(allocator, "4:4"),
40+ .dashed => try buf.appendSlice(allocator, "4:5"),
41 }
42 }
43
44@@ -80,9 +80,9 @@ pub fn emitStyleChange(
45 if (new_style.flags.blink != old_style.flags.blink) {
46 try addSep(&buf, allocator, &first);
47 if (new_style.flags.blink) {
48- try buf.append('5');
49+ try buf.append(allocator, '5');
50 } else {
51- try buf.appendSlice("25");
52+ try buf.appendSlice(allocator, "25");
53 }
54 }
55
56@@ -90,9 +90,9 @@ pub fn emitStyleChange(
57 if (new_style.flags.inverse != old_style.flags.inverse) {
58 try addSep(&buf, allocator, &first);
59 if (new_style.flags.inverse) {
60- try buf.append('7');
61+ try buf.append(allocator, '7');
62 } else {
63- try buf.appendSlice("27");
64+ try buf.appendSlice(allocator, "27");
65 }
66 }
67
68@@ -100,9 +100,9 @@ pub fn emitStyleChange(
69 if (new_style.flags.invisible != old_style.flags.invisible) {
70 try addSep(&buf, allocator, &first);
71 if (new_style.flags.invisible) {
72- try buf.append('8');
73+ try buf.append(allocator, '8');
74 } else {
75- try buf.appendSlice("28");
76+ try buf.appendSlice(allocator, "28");
77 }
78 }
79
80@@ -110,9 +110,9 @@ pub fn emitStyleChange(
81 if (new_style.flags.strikethrough != old_style.flags.strikethrough) {
82 try addSep(&buf, allocator, &first);
83 if (new_style.flags.strikethrough) {
84- try buf.append('9');
85+ try buf.append(allocator, '9');
86 } else {
87- try buf.appendSlice("29");
88+ try buf.appendSlice(allocator, "29");
89 }
90 }
91
92@@ -120,14 +120,14 @@ pub fn emitStyleChange(
93 if (!std.meta.eql(new_style.fg_color, old_style.fg_color)) {
94 try addSep(&buf, allocator, &first);
95 switch (new_style.fg_color) {
96- .none => try buf.appendSlice("39"),
97+ .none => try buf.appendSlice(allocator, "39"),
98 .palette => |idx| {
99- try buf.appendSlice("38;5;");
100- try buf.writer().print("{d}", .{idx});
101+ try buf.appendSlice(allocator, "38;5;");
102+ try std.fmt.format(buf.writer(allocator), "{d}", .{idx});
103 },
104 .rgb => |rgb| {
105- try buf.appendSlice("38;2;");
106- try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
107+ try buf.appendSlice(allocator, "38;2;");
108+ try std.fmt.format(buf.writer(allocator), "{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
109 },
110 }
111 }
112@@ -136,14 +136,14 @@ pub fn emitStyleChange(
113 if (!std.meta.eql(new_style.bg_color, old_style.bg_color)) {
114 try addSep(&buf, allocator, &first);
115 switch (new_style.bg_color) {
116- .none => try buf.appendSlice("49"),
117+ .none => try buf.appendSlice(allocator, "49"),
118 .palette => |idx| {
119- try buf.appendSlice("48;5;");
120- try buf.writer().print("{d}", .{idx});
121+ try buf.appendSlice(allocator, "48;5;");
122+ try std.fmt.format(buf.writer(allocator), "{d}", .{idx});
123 },
124 .rgb => |rgb| {
125- try buf.appendSlice("48;2;");
126- try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
127+ try buf.appendSlice(allocator, "48;2;");
128+ try std.fmt.format(buf.writer(allocator), "{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
129 },
130 }
131 }
132@@ -152,29 +152,29 @@ pub fn emitStyleChange(
133 if (!std.meta.eql(new_style.underline_color, old_style.underline_color)) {
134 try addSep(&buf, allocator, &first);
135 switch (new_style.underline_color) {
136- .none => try buf.appendSlice("59"),
137+ .none => try buf.appendSlice(allocator, "59"),
138 .palette => |idx| {
139- try buf.appendSlice("58;5;");
140- try buf.writer().print("{d}", .{idx});
141+ try buf.appendSlice(allocator, "58;5;");
142+ try std.fmt.format(buf.writer(allocator), "{d}", .{idx});
143 },
144 .rgb => |rgb| {
145- try buf.appendSlice("58;2;");
146- try buf.writer().print("{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
147+ try buf.appendSlice(allocator, "58;2;");
148+ try std.fmt.format(buf.writer(allocator), "{d};{d};{d}", .{ rgb.r, rgb.g, rgb.b });
149 },
150 }
151 }
152
153 // End escape sequence
154- try buf.append('m');
155+ try buf.append(allocator, 'm');
156
157 // If we only added the escape opener and closer with nothing in between,
158 // return empty string (no change needed)
159 if (first) {
160- buf.deinit();
161+ buf.deinit(allocator);
162 return allocator.dupe(u8, "");
163 }
164
165- return buf.toOwnedSlice();
166+ return buf.toOwnedSlice(allocator);
167 }
168
169 test "emitStyleChange: default to default" {
+20,
-3
1@@ -1,5 +1,6 @@
2 const std = @import("std");
3 const ghostty = @import("ghostty-vt");
4+const sgr = @import("sgr.zig");
5
6 /// Extract UTF-8 text content from a cell, including multi-codepoint graphemes
7 fn extractCellText(pin: ghostty.Pin, cell: *const ghostty.Cell, buf: *std.ArrayList(u8), allocator: std.mem.Allocator) !void {
8@@ -62,6 +63,9 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
9 const page = &pin.node.data;
10 const cells = page.getCells(row);
11
12+ // Track style changes to emit SGR sequences
13+ var last_style = ghostty.Style{}; // Start with default style
14+
15 // Extract text from each cell in the row
16 var col_idx: usize = 0;
17 while (col_idx < cells.len) : (col_idx += 1) {
18@@ -77,6 +81,17 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
19 .x = @intCast(col_idx),
20 };
21
22+ // Get the style for this cell
23+ const cell_style = cell_pin.style(cell);
24+
25+ // If style changed, emit SGR sequence
26+ if (!cell_style.eql(last_style)) {
27+ const sgr_seq = try sgr.emitStyleChange(allocator, last_style, cell_style);
28+ defer allocator.free(sgr_seq);
29+ try output.appendSlice(allocator, sgr_seq);
30+ last_style = cell_style;
31+ }
32+
33 try extractCellText(cell_pin, cell, &output, allocator);
34
35 // If this is a wide character, skip the next cell (spacer_tail)
36@@ -84,6 +99,11 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
37 col_idx += 1; // Skip the spacer cell that follows
38 }
39 }
40+
41+ // Reset style at end of row to avoid style bleeding
42+ if (!last_style.default()) {
43+ try output.appendSlice(allocator, "\x1b[0m");
44+ }
45 }
46
47 // Restore cursor position from terminal state
48@@ -117,13 +137,10 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
49 // If we found content, position cursor after the last character
50 if (last_col > 0) {
51 cursor_col = @intCast(last_col + 2); // +1 for after character, +1 for 1-based
52- std.debug.print("Adjusted cursor from x=0 to col={d} (last content at col={d})\n", .{ cursor_col, last_col + 1 });
53 }
54 }
55 }
56
57- std.debug.print("Restoring cursor to row={d} col={d} (original: y={d} x={d})\n", .{ cursor_row, cursor_col, cursor.y, cursor.x });
58-
59 try std.fmt.format(output.writer(allocator), "\x1b[{d};{d}H", .{ cursor_row, cursor_col });
60
61 // Show cursor