Commit 9575587

Eric Bower  ·  2026-08-19 14:05:45 -0400 EDT
parent d201ba3
Revert "feat: term command to print terminal state as json"

This reverts commit df3d8a22a6c65a0fd8b63cfaadb4c200a0dcb1cc.
6 files changed,  +11, -371
+0, -1
......@@ -112,7 +112,6 @@ Commands:
112112 set <name> k=v ... Set session labels
113113 [un]set <name> key ... Remove session labels
114114 [cl]ear <name> Clear all session labels
115- term <name> Get terminal state as JSON
116115 [k]ill <name>... [--force] Kill session and all attached clients
117116 [hi]story <name> [--vt|--html] Output session scrollback
118117 [w]ait <name>... Wait for session tasks to complete
+4, -10
......@@ -32,7 +32,7 @@ const bash_completions =
3232 \\ cur="${COMP_WORDS[COMP_CWORD]}"
3333 \\ prev="${COMP_WORDS[COMP_CWORD-1]}"
3434 \\
35- \\ local commands="attach run send print write detach list kill history get set clear term wait tail completions version help"
35+ \\ local commands="attach run send print write detach list kill history get set clear wait tail completions version help"
3636 \\
3737 \\ if [[ $COMP_CWORD -eq 1 ]]; then
3838 \\ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
......@@ -40,7 +40,7 @@ const bash_completions =
4040 \\ fi
4141 \\
4242 \\ case "$prev" in
43- \\ attach|run|send|print|write|kill|history|get|set|clear|term|wait|tail)
43+ \\ attach|run|send|print|write|kill|history|get|set|clear|wait|tail)
4444 \\ local sessions=$(zmx list --short 2>/dev/null | tr '\n' ' ')
4545 \\ COMPREPLY=($(compgen -W "$sessions" -- "$cur"))
4646 \\ ;;
......@@ -89,7 +89,6 @@ const zsh_completions =
8989 \\ 'get:Get session labels'
9090 \\ 'set:Set session labels'
9191 \\ 'clear:Clear all session labels'
92- \\ 'term:Get terminal state as JSON'
9392 \\ 'version:Show version'
9493 \\ 'help:Show help message'
9594 \\ )
......@@ -97,7 +96,7 @@ const zsh_completions =
9796 \\ ;;
9897 \\ args)
9998 \\ case $words[2] in
100- \\ attach|a|kill|k|run|r|send|s|print|p|write|wr|history|get|g|set|clear|term|hi|wait|w|tail|t)
99+ \\ attach|a|kill|k|run|r|send|s|print|p|write|wr|history|get|g|set|clear|hi|wait|w|tail|t)
101100 \\ _zmx_sessions
102101 \\ ;;
103102 \\ completions|c)
......@@ -152,11 +151,10 @@ const fish_completions =
152151 \\complete -c zmx -n "__fish_is_nth_token 1" -a get -d 'Get session labels'
153152 \\complete -c zmx -n "__fish_is_nth_token 1" -a set -d 'Set session labels'
154153 \\complete -c zmx -n "__fish_is_nth_token 1" -a clear -d 'Clear all session labels'
155- \\complete -c zmx -n "__fish_is_nth_token 1" -a term -d 'Get terminal state as JSON'
156154 \\complete -c zmx -n "__fish_is_nth_token 1" -a help -d 'Show help message'
157155 \\
158156 \\# Complete session names and shells
159- \\complete -c zmx -n "__fish_is_nth_token 2; and __fish_seen_subcommand_from a attach r run s send p print wr write hi history g get se set cl clear term" -a '(zmx list --short 2>/dev/null)' -d 'Session name'
157+ \\complete -c zmx -n "__fish_is_nth_token 2; and __fish_seen_subcommand_from a attach r run s send p print wr write hi history g get se set cl clear" -a '(zmx list --short 2>/dev/null)' -d 'Session name'
160158 \\complete -c zmx -n "not __fish_is_nth_token 1; and __fish_seen_subcommand_from k kill w wait t tail" -a '(zmx list --short 2>/dev/null)' -d 'Session name'
161159 \\
162160 \\complete -c zmx -n "__fish_is_nth_token 2; and __fish_seen_subcommand_from c completions" -a 'bash zsh fish nu' -d Shell
......@@ -232,9 +230,5 @@ const nu_completions =
232230 \\ name?: string@"nu-complete zmx sessions"
233231 \\]
234232 \\
235- \\export extern "zmx term" [
236- \\ name?: string@"nu-complete zmx sessions"
237- \\]
238- \\
239233 \\export extern "zmx help" []
240234 ;
+6, -13
......@@ -23,8 +23,6 @@ pub const Tag = enum(u8) {
2323 LabelClear = 16,
2424 LabelData = 17,
2525 Send = 18,
26- TermGet = 19,
27- TermData = 20,
2826 // Non-exhaustive: this enum comes off the wire via bytesToValue and
2927 // @enumFromInt, so out-of-range values are representable
3028 // rather than UB. Switches must handle `_` (unknown tag).
......@@ -322,7 +320,7 @@ test "Tag wire values are frozen" {
322320 .{ Tag.Run, 9 }, .{ Tag.Ack, 10 }, .{ Tag.Switch, 11 },
323321 .{ Tag.Write, 12 }, .{ Tag.TaskComplete, 13 }, .{ Tag.LabelGet, 14 },
324322 .{ Tag.LabelSet, 15 }, .{ Tag.LabelClear, 16 }, .{ Tag.LabelData, 17 },
325- .{ Tag.Send, 18 }, .{ Tag.TermGet, 19 }, .{ Tag.TermData, 20 },
323+ .{ Tag.Send, 18 },
326324 }) |p| try std.testing.expectEqual(@as(u8, p[1]), @intFromEnum(p[0]));
327325 }
328326
......@@ -346,18 +344,13 @@ pub fn roundTripForTag(
346344 var sb = SocketBuffer.init(alloc) catch return error.Unexpected;
347345 defer sb.deinit();
348346
349- while (true) {
350- const n = sb.read(fd) catch return error.Unexpected;
351- if (n == 0) return error.Unexpected;
347+ const n = sb.read(fd) catch return error.Unexpected;
348+ if (n == 0) return error.Unexpected;
352349
353- while (sb.next()) |msg| {
354- if (msg.header.tag == expected_tag) {
355- return alloc.dupe(u8, msg.payload) catch return error.Unexpected;
356- }
350+ while (sb.next()) |msg| {
351+ if (msg.header.tag == expected_tag) {
352+ return alloc.dupe(u8, msg.payload) catch return error.Unexpected;
357353 }
358-
359- const more = lib_posix.poll(&poll_fds, timeout_ms) catch break;
360- if (more == 0) return error.Timeout;
361354 }
362355 return error.Unexpected;
363356 }
+1, -18
......@@ -11,7 +11,6 @@ const Cfg = @import("cfg.zig");
1111 const signal = @import("signal.zig");
1212 const assert = std.debug.assert;
1313 const daemonize = @import("daemonize.zig");
14-const term_mod = @import("term.zig");
1514 const builtin = @import("builtin");
1615
1716 /// clientLoop sends ipc commands to its corresponding daemon. It uses poll() as its non-blocking
......@@ -474,10 +473,9 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
474473 .LabelGet => try daemon.handleLabelGet(gpa, client),
475474 .LabelSet => try daemon.handleLabelSet(gpa, client, msg.payload),
476475 .LabelClear => try daemon.handleLabelClear(gpa, client),
477- .TermGet => try daemon.handleTermGet(gpa, client, &term),
478476 .History => try daemon.handleHistory(gpa, client, &term, msg.payload),
479477 .Run => try daemon.handleRun(gpa, io, client, msg.payload),
480- .Ack, .TaskComplete, .LabelData, .TermData => {},
478+ .Ack, .TaskComplete, .LabelData => {},
481479 .Write => try daemon.handleWrite(gpa, client, msg.payload),
482480 _ => std.log.warn(
483481 "ignoring unknown IPC tag={d}",
......@@ -1304,21 +1302,6 @@ pub const Daemon = struct {
13041302 try ipc.appendMessage(gpa, &client.write_buf, .Ack, "");
13051303 client.has_pending_output = true;
13061304 }
1307-
1308- fn handleTermGet(self: *Daemon, gpa: std.mem.Allocator, client: *Client, term: *ghostty_vt.Terminal) !void {
1309- self.setPwd(term);
1310- var info = std.mem.zeroes(ipc.Info);
1311- info.clients_len = if (self.clients.items.len > 0) self.clients.items.len - 1 else 0;
1312- info.pid = self.pid;
1313- info.created_at = self.created_at;
1314- info.task_ended_at = self.task_ended_at orelse 0;
1315- info.task_exit_code = self.task_exit_code orelse 0;
1316-
1317- const json = try term_mod.dumpState(gpa, term, &info);
1318- defer gpa.free(json);
1319- try ipc.appendMessage(gpa, &client.write_buf, .TermData, json);
1320- client.has_pending_output = true;
1321- }
13221305 };
13231306
13241307 test "send queues PTY input without changing leader" {
+0, -48
......@@ -91,12 +91,6 @@ pub fn main(init: std.process.Init) !void {
9191 const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_name);
9292 defer gpa.free(sesh);
9393 return labelClear(gpa, io, &cfg, sesh);
94- } else if (std.mem.eql(u8, cmd, "term")) {
95- const sesh_name = args.next() orelse return error.SessionNameRequired;
96- if (detectHelp(sesh_name)) return help(io);
97- const sesh = try socket.resolveSessionOrEnv(gpa, io, sesh_name);
98- defer gpa.free(sesh);
99- return termGet(gpa, io, &cfg, sesh);
10094 } else if (std.mem.eql(u8, cmd, "completions") or std.mem.eql(u8, cmd, "c")) {
10195 const arg = args.next() orelse return;
10296 if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) {
......@@ -427,7 +421,6 @@ fn help(io: std.Io) !void {
427421 \\ [g]et <name> Get session labels
428422 \\ set <name> k=v ... Set session labels (k= to remove)
429423 \\ [cl]ear <name> Clear all session labels
430- \\ term <name> Get terminal state as JSON
431424 \\ [k]ill <name>... [--force] Kill session and all attached clients
432425 \\ [hi]story <name> [--vt|--html] Output session scrollback
433426 \\ [w]ait <name>... Wait for session tasks to complete
......@@ -1136,47 +1129,6 @@ fn labelClear(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []c
11361129 };
11371130 }
11381131
1139-fn printTermError(io: std.Io, session_name: []const u8, err: anyerror) noreturn {
1140- var buf: [4096]u8 = undefined;
1141- var w = std.Io.File.stderr().writer(io, &buf);
1142- switch (err) {
1143- error.Timeout => w.interface.print(
1144- "error: session \"{s}\" does not support term commands (daemon too old?)\n",
1145- .{session_name},
1146- ) catch {},
1147- error.ConnectionRefused, error.Unexpected => w.interface.print(
1148- "error: session \"{s}\" not found or unresponsive\n",
1149- .{session_name},
1150- ) catch {},
1151- else => w.interface.print(
1152- "error: {s}\n",
1153- .{@errorName(err)},
1154- ) catch {},
1155- }
1156- w.interface.flush() catch {};
1157- std.process.exit(1);
1158-}
1159-
1160-fn termGet(alloc: std.mem.Allocator, io: std.Io, cfg: *Cfg, session_name: []const u8) !void {
1161- std.log.info("term get session={s}", .{session_name});
1162-
1163- const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
1164- error.NameTooLong => return socket.printSessionNameTooLong(io, session_name, cfg.socket_dir),
1165- error.OutOfMemory => return err,
1166- };
1167- defer alloc.free(socket_path);
1168-
1169- const payload = ipc.roundTripForTag(alloc, socket_path, .TermGet, "", .TermData) catch |err| {
1170- printTermError(io, session_name, err);
1171- };
1172- defer alloc.free(payload);
1173-
1174- var buf: [4096]u8 = undefined;
1175- var stdout = std.Io.File.stdout().writer(io, &buf);
1176- try stdout.interface.print("{s}", .{payload});
1177- try stdout.interface.flush();
1178-}
1179-
11801132 /// Fetch terminal history from a session socket, returning it as an allocated
11811133 /// string. Caller owns the returned memory and must free it.
11821134 fn fetchHistory(
+0, -281
......@@ -1,281 +0,0 @@
1-const std = @import("std");
2-const ghostty_vt = @import("ghostty-vt");
3-const ipc = @import("ipc.zig");
4-const Allocator = std.mem.Allocator;
5-
6-pub fn dumpState(alloc: Allocator, term: *const ghostty_vt.Terminal, daemon_info: ?*const ipc.Info) ![]u8 {
7- var out: std.Io.Writer.Allocating = .init(alloc);
8- defer out.deinit();
9-
10- var jw: std.json.Stringify = .{
11- .writer = &out.writer,
12- .options = .{ .whitespace = .indent_2 },
13- };
14-
15- try jw.beginObject();
16-
17- // meta
18- try jw.objectField("meta");
19- try jw.beginObject();
20- try jw.objectField("title");
21- try jw.write(term.getTitle() orelse "");
22- try jw.objectField("pwd");
23- try jw.write(term.getPwd() orelse "");
24- if (daemon_info) |info| {
25- try jw.objectField("pid");
26- try jw.write(info.pid);
27- try jw.objectField("clients");
28- try jw.write(info.clients_len);
29- }
30- try jw.endObject();
31-
32- // geometry
33- try jw.objectField("geometry");
34- try jw.beginObject();
35- try jw.objectField("rows");
36- try jw.write(term.rows);
37- try jw.objectField("cols");
38- try jw.write(term.cols);
39- try jw.objectField("width_px");
40- try jw.write(term.width_px);
41- try jw.objectField("height_px");
42- try jw.write(term.height_px);
43- try jw.objectField("scrolling_region");
44- try jw.beginObject();
45- try jw.objectField("top");
46- try jw.write(term.scrolling_region.top);
47- try jw.objectField("bottom");
48- try jw.write(term.scrolling_region.bottom);
49- try jw.endObject();
50- try jw.endObject();
51-
52- // cursor
53- const active_screen = term.screens.active;
54- try jw.objectField("cursor");
55- try jw.beginObject();
56- try jw.objectField("x");
57- try jw.write(active_screen.cursor.x);
58- try jw.objectField("y");
59- try jw.write(active_screen.cursor.y);
60- try jw.objectField("style");
61- try jw.write(@tagName(active_screen.cursor.cursor_style));
62- try jw.objectField("blinking");
63- try jw.write(term.modes.get(.cursor_blinking));
64- try jw.objectField("visible");
65- try jw.write(term.modes.get(.cursor_visible));
66- try jw.endObject();
67-
68- // colors
69- try jw.objectField("colors");
70- try jw.beginObject();
71- if (term.colors.background.get()) |bg| {
72- var buf: [7]u8 = undefined;
73- const hex = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ bg.r, bg.g, bg.b }) catch unreachable;
74- try jw.objectField("background");
75- try jw.write(hex);
76- try jw.objectField("appearance");
77- const lum = @as(f32, @floatFromInt(bg.r)) * 0.299 +
78- @as(f32, @floatFromInt(bg.g)) * 0.587 +
79- @as(f32, @floatFromInt(bg.b)) * 0.114;
80- if (lum < 128.0) {
81- try jw.write("dark");
82- } else {
83- try jw.write("light");
84- }
85- } else {
86- try jw.objectField("background");
87- try jw.write(null);
88- try jw.objectField("appearance");
89- try jw.write("dark");
90- }
91-
92- if (term.colors.foreground.get()) |fg| {
93- var buf: [7]u8 = undefined;
94- const hex = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ fg.r, fg.g, fg.b }) catch unreachable;
95- try jw.objectField("foreground");
96- try jw.write(hex);
97- } else {
98- try jw.objectField("foreground");
99- try jw.write(null);
100- }
101-
102- if (term.colors.cursor.get()) |c| {
103- var buf: [7]u8 = undefined;
104- const hex = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ c.r, c.g, c.b }) catch unreachable;
105- try jw.objectField("cursor");
106- try jw.write(hex);
107- } else {
108- try jw.objectField("cursor");
109- try jw.write(null);
110- }
111-
112- // palette
113- try jw.objectField("palette");
114- try jw.beginArray();
115- for (term.colors.palette.current) |c| {
116- var buf: [7]u8 = undefined;
117- const hex = std.fmt.bufPrint(&buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ c.r, c.g, c.b }) catch unreachable;
118- try jw.write(hex);
119- }
120- try jw.endArray();
121- try jw.endObject();
122-
123- // modes
124- try jw.objectField("modes");
125- try jw.beginObject();
126- try jw.objectField("alt_screen");
127- try jw.write(term.screens.active_key == .alternate);
128- inline for (std.meta.fields(ghostty_vt.modes.Mode)) |f| {
129- const mode = @field(ghostty_vt.modes.Mode, f.name);
130- try jw.objectField(f.name);
131- try jw.write(term.modes.get(mode));
132- }
133- try jw.endObject();
134-
135- // mouse
136- try jw.objectField("mouse");
137- try jw.beginObject();
138- try jw.objectField("shape");
139- try jw.write(@tagName(term.mouse_shape));
140- try jw.objectField("event");
141- try jw.write(@tagName(term.flags.mouse_event));
142- try jw.objectField("format");
143- try jw.write(@tagName(term.flags.mouse_format));
144- try jw.objectField("shift_capture");
145- try jw.write(@tagName(term.flags.mouse_shift_capture));
146- try jw.endObject();
147-
148- // keyboard
149- try jw.objectField("keyboard");
150- try jw.beginObject();
151- const kflags = active_screen.kitty_keyboard.current();
152- try jw.objectField("kitty_flags");
153- try jw.write(kflags.int());
154- try jw.objectField("kitty_disambiguate");
155- try jw.write(kflags.disambiguate);
156- try jw.objectField("kitty_report_events");
157- try jw.write(kflags.report_events);
158- try jw.objectField("kitty_report_alternates");
159- try jw.write(kflags.report_alternates);
160- try jw.objectField("kitty_report_all");
161- try jw.write(kflags.report_all);
162- try jw.objectField("kitty_report_associated");
163- try jw.write(kflags.report_associated);
164- try jw.objectField("modify_other_keys_2");
165- try jw.write(term.flags.modify_other_keys_2);
166- try jw.endObject();
167-
168- // buffer
169- try jw.objectField("buffer");
170- try jw.beginObject();
171- var total_rows: usize = 0;
172- var page_it = active_screen.pages.pages.first;
173- while (page_it) |node| : (page_it = node.next) {
174- total_rows += node.rows();
175- }
176- const scrollback_rows = total_rows - @min(total_rows, @as(usize, active_screen.pages.rows));
177- try jw.objectField("scrollback_rows");
178- try jw.write(scrollback_rows);
179- try jw.objectField("has_saved_cursor");
180- try jw.write(active_screen.saved_cursor != null);
181- if (active_screen.saved_cursor) |sc| {
182- try jw.objectField("saved_cursor");
183- try jw.beginObject();
184- try jw.objectField("x");
185- try jw.write(sc.x);
186- try jw.objectField("y");
187- try jw.write(sc.y);
188- try jw.objectField("origin");
189- try jw.write(sc.origin);
190- try jw.objectField("pending_wrap");
191- try jw.write(sc.pending_wrap);
192- try jw.endObject();
193- }
194- try jw.objectField("has_selection");
195- try jw.write(active_screen.selection != null);
196- try jw.objectField("protected_mode");
197- try jw.write(@tagName(active_screen.protected_mode));
198- try jw.endObject();
199-
200- // shell_integration
201- try jw.objectField("shell_integration");
202- try jw.beginObject();
203- try jw.objectField("semantic_prompt");
204- try jw.write(active_screen.semantic_prompt.seen);
205- try jw.objectField("shell_redraws_prompt");
206- try jw.write(@tagName(term.flags.shell_redraws_prompt));
207- try jw.objectField("password_input");
208- try jw.write(term.flags.password_input);
209- try jw.endObject();
210-
211- // tabstops
212- try jw.objectField("tabstops");
213- try jw.beginArray();
214- var col: usize = 0;
215- while (col < term.cols) : (col += 1) {
216- if (term.tabstops.get(col)) {
217- try jw.write(col);
218- }
219- }
220- try jw.endArray();
221-
222- // charsets
223- try jw.objectField("charsets");
224- try jw.beginObject();
225- try jw.objectField("g0");
226- try jw.write(@tagName(active_screen.charset.charsets.g0));
227- try jw.objectField("g1");
228- try jw.write(@tagName(active_screen.charset.charsets.g1));
229- try jw.objectField("g2");
230- try jw.write(@tagName(active_screen.charset.charsets.g2));
231- try jw.objectField("g3");
232- try jw.write(@tagName(active_screen.charset.charsets.g3));
233- try jw.objectField("gl");
234- try jw.write(@tagName(active_screen.charset.gl));
235- try jw.objectField("gr");
236- try jw.write(@tagName(active_screen.charset.gr));
237- try jw.endObject();
238-
239- // flags
240- try jw.objectField("flags");
241- try jw.beginObject();
242- try jw.objectField("focused");
243- try jw.write(term.flags.focused);
244- try jw.endObject();
245-
246- try jw.endObject();
247-
248- try out.writer.writeAll("\n");
249- return alloc.dupe(u8, out.written());
250-}
251-
252-test "dumpState generates valid json with terminal state" {
253- const testing = std.testing;
254- const alloc = testing.allocator;
255- const io = testing.io;
256-
257- var term = try ghostty_vt.Terminal.init(io, alloc, .{
258- .cols = 80,
259- .rows = 24,
260- });
261- defer term.deinit(alloc);
262-
263- try term.setTitle("my window");
264- try term.setPwd("/home/user");
265-
266- const json = try dumpState(alloc, &term, null);
267- defer alloc.free(json);
268-
269- try testing.expect(std.mem.indexOf(u8, json, "\"title\": \"my window\"") != null);
270- try testing.expect(std.mem.indexOf(u8, json, "\"pwd\": \"/home/user\"") != null);
271- try testing.expect(std.mem.indexOf(u8, json, "\"rows\": 24") != null);
272- try testing.expect(std.mem.indexOf(u8, json, "\"cols\": 80") != null);
273- try testing.expect(std.mem.indexOf(u8, json, "\"cursor\"") != null);
274- try testing.expect(std.mem.indexOf(u8, json, "\"colors\"") != null);
275- try testing.expect(std.mem.indexOf(u8, json, "\"modes\"") != null);
276- try testing.expect(std.mem.indexOf(u8, json, "\"keyboard\"") != null);
277- try testing.expect(std.mem.indexOf(u8, json, "\"buffer\"") != null);
278- try testing.expect(std.mem.indexOf(u8, json, "\"shell_integration\"") != null);
279- try testing.expect(std.mem.indexOf(u8, json, "\"tabstops\"") != null);
280- try testing.expect(std.mem.indexOf(u8, json, "\"charsets\"") != null);
281-}