From 65a4c17767f175ce16cbbce5520a86926d61692d Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 13:38:45 -0300 Subject: 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 --- src/ninep/ctl.zig | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) (limited to 'src/ninep/ctl.zig') 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(); -- cgit v1.3