repos / zmx

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

commit
79fce5d
parent
a32b80d
author
Paul Smith
date
2026-01-24 19:37:56 -0500 EST
feat: indicate current session in listing

This change adds a visual indicator (a prefixed arrow) of the current
session, if any, to session listings with `zmx [l]ist`.

Example:

```
$ zmx l
  session_name=o.quux	pid=38719	clients=0	started_in=/home/example
→ session_name=o.foo.bash	pid=60775	clients=2	started_in=/home/example
  session_name=o.foo.quux	pid=50707	clients=1	started_in=/home/example
  session_name=o.zmx	pid=21531	clients=2	started_in=/home/example
  session_name=o.zmx.bash	pid=17772	clients=1	started_in=/home/example
$ zmx l --short
  o.quux
→ o.foo.bash
  o.foo.quux
  o.zmx
  o.zmx.bash
```

If there is no current session, the output is unchanged.

Fixes #42.
1 files changed,  +113, -15
M src/main.zig
+113, -15
  1@@ -503,11 +503,57 @@ const SessionEntry = struct {
  2     }
  3 };
  4 
  5+const current_arrow = "→";
  6+
  7+/// Formats a session entry for list output (only the name when `short` is
  8+/// true), adding a prefix to indicate the current session, if there is one.
  9+fn writeSessionLine(writer: *std.Io.Writer, session: SessionEntry, short: bool, current_session: ?[]const u8) !void {
 10+    const prefix = if (current_session) |current|
 11+        if (std.mem.eql(u8, current, session.name)) current_arrow ++ " " else "  "
 12+    else
 13+        "";
 14+
 15+    if (short) {
 16+        if (session.is_error) return;
 17+        try writer.print("{s}{s}\n", .{ prefix, session.name });
 18+        return;
 19+    }
 20+
 21+    if (session.is_error) {
 22+        try writer.print("{s}session_name={s}\tstatus={s}\t(cleaning up)\n", .{
 23+            prefix,
 24+            session.name,
 25+            session.error_name.?,
 26+        });
 27+        return;
 28+    }
 29+
 30+    try writer.print("{s}session_name={s}\tpid={d}\tclients={d}", .{
 31+        prefix,
 32+        session.name,
 33+        session.pid.?,
 34+        session.clients_len.?,
 35+    });
 36+    if (session.cwd) |cwd| {
 37+        try writer.print("\tstarted_in={s}", .{cwd});
 38+    }
 39+    if (session.cmd) |cmd| {
 40+        try writer.print("\tcmd={s}", .{cmd});
 41+    }
 42+    try writer.print("\n", .{});
 43+}
 44+
 45 fn list(cfg: *Cfg, short: bool) !void {
 46     var gpa = std.heap.GeneralPurposeAllocator(.{}){};
 47     defer _ = gpa.deinit();
 48     const alloc = gpa.allocator();
 49 
 50+    const current_session = std.process.getEnvVarOwned(alloc, "ZMX_SESSION") catch |err| switch (err) {
 51+        error.EnvironmentVariableNotFound => null,
 52+        else => return err,
 53+    };
 54+    defer if (current_session) |name| alloc.free(name);
 55+
 56     var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{ .iterate = true });
 57     defer dir.close();
 58     var iter = dir.iterate();
 59@@ -578,21 +624,7 @@ fn list(cfg: *Cfg, short: bool) !void {
 60     std.mem.sort(SessionEntry, sessions.items, {}, SessionEntry.lessThan);
 61 
 62     for (sessions.items) |session| {
 63-        if (short) {
 64-            if (session.is_error) continue;
 65-            try w.interface.print("{s}\n", .{session.name});
 66-        } else if (session.is_error) {
 67-            try w.interface.print("session_name={s}\tstatus={s}\t(cleaning up)\n", .{ session.name, session.error_name.? });
 68-        } else {
 69-            try w.interface.print("session_name={s}\tpid={d}\tclients={d}", .{ session.name, session.pid.?, session.clients_len.? });
 70-            if (session.cwd) |cwd| {
 71-                try w.interface.print("\tstarted_in={s}", .{cwd});
 72-            }
 73-            if (session.cmd) |cmd| {
 74-                try w.interface.print("\tcmd={s}", .{cmd});
 75-            }
 76-            try w.interface.print("\n", .{});
 77-        }
 78+        try writeSessionLine(&w.interface, session, short, current_session);
 79         try w.interface.flush();
 80     }
 81 }
 82@@ -1478,6 +1510,72 @@ test "isKittyCtrlBackslash" {
 83     try std.testing.expect(!isKittyCtrlBackslash("garbage"));
 84 }
 85 
 86+test "writeSessionLine formats output for current session and short output" {
 87+    const Case = struct {
 88+        session: SessionEntry,
 89+        short: bool,
 90+        current_session: ?[]const u8,
 91+        expected: []const u8,
 92+    };
 93+
 94+    const session = SessionEntry{
 95+        .name = "dev",
 96+        .pid = 123,
 97+        .clients_len = 2,
 98+        .is_error = false,
 99+        .error_name = null,
100+        .cmd = null,
101+        .cwd = null,
102+    };
103+
104+    const cases = [_]Case{
105+        .{
106+            .session = session,
107+            .short = false,
108+            .current_session = "dev",
109+            .expected = "→ session_name=dev\tpid=123\tclients=2\n",
110+        },
111+        .{
112+            .session = session,
113+            .short = false,
114+            .current_session = "other",
115+            .expected = "  session_name=dev\tpid=123\tclients=2\n",
116+        },
117+        .{
118+            .session = session,
119+            .short = false,
120+            .current_session = null,
121+            .expected = "session_name=dev\tpid=123\tclients=2\n",
122+        },
123+        .{
124+            .session = session,
125+            .short = true,
126+            .current_session = "dev",
127+            .expected = "→ dev\n",
128+        },
129+        .{
130+            .session = session,
131+            .short = true,
132+            .current_session = "other",
133+            .expected = "  dev\n",
134+        },
135+        .{
136+            .session = session,
137+            .short = true,
138+            .current_session = null,
139+            .expected = "dev\n",
140+        },
141+    };
142+
143+    for (cases) |case| {
144+        var builder: std.Io.Writer.Allocating = .init(std.testing.allocator);
145+        defer builder.deinit();
146+
147+        try writeSessionLine(&builder.writer, case.session, case.short, case.current_session);
148+        try std.testing.expectEqualStrings(case.expected, builder.writer.buffered());
149+    }
150+}
151+
152 fn serializeTerminalState(alloc: std.mem.Allocator, term: *ghostty_vt.Terminal) ?[]const u8 {
153     var builder: std.Io.Writer.Allocating = .init(alloc);
154     defer builder.deinit();