From a624a56e289e4a02a411f83741f0f2c9fc0f0b2e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 14 Sep 2026 21:26:47 -0300 Subject: Add Linux Tty9p mounted terminals and forward raw TTY keys --- src/fs.zig | 64 +++++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 15 deletions(-) (limited to 'src/fs.zig') diff --git a/src/fs.zig b/src/fs.zig index 63d2ed3a..5ec09d3e 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -1331,6 +1331,7 @@ pub const namespace_panes: u64 = namespace_root + 1; pub const os_root: u64 = namespace_root + 2; pub const os_node: u64 = 1 << 62; const factory_base: u64 = namespace_root + 256; +const fs_help = @embedFile("fs-help.txt"); pub fn resolveSelf(p: *Pardes, path: []const u8) ?u64 { var path_buf: [4096]u8 = undefined; @@ -1341,6 +1342,7 @@ pub fn resolveSelf(p: *Pardes, path: []const u8) ?u64 { const top = topFileNamed(first) orelse return archiveNode(normalized); if (parts.next()) |name| { if (top != .new or parts.next() != null) return null; + if (std.mem.eql(u8, name, "README")) return @intFromEnum(SelfFile.README); const file = paneFileNamed(name) orelse return null; if (file.inPty()) return null; return factory_base + @intFromEnum(file); @@ -1436,7 +1438,7 @@ pub const Req = struct { pub fn changesPane(req: Req) bool { return switch (req.op) { .write, .setattr => true, - .lookup => req.node == @intFromEnum(SelfFile.new) and !std.mem.eql(u8, req.data, ".."), + .lookup => false, .open => req.node >= factory_base and req.node < factory_base + 16, .getattr, .read, .release, .readdir => false, }; @@ -1538,6 +1540,7 @@ pub const SelfFile = enum(u4) { new = 4, screen = 5, listeners = 6, + README = 7, pub fn name(f: SelfFile) []const u8 { return if (f == .root) "." else @tagName(f); @@ -1546,7 +1549,7 @@ pub const SelfFile = enum(u4) { pub fn mode(f: SelfFile) u16 { return switch (f) { .root, .new => 0o500, - .index, .screen, .listeners => 0o400, + .index, .screen, .listeners, .README => 0o400, .cons => 0o200, }; } @@ -1891,7 +1894,10 @@ pub fn notePtyOutput(p: *Pardes, id: usize, bytes: []const u8) void { pub fn handle(p: *Pardes, req: Req) Reply { if (req.node >= factory_base and req.node < factory_base + 16) { const file = std.enums.fromInt(PaneFile, req.node - factory_base) orelse return Reply.fail(req.tag, E.NOENT); - if (req.op == .getattr) return .{ .tag = req.tag, .attr = .{ .node = req.node, .name = file.name(), .mode = file.mode() } }; + if (file == .dir or file.inPty()) return Reply.fail(req.tag, E.NOENT); + // Truncating a factory is harmless: its next open creates an empty pane. + if (req.op == .getattr or (req.op == .setattr and req.truncate)) + return .{ .tag = req.tag, .attr = .{ .node = req.node, .name = file.name(), .mode = file.mode() } }; if (req.op != .open) return Reply.fail(req.tag, E.PERM); const serial = newPane(p) orelse return Reply.fail(req.tag, E.NFILE); const node = Node.of(serial, file); @@ -1951,6 +1957,7 @@ fn topSize(p: *Pardes, f: SelfFile) u64 { return switch (f) { .root, .new, .cons, .screen, .listeners => 0, .index => indexLen(p), + .README => fs_help.len, }; } @@ -2132,10 +2139,10 @@ fn lookup(p: *Pardes, req: Req, target: Target) Reply { return Reply.fail(req.tag, E.NOENT); }, .new => new: { + if (std.mem.eql(u8, name, "README")) break :new @intFromEnum(SelfFile.README); const want = paneFileNamed(name) orelse return Reply.fail(req.tag, E.NOENT); if (want.inPty()) return Reply.fail(req.tag, E.NOENT); - const serial = newPane(p) orelse return Reply.fail(req.tag, E.NFILE); - break :new Node.of(serial, want); + return handle(p, .{ .tag = req.tag, .op = .getattr, .node = factory_base + @intFromEnum(want) }); }, else => return Reply.fail(req.tag, E.NOTDIR), }, @@ -2194,9 +2201,16 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply { if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, namespace_panes, true, "pane"); if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(SelfFile.screen), false, "screen"); if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(SelfFile.listeners), false, "listeners"); + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(SelfFile.README), false, "README"); stageArchive(p, out, "", &skip); }, - .new => {}, + .new => { + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(SelfFile.README), false, "README"); + inline for (comptime std.enums.values(PaneFile)) |file| { + if (comptime file == .dir or file.inPty()) continue; + if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, factory_base + @intFromEnum(file), false, file.name()); + } + }, else => return Reply.fail(req.tag, E.NOTDIR), }, .pane => |t| { @@ -2320,6 +2334,12 @@ fn handleRead(p: *Pardes, req: Req, target: Target) Reply { switch (target) { .top => |f| return switch (f) { .index => readIndex(p, req), + .README => help: { + const off = @min(req.off, fs_help.len); + const bytes = fs_help[off..][0..@min(req.size, fs_help.len - off)]; + p.fs.stage(p.gpa).appendSlice(p.gpa, bytes) catch break :help Reply.fail(req.tag, E.NOMEM); + break :help .{ .tag = req.tag, .payload = .{ .staged = @intCast(bytes.len) } }; + }, .listeners => listeners: { var buf: [512]u8 = undefined; var text = std.Io.Writer.fixed(&buf); @@ -3233,6 +3253,7 @@ test "filesystem inspection preserves pending and displayed Look hover" { const requests = [_]Req{ .{ .tag = 1, .op = .lookup, .node = @intFromEnum(SelfFile.root), .data = "screen" }, .{ .tag = 1, .op = .lookup, .node = @intFromEnum(SelfFile.new), .data = ".." }, + .{ .tag = 1, .op = .lookup, .node = @intFromEnum(SelfFile.new), .data = "body" }, .{ .tag = 2, .op = .getattr, .node = body }, .{ .tag = 3, .op = .open, .node = body }, .{ .tag = 4, .op = .read, .node = body, .size = 5 }, @@ -3258,7 +3279,6 @@ test "filesystem inspection preserves pending and displayed Look hover" { test "filesystem pane creation and truncation cancel Look hover" { const requests = [_]Req{ - .{ .tag = 1, .op = .lookup, .node = @intFromEnum(SelfFile.new), .data = "body" }, .{ .tag = 2, .op = .open, .node = factory_base + @intFromEnum(PaneFile.body) }, .{ .tag = 3, .op = .setattr, .node = 0, .truncate = true }, }; @@ -3659,7 +3679,15 @@ test "readdir lists the root, a pane directory, and new/ without creating anythi const before = p.next_serial; const new = rdir(p, @intFromEnum(SelfFile.new), 0); try testing.expectEqual(Status.ok, new.reply.status); - try testing.expectEqual(@as(usize, 0), new.bytes.len); + const factories = dirents(new.bytes, &buf); + try testing.expectEqual(@as(usize, 11), factories.len); + try testing.expect(nameAt(factories, "README") != null); + for (factories) |entry| { + const inspected = look_up(p, @intFromEnum(SelfFile.new), entry.name); + try testing.expectEqual(Status.ok, inspected.reply.status); + try testing.expectEqual(entry.node, inspected.reply.attr.node); + try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .getattr, .node = entry.node }).reply.status); + } try testing.expectEqual(before, p.next_serial); try testing.expectEqual(E.NOTDIR, rdir(p, Node.of(serial, .body), 0).errno()); @@ -3693,7 +3721,7 @@ test "lookup resolves top files, pane serials and pane files" { try testing.expectEqual(E.NOTDIR, look_up(p, Node.of(serial, .body), "x").errno()); } -test "a lookup inside new/ creates a pane and resolves that pane's file" { +test "new/ lookup is inert and each open creates a distinct pane" { const gpa = testing.allocator; const p = try withFile(gpa, "first\n"); defer p.deinit(); @@ -3704,11 +3732,17 @@ test "a lookup inside new/ creates a pane and resolves that pane's file" { const a = look_up(p, @intFromEnum(SelfFile.new), "body"); try testing.expectEqual(Status.ok, a.reply.status); - const made: Node = @bitCast(a.reply.attr.node); + try testing.expectEqual(before, p.next_serial); + const opened = call(p, .{ .tag = 1, .op = .open, .node = a.reply.attr.node }); + try testing.expectEqual(Status.ok, opened.reply.status); + const made: Node = @bitCast(opened.reply.attr.node); try testing.expect(made.serial != before); try testing.expectEqual(@intFromEnum(PaneFile.body), made.file); - _ = wr(p, a.reply.attr.node, "hi"); + _ = wr(p, opened.reply.attr.node, "hi"); + const second = call(p, .{ .tag = 2, .op = .open, .node = a.reply.attr.node }); + try testing.expectEqual(Status.ok, second.reply.status); + try testing.expect(second.reply.attr.node != opened.reply.attr.node); const id = p.paneBySerial(@intCast(made.serial)).?; try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content); } @@ -4055,7 +4089,7 @@ test "ctl name promotes a scratch without changing its body or undo history" { const gpa = testing.allocator; const p = try withFile(gpa, "opener\n"); defer p.deinit(); - _ = look_up(p, @intFromEnum(SelfFile.new), "ctl"); + _ = call(p, .{ .tag = 1, .op = .open, .node = factory_base + @intFromEnum(PaneFile.ctl) }); const pane = p.panes[p.active].?; const ctl = Node.of(pane.serial, .ctl); const body = Node.of(pane.serial, .body); @@ -4133,7 +4167,7 @@ test "ctl relative names follow inherited scratch and virtual directories" { defer p.deinit(); const source = p.panes[0].?; try testing.expectEqual(Status.ok, wr(p, Node.of(source.serial, .ctl), "name /project/src/source.zig\n").reply.status); - _ = look_up(p, @intFromEnum(SelfFile.new), "ctl"); + _ = call(p, .{ .tag = 1, .op = .open, .node = factory_base + @intFromEnum(PaneFile.ctl) }); const scratch = p.panes[p.active].?; const ctl = Node.of(scratch.serial, .ctl); try testing.expectEqualStrings("/project/src", Pardes.paneDir(scratch)); @@ -4176,7 +4210,7 @@ test "ctl get reloads the pane from disk and del honours a dirty body" { _ = wr(p, ctl, "dirty\n"); try testing.expectEqual(E.INVAL, wr(p, ctl, "del\n").errno()); try testing.expect(p.paneBySerial(serial) != null); - _ = look_up(p, @intFromEnum(SelfFile.new), "body"); + _ = call(p, .{ .tag = 1, .op = .open, .node = factory_base + @intFromEnum(PaneFile.body) }); try testing.expectEqual(Status.ok, wr(p, ctl, "delete\n").reply.status); try testing.expect(p.paneBySerial(serial) == null); } @@ -4396,7 +4430,7 @@ test "a pane deleted while its event file is open leaves no suppression behind" defer p.deinit(); const serial = serialOf(p); const event = Node.of(serial, .event); - _ = look_up(p, @intFromEnum(SelfFile.new), "body"); + _ = call(p, .{ .tag = 1, .op = .open, .node = factory_base + @intFromEnum(PaneFile.body) }); const a = call(p, .{ .tag = 18, .op = .open, .node = event }); const b = call(p, .{ .tag = 19, .op = .open, .node = event }); -- cgit v1.3