repos / zmx

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

commit
98995bd
parent
79fce5d
author
Eric Bower
date
2026-02-01 20:14:33 -0500 EST
chore: short should be session name only
2 files changed,  +110, -109
M CHANGELOG.md
+1, -0
1@@ -12,6 +12,7 @@ Use spec: https://common-changelog.org/
2 - New command `zmx [c]ompletions <shell>` that outputs auto-completion scripts for a given shell
3 - List command `zmx list` now shows `started_at` showing working directory when creating session
4 - List command `zmx list` now shows `cmd` showing command provided when creating session
5+- List command `zmx list` now shows `→` arrow indicating the current session
6 
7 ### Fixed
8 
M src/main.zig
+109, -109
  1@@ -505,44 +505,6 @@ const SessionEntry = struct {
  2 
  3 const current_arrow = "→";
  4 
  5-/// Formats a session entry for list output (only the name when `short` is
  6-/// true), adding a prefix to indicate the current session, if there is one.
  7-fn writeSessionLine(writer: *std.Io.Writer, session: SessionEntry, short: bool, current_session: ?[]const u8) !void {
  8-    const prefix = if (current_session) |current|
  9-        if (std.mem.eql(u8, current, session.name)) current_arrow ++ " " else "  "
 10-    else
 11-        "";
 12-
 13-    if (short) {
 14-        if (session.is_error) return;
 15-        try writer.print("{s}{s}\n", .{ prefix, session.name });
 16-        return;
 17-    }
 18-
 19-    if (session.is_error) {
 20-        try writer.print("{s}session_name={s}\tstatus={s}\t(cleaning up)\n", .{
 21-            prefix,
 22-            session.name,
 23-            session.error_name.?,
 24-        });
 25-        return;
 26-    }
 27-
 28-    try writer.print("{s}session_name={s}\tpid={d}\tclients={d}", .{
 29-        prefix,
 30-        session.name,
 31-        session.pid.?,
 32-        session.clients_len.?,
 33-    });
 34-    if (session.cwd) |cwd| {
 35-        try writer.print("\tstarted_in={s}", .{cwd});
 36-    }
 37-    if (session.cmd) |cmd| {
 38-        try writer.print("\tcmd={s}", .{cmd});
 39-    }
 40-    try writer.print("\n", .{});
 41-}
 42-
 43 fn list(cfg: *Cfg, short: bool) !void {
 44     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 45     defer _ = gpa.deinit();
 46@@ -1495,6 +1457,44 @@ fn getTerminalSize(fd: i32) ipc.Resize {
 47     return .{ .rows = 24, .cols = 80 };
 48 }
 49 
 50+/// Formats a session entry for list output (only the name when `short` is
 51+/// true), adding a prefix to indicate the current session, if there is one.
 52+fn writeSessionLine(writer: *std.Io.Writer, session: SessionEntry, short: bool, current_session: ?[]const u8) !void {
 53+    const prefix = if (current_session) |current|
 54+        if (std.mem.eql(u8, current, session.name)) current_arrow ++ " " else "  "
 55+    else
 56+        "";
 57+
 58+    if (short) {
 59+        if (session.is_error) return;
 60+        try writer.print("{s}\n", .{session.name});
 61+        return;
 62+    }
 63+
 64+    if (session.is_error) {
 65+        try writer.print("{s}session_name={s}\tstatus={s}\t(cleaning up)\n", .{
 66+            prefix,
 67+            session.name,
 68+            session.error_name.?,
 69+        });
 70+        return;
 71+    }
 72+
 73+    try writer.print("{s}session_name={s}\tpid={d}\tclients={d}", .{
 74+        prefix,
 75+        session.name,
 76+        session.pid.?,
 77+        session.clients_len.?,
 78+    });
 79+    if (session.cwd) |cwd| {
 80+        try writer.print("\tstarted_in={s}", .{cwd});
 81+    }
 82+    if (session.cmd) |cmd| {
 83+        try writer.print("\tcmd={s}", .{cmd});
 84+    }
 85+    try writer.print("\n", .{});
 86+}
 87+
 88 /// Detects Kitty keyboard protocol escape sequence for Ctrl+\
 89 /// 92 = backslash, 5 = ctrl modifier, :1 = key press event
 90 fn isKittyCtrlBackslash(buf: []const u8) bool {
 91@@ -1502,6 +1502,75 @@ fn isKittyCtrlBackslash(buf: []const u8) bool {
 92         std.mem.indexOf(u8, buf, "\x1b[92;5:1u") != null;
 93 }
 94 
 95+fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal) ?[]const u8 {
 96+    var builder: std.Io.Writer.Allocating = .init(alloc);
 97+    defer builder.deinit();
 98+
 99+    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, .vt);
100+    term_formatter.content = .{ .selection = null };
101+    term_formatter.extra = .{
102+        .palette = false,
103+        .modes = true,
104+        .scrolling_region = true,
105+        .tabstops = false, // tabstop restoration moves cursor after CUP, corrupting position
106+        .pwd = true,
107+        .keyboard = true,
108+        .screen = .all,
109+    };
110+
111+    term_formatter.format(&builder.writer) catch |err| {
112+        std.log.warn("failed to format terminal state err={s}", .{@errorName(err)});
113+        return null;
114+    };
115+
116+    const output = builder.writer.buffered();
117+    if (output.len == 0) return null;
118+
119+    return alloc.dupe(u8, output) catch |err| {
120+        std.log.warn("failed to allocate terminal state err={s}", .{@errorName(err)});
121+        return null;
122+    };
123+}
124+
125+fn serializeTerminal(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, format: HistoryFormat) ?[]const u8 {
126+    var builder: std.Io.Writer.Allocating = .init(alloc);
127+    defer builder.deinit();
128+
129+    const opts: ghostty_vt.formatter.Options = switch (format) {
130+        .plain => .plain,
131+        .vt => .vt,
132+        .html => .html,
133+    };
134+    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, opts);
135+    term_formatter.content = .{ .selection = null };
136+    term_formatter.extra = switch (format) {
137+        .plain => .none,
138+        .vt => .{
139+            .palette = false,
140+            .modes = true,
141+            .scrolling_region = true,
142+            .tabstops = false,
143+            .pwd = true,
144+            .keyboard = true,
145+            .screen = .all,
146+        },
147+        .html => .styles,
148+    };
149+
150+    term_formatter.format(&builder.writer) catch |err| {
151+        std.log.warn("failed to format terminal err={s}", .{@errorName(err)});
152+        return null;
153+    };
154+
155+    const output = builder.writer.buffered();
156+    if (output.len == 0) return null;
157+
158+    return alloc.dupe(u8, output) catch |err| {
159+        std.log.warn("failed to allocate terminal output err={s}", .{@errorName(err)});
160+        return null;
161+    };
162+}
163+
164 test "isKittyCtrlBackslash" {
165     try std.testing.expect(isKittyCtrlBackslash("\x1b[92;5u"));
166     try std.testing.expect(isKittyCtrlBackslash("\x1b[92;5:1u"));
167@@ -1551,13 +1620,13 @@ test "writeSessionLine formats output for current session and short output" {
168             .session = session,
169             .short = true,
170             .current_session = "dev",
171-            .expected = "→ dev\n",
172+            .expected = "dev\n",
173         },
174         .{
175             .session = session,
176             .short = true,
177             .current_session = "other",
178-            .expected = "  dev\n",
179+            .expected = "dev\n",
180         },
181         .{
182             .session = session,
183@@ -1575,72 +1644,3 @@ test "writeSessionLine formats output for current session and short output" {
184         try std.testing.expectEqualStrings(case.expected, builder.writer.buffered());
185     }
186 }
187-
188-fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal) ?[]const u8 {
189-    var builder: std.Io.Writer.Allocating = .init(alloc);
190-    defer builder.deinit();
191-
192-    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, .vt);
193-    term_formatter.content = .{ .selection = null };
194-    term_formatter.extra = .{
195-        .palette = false,
196-        .modes = true,
197-        .scrolling_region = true,
198-        .tabstops = false, // tabstop restoration moves cursor after CUP, corrupting position
199-        .pwd = true,
200-        .keyboard = true,
201-        .screen = .all,
202-    };
203-
204-    term_formatter.format(&builder.writer) catch |err| {
205-        std.log.warn("failed to format terminal state err={s}", .{@errorName(err)});
206-        return null;
207-    };
208-
209-    const output = builder.writer.buffered();
210-    if (output.len == 0) return null;
211-
212-    return alloc.dupe(u8, output) catch |err| {
213-        std.log.warn("failed to allocate terminal state err={s}", .{@errorName(err)});
214-        return null;
215-    };
216-}
217-
218-fn serializeTerminal(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal, format: HistoryFormat) ?[]const u8 {
219-    var builder: std.Io.Writer.Allocating = .init(alloc);
220-    defer builder.deinit();
221-
222-    const opts: ghostty_vt.formatter.Options = switch (format) {
223-        .plain => .plain,
224-        .vt => .vt,
225-        .html => .html,
226-    };
227-    var term_formatter = ghostty_vt.formatter.TerminalFormatter.init(term, opts);
228-    term_formatter.content = .{ .selection = null };
229-    term_formatter.extra = switch (format) {
230-        .plain => .none,
231-        .vt => .{
232-            .palette = false,
233-            .modes = true,
234-            .scrolling_region = true,
235-            .tabstops = false,
236-            .pwd = true,
237-            .keyboard = true,
238-            .screen = .all,
239-        },
240-        .html => .styles,
241-    };
242-
243-    term_formatter.format(&builder.writer) catch |err| {
244-        std.log.warn("failed to format terminal err={s}", .{@errorName(err)});
245-        return null;
246-    };
247-
248-    const output = builder.writer.buffered();
249-    if (output.len == 0) return null;
250-
251-    return alloc.dupe(u8, output) catch |err| {
252-        std.log.warn("failed to allocate terminal output err={s}", .{@errorName(err)});
253-        return null;
254-    };
255-}