summaryrefslogtreecommitdiff
path: root/src/exec.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/exec.zig')
-rw-r--r--src/exec.zig122
1 files changed, 95 insertions, 27 deletions
diff --git a/src/exec.zig b/src/exec.zig
index 2faf84de..07d5e92f 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -30,7 +30,8 @@ const Pardes = pardes.Pardes;
pub fn takesCommandLine(p: *const Pardes, id: usize) bool {
const pane = p.panes[id] orelse return false;
- if (!pane.isTerminal()) return false;
+ // A command pane's child is its command, never a shell to type at.
+ if (!pane.isTerminal() or pane.command != null) return false;
// A mounted shell sits behind sudo's process supervisor. Its OSC 133
// prompt marks identify input readiness; the launcher's executable is
// not the shell executable used by the ordinary process heuristic.
@@ -368,26 +369,16 @@ pub fn replaceStillborn(p: *Pardes) void {
if (was_active) p.active = new_id;
}
-fn ttyForDir(p: *Pardes, dir: []const u8) ?usize {
- for (p.panes, 0..) |slot, i| if (slot) |tt| {
- if (!std.mem.eql(u8, tt.cwdSlice(), dir)) continue;
- if (takesCommandLine(p, i)) return i;
- };
- const free = p.freeSlot() orelse return null;
- const nt = p.newShell(free, dir) catch return null;
- nt.greet = false;
- const rc = if (p.ncol > 0) p.ncol - 1 else 0;
- if (p.col_n[rc] > 0) {
- const src = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
- const f = layout.findPane(p, src).?;
- layout.splitBelow(p, src, nt);
- layout.insert(p, f.col, f.idx + 1, free);
- } else layout.insert(p, rc, p.col_n[rc], free);
- return free;
-}
+/// The longest line a command pane runs.
+pub const command_max = 1024;
const max_exec_depth = 8;
+/// A word no builtin knows (acme's exec.c run()). Clicked in an interactive
+/// terminal, it is typed into that shell, whose state the click can see;
+/// from anywhere else it runs as its own command pane, a terminal whose
+/// child is `$SHELL -c` the line, which shows its output and `exit N` when
+/// done, and which Kill signals. Answers the pane it went to.
pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
const pane = p.panes[id] orelse return null;
const cmd = commandText(txt);
@@ -397,16 +388,93 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
if (p.exec_depth >= max_exec_depth) return null;
p.exec_depth += 1;
defer p.exec_depth -= 1;
- const dst = (if (takesCommandLine(p, id)) id else ttyForDir(p, Pardes.paneDir(pane))) orelse return null;
- panes.Terminal.padOutputBelowEdits(p, dst);
- panes.Terminal.noteCommand(p.panes[dst].?, cmd);
- if (panes.Terminal.queuePendingCommand(p.panes[dst].?, cmd) catch |err| {
+ if (!takesCommandLine(p, id)) return runCommand(p, id, cmd);
+ panes.Terminal.padOutputBelowEdits(p, id);
+ panes.Terminal.noteCommand(pane, cmd);
+ if (panes.Terminal.queuePendingCommand(pane, cmd) catch |err| {
p.reportError(id, "queue command", err);
- return dst;
- }) return dst;
- p.emitWrite(dst, cmd);
- p.emitWrite(dst, "\r");
- return dst;
+ return id;
+ }) return id;
+ p.emitWrite(id, cmd);
+ p.emitWrite(id, "\r");
+ return id;
+}
+
+/// Runs `line` in a command pane for `from`'s directory: `from` itself if
+/// it is one whose command is done, else the directory's command pane that
+/// is done, else a new one below the last pane of the rightmost column.
+/// A pane reused keeps what it showed: the new command runs below a line
+/// naming it, as acme appends to +Errors and never clears it (util.c:213).
+fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
+ if (line.len > command_max) {
+ p.reportFailure(from, "exec: a command line is at most 1024 bytes");
+ return null;
+ }
+ const src = p.panes[from] orelse return null;
+ const dir = Pardes.paneDir(src);
+ const reuse: ?usize = if (src.command != null and src.command_done) from else for (p.panes, 0..) |slot, i| {
+ const other = slot orelse continue;
+ if (other.command != null and other.command_done and std.mem.eql(u8, other.cwdSlice(), dir)) break i;
+ } else null;
+ if (reuse) |id| {
+ const pane = p.panes[id].?;
+ const owned = p.gpa.dupe(u8, line) catch |err| {
+ p.reportError(from, "exec", err);
+ return null;
+ };
+ p.gpa.free(pane.command.?);
+ pane.command = owned;
+ pane.command_done = false;
+ pane.command_status = null;
+ pane.body.mode = .tty;
+ var buf: [command_max + 8]u8 = undefined;
+ const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else "";
+ panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&buf, "{s}% {s}\r\n", .{ lead, line }) catch "%\r\n");
+ p.emit(.{ .spawn = .{ .pane = @intCast(id), .cwd = .from(pane.cwdSlice()) } });
+ noteRun(p, pane, "run", line);
+ return id;
+ }
+ const free = p.freeSlot() orelse {
+ p.reportFailure(from, "exec: no pane free for the command");
+ return null;
+ };
+ const pane = p.newCommand(free, dir, line) catch |err| {
+ p.reportError(from, "exec", err);
+ return null;
+ };
+ const rc = if (p.ncol > 0) p.ncol - 1 else 0;
+ if (p.col_n[rc] > 0) {
+ const parent = layout.splitParent(p, p.col_panes[rc][p.col_n[rc] - 1]);
+ const f = layout.findPane(p, parent).?;
+ layout.splitBelow(p, parent, pane);
+ layout.insert(p, f.col, f.idx + 1, free);
+ } else layout.insert(p, rc, p.col_n[rc], free);
+ noteRun(p, pane, "run", line);
+ return free;
+}
+
+/// `run <serial> <word>` or `exit <serial> <N|?>` in the log.
+fn noteRun(p: *Pardes, pane: *Pane, what: []const u8, said: []const u8) void {
+ var buf: [96]u8 = undefined;
+ const word = std.mem.trim(u8, said, " \t");
+ const first = word[0 .. std.mem.indexOfAny(u8, word, " \t") orelse word.len];
+ pardes.ctlfs.events.notePath(p, what, std.fmt.bufPrint(&buf, "{d} {s}", .{ pane.serial, first[0..@min(first.len, 64)] }) catch return);
+}
+
+/// A command pane's child is gone: the pane stays, showing what it said
+/// and how it ended, and is the next command's for its directory.
+pub fn commandDone(p: *Pardes, id: usize, status: ?u8) void {
+ const pane = p.panes[id] orelse return;
+ pane.command_done = true;
+ pane.command_status = status;
+ pane.body.mode = .normal;
+ var buf: [16]u8 = undefined;
+ const code = if (status) |n| std.fmt.bufPrint(&buf, "{d}", .{n}) catch "?" else "?";
+ var said: [32]u8 = undefined;
+ const lead = if (panes.Terminal.gridCursor(pane).x != 0) "\r\n" else "";
+ panes.Terminal.feedOutput(p, pane, std.fmt.bufPrint(&said, "{s}exit {s}\r\n", .{ lead, code }) catch "exit ?\r\n");
+ noteRun(p, pane, "exit", code);
+ p.needs_frame = true;
}
/// Trim and unwrap clickable command-word notation once for every command