repos / zmx

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

commit
e2c3da5
parent
fced2b8
author
Eric Bower
date
2025-10-15 14:06:14 -0400 EDT
chore: checkpoint
1 files changed,  +5, -108
M src/daemon.zig
+5, -108
  1@@ -888,115 +888,12 @@ fn renderTerminalSnapshot(session: *Session, allocator: std.mem.Allocator) ![]u8
  2     var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
  3     errdefer output.deinit(allocator);
  4 
  5-    const vt = &session.vt;
  6-    const screen = &session.vt.screen;
  7-    const scroll_region = &vt.scrolling_region;
  8-
  9-    // Debug: Print all enabled modes
 10-    std.debug.print("Terminal modes for session {s}:\n", .{session.name});
 11-    inline for (@typeInfo(ghostty.Mode).@"enum".fields) |field| {
 12-        const mode: ghostty.Mode = @field(ghostty.Mode, field.name);
 13-        const value = vt.modes.get(mode);
 14-        const tag: ghostty.modes.ModeTag = @bitCast(@as(ghostty.modes.ModeTag.Backing, field.value));
 15-        const mode_default = @field(vt.modes.default, field.name);
 16-
 17-        if (value != mode_default) {
 18-            std.debug.print("  {s}{d} {s} = {}\n", .{
 19-                if (tag.ansi) "" else "?",
 20-                tag.value,
 21-                field.name,
 22-                value,
 23-            });
 24-        }
 25-    }
 26-
 27-    // Step 1: Neutralize constraints for predictable snapshot printing
 28-    try output.appendSlice(allocator, "\x1b[?69l"); // Disable left/right margins
 29-    try output.appendSlice(allocator, "\x1b[r"); // Reset scroll region to full screen
 30-    try output.appendSlice(allocator, "\x1b[?6l"); // Disable origin mode temporarily
 31-    try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear and home
 32-
 33-    // Step 2: Print terminal content with CRLF translation
 34-    // In raw mode (no ONLCR), we need explicit CR with each LF
 35-    const content = try session.vt.plainStringUnwrapped(allocator);
 36-    defer allocator.free(content);
 37-    var prev: u8 = 0;
 38-    for (content) |b| {
 39-        if (b == '\n' and prev != '\r') {
 40-            try output.append(allocator, '\r');
 41-            try output.append(allocator, '\n');
 42-        } else {
 43-            try output.append(allocator, b);
 44-        }
 45-        prev = b;
 46-    }
 47-
 48-    // Step 3: Restore scroll regions (if non-default)
 49-    const default_top: u16 = 0;
 50-    const default_bottom: u16 = vt.rows - 1;
 51-    const default_left: u16 = 0;
 52-    const default_right: u16 = vt.cols - 1;
 53-
 54-    if (scroll_region.top != default_top or scroll_region.bottom != default_bottom) {
 55-        // DECSTBM - set top/bottom margins (1-based)
 56-        try output.writer(allocator).print("\x1b[{d};{d}r", .{
 57-            scroll_region.top + 1,
 58-            scroll_region.bottom + 1,
 59-        });
 60-    }
 61-
 62-    if (scroll_region.left != default_left or scroll_region.right != default_right) {
 63-        // Enable LRMM first, then set left/right margins (1-based)
 64-        try output.appendSlice(allocator, "\x1b[?69h"); // Enable left/right margin mode
 65-        try output.writer(allocator).print("\x1b[{d};{d}s", .{
 66-            scroll_region.left + 1,
 67-            scroll_region.right + 1,
 68-        });
 69-    }
 70-
 71-    // Step 4: Restore terminal modes (except origin, which we do later)
 72-    inline for (@typeInfo(ghostty.Mode).@"enum".fields) |field| {
 73-        @setEvalBranchQuota(6000);
 74-        const mode: ghostty.Mode = @field(ghostty.Mode, field.name);
 75-
 76-        // Skip origin mode - we'll restore it after cursor positioning
 77-        if (mode == .origin) continue;
 78-
 79-        const value = vt.modes.get(mode);
 80-        const tag: ghostty.modes.ModeTag = @bitCast(@as(ghostty.modes.ModeTag.Backing, field.value));
 81-        const mode_default = @field(vt.modes.default, field.name);
 82-
 83-        // Only send escape sequences for modes that differ from their defaults
 84-        if (value != mode_default) {
 85-            const suffix: u8 = if (value) 'h' else 'l';
 86-            if (tag.ansi) {
 87-                // ANSI mode: CSI <n> h/l
 88-                try output.writer(allocator).print("\x1b[{d}{c}", .{ tag.value, suffix });
 89-            } else {
 90-                // DEC mode: CSI ? <n> h/l
 91-                try output.writer(allocator).print("\x1b[?{d}{c}", .{ tag.value, suffix });
 92-            }
 93-        }
 94-    }
 95-
 96-    // Step 5: Restore origin mode if it was enabled
 97-    const origin_mode = vt.modes.get(.origin);
 98-    if (origin_mode) {
 99-        try output.appendSlice(allocator, "\x1b[?6h");
100-    }
101+    _ = session;
102 
103-    // Step 6: Position cursor (origin-aware, 1-based)
104-    const cursor = screen.cursor;
105-    const cursor_row: u16 = if (origin_mode)
106-        (cursor.y -| scroll_region.top) + 1
107-    else
108-        cursor.y + 1;
109-    const cursor_col: u16 = if (origin_mode)
110-        (cursor.x -| scroll_region.left) + 1
111-    else
112-        cursor.x + 1;
113-
114-    try output.writer(allocator).print("\x1b[{d};{d}H", .{ cursor_row, cursor_col });
115+    // Clear screen when reattaching - let shell repaint naturally
116+    // TODO: Properly restore scrollback once we figure out why ghostty-vt
117+    // buffer contains corrupted escape sequence remnants
118+    try output.appendSlice(allocator, "\x1b[2J\x1b[H"); // Clear screen and home cursor
119 
120     return output.toOwnedSlice(allocator);
121 }