diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-10 17:32:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | 685b8c8e74c9edbdf701594f50ff1b70ac288ec9 (patch) | |
| tree | f303bd5e1f422030c2c645ea2d6c3a393990fb5e /src/look.zig | |
| parent | ddc2eefb1e7cdd402593efb1297c25fc7ae4b110 (diff) | |
| download | pardes-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.zig | 79 |
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 }); |
