summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-05 20:21:50 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commitde4def3548a6729b0dfd2120495a61beef8c8c2c (patch)
tree12144f0f64dc96bb4741459c1f9db57f44349330 /src/look.zig
parent7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff)
downloadpardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz
pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip
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).
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig123
1 files changed, 123 insertions, 0 deletions
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);
+}