repos / zmx

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

commit
cfe70f6
parent
2b8ec0f
author
Eric Bower
date
2025-12-19 20:10:43 -0500 EST
fix: correct cursor position when reattaching to session

Serialize terminal state BEFORE resizing the terminal in handleInit.
Previously, the resize was done first, which caused cursor reflow and
moved the cursor position. The shell's SIGWINCH-triggered prompt redraw
would then run after our snapshot was sent, leaving the cursor at the
wrong position (end of line instead of after the prompt).

By capturing the terminal state before resize, we preserve the correct
cursor position from when the user detached.
1 files changed,  +17, -11
M src/main.zig
+17, -11
 1@@ -175,18 +175,13 @@ const Daemon = struct {
 2         if (payload.len != @sizeOf(ipc.Resize)) return;
 3 
 4         const resize = std.mem.bytesToValue(ipc.Resize, payload);
 5-        var ws: c.struct_winsize = .{
 6-            .ws_row = resize.rows,
 7-            .ws_col = resize.cols,
 8-            .ws_xpixel = 0,
 9-            .ws_ypixel = 0,
10-        };
11-        _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
12-        try term.resize(self.alloc, resize.cols, resize.rows);
13-
14-        std.log.debug("init resize rows={d} cols={d}", .{ resize.rows, resize.cols });
15 
16+        // Serialize terminal state BEFORE resize to capture correct cursor position.
17+        // Resizing triggers reflow which can move the cursor, and the shell's
18+        // SIGWINCH-triggered redraw will run after our snapshot is sent.
19         if (self.has_pty_output) {
20+            const cursor = &term.screens.active.cursor;
21+            std.log.debug("cursor before serialize: x={d} y={d} pending_wrap={}", .{ cursor.x, cursor.y, cursor.pending_wrap });
22             if (serializeTerminalState(self.alloc, term)) |term_output| {
23                 defer self.alloc.free(term_output);
24                 ipc.appendMessage(self.alloc, &client.write_buf, .Output, term_output) catch |err| {
25@@ -195,6 +190,17 @@ const Daemon = struct {
26                 client.has_pending_output = true;
27             }
28         }
29+
30+        var ws: c.struct_winsize = .{
31+            .ws_row = resize.rows,
32+            .ws_col = resize.cols,
33+            .ws_xpixel = 0,
34+            .ws_ypixel = 0,
35+        };
36+        _ = c.ioctl(pty_fd, c.TIOCSWINSZ, &ws);
37+        try term.resize(self.alloc, resize.cols, resize.rows);
38+
39+        std.log.debug("init resize rows={d} cols={d}", .{ resize.rows, resize.cols });
40     }
41 
42     pub fn handleResize(self: *Daemon, pty_fd: i32, term: *ghostty_vt.Terminal, payload: []const u8) !void {
43@@ -1197,7 +1203,7 @@ fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal)
44         .palette = false,
45         .modes = true,
46         .scrolling_region = true,
47-        .tabstops = true,
48+        .tabstops = false, // tabstop restoration moves cursor after CUP, corrupting position
49         .pwd = true,
50         .keyboard = true,
51         .screen = .all,