summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 13:38:45 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit65a4c17767f175ce16cbbce5520a86926d61692d (patch)
tree92c1de60b2be3a79cfffb474e38efd546b20daa0 /src
parentc4504325deefd4a8fb905db95b4e8b5003fb2a6a (diff)
downloadpardes-65a4c17767f175ce16cbbce5520a86926d61692d.tar.gz
pardes-65a4c17767f175ce16cbbce5520a86926d61692d.zip
With no pane at all, the root look opens its file and the root exec runs its command in the session's directory
/look and /exec run at the active pane, and with every pane closed they failed ENOENT, "file does not exist", about a file the writer never named. Now an exec runs as the workspace tag's would, in the session's directory: a builtin at the stand-in, a command in a command pane. An empty window's click now runs a command line too, not only session words. A look runs from a scratch made as New makes one, which also makes the column. That scratch is dropped, unannounced, when the look went to another pane, so opening a file leaves only the file. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/exec.zig15
-rw-r--r--src/ninep/ctl.zig61
2 files changed, 69 insertions, 7 deletions
diff --git a/src/exec.zig b/src/exec.zig
index eed9297a..6070d038 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -644,11 +644,12 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
/// such text may be a bound REPL's. Run by name (`Exec ...`, whose line
/// comes back through `execute`) or written as a command word, never.
pub fn executeFrom(p: *Pardes, id: usize, txt: []const u8, from_body: bool) ?usize {
- // An empty window runs its session words, and nothing else: no pane to
- // run a command for.
+ // An empty window runs its session words, and a command line in the
+ // session's directory (execDir), as its workspace tag would.
const pane = p.panes[id] orelse {
- _ = executeBuiltinLine(p, id, commandText(txt));
- return null;
+ const cmd = commandText(txt);
+ if (cmd.len == 0 or executeBuiltinLine(p, id, cmd)) return null;
+ return runCommand(p, id, cmd);
};
const gesture = from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null;
const cmd = commandText(txt);
@@ -699,7 +700,9 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
p.reportFailure(from, "exec: a command line is at most 1024 bytes");
return null;
}
- const src = p.panes[from] orelse return null;
+ // None asking (an empty window): the session's directory, no pane of
+ // its own to reuse.
+ const src = p.panes[from];
const dir = p.execDir(from);
// A directory that is not there runs nothing, as Tty makes nothing
// there: no pane, and the write fails ENOENT.
@@ -712,7 +715,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
// would hang that job up.
// From a column's tag, only a finished pane in that column is reused,
// as the new one goes in that column (placeNew).
- const reuse: ?usize = if (p.exec_column == null and src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| {
+ const reuse: ?usize = if (p.exec_column == null and src != null and src.?.command != null and src.?.command_done and !src.?.command_pty) from else for (p.panes, 0..) |slot, i| {
const other = slot orelse continue;
if (p.exec_column) |c| if (if (layout.findPane(p, i)) |f| f.col != c else true) continue;
if (other.command != null and other.command_done and !other.command_pty and std.mem.eql(u8, other.cwdSlice(), dir)) break i;
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 0bbc0319..f4e6bb9c 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -192,8 +192,30 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool, in_tag: bool) Rep
for (text) |c| if ((c < ' ' and c != '\t' and c != '\n') or c == 0x7f) return tree.failText(req.tag, E.INVAL, e_control);
if (exec) if (tooLong(req, text)) |refusal| return refusal;
if (!apply) continue;
- const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
+ var id = if (serial) |s| p.paneBySerial(s) orelse break else p.active;
+ // No pane at all: the root's look and exec act in the session's
+ // directory. An exec runs as the workspace tag's; a look from a
+ // scratch made as New makes one (and its column), dropped
+ // unannounced when the look went elsewhere.
+ var scratch: ?usize = null;
+ if (p.panes[id] == null and serial == null and p.anyPane() == null) {
+ if (exec) {
+ p.exec_header = true;
+ defer p.exec_header = false;
+ if (captured(p, req, id, exec, text, true)) |refusal| return refusal;
+ continue;
+ }
+ const free = p.freeSlot() orelse return Reply.fail(req.tag, E.NOSPC);
+ p.newScratchFor(id, .scratch);
+ if (p.panes[free] == null) return Reply.fail(req.tag, E.NOSPC);
+ p.active = free;
+ id = free;
+ scratch = free;
+ }
if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT);
+ defer if (scratch) |s| if (p.active != s) if (p.panes[s]) |sp| if (sp.file) |f| {
+ if (f.content.len == 0) p.removePane(s, null) catch {};
+ };
p.fs.ran_at = p.panes[id].?.serial;
p.fs.no_pane_slot = false;
if (captured(p, req, id, exec, text, in_tag)) |refusal| return refusal;
@@ -2445,6 +2467,43 @@ test "a look that misses file:/re/ names the file, and is logged as its pane's"
try testing.expect(th.logHas(p, try std.fmt.bufPrint(&want, "err {d} look: {s}/f.txt: no match for regexp", .{ f_serial, dir })));
}
+test "with no pane at all, the root look opens its file and the root exec runs in the session's directory" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ var tmp = testing.tmpDir(.{});
+ defer tmp.cleanup();
+ try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "file\n" });
+ var buf: [4096]u8 = undefined;
+ const session = buf[0..try tmp.dir.realPath(testing.io, &buf)];
+ @memcpy(p.session_dir[0..session.len], session);
+ p.session_dir_len = @intCast(session.len);
+ const Empty = struct {
+ fn all(q: *Pardes) !void {
+ while (q.anyPane()) |k| _ = wr(q, Node.of(q.panes[k].?.serial, .ctl), "delete\n");
+ }
+ };
+ try Empty.all(p);
+ const look_node = @intFromEnum(tree.TopFile.look);
+ try testing.expectEqual(Status.ok, wr(p, look_node, "f.txt\n").reply.status);
+ var want: [4200]u8 = undefined;
+ const path = try std.fmt.bufPrint(&want, "{s}/f.txt", .{session});
+ var opened: usize = 0;
+ var scratches: usize = 0;
+ for (p.panes) |slot| {
+ const f = (slot orelse continue).file orelse continue;
+ opened += @intFromBool(std.mem.eql(u8, f.path, path));
+ scratches += @intFromBool(std.mem.endsWith(u8, f.path, "/+New"));
+ }
+ try testing.expectEqual(@as(usize, 1), opened);
+ try testing.expectEqual(@as(usize, 0), scratches);
+
+ try Empty.all(p);
+ try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.exec), "echo hi\n").reply.status);
+ const made = p.panes[p.anyPane().?].?;
+ try testing.expect(made.command != null);
+ try testing.expectEqualStrings(session, made.cwdSlice());
+}
+
test "the ctl word name refuses a leading blank, as the name file does" {
const p = try withFile(testing.allocator, "x\n");
defer p.deinit();