- commit
- 79f25b5
- parent
- 9a25a8a
- author
- Eric Bower
- date
- 2025-10-11 16:52:22 -0400 EDT
refactor: use new protocol module
6 files changed,
+272,
-369
+126,
-98
1@@ -4,6 +4,7 @@ const xevg = @import("xev");
2 const xev = xevg.Dynamic;
3 const clap = @import("clap");
4 const config_mod = @import("config.zig");
5+const protocol = @import("protocol.zig");
6
7 const c = @cImport({
8 @cInclude("termios.h");
9@@ -85,13 +86,6 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
10 c.cfmakeraw(&raw_termios);
11 _ = c.tcsetattr(posix.STDIN_FILENO, c.TCSANOW, &raw_termios);
12
13- const request = try std.fmt.allocPrint(
14- allocator,
15- "{{\"type\":\"attach_session_request\",\"payload\":{{\"session_name\":\"{s}\"}}}}\n",
16- .{session_name},
17- );
18- defer allocator.free(request);
19-
20 const ctx = try allocator.create(Context);
21 ctx.* = .{
22 .stream = xev.Stream.initFd(socket_fd),
23@@ -101,6 +95,22 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
24 .session_name = session_name,
25 };
26
27+ const request_payload = protocol.AttachSessionRequest{ .session_name = session_name };
28+ var out: std.io.Writer.Allocating = .init(allocator);
29+ defer out.deinit();
30+
31+ const msg = protocol.Message(@TypeOf(request_payload)){
32+ .type = protocol.MessageType.attach_session_request.toString(),
33+ .payload = request_payload,
34+ };
35+
36+ var stringify: std.json.Stringify = .{ .writer = &out.writer };
37+ try stringify.write(msg);
38+ try out.writer.writeByte('\n');
39+
40+ const request = try allocator.dupe(u8, out.written());
41+ defer allocator.free(request);
42+
43 const write_completion = try allocator.create(xev.Completion);
44 ctx.stream.write(&loop, write_completion, .{ .slice = request }, Context, ctx, writeCallback);
45
46@@ -175,77 +185,99 @@ fn readCallback(
47 const msg_line = data[0..newline_idx];
48 // std.debug.print("Parsing message ({d} bytes): {s}\n", .{msg_line.len, msg_line});
49
50- const parsed = std.json.parseFromSlice(
51- std.json.Value,
52- ctx.allocator,
53- msg_line,
54- .{},
55- ) catch |err| {
56+ const msg_type_parsed = protocol.parseMessageType(ctx.allocator, msg_line) catch |err| {
57 std.debug.print("JSON parse error: {s}\n", .{@errorName(err)});
58 return .rearm;
59 };
60- defer parsed.deinit();
61-
62- const root = parsed.value.object;
63- const msg_type = root.get("type").?.string;
64- const payload = root.get("payload").?.object;
65-
66- if (std.mem.eql(u8, msg_type, "attach_session_response")) {
67- const status = payload.get("status").?.string;
68- if (std.mem.eql(u8, status, "ok")) {
69- // Get client_fd from response
70- const client_fd = payload.get("client_fd").?.integer;
71-
72- // Write client_fd to a file so shell commands can read it
73- const home_dir = posix.getenv("HOME") orelse "/tmp";
74- const client_fd_path = std.fmt.allocPrint(
75- ctx.allocator,
76- "{s}/.zmx_client_fd_{s}",
77- .{ home_dir, ctx.session_name },
78- ) catch |err| {
79- std.debug.print("Failed to create client_fd path: {s}\n", .{@errorName(err)});
80- return .rearm;
81- };
82- defer ctx.allocator.free(client_fd_path);
83+ defer msg_type_parsed.deinit();
84
85- const file = std.fs.cwd().createFile(client_fd_path, .{ .truncate = true }) catch |err| {
86- std.debug.print("Failed to create client_fd file: {s}\n", .{@errorName(err)});
87+ const msg_type = protocol.MessageType.fromString(msg_type_parsed.value.type) orelse {
88+ std.debug.print("Unknown message type: {s}\n", .{msg_type_parsed.value.type});
89+ return .rearm;
90+ };
91+
92+ switch (msg_type) {
93+ .attach_session_response => {
94+ const parsed = protocol.parseMessage(protocol.AttachSessionResponse, ctx.allocator, msg_line) catch |err| {
95+ std.debug.print("Failed to parse attach response: {s}\n", .{@errorName(err)});
96 return .rearm;
97 };
98- defer file.close();
99-
100- const fd_str = std.fmt.allocPrint(ctx.allocator, "{d}", .{client_fd}) catch return .rearm;
101- defer ctx.allocator.free(fd_str);
102-
103- file.writeAll(fd_str) catch |err| {
104- std.debug.print("Failed to write client_fd: {s}\n", .{@errorName(err)});
105+ defer parsed.deinit();
106+
107+ if (std.mem.eql(u8, parsed.value.payload.status, "ok")) {
108+ const client_fd = parsed.value.payload.client_fd orelse {
109+ std.debug.print("Missing client_fd in response\n", .{});
110+ return .rearm;
111+ };
112+
113+ // Write client_fd to a file so shell commands can read it
114+ const home_dir = posix.getenv("HOME") orelse "/tmp";
115+ const client_fd_path = std.fmt.allocPrint(
116+ ctx.allocator,
117+ "{s}/.zmx_client_fd_{s}",
118+ .{ home_dir, ctx.session_name },
119+ ) catch |err| {
120+ std.debug.print("Failed to create client_fd path: {s}\n", .{@errorName(err)});
121+ return .rearm;
122+ };
123+ defer ctx.allocator.free(client_fd_path);
124+
125+ const file = std.fs.cwd().createFile(client_fd_path, .{ .truncate = true }) catch |err| {
126+ std.debug.print("Failed to create client_fd file: {s}\n", .{@errorName(err)});
127+ return .rearm;
128+ };
129+ defer file.close();
130+
131+ const fd_str = std.fmt.allocPrint(ctx.allocator, "{d}", .{client_fd}) catch return .rearm;
132+ defer ctx.allocator.free(fd_str);
133+
134+ file.writeAll(fd_str) catch |err| {
135+ std.debug.print("Failed to write client_fd: {s}\n", .{@errorName(err)});
136+ return .rearm;
137+ };
138+
139+ startStdinReading(ctx);
140+ } else {
141+ _ = posix.write(posix.STDERR_FILENO, "Attach failed: ") catch {};
142+ _ = posix.write(posix.STDERR_FILENO, parsed.value.payload.status) catch {};
143+ _ = posix.write(posix.STDERR_FILENO, "\n") catch {};
144+ }
145+ },
146+ .detach_session_response => {
147+ const parsed = protocol.parseMessage(protocol.DetachSessionResponse, ctx.allocator, msg_line) catch |err| {
148+ std.debug.print("Failed to parse detach response: {s}\n", .{@errorName(err)});
149 return .rearm;
150 };
151+ defer parsed.deinit();
152
153- startStdinReading(ctx);
154- } else {
155- _ = posix.write(posix.STDERR_FILENO, "Attach failed: ") catch {};
156- _ = posix.write(posix.STDERR_FILENO, status) catch {};
157- _ = posix.write(posix.STDERR_FILENO, "\n") catch {};
158- }
159- } else if (std.mem.eql(u8, msg_type, "detach_session_response")) {
160- const status = payload.get("status").?.string;
161- if (std.mem.eql(u8, status, "ok")) {
162+ if (std.mem.eql(u8, parsed.value.payload.status, "ok")) {
163+ cleanupClientFdFile(ctx);
164+ _ = posix.write(posix.STDERR_FILENO, "\r\nDetached from session\r\n") catch {};
165+ return cleanup(ctx, completion);
166+ }
167+ },
168+ .detach_notification => {
169 cleanupClientFdFile(ctx);
170- _ = posix.write(posix.STDERR_FILENO, "\r\nDetached from session\r\n") catch {};
171+ _ = posix.write(posix.STDERR_FILENO, "\r\nDetached from session (external request)\r\n") catch {};
172 return cleanup(ctx, completion);
173- }
174- } else if (std.mem.eql(u8, msg_type, "detach_notification")) {
175- cleanupClientFdFile(ctx);
176- _ = posix.write(posix.STDERR_FILENO, "\r\nDetached from session (external request)\r\n") catch {};
177- return cleanup(ctx, completion);
178- } else if (std.mem.eql(u8, msg_type, "kill_notification")) {
179- cleanupClientFdFile(ctx);
180- _ = posix.write(posix.STDERR_FILENO, "\r\nSession killed\r\n") catch {};
181- return cleanup(ctx, completion);
182- } else if (std.mem.eql(u8, msg_type, "pty_out")) {
183- const text = payload.get("text").?.string;
184- _ = posix.write(posix.STDOUT_FILENO, text) catch {};
185+ },
186+ .kill_notification => {
187+ cleanupClientFdFile(ctx);
188+ _ = posix.write(posix.STDERR_FILENO, "\r\nSession killed\r\n") catch {};
189+ return cleanup(ctx, completion);
190+ },
191+ .pty_out => {
192+ const parsed = protocol.parseMessage(protocol.PtyOutput, ctx.allocator, msg_line) catch |err| {
193+ std.debug.print("Failed to parse pty output: {s}\n", .{@errorName(err)});
194+ return .rearm;
195+ };
196+ defer parsed.deinit();
197+
198+ _ = posix.write(posix.STDOUT_FILENO, parsed.value.payload.text) catch {};
199+ },
200+ else => {
201+ std.debug.print("Unexpected message type in attach client: {s}\n", .{msg_type.toString()});
202+ },
203 }
204
205 return .rearm;
206@@ -292,17 +324,25 @@ fn cleanupClientFdFile(ctx: *Context) void {
207 }
208
209 fn sendDetachRequest(ctx: *Context) void {
210- const request = std.fmt.allocPrint(
211- ctx.allocator,
212- "{{\"type\":\"detach_session_request\",\"payload\":{{\"session_name\":\"{s}\"}}}}\n",
213- .{ctx.session_name},
214- ) catch return;
215- defer ctx.allocator.free(request);
216+ const request_payload = protocol.DetachSessionRequest{ .session_name = ctx.session_name };
217+ var out: std.io.Writer.Allocating = .init(ctx.allocator);
218+ defer out.deinit();
219+
220+ const msg = protocol.Message(@TypeOf(request_payload)){
221+ .type = protocol.MessageType.detach_session_request.toString(),
222+ .payload = request_payload,
223+ };
224+
225+ var stringify: std.json.Stringify = .{ .writer = &out.writer };
226+ stringify.write(msg) catch return;
227+ out.writer.writeByte('\n') catch return;
228+
229+ const request = ctx.allocator.dupe(u8, out.written()) catch return;
230
231 const write_ctx = ctx.allocator.create(StdinWriteContext) catch return;
232 write_ctx.* = .{
233 .allocator = ctx.allocator,
234- .message = ctx.allocator.dupe(u8, request) catch return,
235+ .message = request,
236 };
237
238 const write_completion = ctx.allocator.create(xev.Completion) catch return;
239@@ -310,32 +350,20 @@ fn sendDetachRequest(ctx: *Context) void {
240 }
241
242 fn sendPtyInput(ctx: *Context, data: []const u8) void {
243- var msg_buf = std.ArrayList(u8).initCapacity(ctx.allocator, 4096) catch return;
244- defer msg_buf.deinit(ctx.allocator);
245-
246- msg_buf.appendSlice(ctx.allocator, "{\"type\":\"pty_in\",\"payload\":{\"text\":\"") catch return;
247-
248- for (data) |byte| {
249- switch (byte) {
250- '"' => msg_buf.appendSlice(ctx.allocator, "\\\"") catch return,
251- '\\' => msg_buf.appendSlice(ctx.allocator, "\\\\") catch return,
252- '\n' => msg_buf.appendSlice(ctx.allocator, "\\n") catch return,
253- '\r' => msg_buf.appendSlice(ctx.allocator, "\\r") catch return,
254- '\t' => msg_buf.appendSlice(ctx.allocator, "\\t") catch return,
255- 0x08 => msg_buf.appendSlice(ctx.allocator, "\\b") catch return,
256- 0x0C => msg_buf.appendSlice(ctx.allocator, "\\f") catch return,
257- 0x00...0x07, 0x0B, 0x0E...0x1F, 0x7F => {
258- const escaped = std.fmt.allocPrint(ctx.allocator, "\\u{x:0>4}", .{byte}) catch return;
259- defer ctx.allocator.free(escaped);
260- msg_buf.appendSlice(ctx.allocator, escaped) catch return;
261- },
262- else => msg_buf.append(ctx.allocator, byte) catch return,
263- }
264- }
265+ const request_payload = protocol.PtyInput{ .text = data };
266+ var out: std.io.Writer.Allocating = .init(ctx.allocator);
267+ defer out.deinit();
268+
269+ const msg = protocol.Message(@TypeOf(request_payload)){
270+ .type = protocol.MessageType.pty_in.toString(),
271+ .payload = request_payload,
272+ };
273
274- msg_buf.appendSlice(ctx.allocator, "\"}}\n") catch return;
275+ var stringify: std.json.Stringify = .{ .writer = &out.writer };
276+ stringify.write(msg) catch return;
277+ out.writer.writeByte('\n') catch return;
278
279- const owned_message = ctx.allocator.dupe(u8, msg_buf.items) catch return;
280+ const owned_message = ctx.allocator.dupe(u8, out.written()) catch return;
281
282 const write_ctx = ctx.allocator.create(StdinWriteContext) catch return;
283 write_ctx.* = .{
+113,
-198
1@@ -4,6 +4,7 @@ const xevg = @import("xev");
2 const xev = xevg.Dynamic;
3 const clap = @import("clap");
4 const config_mod = @import("config.zig");
5+const protocol = @import("protocol.zig");
6
7 const ghostty = @import("ghostty-vt");
8
9@@ -14,17 +15,6 @@ const c = @cImport({
10 @cInclude("sys/ioctl.h");
11 });
12
13-// Generic JSON message structure used for parsing incoming protocol messages from clients
14-const Message = struct {
15- type: []const u8,
16- payload: std.json.Value,
17-};
18-
19-// Request payload for attaching to a session
20-const AttachRequest = struct {
21- session_name: []const u8,
22-};
23-
24 // Handler for processing VT sequences
25 const VTHandler = struct {
26 terminal: *ghostty.Terminal,
27@@ -289,44 +279,51 @@ fn readCallback(
28 fn handleMessage(client: *Client, data: []const u8) !void {
29 std.debug.print("Received message from client fd={d}: {s}", .{ client.fd, data });
30
31- // Parse JSON message
32- const parsed = try std.json.parseFromSlice(
33- std.json.Value,
34- client.allocator,
35- data,
36- .{},
37- );
38- defer parsed.deinit();
39-
40- const root = parsed.value.object;
41- const msg_type = root.get("type").?.string;
42- const payload = root.get("payload").?.object;
43-
44- if (std.mem.eql(u8, msg_type, "attach_session_request")) {
45- const session_name = payload.get("session_name").?.string;
46- std.debug.print("Handling attach request for session: {s}\n", .{session_name});
47- try handleAttachSession(client.server_ctx, client, session_name);
48- } else if (std.mem.eql(u8, msg_type, "detach_session_request")) {
49- const session_name = payload.get("session_name").?.string;
50- const target_client_fd = if (payload.get("client_fd")) |fd_value| fd_value.integer else null;
51- std.debug.print("Handling detach request for session: {s}, target_fd: {any}\n", .{ session_name, target_client_fd });
52- try handleDetachSession(client, session_name, target_client_fd);
53- } else if (std.mem.eql(u8, msg_type, "kill_session_request")) {
54- const session_name = payload.get("session_name").?.string;
55- std.debug.print("Handling kill request for session: {s}\n", .{session_name});
56- try handleKillSession(client, session_name);
57- } else if (std.mem.eql(u8, msg_type, "list_sessions_request")) {
58- std.debug.print("Handling list sessions request\n", .{});
59- try handleListSessions(client.server_ctx, client);
60- } else if (std.mem.eql(u8, msg_type, "pty_in")) {
61- const text = payload.get("text").?.string;
62- try handlePtyInput(client, text);
63- } else if (std.mem.eql(u8, msg_type, "window_resize")) {
64- const rows = @as(u16, @intCast(payload.get("rows").?.integer));
65- const cols = @as(u16, @intCast(payload.get("cols").?.integer));
66- try handleWindowResize(client, rows, cols);
67- } else {
68- std.debug.print("Unknown message type: {s}\n", .{msg_type});
69+ // Parse message type first for dispatching
70+ const type_parsed = try protocol.parseMessageType(client.allocator, data);
71+ defer type_parsed.deinit();
72+
73+ const msg_type = protocol.MessageType.fromString(type_parsed.value.type) orelse {
74+ std.debug.print("Unknown message type: {s}\n", .{type_parsed.value.type});
75+ return;
76+ };
77+
78+ switch (msg_type) {
79+ .attach_session_request => {
80+ const parsed = try protocol.parseMessage(protocol.AttachSessionRequest, client.allocator, data);
81+ defer parsed.deinit();
82+ std.debug.print("Handling attach request for session: {s}\n", .{parsed.value.payload.session_name});
83+ try handleAttachSession(client.server_ctx, client, parsed.value.payload.session_name);
84+ },
85+ .detach_session_request => {
86+ const parsed = try protocol.parseMessage(protocol.DetachSessionRequest, client.allocator, data);
87+ defer parsed.deinit();
88+ std.debug.print("Handling detach request for session: {s}, target_fd: {any}\n", .{ parsed.value.payload.session_name, parsed.value.payload.client_fd });
89+ try handleDetachSession(client, parsed.value.payload.session_name, parsed.value.payload.client_fd);
90+ },
91+ .kill_session_request => {
92+ const parsed = try protocol.parseMessage(protocol.KillSessionRequest, client.allocator, data);
93+ defer parsed.deinit();
94+ std.debug.print("Handling kill request for session: {s}\n", .{parsed.value.payload.session_name});
95+ try handleKillSession(client, parsed.value.payload.session_name);
96+ },
97+ .list_sessions_request => {
98+ std.debug.print("Handling list sessions request\n", .{});
99+ try handleListSessions(client.server_ctx, client);
100+ },
101+ .pty_in => {
102+ const parsed = try protocol.parseMessage(protocol.PtyInput, client.allocator, data);
103+ defer parsed.deinit();
104+ try handlePtyInput(client, parsed.value.payload.text);
105+ },
106+ .window_resize => {
107+ const parsed = try protocol.parseMessage(protocol.WindowResize, client.allocator, data);
108+ defer parsed.deinit();
109+ try handleWindowResize(client, parsed.value.payload.rows, parsed.value.payload.cols);
110+ },
111+ else => {
112+ std.debug.print("Unexpected message type: {s}\n", .{type_parsed.value.type});
113+ },
114 }
115 }
116
117@@ -335,17 +332,12 @@ fn handleDetachSession(client: *Client, session_name: []const u8, target_client_
118
119 // Check if the session exists
120 const session = ctx.sessions.get(session_name) orelse {
121- const error_response = try std.fmt.allocPrint(
122- client.allocator,
123- "{{\"type\":\"detach_session_response\",\"payload\":{{\"status\":\"error\",\"error_message\":\"Session not found: {s}\"}}}}\n",
124- .{session_name},
125- );
126- defer client.allocator.free(error_response);
127-
128- _ = posix.write(client.fd, error_response) catch |err| {
129- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
130- return err;
131- };
132+ const error_msg = try std.fmt.allocPrint(client.allocator, "Session not found: {s}", .{session_name});
133+ defer client.allocator.free(error_msg);
134+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
135+ .status = "error",
136+ .error_message = error_msg,
137+ });
138 return;
139 };
140
141@@ -359,84 +351,64 @@ fn handleDetachSession(client: *Client, session_name: []const u8, target_client_
142 _ = session.attached_clients.remove(target_fd_cast);
143
144 // Send notification to the target client
145- const notification = "{\"type\":\"detach_notification\",\"payload\":{\"status\":\"ok\"}}\n";
146- _ = posix.write(target_client.fd, notification) catch |err| {
147+ protocol.writeJson(target_client.allocator, target_client.fd, .detach_notification, protocol.DetachNotification{
148+ .session_name = session_name,
149+ }) catch |err| {
150 std.debug.print("Error notifying client fd={d}: {s}\n", .{ target_client.fd, @errorName(err) });
151 };
152
153- // Send response to the requesting client
154- const response = "{\"type\":\"detach_session_response\",\"payload\":{\"status\":\"ok\"}}\n";
155 std.debug.print("Detached client fd={d} from session: {s}\n", .{ target_fd_cast, session_name });
156
157- _ = posix.write(client.fd, response) catch |err| {
158- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
159- return err;
160- };
161+ // Send response to the requesting client
162+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
163+ .status = "ok",
164+ });
165 return;
166 } else {
167- const error_response = try std.fmt.allocPrint(
168- client.allocator,
169- "{{\"type\":\"detach_session_response\",\"payload\":{{\"status\":\"error\",\"error_message\":\"Target client not attached to session: {s}\"}}}}\n",
170- .{session_name},
171- );
172- defer client.allocator.free(error_response);
173-
174- _ = posix.write(client.fd, error_response) catch |err| {
175- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
176- return err;
177- };
178+ const error_msg = try std.fmt.allocPrint(client.allocator, "Target client not attached to session: {s}", .{session_name});
179+ defer client.allocator.free(error_msg);
180+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
181+ .status = "error",
182+ .error_message = error_msg,
183+ });
184 return;
185 }
186 }
187 }
188
189- const error_response = try std.fmt.allocPrint(
190- client.allocator,
191- "{{\"type\":\"detach_session_response\",\"payload\":{{\"status\":\"error\",\"error_message\":\"Target client fd={d} not found\"}}}}\n",
192- .{target_fd},
193- );
194- defer client.allocator.free(error_response);
195-
196- _ = posix.write(client.fd, error_response) catch |err| {
197- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
198- return err;
199- };
200+ const error_msg = try std.fmt.allocPrint(client.allocator, "Target client fd={d} not found", .{target_fd});
201+ defer client.allocator.free(error_msg);
202+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
203+ .status = "error",
204+ .error_message = error_msg,
205+ });
206 return;
207 }
208
209 // No target_client_fd provided, check if requesting client is attached
210 if (client.attached_session) |attached| {
211 if (!std.mem.eql(u8, attached, session_name)) {
212- const error_response = try std.fmt.allocPrint(
213- client.allocator,
214- "{{\"type\":\"detach_session_response\",\"payload\":{{\"status\":\"error\",\"error_message\":\"Not attached to session: {s}\"}}}}\n",
215- .{session_name},
216- );
217- defer client.allocator.free(error_response);
218-
219- _ = posix.write(client.fd, error_response) catch |err| {
220- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
221- return err;
222- };
223+ const error_msg = try std.fmt.allocPrint(client.allocator, "Not attached to session: {s}", .{session_name});
224+ defer client.allocator.free(error_msg);
225+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
226+ .status = "error",
227+ .error_message = error_msg,
228+ });
229 return;
230 }
231
232 client.attached_session = null;
233 _ = session.attached_clients.remove(client.fd);
234
235- const response = "{\"type\":\"detach_session_response\",\"payload\":{\"status\":\"ok\"}}\n";
236- std.debug.print("Sending detach response to client fd={d}: {s}", .{ client.fd, response });
237-
238- _ = posix.write(client.fd, response) catch |err| {
239- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
240- return err;
241- };
242+ std.debug.print("Sending detach response to client fd={d}\n", .{client.fd});
243+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
244+ .status = "ok",
245+ });
246 } else {
247- const error_response = "{\"type\":\"detach_session_response\",\"payload\":{\"status\":\"error\",\"error_message\":\"Not attached to any session\"}}\n";
248- _ = posix.write(client.fd, error_response) catch |err| {
249- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
250- return err;
251- };
252+ try protocol.writeJson(client.allocator, client.fd, .detach_session_response, protocol.DetachSessionResponse{
253+ .status = "error",
254+ .error_message = "Not attached to any session",
255+ });
256 }
257 }
258
259@@ -445,17 +417,12 @@ fn handleKillSession(client: *Client, session_name: []const u8) !void {
260
261 // Check if the session exists
262 const session = ctx.sessions.get(session_name) orelse {
263- const error_response = try std.fmt.allocPrint(
264- client.allocator,
265- "{{\"type\":\"kill_session_response\",\"payload\":{{\"status\":\"error\",\"error_message\":\"Session not found: {s}\"}}}}\n",
266- .{session_name},
267- );
268- defer client.allocator.free(error_response);
269-
270- _ = posix.write(client.fd, error_response) catch |err| {
271- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
272- return err;
273- };
274+ const error_msg = try std.fmt.allocPrint(client.allocator, "Session not found: {s}", .{session_name});
275+ defer client.allocator.free(error_msg);
276+ try protocol.writeJson(client.allocator, client.fd, .kill_session_response, protocol.KillSessionResponse{
277+ .status = "error",
278+ .error_message = error_msg,
279+ });
280 return;
281 };
282
283@@ -468,8 +435,9 @@ fn handleKillSession(client: *Client, session_name: []const u8) !void {
284 attached_client.attached_session = null;
285
286 // Send kill notification to client
287- const notification = "{\"type\":\"kill_notification\",\"payload\":{\"status\":\"ok\"}}\n";
288- _ = posix.write(attached_client.fd, notification) catch |err| {
289+ protocol.writeJson(attached_client.allocator, attached_client.fd, .kill_notification, protocol.KillNotification{
290+ .session_name = session_name,
291+ }) catch |err| {
292 std.debug.print("Error notifying client fd={d}: {s}\n", .{ attached_client.fd, @errorName(err) });
293 };
294 }
295@@ -495,16 +463,14 @@ fn handleKillSession(client: *Client, session_name: []const u8) !void {
296 ctx.allocator.destroy(session);
297
298 // Send response to requesting client
299- const response = "{\"type\":\"kill_session_response\",\"payload\":{\"status\":\"ok\"}}\n";
300 std.debug.print("Killed session: {s}\n", .{session_name});
301-
302- _ = posix.write(client.fd, response) catch |err| {
303- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
304- return err;
305- };
306+ try protocol.writeJson(client.allocator, client.fd, .kill_session_response, protocol.KillSessionResponse{
307+ .status = "ok",
308+ });
309 }
310
311 fn handleListSessions(ctx: *ServerContext, client: *Client) !void {
312+ // TODO: Refactor to use protocol.writeJson() once we have a better approach for dynamic arrays
313 var response = try std.ArrayList(u8).initCapacity(client.allocator, 1024);
314 defer response.deinit(client.allocator);
315
316@@ -583,33 +549,16 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
317
318 // For first attach to new session, clear the client's terminal
319 if (!is_reattach) {
320- var out: std.io.Writer.Allocating = .init(ctx.allocator);
321- defer out.deinit();
322- var json_writer: std.json.Stringify = .{ .writer = &out.writer };
323-
324- try json_writer.beginObject();
325- try json_writer.objectField("type");
326- try json_writer.write("pty_out");
327- try json_writer.objectField("payload");
328- try json_writer.beginObject();
329- try json_writer.objectField("text");
330- try json_writer.write("\x1b[2J\x1b[H"); // Clear screen and move cursor to home
331- try json_writer.endObject();
332- try json_writer.endObject();
333-
334- const response = try std.fmt.allocPrint(ctx.allocator, "{s}\n", .{out.written()});
335- defer ctx.allocator.free(response);
336- _ = try posix.write(client.fd, response);
337+ try protocol.writeJson(ctx.allocator, client.fd, .pty_out, protocol.PtyOutput{
338+ .text = "\x1b[2J\x1b[H", // Clear screen and move cursor to home
339+ });
340 }
341 } else {
342 // Send attach success response for additional clients
343- const response = try std.fmt.allocPrint(
344- ctx.allocator,
345- "{{\"type\":\"attach_session_response\",\"payload\":{{\"status\":\"ok\",\"client_fd\":{d}}}}}\n",
346- .{client.fd},
347- );
348- defer ctx.allocator.free(response);
349- _ = try posix.write(client.fd, response);
350+ try protocol.writeJson(ctx.allocator, client.fd, .attach_session_response, protocol.AttachSessionResponse{
351+ .status = "ok",
352+ .client_fd = client.fd,
353+ });
354 }
355
356 // If reattaching, send the scrollback buffer (raw PTY output with colors)
357@@ -626,35 +575,9 @@ fn handleAttachSession(ctx: *ServerContext, client: *Client, session_name: []con
358
359 std.debug.print("Sending slice: {d} bytes (from offset {d})\n", .{ buffer_slice.len, buffer_start });
360
361- // Use Zig's JSON formatting to properly escape the buffer
362- var out: std.io.Writer.Allocating = .init(ctx.allocator);
363- defer out.deinit();
364-
365- var json_writer: std.json.Stringify = .{ .writer = &out.writer };
366-
367- json_writer.beginObject() catch |err| {
368- std.debug.print("JSON beginObject error: {s}\n", .{@errorName(err)});
369- return;
370- };
371- json_writer.objectField("type") catch return;
372- json_writer.write("pty_out") catch return;
373- json_writer.objectField("payload") catch return;
374- json_writer.beginObject() catch return;
375- json_writer.objectField("text") catch return;
376- json_writer.write(buffer_slice) catch |err| {
377- std.debug.print("JSON write buffer error: {s}\n", .{@errorName(err)});
378- return;
379- };
380- json_writer.endObject() catch return;
381- json_writer.endObject() catch return;
382-
383- const json_output = out.written();
384- std.debug.print("JSON output length: {d} bytes\n", .{json_output.len});
385- std.debug.print("JSON first 100 bytes: {s}\n", .{json_output[0..@min(100, json_output.len)]});
386-
387- const response = try std.fmt.allocPrint(ctx.allocator, "{s}\n", .{json_output});
388- defer ctx.allocator.free(response);
389- _ = try posix.write(client.fd, response);
390+ try protocol.writeJson(ctx.allocator, client.fd, .pty_out, protocol.PtyOutput{
391+ .text = buffer_slice,
392+ });
393 std.debug.print("Sent scrollback buffer to client fd={d}\n", .{client.fd});
394 }
395 }
396@@ -726,20 +649,12 @@ fn readFromPty(ctx: *ServerContext, client: *Client, session: *Session) !void {
397 readPtyCallback,
398 );
399
400- const response = try std.fmt.allocPrint(
401- client.allocator,
402- "{{\"type\":\"attach_session_response\",\"payload\":{{\"status\":\"ok\",\"client_fd\":{d}}}}}\n",
403- .{client.fd},
404- );
405- defer client.allocator.free(response);
406+ std.debug.print("Sending attach response to client fd={d}\n", .{client.fd});
407
408- std.debug.print("Sending response to client fd={d}: {s}", .{ client.fd, response });
409-
410- const written = posix.write(client.fd, response) catch |err| {
411- std.debug.print("Error writing to fd={d}: {s}\n", .{ client.fd, @errorName(err) });
412- return err;
413- };
414- _ = written;
415+ try protocol.writeJson(client.allocator, client.fd, .attach_session_response, protocol.AttachSessionResponse{
416+ .status = "ok",
417+ .client_fd = client.fd,
418+ });
419 }
420
421 fn getSessionForClient(ctx: *ServerContext, client: *Client) ?*Session {
+10,
-27
1@@ -2,6 +2,7 @@ const std = @import("std");
2 const posix = std.posix;
3 const clap = @import("clap");
4 const config_mod = @import("config.zig");
5+const protocol = @import("protocol.zig");
6
7 const params = clap.parseParamsComptime(
8 \\-s, --socket-path <str> Path to the Unix socket file
9@@ -87,21 +88,12 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
10 return err;
11 };
12
13- const request = if (client_fd) |fd|
14- try std.fmt.allocPrint(
15- allocator,
16- "{{\"type\":\"detach_session_request\",\"payload\":{{\"session_name\":\"{s}\",\"client_fd\":{d}}}}}\n",
17- .{ session_name.?, fd },
18- )
19- else
20- try std.fmt.allocPrint(
21- allocator,
22- "{{\"type\":\"detach_session_request\",\"payload\":{{\"session_name\":\"{s}\"}}}}\n",
23- .{session_name.?},
24- );
25- defer allocator.free(request);
26-
27- _ = try posix.write(socket_fd, request);
28+ const request_payload = protocol.DetachSessionRequest{
29+ .session_name = session_name.?,
30+ .client_fd = client_fd,
31+ };
32+
33+ try protocol.writeJson(allocator, socket_fd, .detach_session_request, request_payload);
34
35 var buffer: [4096]u8 = undefined;
36 const bytes_read = try posix.read(socket_fd, &buffer);
37@@ -115,22 +107,13 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
38 const newline_idx = std.mem.indexOf(u8, response, "\n") orelse bytes_read;
39 const msg_line = response[0..newline_idx];
40
41- const parsed = try std.json.parseFromSlice(
42- std.json.Value,
43- allocator,
44- msg_line,
45- .{},
46- );
47+ const parsed = try protocol.parseMessage(protocol.DetachSessionResponse, allocator, msg_line);
48 defer parsed.deinit();
49
50- const root = parsed.value.object;
51- const payload = root.get("payload").?.object;
52- const status = payload.get("status").?.string;
53-
54- if (std.mem.eql(u8, status, "ok")) {
55+ if (std.mem.eql(u8, parsed.value.payload.status, "ok")) {
56 std.debug.print("Detached from session: {s}\n", .{session_name.?});
57 } else {
58- const error_msg = payload.get("error_message").?.string;
59+ const error_msg = parsed.value.payload.error_message orelse "Unknown error";
60 std.debug.print("Failed to detach: {s}\n", .{error_msg});
61 std.process.exit(1);
62 }
+8,
-18
1@@ -2,6 +2,7 @@ const std = @import("std");
2 const posix = std.posix;
3 const clap = @import("clap");
4 const config_mod = @import("config.zig");
5+const protocol = @import("protocol.zig");
6
7 const params = clap.parseParamsComptime(
8 \\-s, --socket-path <str> Path to the Unix socket file
9@@ -47,14 +48,12 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
10 return err;
11 };
12
13- const request = try std.fmt.allocPrint(
14+ try protocol.writeJson(
15 allocator,
16- "{{\"type\":\"kill_session_request\",\"payload\":{{\"session_name\":\"{s}\"}}}}\n",
17- .{session_name},
18+ socket_fd,
19+ .kill_session_request,
20+ protocol.KillSessionRequest{ .session_name = session_name },
21 );
22- defer allocator.free(request);
23-
24- _ = try posix.write(socket_fd, request);
25
26 var buffer: [4096]u8 = undefined;
27 const bytes_read = try posix.read(socket_fd, &buffer);
28@@ -68,22 +67,13 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
29 const newline_idx = std.mem.indexOf(u8, response, "\n") orelse bytes_read;
30 const msg_line = response[0..newline_idx];
31
32- const parsed = try std.json.parseFromSlice(
33- std.json.Value,
34- allocator,
35- msg_line,
36- .{},
37- );
38+ const parsed = try protocol.parseMessage(protocol.KillSessionResponse, allocator, msg_line);
39 defer parsed.deinit();
40
41- const root = parsed.value.object;
42- const payload = root.get("payload").?.object;
43- const status = payload.get("status").?.string;
44-
45- if (std.mem.eql(u8, status, "ok")) {
46+ if (std.mem.eql(u8, parsed.value.payload.status, "ok")) {
47 std.debug.print("Killed session: {s}\n", .{session_name});
48 } else {
49- const error_msg = payload.get("error_message").?.string;
50+ const error_msg = parsed.value.payload.error_message orelse "Unknown error";
51 std.debug.print("Failed to kill session: {s}\n", .{error_msg});
52 std.process.exit(1);
53 }
+9,
-24
1@@ -2,6 +2,7 @@ const std = @import("std");
2 const posix = std.posix;
3 const clap = @import("clap");
4 const config_mod = @import("config.zig");
5+const protocol = @import("protocol.zig");
6
7 const params = clap.parseParamsComptime(
8 \\-s, --socket-path <str> Path to the Unix socket file
9@@ -41,8 +42,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
10 return err;
11 };
12
13- const request = "{\"type\":\"list_sessions_request\",\"payload\":{}}\n";
14- _ = try posix.write(socket_fd, request);
15+ try protocol.writeJson(allocator, socket_fd, .list_sessions_request, protocol.ListSessionsRequest{});
16
17 var buffer: [8192]u8 = undefined;
18 const bytes_read = try posix.read(socket_fd, &buffer);
19@@ -56,27 +56,18 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
20 const newline_idx = std.mem.indexOf(u8, response, "\n") orelse bytes_read;
21 const msg_line = response[0..newline_idx];
22
23- const parsed = try std.json.parseFromSlice(
24- std.json.Value,
25- allocator,
26- msg_line,
27- .{},
28- );
29+ const parsed = try protocol.parseMessage(protocol.ListSessionsResponse, allocator, msg_line);
30 defer parsed.deinit();
31
32- const root = parsed.value.object;
33- const payload = root.get("payload").?.object;
34- const status = payload.get("status").?.string;
35+ const payload = parsed.value.payload;
36
37- if (!std.mem.eql(u8, status, "ok")) {
38- const error_msg = payload.get("error_message").?.string;
39+ if (!std.mem.eql(u8, payload.status, "ok")) {
40+ const error_msg = payload.error_message orelse "Unknown error";
41 std.debug.print("Error: {s}\n", .{error_msg});
42 return;
43 }
44
45- const sessions = payload.get("sessions").?.array;
46-
47- if (sessions.items.len == 0) {
48+ if (payload.sessions.len == 0) {
49 std.debug.print("No active sessions\n", .{});
50 return;
51 }
52@@ -85,13 +76,7 @@ pub fn main(config: config_mod.Config, iter: *std.process.ArgIterator) !void {
53 std.debug.print("{s:<20} {s:<12} {s:<8} {s}\n", .{ "NAME", "STATUS", "CLIENTS", "CREATED" });
54 std.debug.print("{s}\n", .{"-" ** 60});
55
56- for (sessions.items) |session_value| {
57- const session = session_value.object;
58- const name = session.get("name").?.string;
59- const session_status = session.get("status").?.string;
60- const clients = session.get("clients").?.integer;
61- const created_at = session.get("created_at").?.string;
62-
63- std.debug.print("{s:<20} {s:<12} {d:<8} {s}\n", .{ name, session_status, clients, created_at });
64+ for (payload.sessions) |session| {
65+ std.debug.print("{s:<20} {s:<12} {d:<8} {s}\n", .{ session.name, session.status, session.clients, session.created_at });
66 }
67 }
+6,
-4
1@@ -145,7 +145,8 @@ pub fn writeJson(allocator: std.mem.Allocator, fd: posix.fd_t, msg_type: Message
2 .payload = payload,
3 };
4
5- try std.json.stringify(msg, .{}, &out.writer);
6+ var stringify: std.json.Stringify = .{ .writer = &out.writer };
7+ try stringify.write(msg);
8 try out.writer.writeByte('\n');
9
10 _ = try posix.write(fd, out.written());
11@@ -167,9 +168,10 @@ pub fn parseMessage(comptime T: type, allocator: std.mem.Allocator, line: []cons
12 }
13
14 // Helper to parse just the message type from a line (for dispatching)
15-pub fn parseMessageType(allocator: std.mem.Allocator, line: []const u8) !std.json.Parsed(struct { type: []const u8 }) {
16+const MessageTypeOnly = struct { type: []const u8 };
17+pub fn parseMessageType(allocator: std.mem.Allocator, line: []const u8) !std.json.Parsed(MessageTypeOnly) {
18 return try std.json.parseFromSlice(
19- struct { type: []const u8 },
20+ MessageTypeOnly,
21 allocator,
22 line,
23 .{ .ignore_unknown_fields = true },
24@@ -250,7 +252,7 @@ pub fn readBinaryFrame(allocator: std.mem.Allocator, fd: posix.fd_t) !struct { f
25 const read_len = try posix.read(fd, &header_bytes);
26 if (read_len != @sizeOf(FrameHeader)) return error.IncompleteFrame;
27
28- const header: *const FrameHeader = @alignCast(@ptrCast(&header_bytes));
29+ const header: *const FrameHeader = @ptrCast(@alignCast(&header_bytes));
30 const payload = try allocator.alloc(u8, header.length);
31 errdefer allocator.free(payload);
32