repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
7bb755b
parent
280e122
author
Eric Bower
date
2025-10-15 14:41:04 -0400 EDT
chore: Wide/Double-Width Character Handling
1 files changed,  +34, -1
M src/terminal_snapshot.zig
+34, -1
 1@@ -49,7 +49,13 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
 2         const cells = page.getCells(row);
 3 
 4         // Extract text from each cell in the row
 5-        for (cells, 0..) |*cell, col_idx| {
 6+        var col_idx: usize = 0;
 7+        while (col_idx < cells.len) : (col_idx += 1) {
 8+            const cell = &cells[col_idx];
 9+
10+            // Skip spacer cells (already handled by extractCellText, but we still need to skip the iteration)
11+            if (cell.wide == .spacer_tail or cell.wide == .spacer_head) continue;
12+
13             // Create a pin for this specific cell to access graphemes
14             const cell_pin = ghostty.Pin{
15                 .node = pin.node,
16@@ -58,6 +64,11 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
17             };
18 
19             try extractCellText(cell_pin, cell, &output, allocator);
20+
21+            // If this is a wide character, skip the next cell (spacer_tail)
22+            if (cell.wide == .wide) {
23+                col_idx += 1; // Skip the spacer cell that follows
24+            }
25         }
26 
27         // Add newline after each row
28@@ -160,3 +171,25 @@ test "extractCellText: multi-codepoint grapheme (emoji)" {
29     // Should have both codepoints encoded as UTF-8
30     try testing.expect(buf.items.len > 4); // At least 2 multi-byte UTF-8 sequences
31 }
32+
33+test "wide character handling: skip spacer cells" {
34+    const testing = std.testing;
35+    const allocator = testing.allocator;
36+
37+    var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
38+    defer vt.deinit(allocator);
39+
40+    // Write wide character (emoji) followed by ASCII
41+    try vt.print(0x1F44B); // 👋 (wide, takes 2 cells)
42+    try vt.print('A');
43+    try vt.print('B');
44+
45+    // Render the terminal
46+    const result = try render(&vt, allocator);
47+    defer allocator.free(result);
48+
49+    // Should have emoji + AB + newline (not emoji + A + B with drift)
50+    // The emoji is UTF-8 encoded, so we just check we have content
51+    try testing.expect(result.len > 0);
52+    try testing.expect(std.mem.indexOf(u8, result, "AB") != null);
53+}