Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/daemon.zig
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,66 @@ pub const Daemon = struct {
}
}
conn.send(.ok, out.items);
} else if (std.mem.eql(u8, cmd, "inspect")) {
// A richer report than `info`, for `boo inspect`: one
// key<TAB>value per line. The client splits each line on its
// first tab only, so cwd, command, and title values may
// themselves contain spaces.
var clients: usize = 0;
for (self.conns.items) |c| {
if (c.attached and !c.closed) clients += 1;
}
const w = self.liveWindow();
const idle: i64 = @max(0, now - self.last_activity_ms);
const out_idle: i64 = if (w) |lw| @max(0, now - lw.last_output_ms) else 0;
const pid: posix.pid_t = if (w) |lw| lw.child_pid else 0;
const screen: []const u8 = if (w) |lw|
(if (lw.onAltScreen()) "alt" else "primary")
else
"primary";

var out: std.ArrayList(u8) = .empty;
defer out.deinit(self.alloc);
try out.print(self.alloc, "name\t{s}\n", .{self.owned_name orelse self.opts.name});
try out.print(self.alloc, "state\t{s}\n", .{if (clients > 0) "Attached" else "Detached"});
try out.print(self.alloc, "clients\t{d}\n", .{clients});
try out.print(self.alloc, "pid\t{d}\n", .{pid});

// command: the requested argv, or the resolved shell for a
// session started with no command.
try out.appendSlice(self.alloc, "command\t");
if (self.opts.argv.len > 0) {
for (self.opts.argv, 0..) |a, i| {
if (i > 0) try out.append(self.alloc, ' ');
try out.appendSlice(self.alloc, a);
}
} else if (w) |lw| {
try out.appendSlice(self.alloc, lw.command_title);
}
try out.append(self.alloc, '\n');

try out.appendSlice(self.alloc, "cwd\t");
if (w) |lw| {
var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
if (cwd.ofPid(&cwd_buf, lw.child_pid)) |d| try out.appendSlice(self.alloc, d);
}
try out.append(self.alloc, '\n');

try out.print(self.alloc, "rows\t{d}\n", .{self.rows});
try out.print(self.alloc, "cols\t{d}\n", .{self.cols});
try out.print(self.alloc, "idle_ms\t{d}\n", .{idle});
try out.print(self.alloc, "out_idle_ms\t{d}\n", .{out_idle});
try out.print(self.alloc, "screen\t{s}\n", .{screen});

// title last; sanitized, so it carries no tabs or newlines.
try out.appendSlice(self.alloc, "title\t");
if (w) |lw| {
for (lw.title()) |byte| {
if (byte < 0x20 or byte == 0x7f) continue;
try out.append(self.alloc, byte);
}
}
conn.send(.ok, out.items);
} else if (std.mem.eql(u8, cmd, "cwd")) {
// Report the session command's current working directory so
// a new session created from `boo ui` can be born there.
Expand Down
16 changes: 16 additions & 0 deletions src/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub const overview =
\\ attach, at, a <name> attach a session (steals politely)
\\ ui, i manage sessions in a full-screen UI
\\ ls [--json] list sessions
\\ inspect <name> [--json] show one session in detail
\\
\\ Interaction
\\ send <name> [flags] type into a session
Expand Down Expand Up @@ -195,6 +196,21 @@ pub const commands = [_]Entry{
\\
,
},
.{
.name = "inspect",
.body =
\\usage: boo inspect <name> [--json]
\\
\\Show one session in detail: name, attach state and client
\\count, child PID, command, working directory, terminal size,
\\idle time (and time since last output), screen mode, title, and
\\socket path. Accepts a unique name prefix.
\\
\\flags:
\\ --json emit a single JSON object with the same fields
\\
,
},
.{
.name = "send",
.body =
Expand Down
108 changes: 108 additions & 0 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub fn main() !void {
if (eql(cmd, "attach") or eql(cmd, "at") or eql(cmd, "a")) return cmdAttach(alloc, rest);
if (eql(cmd, "ui") or eql(cmd, "i")) return cmdUi(alloc, rest);
if (eql(cmd, "ls") or eql(cmd, "list")) return cmdLs(alloc, rest);
if (eql(cmd, "inspect")) return cmdInspect(alloc, rest);
if (eql(cmd, "send")) return cmdSend(alloc, rest);
if (eql(cmd, "peek")) return cmdPeek(alloc, rest);
if (eql(cmd, "wait")) return cmdWait(alloc, rest);
Expand Down Expand Up @@ -499,6 +500,113 @@ fn cmdLs(alloc: std.mem.Allocator, args: []const [:0]const u8) !void {
try stdoutWrite(out.items);
}

fn cmdInspect(alloc: std.mem.Allocator, args: []const [:0]const u8) !void {
var json = false;
var name_arg: ?[]const u8 = null;
for (args) |arg| {
if (isHelpFlag(arg)) return printHelpPage("inspect");
if (std.mem.eql(u8, arg, "--json")) {
json = true;
} else if (arg.len > 0 and arg[0] == '-') {
usageFail("inspect", "unknown flag '{s}'", .{arg});
} else if (name_arg == null) {
name_arg = arg;
} else {
usageFail("inspect", "unexpected argument '{s}'", .{arg});
}
}
const want = name_arg orelse usageFail("inspect", "usage: boo inspect <name> [--json]", .{});

const dir = try paths.socketDir(alloc);
defer alloc.free(dir);
const name = try resolveSession(alloc, dir, want);
defer alloc.free(name);

const result = try mustControl(alloc, dir, name, &.{"inspect"});
defer alloc.free(result.text);
if (!result.ok) fail(exit_runtime, "inspect failed: {s}", .{result.text});

const sock = try paths.socketPath(alloc, dir, name);
defer alloc.free(sock);

if (json) {
try inspectJson(alloc, result.text, sock);
} else {
try inspectHuman(alloc, result.text, sock);
}
}

/// Look up a value in the daemon's key<TAB>value-per-line inspect
/// payload. Each line is split on its first tab only, so a value may
/// itself contain spaces or tabs. Returns "" when the key is absent.
fn inspectField(text: []const u8, key: []const u8) []const u8 {
var lines = std.mem.splitScalar(u8, text, '\n');
while (lines.next()) |line| {
const tab = std.mem.indexOfScalar(u8, line, '\t') orelse continue;
if (std.mem.eql(u8, line[0..tab], key)) return line[tab + 1 ..];
}
return "";
}

fn inspectHuman(alloc: std.mem.Allocator, text: []const u8, sock: []const u8) !void {
var out: std.ArrayList(u8) = .empty;
defer out.deinit(alloc);

const state: []const u8 = if (std.mem.eql(u8, inspectField(text, "state"), "Attached"))
"attached"
else
"detached";
const idle_ms = std.fmt.parseInt(i64, inspectField(text, "idle_ms"), 10) catch 0;
const out_idle_ms = std.fmt.parseInt(i64, inspectField(text, "out_idle_ms"), 10) catch 0;
var ibuf: [32]u8 = undefined;
var obuf: [32]u8 = undefined;

try out.print(alloc, "Session: {s}\n", .{inspectField(text, "name")});
try out.print(alloc, "State: {s}\n", .{state});
try out.print(alloc, "Clients: {s}\n", .{inspectField(text, "clients")});
try out.print(alloc, "PID: {s}\n", .{inspectField(text, "pid")});
try out.print(alloc, "Command: {s}\n", .{inspectField(text, "command")});
try out.print(alloc, "Directory: {s}\n", .{inspectField(text, "cwd")});
try out.print(alloc, "Size: {s}x{s}\n", .{ inspectField(text, "rows"), inspectField(text, "cols") });
try out.print(alloc, "Idle: {s} (output {s})\n", .{ fmtIdle(&ibuf, idle_ms), fmtIdle(&obuf, out_idle_ms) });
try out.print(alloc, "Screen: {s}\n", .{inspectField(text, "screen")});
try out.print(alloc, "Title: {s}\n", .{inspectField(text, "title")});
try out.print(alloc, "Socket: {s}\n", .{sock});
try stdoutWrite(out.items);
}

fn inspectJson(alloc: std.mem.Allocator, text: []const u8, sock: []const u8) !void {
var out: std.ArrayList(u8) = .empty;
defer out.deinit(alloc);

const attached = std.mem.eql(u8, inspectField(text, "state"), "Attached");
const clients = std.fmt.parseInt(i64, inspectField(text, "clients"), 10) catch 0;
const pid = std.fmt.parseInt(i64, inspectField(text, "pid"), 10) catch 0;
const rows = std.fmt.parseInt(i64, inspectField(text, "rows"), 10) catch 0;
const cols = std.fmt.parseInt(i64, inspectField(text, "cols"), 10) catch 0;
const idle_ms = std.fmt.parseInt(i64, inspectField(text, "idle_ms"), 10) catch 0;
const out_idle_ms = std.fmt.parseInt(i64, inspectField(text, "out_idle_ms"), 10) catch 0;

try out.appendSlice(alloc, "{\"name\":");
try appendJsonString(alloc, &out, inspectField(text, "name"));
const head = try std.fmt.allocPrint(alloc, ",\"attached\":{},\"clients\":{d},\"pid\":{d},\"command\":", .{ attached, clients, pid });
defer alloc.free(head);
try out.appendSlice(alloc, head);
try appendJsonString(alloc, &out, inspectField(text, "command"));
try out.appendSlice(alloc, ",\"cwd\":");
try appendJsonString(alloc, &out, inspectField(text, "cwd"));
const mid = try std.fmt.allocPrint(alloc, ",\"rows\":{d},\"cols\":{d},\"idle_ms\":{d},\"out_idle_ms\":{d},\"screen\":", .{ rows, cols, idle_ms, out_idle_ms });
defer alloc.free(mid);
try out.appendSlice(alloc, mid);
try appendJsonString(alloc, &out, inspectField(text, "screen"));
try out.appendSlice(alloc, ",\"title\":");
try appendJsonString(alloc, &out, inspectField(text, "title"));
try out.appendSlice(alloc, ",\"socket\":");
try appendJsonString(alloc, &out, sock);
try out.appendSlice(alloc, "}\n");
try stdoutWrite(out.items);
}

fn cutTab(rest: *[]const u8) ?[]const u8 {
const idx = std.mem.indexOfScalar(u8, rest.*, '\t') orelse return null;
const field = rest.*[0..idx];
Expand Down
72 changes: 72 additions & 0 deletions test/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,78 @@ test "ui: a session that rang the bell is marked your turn" {
try ui.waitFor("\u{25CF}");
}

test "inspect: shows detailed fields for a session" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
defer h.deinit();

try h.startDetached("work", &.{"cat"});

const res = try h.run(&.{ "inspect", "work" });
defer alloc.free(res.stdout);
defer alloc.free(res.stderr);
try std.testing.expect(res.term == .Exited and res.term.Exited == 0);

// The detailed view names the session, its command, its working
// directory (absolute), and the screen mode a plain 'cat' is in.
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "work") != null);
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "cat") != null);
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "detached") != null);
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "primary") != null);
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "/") != null);
}

test "inspect: --json emits the session's fields" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
defer h.deinit();

try h.startDetached("js", &.{"cat"});

const res = try h.run(&.{ "inspect", "js", "--json" });
defer alloc.free(res.stdout);
defer alloc.free(res.stderr);
try std.testing.expect(res.term == .Exited and res.term.Exited == 0);

var parsed = try std.json.parseFromSlice(std.json.Value, alloc, res.stdout, .{});
defer parsed.deinit();
const obj = parsed.value.object;
try std.testing.expectEqualStrings("js", obj.get("name").?.string);
try std.testing.expectEqual(false, obj.get("attached").?.bool);
try std.testing.expectEqual(@as(i64, 0), obj.get("clients").?.integer);
try std.testing.expect(obj.get("pid").?.integer > 0);
try std.testing.expectEqual(@as(i64, 24), obj.get("rows").?.integer);
try std.testing.expectEqual(@as(i64, 80), obj.get("cols").?.integer);
try std.testing.expectEqualStrings("cat", obj.get("command").?.string);
try std.testing.expectEqualStrings("primary", obj.get("screen").?.string);
try std.testing.expect(obj.get("idle_ms").?.integer >= 0);
try std.testing.expect(obj.get("out_idle_ms").?.integer >= 0);
try std.testing.expect(std.mem.startsWith(u8, obj.get("cwd").?.string, "/"));
try std.testing.expectEqualStrings("cat", obj.get("title").?.string);
}

test "inspect: unknown session exits 3" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
defer h.deinit();

try h.runExit(&.{ "inspect", "ghost" }, 3);
}

test "inspect: resolves a unique prefix" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
defer h.deinit();

try h.startDetached("alpha", &.{"cat"});

const res = try h.run(&.{ "inspect", "al" });
defer alloc.free(res.stdout);
defer alloc.free(res.stderr);
try std.testing.expect(res.term == .Exited and res.term.Exited == 0);
try std.testing.expect(std.mem.indexOf(u8, res.stdout, "alpha") != null);
}

test "peek --json includes geometry, cursor, and screen content" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
Expand Down