- commit
- ddf78a8
- parent
- 25acaf8
- author
- Eric Bower
- date
- 2025-10-10 22:36:20 -0400 EDT
fix: alt-screen
1 files changed,
+53,
-8
+53,
-8
1@@ -57,6 +57,10 @@ const Session = struct {
2 vt_handler: VTHandler,
3 attached_clients: std.AutoHashMap(std.posix.fd_t, void),
4
5+ // Buffer for incomplete UTF-8 sequences from previous read
6+ utf8_partial: [3]u8,
7+ utf8_partial_len: usize,
8+
9 fn deinit(self: *Session) void {
10 self.allocator.free(self.name);
11 self.buffer.deinit(self.allocator);
12@@ -756,29 +760,68 @@ fn readPtyCallback(
13 return .disarm;
14 }
15
16- const data = read_buffer.slice[0..bytes_read];
17- std.debug.print("PTY output ({d} bytes)\n", .{bytes_read});
18+ // Combine any partial UTF-8 from previous read with new data
19+ var combined_buf: [4096 + 3]u8 = undefined;
20+ const total_len = session.utf8_partial_len + bytes_read;
21+
22+ if (session.utf8_partial_len > 0) {
23+ @memcpy(combined_buf[0..session.utf8_partial_len], session.utf8_partial[0..session.utf8_partial_len]);
24+ @memcpy(combined_buf[session.utf8_partial_len..total_len], read_buffer.slice[0..bytes_read]);
25+ } else {
26+ @memcpy(combined_buf[0..bytes_read], read_buffer.slice[0..bytes_read]);
27+ }
28+
29+ const data = combined_buf[0..total_len];
30+ std.debug.print("PTY output ({d} bytes, {d} from partial)\n", .{ bytes_read, session.utf8_partial_len });
31+
32+ // Check for incomplete UTF-8 sequence at end
33+ var valid_len = total_len;
34+ session.utf8_partial_len = 0;
35+
36+ if (total_len > 0) {
37+ // Scan backwards to find if we have a partial UTF-8 sequence
38+ var i = total_len;
39+ while (i > 0 and i > total_len - 4) {
40+ i -= 1;
41+ const byte = data[i];
42+ // Check if this is a UTF-8 start byte
43+ if (byte & 0x80 == 0) break; // ASCII, we're good
44+ if (byte & 0xC0 == 0xC0) {
45+ // This is a UTF-8 start byte, check if sequence is complete
46+ const expected_len: usize = if (byte & 0xE0 == 0xC0) 2 else if (byte & 0xF0 == 0xE0) 3 else if (byte & 0xF8 == 0xF0) 4 else 1;
47+ if (i + expected_len > total_len) {
48+ // Save partial sequence for next read
49+ session.utf8_partial_len = total_len - i;
50+ @memcpy(session.utf8_partial[0..session.utf8_partial_len], data[i..total_len]);
51+ valid_len = i;
52+ }
53+ break;
54+ }
55+ }
56+ }
57+
58+ const valid_data = data[0..valid_len];
59
60 // Store PTY output in buffer for session restore
61- session.buffer.appendSlice(session.allocator, data) catch |err| {
62+ session.buffer.appendSlice(session.allocator, valid_data) catch |err| {
63 std.debug.print("Buffer append error: {s}\n", .{@errorName(err)});
64 };
65
66 // ALWAYS parse through libghostty-vt to maintain state
67- session.vt_stream.nextSlice(data) catch |err| {
68+ session.vt_stream.nextSlice(valid_data) catch |err| {
69 std.debug.print("VT parse error: {s}\n", .{@errorName(err)});
70 };
71
72 // Only proxy to clients if someone is attached
73- if (session.attached_clients.count() > 0) {
74+ if (session.attached_clients.count() > 0 and valid_len > 0) {
75 // Build JSON response with properly escaped text
76 var response_buf = std.ArrayList(u8).initCapacity(session.allocator, 4096) catch return .disarm;
77 defer response_buf.deinit(session.allocator);
78
79 response_buf.appendSlice(session.allocator, "{\"type\":\"pty_out\",\"payload\":{\"text\":\"") catch return .disarm;
80
81- // Manually escape JSON special characters
82- for (data) |byte| {
83+ // Escape JSON special characters while preserving UTF-8 sequences
84+ for (valid_data) |byte| {
85 switch (byte) {
86 '"' => response_buf.appendSlice(session.allocator, "\\\"") catch return .disarm,
87 '\\' => response_buf.appendSlice(session.allocator, "\\\\") catch return .disarm,
88@@ -787,7 +830,7 @@ fn readPtyCallback(
89 '\t' => response_buf.appendSlice(session.allocator, "\\t") catch return .disarm,
90 0x08 => response_buf.appendSlice(session.allocator, "\\b") catch return .disarm,
91 0x0C => response_buf.appendSlice(session.allocator, "\\f") catch return .disarm,
92- 0x00...0x07, 0x0B, 0x0E...0x1F, 0x7F...0xFF => {
93+ 0x00...0x07, 0x0B, 0x0E...0x1F, 0x7F => {
94 const escaped = std.fmt.allocPrint(session.allocator, "\\u{x:0>4}", .{byte}) catch return .disarm;
95 defer session.allocator.free(escaped);
96 response_buf.appendSlice(session.allocator, escaped) catch return .disarm;
97@@ -944,6 +987,8 @@ fn createSession(allocator: std.mem.Allocator, session_name: []const u8) !*Sessi
98 .vt_handler = VTHandler{ .terminal = &session.vt },
99 .vt_stream = undefined,
100 .attached_clients = std.AutoHashMap(std.posix.fd_t, void).init(allocator),
101+ .utf8_partial = undefined,
102+ .utf8_partial_len = 0,
103 };
104
105 // Initialize the stream after session is created since handler needs terminal pointer