From de4def3548a6729b0dfd2120495a61beef8c8c2c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 5 Jul 2026 20:21:50 -0300 Subject: pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options). --- src/look.zig | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/look.zig (limited to 'src/look.zig') diff --git a/src/look.zig b/src/look.zig new file mode 100644 index 00000000..67fbf8c5 --- /dev/null +++ b/src/look.zig @@ -0,0 +1,123 @@ +//! What a click on text MEANS. The acme "look" (right click / Enter): expand +//! the click to a file-ish word, then resolve it against the pane's directory. +//! This is the one deliberately platform-divergent file — the divergence is a +//! comptime switch on pardes.platform, used the way the stdlib switches on +//! os.tag, so every platform's behavior sits in the same screenful: +//! tty/gui — the word resolves through the real filesystem (realpath, +//! open(O_DIRECTORY)); dirs open shells, files open file panes. +//! web — there is no filesystem; only URLs mean anything, and a URL +//! opens in a new tab (the open_link effect). +const std = @import("std"); +const linux = std.os.linux; +const pardes = @import("pardes.zig"); + +extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8; + +/// file-ish word chars (acme isfilec): alnum + . - + / : @ _ ~ +pub fn isFileChar(c: u8) bool { + return std.ascii.isAlphanumeric(c) or switch (c) { + '.', '-', '+', '/', ':', '@', '_', '~' => true, + else => false, + }; +} + +/// peel a trailing :NNN (or :NNN:) line suffix: main.zig:100 -> {main.zig, 100} +pub fn parsePathLine(tok: []const u8) struct { path: []const u8, line: usize } { + var sep: usize = 0; + while (sep < tok.len) : (sep += 1) { + if (tok[sep] != ':') continue; + var j = sep + 1; + var line: usize = 0; + while (j < tok.len and std.ascii.isDigit(tok[j])) : (j += 1) line = line * 10 + (tok[j] - '0'); + if (j == sep + 1) continue; // no digits after ':' + if (j < tok.len and tok[j] != ':') continue; // junk after the number + return .{ .path = tok[0..sep], .line = line }; + } + return .{ .path = tok, .line = 0 }; +} + +pub const Target = union(enum) { + none, + dir: []const u8, // resolved absolute path, in caller's buf + file: struct { path: []const u8, line: usize }, + image: struct { path: []const u8 }, + url: []const u8, +}; + +const image_exts = [_][]const u8{ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ppm", ".pgm", ".tga" }; + +pub fn isImagePath(path: []const u8) bool { + for (image_exts) |ext| { + if (std.ascii.endsWithIgnoreCase(path, ext)) return true; + } + return false; +} + +/// Resolve a looked-at word against the pane's directory. `realbuf` must +/// outlive the returned Target (dir/file/image paths point into it). +pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target { + const trimmed = std.mem.trim(u8, word_raw, " \t\r\n"); + const pl = parsePathLine(trimmed); + const word = pl.path; + if (word.len == 0) return .none; + + if (platform_has_fs) { + var joinbuf: [2048]u8 = undefined; + const joined: ?[:0]u8 = if (word[0] == '/') + (std.fmt.bufPrintSentinel(&joinbuf, "{s}", .{word}, 0) catch null) + else + (std.fmt.bufPrintSentinel(&joinbuf, "{s}/{s}", .{ cwd, word }, 0) catch null); + const jz = joined orelse return .none; + const rp = realpath(jz.ptr, realbuf) orelse return .none; + const resolved = std.mem.span(rp); + if (isDir(rp)) return .{ .dir = resolved }; + if (isImagePath(resolved)) return .{ .image = .{ .path = resolved } }; + return .{ .file = .{ .path = resolved, .line = pl.line } }; + } else { + // web: no filesystem. A URL opens in a new tab; everything else is inert. + if (std.mem.startsWith(u8, trimmed, "http://") or std.mem.startsWith(u8, trimmed, "https://")) + return .{ .url = trimmed }; + return .none; + } +} + +const platform_has_fs = switch (pardes.platform) { + .tty, .gui => true, + .web => false, +}; + +/// true if `path` exists and is a directory (open(O_DIRECTORY), no stat needed) +fn isDir(path: [*:0]const u8) bool { + const rc = linux.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }, 0); + const sfd: isize = @bitCast(rc); + if (sfd < 0) return false; + _ = linux.close(@intCast(sfd)); + return true; +} + +/// read a whole file (gpa-owned) — the look side of opening a file pane. +/// Compiled out on web (no fs); dumps provide file content there instead. +pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { + if (!platform_has_fs) return error.NoFilesystem; + var pathbuf: [4096]u8 = undefined; + const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch return error.PathTooLong; + const rc = linux.open(path_z, .{ .ACCMODE = .RDONLY }, 0); + const sfd: isize = @bitCast(rc); + if (sfd < 0) return error.OpenFailed; + const fd: i32 = @intCast(sfd); + defer _ = linux.close(fd); + var buf: std.ArrayList(u8) = .empty; + errdefer buf.deinit(gpa); + var chunk: [16384]u8 = undefined; + while (true) { + const r = linux.read(fd, &chunk, chunk.len); + const n: isize = @bitCast(r); + if (n < 0) { + if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue; + return error.ReadFailed; + } + if (n == 0) break; + try buf.appendSlice(gpa, chunk[0..@intCast(n)]); + } + return buf.toOwnedSlice(gpa); +} -- cgit v1.3