summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-10 17:32:43 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit685b8c8e74c9edbdf701594f50ff1b70ac288ec9 (patch)
treef303bd5e1f422030c2c645ea2d6c3a393990fb5e /src/look.zig
parentddc2eefb1e7cdd402593efb1297c25fc7ae4b110 (diff)
downloadpardes-685b8c8e74c9edbdf701594f50ff1b70ac288ec9.tar.gz
pardes-685b8c8e74c9edbdf701594f50ff1b70ac288ec9.zip
web snapshots: add switch for saving/loading exports, update web version, web snapshot tooling
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig79
1 files changed, 72 insertions, 7 deletions
diff --git a/src/look.zig b/src/look.zig
index 8744da24..fb74353f 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -5,12 +5,16 @@
//! 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).
+//! web — tracked Pardes .zig sources form a build-generated read-only
+//! filesystem; URLs still open in a new tab.
const std = @import("std");
const builtin = @import("builtin");
const libc = std.c;
const pardes = @import("pardes.zig");
+const embedded_sources = if (pardes.platform == .web) @import("embedded_sources") else struct {
+ pub const Source = struct { path: []const u8, contents: []const u8 };
+ pub const all = [_]Source{};
+};
extern "c" fn realpath(path: [*:0]const u8, resolved: [*]u8) ?[*:0]u8;
@@ -55,7 +59,8 @@ pub fn isImagePath(path: []const u8) bool {
}
/// Resolve a looked-at word against the pane's directory. `realbuf` must
-/// outlive the returned Target (dir/file/image paths point into it).
+/// outlive the returned Target (native paths point into it; web paths are
+/// process-lifetime slices in the embedded source archive).
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);
@@ -75,13 +80,68 @@ pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target
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.
+ // web: URLs leave the app; tracked Zig sources resolve inside the
+ // build-generated, read-only source filesystem.
if (std.mem.startsWith(u8, trimmed, "http://") or std.mem.startsWith(u8, trimmed, "https://"))
return .{ .url = trimmed };
+ if (resolveEmbedded(word, cwd, realbuf)) |source|
+ return .{ .file = .{ .path = source.path, .line = pl.line } };
return .none;
}
}
+/// Resolve a source path without teaching the core about a browser filesystem.
+/// Cwd-relative and absolute dump paths are normalized, with printed archive
+/// paths also accepted root-relative. The suffix match lets a dump made in
+/// `/host/repo` address names that deliberately remain relative to the root.
+fn resolveEmbedded(word: []const u8, cwd: []const u8, scratch: *[4096]u8) ?embedded_sources.Source {
+ var wordbuf: [4096]u8 = undefined;
+ const normalized_word = normalizeVirtualPath(word, &wordbuf) orelse return null;
+ if (word.len > 0 and word[0] == '/') return findEmbeddedSource(normalized_word, true);
+
+ var joined: [4096]u8 = undefined;
+ if (std.fmt.bufPrint(&joined, "{s}/{s}", .{ cwd, word }) catch null) |candidate|
+ if (normalizeVirtualPath(candidate, scratch)) |normalized|
+ if (findEmbeddedSource(normalized, true)) |source| return source;
+ // A printed archive path is root-relative even when its surrounding dump
+ // pane came from some unrelated cwd.
+ return findEmbeddedSource(normalized_word, false);
+}
+
+fn normalizeVirtualPath(path: []const u8, out: *[4096]u8) ?[]const u8 {
+ var len: usize = 0;
+ var parts = std.mem.tokenizeAny(u8, path, "/\\");
+ while (parts.next()) |part| {
+ if (std.mem.eql(u8, part, ".")) continue;
+ if (std.mem.eql(u8, part, "..")) {
+ while (len > 0 and out[len - 1] != '/') len -= 1;
+ if (len > 0) len -= 1;
+ continue;
+ }
+ const extra = part.len + @intFromBool(len != 0);
+ if (len + extra > out.len) return null;
+ if (len != 0) {
+ out[len] = '/';
+ len += 1;
+ }
+ @memcpy(out[len..][0..part.len], part);
+ len += part.len;
+ }
+ if (len == 0) return null;
+ return out[0..len];
+}
+
+fn findEmbeddedSource(path: []const u8, allow_root_suffix: bool) ?embedded_sources.Source {
+ for (embedded_sources.all) |source|
+ if (std.mem.eql(u8, source.path, path)) return source;
+ if (!allow_root_suffix) return null;
+ for (embedded_sources.all) |source| {
+ if (path.len <= source.path.len or path[path.len - source.path.len - 1] != '/') continue;
+ if (std.mem.endsWith(u8, path, source.path)) return source;
+ }
+ return null;
+}
+
const platform_has_fs = switch (pardes.platform) {
.tty, .gui => true,
.web => false,
@@ -95,10 +155,15 @@ fn isDir(path: [*:0]const u8) bool {
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.
+/// Read a whole file (gpa-owned) — the look side of opening a file pane. Web
+/// reads from the generated source archive; native shells read the real fs.
pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 {
- if (!platform_has_fs) return error.NoFilesystem;
+ if (!platform_has_fs) {
+ var normalized_buf: [4096]u8 = undefined;
+ const normalized = normalizeVirtualPath(path, &normalized_buf) orelse return error.OpenFailed;
+ const source = findEmbeddedSource(normalized, true) orelse return error.OpenFailed;
+ return gpa.dupe(u8, source.contents);
+ }
var pathbuf: [4096]u8 = undefined;
const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch return error.PathTooLong;
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });