repos / zmx

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

commit
6f881c7
parent
7bb755b
author
Eric Bower
date
2025-10-15 14:51:21 -0400 EDT
chore: cursor position and escape sequences
1 files changed,  +56, -6
M src/terminal_snapshot.zig
+56, -6
 1@@ -27,12 +27,19 @@ fn extractCellText(pin: ghostty.Pin, cell: *const ghostty.Cell, buf: *std.ArrayL
 2     }
 3 }
 4 
 5-/// Render the current terminal viewport state as text (with escape sequences to come)
 6+/// Render the current terminal viewport state as text with proper escape sequences
 7 /// Returns owned slice that must be freed by caller
 8 pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
 9     var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
10     errdefer output.deinit(allocator);
11 
12+    // Prepare terminal: hide cursor, reset scroll region, reset SGR, clear screen, home cursor
13+    try output.appendSlice(allocator, "\x1b[?25l"); // Hide cursor
14+    try output.appendSlice(allocator, "\x1b[r"); // Reset scroll region
15+    try output.appendSlice(allocator, "\x1b[0m"); // Reset SGR (colors/styles)
16+    try output.appendSlice(allocator, "\x1b[2J"); // Clear entire screen
17+    try output.appendSlice(allocator, "\x1b[H"); // Home cursor (1,1)
18+
19     // Get the terminal's page list
20     const pages = &vt.screen.pages;
21 
22@@ -42,6 +49,13 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
23     // Iterate through viewport rows
24     var row_idx: usize = 0;
25     while (row_it.next()) |pin| : (row_idx += 1) {
26+        // Position cursor at the start of this row (1-based indexing)
27+        const row_num = row_idx + 1;
28+        try std.fmt.format(output.writer(allocator), "\x1b[{d};1H", .{row_num});
29+
30+        // Clear the entire line to avoid stale content
31+        try output.appendSlice(allocator, "\x1b[2K");
32+
33         // Get row and cell data from pin
34         const rac = pin.rowAndCell();
35         const row = rac.row;
36@@ -70,14 +84,50 @@ 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+
42+    // Restore cursor position from terminal state
43+    const cursor = vt.screen.cursor;
44+    const cursor_row = cursor.y + 1; // Convert to 1-based
45+    var cursor_col: u16 = @intCast(cursor.x + 1); // Convert to 1-based
46+
47+    // If cursor is at x=0, try to find the actual end of content on that row
48+    // This handles race conditions where the cursor position wasn't updated yet
49+    if (cursor.x == 0) {
50+        const cursor_pin = pages.pin(.{ .active = .{ .x = 0, .y = cursor.y } });
51+        if (cursor_pin) |cpin| {
52+            const crac = cpin.rowAndCell();
53+            const crow = crac.row;
54+            const cpage = &cpin.node.data;
55+            const ccells = cpage.getCells(crow);
56+
57+            // Find the last non-empty cell (including spaces)
58+            var last_col: usize = 0;
59+            var col: usize = 0;
60+            while (col < ccells.len) : (col += 1) {
61+                const cell = &ccells[col];
62+                if (cell.wide == .spacer_tail or cell.wide == .spacer_head) continue;
63+                const cp = cell.codepoint();
64+                if (cp != 0) { // Include spaces, just not null
65+                    last_col = col;
66+                }
67+                if (cell.wide == .wide) col += 1;
68+            }
69 
70-        // Add newline after each row
71-        try output.appendSlice(allocator, "\n");
72+            // If we found content, position cursor after the last character
73+            if (last_col > 0) {
74+                cursor_col = @intCast(last_col + 2); // +1 for after character, +1 for 1-based
75+                std.debug.print("Adjusted cursor from x=0 to col={d} (last content at col={d})\n", .{ cursor_col, last_col + 1 });
76+            }
77+        }
78     }
79 
80-    // TODO: Properly restore scrollback with colors and cursor position
81-    // For now, just return the text content
82-    // try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear screen and home cursor
83+    std.debug.print("Restoring cursor to row={d} col={d} (original: y={d} x={d})\n", .{ cursor_row, cursor_col, cursor.y, cursor.x });
84+
85+    try std.fmt.format(output.writer(allocator), "\x1b[{d};{d}H", .{ cursor_row, cursor_col });
86+
87+    // Show cursor
88+    try output.appendSlice(allocator, "\x1b[?25h");
89 
90     return output.toOwnedSlice(allocator);
91 }