summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/fs-help.txt6
-rw-r--r--src/ninep/ctl.zig61
-rw-r--r--src/ninep/tree.zig15
3 files changed, 73 insertions, 9 deletions
diff --git a/src/fs-help.txt b/src/fs-help.txt
index efdecf46..235ce072 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -9,11 +9,11 @@ log recent events, one a line: new/del/rename/save <serial> <name>, and
msg <serial|-> <text>, what the editor said; write follow to wait (tail -f won't)
screen rendered screen as JSON, frozen from open to close
listeners the session's dial addresses
+focus the serial of the pane with the keyboard; write a serial to give it the keyboard
pane/new open it to make a pane; the read answers that pane's serial
pane/<n>/ name body tag ctl addr dot limit data xdata sel dirty mark scroll
errors event look exec, and pty/ for terminals; rmdir closes it
-os/ the host filesystem
-src/ the editor's own sources, only in a -Dembed-sources=true build
+os/, src/ the host filesystem; the editor's own sources (-Dembed-sources=true builds only)
Below, $m is the mount point (PARDES_MOUNT in a Tty9p shell; 9ns and 9p work too):
@@ -42,4 +42,4 @@ Pitfalls, one each:
tag reads path++text; writes append to the text (newlines too); truncating clears it, and dot or addr.
A terminal's body is a history snapshot frozen per open; pty/data is the live stream.
A failing command is reported in the editor and in log, not as a write error; a bad line fails the write.
- pane/<n>/ctl: acme's status line; takes get (reload), lock/unlock on a held fd (exec 3>ctl; echo lock >&3).
+ pane/<n>/ctl: acme's status line, then current or notcurrent; takes get (reload), lock/unlock on a held fd.
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 484b08c9..5cc238a7 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -10,6 +10,7 @@ const look = @import("../look.zig");
const panes = @import("../panes.zig");
const config = @import("../config.zig");
const fs = @import("../fs.zig");
+const tagline = @import("../tagline.zig");
const tree = @import("tree.zig");
const pane_files = @import("pane.zig");
@@ -154,10 +155,13 @@ fn writeQuoted(w: *std.Io.Writer, s: []const u8) !void {
try w.writeByte('\'');
}
-pub const pane_text_cap = 7 * 12 + 2 * 256 + 4;
+pub const pane_text_cap = 7 * 12 + 2 * 256 + 4 + "notcurrent ".len;
/// acme's winctlprint: serial, tag length, body length, a reserved zero, the
-/// dirty flag, then the width in cells, the font and the tab width.
+/// dirty flag, then the width in cells, the font and the tab width. Then
+/// rio's word for whether the window has the keyboard, `current` or
+/// `notcurrent` (rio(4), wctl), which acme's line has no room for because
+/// acme has one window for input at a time only by the mouse.
pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
var w = std.Io.Writer.fixed(buf);
w.print("{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} ", .{
@@ -169,10 +173,32 @@ pub fn paneText(p: *Pardes, pane: *Pane, buf: []u8) []const u8 {
pane.cols,
}) catch return w.buffered();
writeQuoted(&w, fontName(p)) catch return w.buffered();
- w.print(" {d:>11} ", .{config.tab_width}) catch {};
+ w.print(" {d:>11} {s} ", .{ config.tab_width, if (p.panes[p.active] == pane) "current" else "notcurrent" }) catch {};
return w.buffered();
}
+// ---- /focus ----
+
+/// The pane with the keyboard, which a write of a serial moves, the way
+/// writing `current` to rio's wctl does (rio(4)).
+pub fn readFocus(p: *Pardes, req: Req) Reply {
+ const pane = p.panes[p.active] orelse return tree.stagedReply(p, req);
+ p.fs.stage(p.gpa).print(p.gpa, "{d}\n", .{pane.serial}) catch return Reply.fail(req.tag, E.NOMEM);
+ return tree.stagedReply(p, req);
+}
+
+pub fn writeFocus(p: *Pardes, req: Req) Reply {
+ const text = std.mem.trim(u8, req.data, " \t\r\n");
+ const serial = std.fmt.parseInt(u32, text, 10) catch return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl);
+ const id = p.paneBySerial(serial) orelse return tree.failText(req.tag, E.NOENT, e_no_pane);
+ // The keyboard goes to the pane itself, off any header that had it.
+ tagline.exitHeader(p);
+ p.active = id;
+ return .{ .tag = req.tag, .written = @intCast(req.data.len) };
+}
+
+const e_no_pane = "no such window";
+
pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply {
var buf: [pane_text_cap]u8 = undefined;
const out = p.fs.stage(p.gpa);
@@ -266,7 +292,7 @@ const root_status = @intFromEnum(tree.TopFile.status);
const root_look = @intFromEnum(tree.TopFile.look);
const root_exec = @intFromEnum(tree.TopFile.exec);
-test "pane ctl read is index's five fields plus width in cells, font and tab width" {
+test "pane ctl read is index's five fields plus width in cells, font, tab width and whether it is current" {
const gpa = testing.allocator;
const p = try withFile(gpa, "x\n");
defer p.deinit();
@@ -276,7 +302,7 @@ test "pane ctl read is index's five fields plus width in cells, font and tab wid
try testing.expectEqual(Status.ok, a.reply.status);
var want: std.ArrayList(u8) = .empty;
defer want.deinit(gpa);
- try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} ", .{
+ try want.print(gpa, "{d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {d:>11} {s} {d:>11} current ", .{
pane.serial, pane_files.tagOf(p, pane).len, @as(usize, 2), 0, 0, pane.cols, "default", config.tab_width,
});
try testing.expectEqualStrings(want.items, a.bytes);
@@ -291,6 +317,31 @@ test "pane ctl read is index's five fields plus width in cells, font and tab wid
try testing.expectEqualStrings("'it''s'", w.buffered());
}
+test "focus reads the pane with the keyboard, and a serial written to it moves the keyboard there" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const first = serialOf(p);
+ const second = try th.newPane(p);
+ const focus = @intFromEnum(tree.TopFile.focus);
+ var line: [16]u8 = undefined;
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&line, "{d}\n", .{second}), rd(p, focus, 0, 64).bytes);
+ try testing.expectEqual(@as(u64, rd(p, focus, 0, 64).bytes.len), call(p, .{ .tag = 1, .op = .getattr, .node = focus }).reply.attr.size);
+ try testing.expect(std.mem.endsWith(u8, rd(p, Node.of(first, .ctl), 0, 4096).bytes, " notcurrent "));
+
+ // A header had the keyboard: the write takes it back to the pane.
+ tagline.enterHeader(p, null);
+ try testing.expectEqual(Status.ok, wr(p, focus, try std.fmt.bufPrint(&line, "{d}\n", .{first})).reply.status);
+ try testing.expect(!p.header_focus);
+ try testing.expectEqual(p.paneBySerial(first).?, p.active);
+ try testing.expect(std.mem.endsWith(u8, rd(p, Node.of(first, .ctl), 0, 4096).bytes, " current "));
+ try testing.expect(std.mem.endsWith(u8, rd(p, Node.of(second, .ctl), 0, 4096).bytes, " notcurrent "));
+
+ try testing.expectEqualStrings(e_no_pane, wr(p, focus, "99999\n").reply.ename);
+ try testing.expectEqual(E.NOENT, wr(p, focus, "99999").errno());
+ for ([_][]const u8{ "", "two", "-1", "1 2" }) |bad| try testing.expectEqual(E.INVAL, wr(p, focus, bad).errno());
+ try testing.expectEqual(p.paneBySerial(first).?, p.active);
+}
+
test "the pane ctl takes get, lock and unlock, and nothing that a file of its own now answers" {
const gpa = testing.allocator;
const p = try withFile(gpa, "one\ntwo\n");
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index f93ab203..b0de446e 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -174,6 +174,7 @@ pub const TopFile = enum(u4) {
listeners,
pane,
new,
+ focus,
pub fn fileName(f: TopFile) []const u8 {
return if (f == .root) "/" else @tagName(f);
@@ -182,7 +183,7 @@ pub const TopFile = enum(u4) {
pub fn mode(f: TopFile) u16 {
return switch (f) {
.root, .pane => 0o755,
- .look, .exec, .log => 0o666,
+ .look, .exec, .log, .focus => 0o666,
.README, .index, .status, .screen, .listeners, .new => 0o444,
};
}
@@ -444,6 +445,11 @@ fn topSize(p: *Pardes, f: TopFile) u64 {
.look, .exec => ctl.resultsLen(p),
.log => events.logLen(&p.fs.log),
.listeners => screen.listenersLen(p),
+ .focus => focus: {
+ const pn = p.panes[p.active] orelse break :focus 0;
+ var digits: [16]u8 = undefined;
+ break :focus (std.fmt.bufPrint(&digits, "{d}\n", .{pn.serial}) catch unreachable).len;
+ },
};
}
@@ -516,6 +522,10 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os");
+ // After the first ten, which older clients list by position.
+ inline for (.{TopFile.focus}) |t| {
+ if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
+ }
sources.stage(p, out, "", &skip);
},
.pane => {
@@ -705,6 +715,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
.log => events.readLog(p, req),
.screen => screen.readSnapshot(p, req, null),
.listeners => screen.readListeners(p, req),
+ .focus => ctl.readFocus(p, req),
// The serial the open handed this fid, so that two reads of one
// fid answer the same pane: the read observes, the open acted.
.new => serial: {
@@ -737,6 +748,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply {
.top => |f| return switch (f) {
.look, .exec => ctl.command(p, req, null, f == .exec),
.log => events.writeLog(p, req),
+ .focus => ctl.writeFocus(p, req),
else => Reply.fail(req.tag, E.PERM),
},
.pane => |t| {
@@ -894,6 +906,7 @@ test "readdir lists the root and a pane directory without creating anything" {
}
try testing.expectEqual(@intFromEnum(TopFile.pane), top[8].node);
try testing.expectEqual(fs.os_root, top[9].node);
+ try testing.expectEqualStrings("focus", top[10].name);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);