From 0ee8d748c028ad5257f95e15db38646aecd2851b Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 09:32:02 -0300 Subject: A root focus file names the pane with the keyboard and moves it; a pane's ctl says whether it is current Scripts had no way to ask which pane has the keyboard or to give it one. rio answers both through a window's wctl: its read ends with current or notcurrent, and a write of current takes the keyboard (rio(4)). A pane's ctl line now ends the same way, and since there is one keyboard for the whole tree, /focus reads the focused pane's serial and takes a serial to move the keyboard there, off any header that had it; a serial no pane has fails with "no such window". fs-bench's stale readdir check is fixed too. Co-Authored-By: Claude Opus 5.5 --- src/ninep/ctl.zig | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 5 deletions(-) (limited to 'src/ninep/ctl.zig') 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"); -- cgit v1.3