summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 03:13:10 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit6dfc6838c1c02978c026b650d69f87f23e3ff81c (patch)
treea488db19607f2a1a7759f2e66a4688182e371124
parent073695995ecf4a269da8d338b23bfab1d3369c05 (diff)
downloadpardes-6dfc6838c1c02978c026b650d69f87f23e3ff81c.tar.gz
pardes-6dfc6838c1c02978c026b650d69f87f23e3ff81c.zip
Words from the workspace and column tags run in the session's directory
A command or Tty run from the workspace tag or a column tag started in the focused pane's directory, though those tags have none of their own. As acme's row and column tags do, they now run in the session's directory, where pardes started (Pardes.sessionDir); a pane's tag and text keep the pane's. The same for /tagexec, a column's exec and ctl. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--docs/fs.md3
-rw-r--r--docs/tags.md4
-rw-r--r--src/exec.zig6
-rw-r--r--src/mouse.zig2
-rw-r--r--src/ninep/cols.zig22
-rw-r--r--src/pardes.zig25
6 files changed, 58 insertions, 4 deletions
diff --git a/docs/fs.md b/docs/fs.md
index e64cd6b3..e9dfac65 100644
--- a/docs/fs.md
+++ b/docs/fs.md
@@ -378,7 +378,8 @@ pane's `pty/ctl` interrupts it as Ctrl-C would. Bindings are not dumped, so a Re
The root's pair clicks at the active pane and `/pane/<n>/look` and
`/pane/<n>/exec` at that pane; `/tagexec` and `/col/<n>/exec` click in the
workspace's or that column's tag (never an event reader's, which hears
-only its pane's), and what the word says is logged as the session's,
+only its pane's; a command they run starts in the session's directory,
+where pardes started, not the focused pane's), and what the word says is logged as the session's,
`msg -`. Blank lines are skipped, and every other line
is checked before any of them runs, so a control character fails the whole
write with EINVAL; a command that fails inside the editor is reported on the
diff --git a/docs/tags.md b/docs/tags.md
index cee8e335..a113d86a 100644
--- a/docs/tags.md
+++ b/docs/tags.md
@@ -5,6 +5,10 @@ column, and each pane's tag. Column commands act on that column and run in
its active pane (or its first pane when focus comes from another column); a
column with no pane is described under [empty columns](#empty-columns). This makes
`New`, `Tty`, `Find`, and `Grep` available beside the work they act on.
+A command or a `Tty` run from the workspace tag or a column tag starts in
+the session's directory, where pardes was started, as acme's row and column
+tags have no directory of their own; one run from a pane's tag or text
+starts in that pane's directory.
`New` appears only in the column tag by default; pane tags keep their own
save, terminal, close, and collapse commands. `Tty` opens a new embedded terminal.
In a terminal's own tag the word names the shell it runs, `Tty+fish`, tinted
diff --git a/src/exec.zig b/src/exec.zig
index bb4309a4..484b85d7 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -310,12 +310,12 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
}
fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
- const src = p.panes[from] orelse return null;
+ if (p.panes[from] == null) return null;
const free = p.freeSlot() orelse {
p.reportError(from, "Tty", error.NoPaneSlots);
return null;
};
- const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| {
+ const nt = p.newShell(free, p.execDir(from)) catch |err| {
p.reportError(from, "Tty", err);
return null;
};
@@ -577,7 +577,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
return null;
}
const src = p.panes[from] orelse return null;
- const dir = Pardes.paneDir(src);
+ const dir = p.execDir(from);
// Not one whose pty a job it left behind still prints to: reusing it
// would hang that job up.
const reuse: ?usize = if (src.command != null and src.command_done and !src.command_pty) from else for (p.panes, 0..) |slot, i| {
diff --git a/src/mouse.zig b/src/mouse.zig
index 1ca53017..6b58a489 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -385,6 +385,8 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
// A column tag's command acts on that column, empty or not.
p.exec_column = column;
defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
if (word.len > 0) {
if (m.button == config.exec_button) {
const named = std.meta.stringToEnum(Builtin, word);
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index ad7d30f8..c55c1e73 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -140,6 +140,8 @@ pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
p.exec_column = col;
defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
if (pardes.ctlfs.ctl.runBuiltin(p, req, p.active, line)) |refusal| return refusal;
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -163,6 +165,8 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply {
if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT);
p.exec_column = col;
defer p.exec_column = null;
+ p.exec_header = true;
+ defer p.exec_header = false;
pardes.ctlfs.ctl.run(p, p.active, true, line, true);
}
return .{ .tag = req.tag, .written = @intCast(req.data.len) };
@@ -315,3 +319,21 @@ test "tagexec runs a word as a click in the workspace tag; exec files read back
try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(right, .exec), "Msg from a column\n").reply.status);
try testing.expect(th.logHas(p, "msg - from a column\n"));
}
+
+test "a word run from the workspace's or a column's tag runs in the session's directory, a pane's in its own" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const session = p.sessionDir();
+ try testing.expect(!std.mem.eql(u8, session, pardes.Pardes.paneDir(p.panes[0].?))); // the file is elsewhere
+ const dirs = [_]u64{ @intFromEnum(tree.TopFile.tagexec), Node.ofCol(layout.columnSerial(p, 0), .exec) };
+ for (dirs) |node| {
+ try testing.expectEqual(tree.Status.ok, th.wr(p, node, "true\n").reply.status);
+ const made = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expectEqualStrings(session, pardes.Pardes.paneDir(p.panes[made].?));
+ try p.removePane(made, null);
+ }
+ const from_pane = p.panes[0].?.serial;
+ _ = th.wr(p, Node.of(from_pane, .exec), "true\n");
+ const made = p.paneBySerial(p.fs.results[0]).?;
+ try testing.expectEqualStrings(pardes.Pardes.paneDir(p.panes[0].?), pardes.Pardes.paneDir(p.panes[made].?));
+}
diff --git a/src/pardes.zig b/src/pardes.zig
index ac6d52cd..42f1b6e4 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4275,6 +4275,12 @@ pub const Pardes = struct {
/// (exec.placeNew): closed at the step's end, once whatever made them
/// is done with them.
unplaced: std.StaticBitSet(MAX_PANES) = .initEmpty(),
+ /// The running word came from the workspace's or a column's tag, which
+ /// have no directory of their own: it runs in the session's
+ /// (`sessionDir`), as acme's row and column tags do.
+ exec_header: bool = false,
+ session_dir: [limits.host_path_cap]u8 = undefined,
+ session_dir_len: ?u16 = null,
/// acme's activecol (dat.c:37), by column serial, 0 for none: the
/// column last typed in (acme.c:487), clicked in with the select button
/// (acme.c:659), dropped into by a grip (acme.c:640), or given a new
@@ -5776,6 +5782,8 @@ pub const Pardes = struct {
// A column tag's command acts on that column, empty or not.
p.exec_column = if (header != null) p.header_column else null;
defer p.exec_column = null;
+ p.exec_header = header != null;
+ defer p.exec_header = false;
// Out of the tag or header FIRST: Del frees the pane, Kill
// and Restore the whole session.
if (header != null) tagline.exitHeader(p) else pane.focus = .body;
@@ -5878,6 +5886,23 @@ pub const Pardes = struct {
// ---- search (`/`, Find, n/N): results into an output buffer, look the rows ----
+ /// Where pardes started: the directory of what runs from a header.
+ pub fn sessionDir(p: *Pardes) []const u8 {
+ if (p.session_dir_len) |n| return p.session_dir[0..n];
+ if (comptime !hosted) return "/";
+ if (std.c.getcwd(&p.session_dir, p.session_dir.len) == null) return "/";
+ const n: u16 = @intCast(std.mem.indexOfScalar(u8, &p.session_dir, 0) orelse return "/");
+ p.session_dir_len = n;
+ return p.session_dir[0..n];
+ }
+
+ /// The directory a word run from pane `id`'s text or tag runs in, or,
+ /// from a header, the session's.
+ pub fn execDir(p: *Pardes, id: usize) []const u8 {
+ if (p.exec_header) return p.sessionDir();
+ return paneDir(p.panes[id] orelse return p.sessionDir());
+ }
+
pub fn paneDir(pane: *const Pane) []const u8 {
switch (pane.cwd) {
.inherited => |src| return paneDir(src),