summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-14 21:26:47 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commita624a56e289e4a02a411f83741f0f2c9fc0f0b2e (patch)
treeb87e3ca10dd1c4417112164f792b5da1e26de0a6 /src/fs.zig
parent95681ff7017b8a9e4c8f9fa6a7371d1233432f2f (diff)
downloadpardes-a624a56e289e4a02a411f83741f0f2c9fc0f0b2e.tar.gz
pardes-a624a56e289e4a02a411f83741f0f2c9fc0f0b2e.zip
Add Linux Tty9p mounted terminals and forward raw TTY keys
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig64
1 files changed, 49 insertions, 15 deletions
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 });