- commit
- fa0955b
- parent
- 17988b5
- author
- Eric Bower
- date
- 2025-10-15 15:32:28 -0400 EDT
fix: restore errors
2 files changed,
+116,
-16
+28,
-11
1@@ -265,6 +265,9 @@ const Client = struct {
2 attached_session: ?[]const u8,
3 server_ctx: *ServerContext,
4 message_buffer: std.ArrayList(u8),
5+ /// Gate for preventing live PTY output during snapshot send
6+ /// When true, this client will not receive live PTY frames
7+ muted: bool = false,
8 };
9
10 // Main daemon server state that manages the event loop, Unix socket server,
11@@ -778,10 +781,31 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
12
13 // Mark client as attached
14 client.attached_session = session.name;
15+
16+ // Mute client before adding to attached_clients to prevent PTY interleaving during snapshot
17+ if (is_reattach) {
18+ client.muted = true;
19+ }
20+
21 try session.attached_clients.put(client.fd, {});
22
23 // Start reading from PTY if not already started (first client)
24- if (session.attached_clients.count() == 1) {
25+ const is_first_client = session.attached_clients.count() == 1;
26+
27+ // For reattaching clients, send snapshot BEFORE starting PTY reads to prevent interleaving
28+ if (is_reattach) {
29+ const buffer_slice = try terminal_snapshot.render(&session.vt, client.allocator);
30+ defer client.allocator.free(buffer_slice);
31+
32+ try protocol.writeBinaryFrame(client.fd, .pty_binary, buffer_slice);
33+ std.debug.print("Sent scrollback buffer to client fd={d} ({d} bytes)\n", .{ client.fd, buffer_slice.len });
34+
35+ // Unmute client now that snapshot is sent
36+ client.muted = false;
37+ }
38+
39+ if (is_first_client) {
40+ // Start PTY reads AFTER snapshot is sent
41 try readFromPty(ctx, client, session);
42
43 // For first attach to new session, clear the client's terminal
44@@ -795,15 +819,6 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
45 .client_fd = client.fd,
46 });
47 }
48-
49- // If reattaching, send the scrollback buffer as binary frame
50- if (is_reattach) {
51- const buffer_slice = try terminal_snapshot.render(&session.vt, client.allocator);
52- defer client.allocator.free(buffer_slice);
53-
54- try protocol.writeBinaryFrame(client.fd, .pty_binary, buffer_slice);
55- std.debug.print("Sent scrollback buffer to client fd={d} ({d} bytes)\n", .{ client.fd, buffer_slice.len });
56- }
57 }
58
59 fn handlePtyInput(client: *Client, text: []const u8) !void {
60@@ -989,10 +1004,12 @@ fn readPtyCallback(
61 frame_buf.appendSlice(session.allocator, header_bytes) catch return .disarm;
62 frame_buf.appendSlice(session.allocator, data) catch return .disarm;
63
64- // Send to all attached clients using async write
65+ // Send to all attached clients using async write (skip muted clients)
66 var it = session.attached_clients.keyIterator();
67 while (it.next()) |client_fd| {
68 const attached_client = ctx.clients.get(client_fd.*) orelse continue;
69+ // Skip muted clients (during snapshot send)
70+ if (attached_client.muted) continue;
71 const owned_frame = session.allocator.dupe(u8, frame_buf.items) catch continue;
72
73 const write_ctx = session.allocator.create(PtyWriteContext) catch {
+88,
-5
1@@ -57,14 +57,25 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
2 var output = try std.ArrayList(u8).initCapacity(allocator, 4096);
3 errdefer output.deinit(allocator);
4
5- // Prepare terminal: hide cursor, reset scroll region, reset SGR, clear screen, home cursor
6+ // Check if we're on alternate screen (vim, less, etc.)
7+ const is_alt_screen = vt.active_screen == .alternate;
8+
9+ // Prepare terminal: hide cursor, reset scroll region, reset SGR
10 try output.appendSlice(allocator, "\x1b[?25l"); // Hide cursor
11 try output.appendSlice(allocator, "\x1b[r"); // Reset scroll region
12 try output.appendSlice(allocator, "\x1b[0m"); // Reset SGR (colors/styles)
13- try output.appendSlice(allocator, "\x1b[2J"); // Clear entire screen
14- try output.appendSlice(allocator, "\x1b[H"); // Home cursor (1,1)
15
16- // Get the terminal's page list
17+ // If alternate screen, switch to it before rendering
18+ if (is_alt_screen) {
19+ try output.appendSlice(allocator, "\x1b[?1049h"); // Enter alt screen (save cursor, switch, clear)
20+ try output.appendSlice(allocator, "\x1b[2J"); // Clear alt screen explicitly
21+ try output.appendSlice(allocator, "\x1b[H"); // Home cursor
22+ } else {
23+ try output.appendSlice(allocator, "\x1b[2J"); // Clear entire screen
24+ try output.appendSlice(allocator, "\x1b[H"); // Home cursor (1,1)
25+ }
26+
27+ // Get the terminal's page list (for active screen)
28 const pages = &vt.screen.pages;
29
30 // Create row iterator for active viewport
31@@ -164,7 +175,56 @@ pub fn render(vt: *ghostty.Terminal, allocator: std.mem.Allocator) ![]u8 {
32 }
33 }
34
35- try std.fmt.format(output.writer(allocator), "\x1b[{d};{d}H", .{ cursor_row, cursor_col });
36+ // Restore scroll margins from terminal state (critical for vim scrolling)
37+ const scroll = vt.scrolling_region;
38+ const is_full_tb = (scroll.top == 0 and scroll.bottom == vt.rows - 1);
39+
40+ if (!is_full_tb) {
41+ // Restore top/bottom margins
42+ const top = scroll.top + 1; // Convert to 1-based
43+ const bottom = scroll.bottom + 1; // Convert to 1-based
44+ try std.fmt.format(output.writer(allocator), "\x1b[{d};{d}r", .{ top, bottom });
45+ }
46+
47+ // Restore terminal modes (critical for vim and other apps)
48+ // These modes affect cursor positioning, scrolling, and input behavior
49+
50+ // Origin mode (?6 / DECOM): cursor positioning relative to margins
51+ const origin = vt.modes.get(.origin);
52+ if (origin) {
53+ try output.appendSlice(allocator, "\x1b[?6h");
54+ }
55+
56+ // Wraparound mode (?7 / DECAWM): automatic line wrapping
57+ const wrap = vt.modes.get(.wraparound);
58+ if (!wrap) { // Default is true, so only emit if disabled
59+ try output.appendSlice(allocator, "\x1b[?7l");
60+ }
61+
62+ // Reverse wraparound (?45): bidirectional wrapping
63+ const reverse_wrap = vt.modes.get(.reverse_wrap);
64+ if (reverse_wrap) {
65+ try output.appendSlice(allocator, "\x1b[?45h");
66+ }
67+
68+ // Bracketed paste (?2004): paste detection
69+ const bracketed = vt.modes.get(.bracketed_paste);
70+ if (bracketed) {
71+ try output.appendSlice(allocator, "\x1b[?2004h");
72+ }
73+
74+ // TODO: Restore left/right margins if enabled (need to check modes for left_right_margins)
75+
76+ // Compute cursor position (may be relative to scroll margins if origin mode is on)
77+ var final_cursor_row = cursor_row;
78+ const final_cursor_col = cursor_col;
79+
80+ // If origin mode is on, cursor is relative to the top margin
81+ if (origin and !is_full_tb) {
82+ final_cursor_row = cursor_row - scroll.top;
83+ }
84+
85+ try std.fmt.format(output.writer(allocator), "\x1b[{d};{d}H", .{ final_cursor_row, final_cursor_col });
86
87 // Show cursor
88 try output.appendSlice(allocator, "\x1b[?25h");
89@@ -336,3 +396,26 @@ test "render: cursor position restoration" {
90 try testing.expect(std.mem.indexOf(u8, result, "\x1b[?25h") != null); // Show cursor at end
91 try testing.expect(std.mem.indexOf(u8, result, "Test") != null);
92 }
93+
94+test "render: alternate screen detection" {
95+ const testing = std.testing;
96+ const allocator = testing.allocator;
97+
98+ var vt = try ghostty.Terminal.init(allocator, 80, 24, 100);
99+ defer vt.deinit(allocator);
100+
101+ // Switch to alternate screen
102+ _ = vt.switchScreen(.alternate);
103+
104+ // Write content to alt screen
105+ try vt.print('V');
106+ try vt.print('I');
107+ try vt.print('M');
108+
109+ const result = try render(&vt, allocator);
110+ defer allocator.free(result);
111+
112+ // Should contain alt screen switch sequence
113+ try testing.expect(std.mem.indexOf(u8, result, "\x1b[?1049h") != null);
114+ try testing.expect(std.mem.indexOf(u8, result, "VIM") != null);
115+}