repos / zmx

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

commit
48d9a17
parent
27bd9b8
author
Vishal
date
2026-03-19 08:35:37 -0400 EDT
fix: avoid replaying synchronized output on reattach (#95)
1 files changed,  +49, -0
M src/util.zig
+49, -0
 1@@ -212,6 +212,17 @@ pub fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Termin
 2     var builder: std.Io.Writer.Allocating = .init(alloc);
 3     defer builder.deinit();
 4 
 5+    // Synchronized output (DECSET 2026) is a transient rendering handshake
 6+    // between a program and its current terminal client. Replaying it to a
 7+    // newly attached client can leave that client deferring renders until its
 8+    // local timeout fires, so temporarily exclude it from restored state and
 9+    // restore the original mode before returning.
10+    const had_synchronized_output = term.modes.get(.synchronized_output);
11+    if (had_synchronized_output) {
12+        term.modes.set(.synchronized_output, false);
13+        defer term.modes.set(.synchronized_output, true);
14+    }
15+
16     var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, .vt);
17     term_formatter.content = .{ .selection = null };
18     term_formatter.extra = .{
19@@ -528,3 +539,41 @@ test "isKittyCtrlBackslash" {
20     try std.testing.expect(!isKittyCtrlBackslash("\x1b[92;1u"));
21     try std.testing.expect(!isKittyCtrlBackslash("garbage"));
22 }
23+
24+test "serializeTerminalState excludes synchronized output replay" {
25+    const alloc = std.testing.allocator;
26+
27+    var term = try ghostty_vt.Terminal.init(alloc, .{
28+        .cols = 80,
29+        .rows = 24,
30+    });
31+    defer term.deinit(alloc);
32+
33+    var stream = term.vtStream();
34+    defer stream.deinit();
35+
36+    stream.nextSlice("\x1b[?2004h"); // Bracketed paste
37+    stream.nextSlice("\x1b[?2026h"); // Synchronized output
38+    stream.nextSlice("hello");
39+
40+    try std.testing.expect(term.modes.get(.bracketed_paste));
41+    try std.testing.expect(term.modes.get(.synchronized_output));
42+
43+    const output = serializeTerminalState(alloc, &term) orelse return error.TestUnexpectedNull;
44+    defer alloc.free(output);
45+
46+    try std.testing.expect(term.modes.get(.synchronized_output));
47+
48+    var restored = try ghostty_vt.Terminal.init(alloc, .{
49+        .cols = 80,
50+        .rows = 24,
51+    });
52+    defer restored.deinit(alloc);
53+
54+    var restored_stream = restored.vtStream();
55+    defer restored_stream.deinit();
56+    restored_stream.nextSlice(output);
57+
58+    try std.testing.expect(restored.modes.get(.bracketed_paste));
59+    try std.testing.expect(!restored.modes.get(.synchronized_output));
60+}