summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/dump.zig25
-rw-r--r--src/fs-help.txt1
-rw-r--r--src/ninep/cols.zig82
-rw-r--r--src/ninep/ctl.zig11
-rw-r--r--src/ninep/events.zig4
-rw-r--r--src/ninep/tree.zig38
-rw-r--r--src/pardes.zig2
7 files changed, 145 insertions, 18 deletions
diff --git a/src/dump.zig b/src/dump.zig
index 61aeabbf..5b0983ad 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -180,6 +180,9 @@ pub const Pane = struct {
};
pub const Column = struct {
+ /// Its serial when dumped (0 in older dumps): a Restore logs
+ /// `restoredcol <old> <new>` for it.
+ serial: u32 = 0,
tag: ?[]const u8 = null,
weight: f32 = 1,
panes: []const usize = &.{},
@@ -665,6 +668,7 @@ pub fn dumpState(p: *Pardes) !void {
ids_len += 1;
};
columns[columns_len] = .{
+ .serial = layout.columnSerial(p, c),
.tag = p.col_tags[c].own,
.weight = @as(f32, @floatFromInt(p.col_weight[c])) / @as(f32, @floatFromInt(column_weight_unit)),
.panes = column_ids[c][0..ids_len],
@@ -733,14 +737,18 @@ pub fn restore(p: *Pardes, zon_bytes: []const u8, from: []const u8) !*Pardes {
var opts = p.opts;
opts.cols = p.screen_w;
opts.rows = p.screen_h;
- var old: [MAX_PANES]u32 = @splat(0);
+ var old: OldSerials = .{};
const replacement = try initDump(p.gpa, opts, zon_bytes, p, &old);
pardes.ctlfs.events.notePath(replacement, "restore", from);
- // Which serial each pane had, for a script that held the old ones.
- for (replacement.panes, old) |slot, was| if (slot) |pane| if (was != 0) {
+ // Which serial each pane and column had, for a script that held them.
+ for (replacement.panes, old.panes) |slot, was| if (slot) |pane| if (was != 0) {
var rec: [32]u8 = undefined;
pardes.ctlfs.events.notePath(replacement, "restored", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, pane.serial }) catch continue);
};
+ for (old.cols[0..replacement.ncol], 0..) |was, c| if (was != 0) {
+ var rec: [32]u8 = undefined;
+ pardes.ctlfs.events.notePath(replacement, "restoredcol", std.fmt.bufPrint(&rec, "{d} {d}", .{ was, layout.columnSerial(replacement, c) }) catch continue);
+ };
return replacement;
}
@@ -748,7 +756,13 @@ pub fn initFromDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8
return initDump(gpa, opts, zon_bytes, null, null);
}
-fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*[MAX_PANES]u32) !*Pardes {
+/// The serials a dump's panes and columns had, read as it is restored.
+const OldSerials = struct {
+ panes: [MAX_PANES]u32 = @splat(0),
+ cols: [MAX_COLS]u32 = @splat(0),
+};
+
+fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previous: ?*const Pardes, old_serials: ?*OldSerials) !*Pardes {
const image_gpa = opts.image_allocator orelse gpa;
const pdf_gpa = opts.pdf_allocator orelse gpa;
const tree_sitter_gpa = opts.tree_sitter_allocator orelse gpa;
@@ -799,7 +813,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
}
for (st.panes, 0..) |src, i| {
if (i >= MAX_PANES) break;
- if (old_serials) |o| o[i] = src.serial;
+ if (old_serials) |o| o.panes[i] = src.serial;
const pane: *pardes.panes.Pane = switch (src.kind) {
.terminal => terminal: {
const t = src.terminal.?;
@@ -869,6 +883,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo
p.col_serial = @splat(0);
if (st.topbar_custom) |bar| p.global_tag.own = try p.gpa.dupe(u8, bar);
for (st.columns[0..p.ncol], 0..) |col, c| {
+ if (old_serials) |o| o.cols[c] = col.serial;
if (col.tag) |bar| p.col_tags[c].own = try p.gpa.dupe(u8, bar);
const scaled = @as(f64, @floatCast(col.weight)) * @as(f64, @floatFromInt(column_weight_unit));
const bounded = @min(scaled, @as(f64, @floatFromInt(max_column_weight)));
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 50e9e7fc..08a7e262 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -14,6 +14,7 @@ commands every builtin: its word, arg if it takes one, and root or pane: whic
layout one line per column: serial index x width current|notcurrent empty|full pane-serials...; then active <serial>
tag the workspace tag: read it, > replaces it, >> appends (one line)
col/<n>/tag the tag of the column with serial n, the same way (a serial stays while the column lives)
+col/<n>/ctl Delcol, Joincol, New or Tty on that column; col/<n>/exec a word as a click in its tag; rmdir col/<n> closes an empty one
pane/new open it to make a pane (named +New) in the active column, filling it if empty; the read answers its serial
pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll errors (write-only:
appended to its directory's +Errors pane) event look exec, pty/ on terminals; rmdir closes
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig
index 79943b1b..b6dc7d26 100644
--- a/src/ninep/cols.zig
+++ b/src/ninep/cols.zig
@@ -122,6 +122,52 @@ pub fn released(p: *Pardes, serial: ?u32) void {
p.fs.header_held = false;
}
+/// A column's ctl: the words that act on a column, as its tag's clicks do --
+/// Delcol, Joincol, New, Tty -- one a line, each run as from that tag.
+pub fn writeCtl(p: *Pardes, req: Req, serial: u32) Reply {
+ var lines = std.mem.splitScalar(u8, req.data, '\n');
+ while (lines.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (line.len == 0) continue;
+ const word = line[0 .. std.mem.indexOfAny(u8, line, " \t") orelse line.len];
+ const takes = for ([_][]const u8{ "Delcol", "Joincol", "New", "Tty" }) |w| {
+ if (std.mem.eql(u8, w, word)) break true;
+ } else false;
+ if (!takes) return tree.failText(req.tag, E.INVAL, "a column's ctl takes Delcol, Joincol, New or Tty");
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.exec_column = col;
+ defer p.exec_column = null;
+ if (pardes.ctlfs.ctl.runBuiltin(p, req, p.active, line)) |refusal| return refusal;
+ }
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+/// A column's exec: each line run as a middle click on it in that column's
+/// tag, acting on that column.
+pub fn writeExec(p: *Pardes, req: Req, serial: u32) Reply {
+ var lines = std.mem.splitScalar(u8, req.data, '\n');
+ while (lines.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (line.len == 0) continue;
+ for (line) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL);
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.exec_column = col;
+ defer p.exec_column = null;
+ pardes.ctlfs.ctl.run(p, p.active, true, line);
+ }
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+/// rmdir of a column closes it when it is empty; one with panes is
+/// refused, saying so (close them, or Delcol on its ctl, which asks first).
+pub fn remove(p: *Pardes, req: Req, serial: u32, file: tree.ColFile) Reply {
+ if (file != .dir) return Reply.fail(req.tag, E.PERM);
+ const col = layout.columnBySerial(p, serial) orelse return Reply.fail(req.tag, E.NOENT);
+ if (p.col_n[col] > 0) return tree.failText(req.tag, E.NOTEMPTY, "column not empty: close its panes, or write Delcol to its ctl");
+ p.removeColumn(col) catch return Reply.fail(req.tag, E.IO);
+ return .{ .tag = req.tag };
+}
+
// ---- tests ----
const testing = std.testing;
@@ -200,3 +246,39 @@ test "a focus write makes its pane's column the active one: layout says so and p
try testing.expectEqual(@as(usize, 0), layout.findPane(p, p.paneBySerial(made).?).?.col);
}
}
+
+test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" {
+ const p = try th.withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const first = layout.columnSerial(p, 0);
+ p.newColumn(p.active);
+ const empty = layout.columnSerial(p, 1);
+ // New on the empty column's ctl fills it, wherever the keyboard is.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .ctl), "New\n").reply.status);
+ try testing.expectEqual(@as(usize, 1), p.col_n[1]);
+ try testing.expectEqual(E.INVAL, th.wr(p, Node.ofCol(empty, .ctl), "Exit\n").errno());
+ // rmdir refuses a column with a pane, and says why.
+ const full = th.call(p, .{ .tag = 1, .op = .release, .node = Node.ofCol(empty, .dir), .remove = true });
+ try testing.expectEqual(E.NOTEMPTY, full.errno());
+ try testing.expect(th.logHas(p, "column not empty"));
+ // exec runs a word as a click in that column's tag: Delcol closes it.
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(empty, .exec), "Delcol\n").reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, empty));
+ // An empty column goes on rmdir.
+ p.newColumn(p.active);
+ const made = layout.columnSerial(p, 1);
+ try testing.expectEqual(tree.Status.ok, th.call(p, .{ .tag = 2, .op = .release, .node = Node.ofCol(made, .dir), .remove = true }).reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, made));
+ try testing.expectEqual(first, layout.columnSerial(p, 0));
+ // Joincol folds a column into the one on its right, which keeps its own
+ // serial and tag.
+ p.newColumn(p.active);
+ const right = layout.columnSerial(p, 1);
+ _ = th.wr(p, Node.ofCol(right, .tag), " Mine");
+ try testing.expectEqual(tree.Status.ok, th.wr(p, Node.ofCol(first, .ctl), "Joincol\n").reply.status);
+ try testing.expectEqual(@as(?usize, null), layout.columnBySerial(p, first));
+ try testing.expectEqual(@as(?usize, 0), layout.columnBySerial(p, right));
+ try testing.expect(std.mem.endsWith(u8, headerText(p, right).?, " Mine"));
+ // The root's ctl points a column word at the column's own.
+ try testing.expect(std.mem.endsWith(u8, th.wr(p, @intFromEnum(tree.TopFile.ctl), "Delcol\n").reply.ename, "write it to col/<serial>/ctl"));
+}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 98120555..e1dd3fc5 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -51,7 +51,7 @@ fn noteResult(p: *Pardes, serial: u32) void {
/// Runs one look or exec from the pane `id`, then records the panes it
/// created; failing that, a look records the pane it focused and an exec
/// the pane it acted on (even one it closed).
-fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
+pub fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void {
const context = p.panes[id].?.serial;
// A pane an event reader holds hears its clicks instead of acting on
// them (editors/acme/look.c:53 and exec.c:173 send them to winevent),
@@ -277,7 +277,12 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
};
// Edit is the one pane word the root takes too: at the active pane.
if (builtins.registry.scope(b) != scope and b != .Edit)
- return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl");
+ return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane)
+ "/ctl"
+ else if (b == .Delcol or b == .Joincol)
+ "col/<serial>/ctl" // a column's own ctl, the one /layout names
+ else
+ "pane/<n>/ctl");
if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line);
if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line);
const setting = config.Runtime.find(word) orelse return null;
@@ -308,7 +313,7 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ?
/// would. Answers the refusal when it would have opened a prompt for an
/// argument, which nobody writing to a file is there to type, or when it
/// reported an error: `Mount: AlreadyMounted "Mount peer /tmp/s"`.
-fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
+pub fn runBuiltin(p: *Pardes, req: Req, id: usize, line: []const u8) ?Reply {
p.fs.no_prompt = true;
p.fs.refused = false;
p.fs.failure_len = 0;
diff --git a/src/ninep/events.zig b/src/ninep/events.zig
index e918f09a..ccc61352 100644
--- a/src/ninep/events.zig
+++ b/src/ninep/events.zig
@@ -944,7 +944,9 @@ test "a Dump written and a Restore made are in the log, with their files" {
const log = try freezeLog(restored);
defer restored.gpa.free(log.bytes);
var want: [64]u8 = undefined;
- try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial })));
+ try testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "restore /tmp/pardes.dump.zon\nrestored {d} {d}\n", .{ p.panes[0].?.serial, restored.panes[0].?.serial })) != null);
+ // ...and each column's: restoredcol <old> <new>.
+ try testing.expect(std.mem.endsWith(u8, log.bytes, try std.fmt.bufPrint(&want, "restoredcol {d} {d}\n", .{ pardes.layout.columnSerial(p, 0), pardes.layout.columnSerial(restored, 0) })));
}
test "opens of the log share the open records, and a closed one frees its record" {
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index c4ade1f7..baf8c772 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -272,7 +272,16 @@ pub const PaneFile = enum(u5) {
/// /col/<serial> and its tag: a column is named by its serial, which stays
/// while it lives as a pane's does; this bit, past a pane serial's 32, keeps
/// the two apart.
-pub const ColFile = enum(u5) { dir = 0, tag = 1 };
+pub const ColFile = enum(u5) {
+ dir = 0,
+ tag = 1,
+ ctl = 2,
+ exec = 3,
+
+ pub fn fileName(f: ColFile) []const u8 {
+ return @tagName(f);
+ }
+};
const col_bit: u59 = 1 << 58;
pub const Node = packed struct(u64) {
@@ -454,10 +463,14 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.col => |c| {
if (layout.columnBySerial(p, c.serial) == null) return null;
return .{
- .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else "tag",
+ .name = if (c.file == .dir) (std.fmt.bufPrint(&p.fs.node_name, "{d}", .{c.serial}) catch unreachable) else c.file.fileName(),
.node = Node.ofCol(c.serial, c.file),
.dir = c.file == .dir,
- .mode = if (c.file == .dir) 0o755 else 0o666,
+ .mode = switch (c.file) {
+ .dir => 0o755,
+ .ctl, .exec => 0o222,
+ .tag => 0o666,
+ },
.size = if (c.file == .tag) cols.headerText(p, c.serial).?.len + 1 else 0,
.mtime = p.fs.started,
.append = c.file == .tag,
@@ -562,8 +575,9 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply {
},
.col => |c| col: {
if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR);
- if (!std.mem.eql(u8, name, "tag")) return Reply.fail(req.tag, E.NOENT);
- break :col Node.ofCol(c.serial, .tag);
+ const f = std.meta.stringToEnum(ColFile, name) orelse return Reply.fail(req.tag, E.NOENT);
+ if (f == .dir) return Reply.fail(req.tag, E.NOENT);
+ break :col Node.ofCol(c.serial, f);
},
.pane => |t| pane: {
_ = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -638,7 +652,9 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
.col => |c| {
if (layout.columnBySerial(p, c.serial) == null) return Reply.fail(req.tag, E.NOENT);
if (c.file != .dir) return Reply.fail(req.tag, E.NOTDIR);
- if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, .tag), false, "tag");
+ inline for (.{ ColFile.tag, ColFile.ctl, ColFile.exec }) |f| {
+ if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, Node.ofCol(c.serial, f), false, f.fileName());
+ }
},
.pane => |t| {
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -729,7 +745,8 @@ fn remove(p: *Pardes, req: Req) Reply {
const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
const t = switch (target) {
.pane => |t| t,
- .top, .col => return Reply.fail(req.tag, E.PERM),
+ .top => return Reply.fail(req.tag, E.PERM),
+ .col => |c| return cols.remove(p, req, c.serial, c.file),
};
if (t.file != .dir) return Reply.fail(req.tag, E.PERM);
const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
@@ -902,7 +919,12 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
return pane.write(p, req, id, pn, t.file);
},
- .col => |c| return if (c.file == .tag) cols.write(p, req, c.serial) else Reply.fail(req.tag, E.PERM),
+ .col => |c| return switch (c.file) {
+ .tag => cols.write(p, req, c.serial),
+ .ctl => cols.writeCtl(p, req, c.serial),
+ .exec => cols.writeExec(p, req, c.serial),
+ .dir => Reply.fail(req.tag, E.PERM),
+ },
}
}
diff --git a/src/pardes.zig b/src/pardes.zig
index a6a088de..8b3687a0 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5309,7 +5309,7 @@ pub const Pardes = struct {
p.raw_hover_intent = false;
look.cancelLookHover(p);
const reply = ctlfs.handle(p, req);
- if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE))) ctlfs.events.noteError(p, req, reply);
+ if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE) or (req.op == .release and req.remove))) ctlfs.events.noteError(p, req, reply);
// The request was a whole step of its own, so it settles the way a
// step does: the cursor and scroll reconciled, the scripted panes
// told, and the panes it made announced to /log now rather than at