╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │649 }; │ │650 } │ │651 │ │652 +// where a repo page reads its on-disk repo from │ │653 +pub const RepoSource = struct { │ │654 + path: []const u8, │ │655 + repo_kind: rp.RepoKind, │ │656 +}; │ │657 + │ │658 // per-connection mutable state. each SSH session / web session / local TUI│ │659 // run gets its own. │ │660 pub const Session = struct { │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │673 // rebuilds pages from the serialized snapshot rather than from disk. │ │674 io: ?std.Io = null, │ │675 repos_dir: ?[]const u8 = null, │ │676 + // the single on-disk repo this session views, when running in local mode │ │677 + // (haxy invoked with no arguments inside a repo). null on the server paths, │ │678 + // which resolve repos from the admin db instead. │ │679 + local: ?RepoSource = null, │ │680 pending: std.ArrayList(Action) = .empty, // actions queued by widgets this frame│ │681 // focus id -> the live TextInput, refreshed each frame by the views that own │ │682 // inputs. web/wasm form handling looks widgets up here by focus id. │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │792 } │ │793 }; │ │794 │ │764 -pub fn run(io: std.Io, allocator: std.mem.Allocator, session: *Session, repo: *rp.Repo(.xit, evt.admin_repo_opts)) !void { │ │795 +pub fn run(io: std.Io, allocator: std.mem.Allocator, session: *Session, repo_maybe: ?*rp.Repo(.xit, evt.admin_repo_opts)) !void {│ │796 var nav = try Nav.init(allocator, session); │ │797 defer nav.deinit(allocator); │ │798 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │831 session.applyPending(); │ │832 │ │833 // pick up data written by other handles so the next navigation │ │803 - // builds its page from a current moment │ │804 - try session.reloadMoment(repo); │ │834 + // builds its page from a current moment (local mode has no admin repo)│ │835 + if (repo_maybe) |repo| try session.reloadMoment(repo); │ │836 │ │837 // reconcile navigation: forward to a new page, or back on escape. │ │838 try nav.sync(allocator, session); │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │986 // refresh: rebuild the current page in place from a current moment │ │987 if (session.refresh_requested) { │ │988 session.refresh_requested = false; │ │958 - if (session.haxy_moment != null) { │ │989 + if (session.haxy_moment != null or session.local != null) { │ │990 const arena = try allocator.create(std.heap.ArenaAllocator);│ │991 arena.* = std.heap.ArenaAllocator.init(allocator); │ │992 errdefer freeArena(allocator, arena); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1046 // cross-page link or tab change crossing pages) │ │1047 if (session.next_page) |route| { │ │1048 session.next_page = null; │ │1018 - if (session.haxy_moment == null) return; │ │1049 + if (session.haxy_moment == null and session.local == null) return;│ │1050 // the page we navigated to becomes the current page │ │1051 session.data.current_page = route; │ │1052 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1729 │ │1730 _ = self.arena.reset(.retain_capacity); │ │1731 const aa = self.arena.allocator(); │ │1701 - const path = self.session.data.current_page.urlAlloc(&self.arena) catch return; │ │1732 + const page = self.session.data.current_page; │ │1733 + const path = (if (self.session.local != null) page.urlAllocLocal(&self.arena) else page.urlAlloc(&self.arena)) catch return;│ │1734 const text = if (self.session.web_port) |port| │ │1735 std.fmt.allocPrint(aa, "http://localhost:{d}{s}", .{ port, path }) catch return │ │1736 else │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │55 ) !Self { │ │56 const DB = evt.AdminDB; │ │57 const hash_kind = evt.admin_repo_opts.hash; │ │58 - const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │58 │ │59 // every repo route's stored string starts with "owner/name"; the files and│ │60 // commits routes additionally encode a ref/oid (files also a directory). │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 else => "", │ │118 }; │ │119 │ │121 - const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, rf.owner, rf.name)) orelse return error.NotFound; │ │122 - const repo = found.repo; │ │120 + // where the on-disk repo lives (null keeps the views' empty fallback), plus │ │121 + // the repo and owner-name metadata the header shows. local mode already │ │122 + // knows all three; the server paths resolve them from the admin db. │ │123 + var source: ?ui.RepoSource = null; │ │124 + var repo: evt.Repo = undefined; │ │125 + var owner_name: []const u8 = undefined; │ │126 + if (session.local) |local| { │ │127 + source = local; │ │128 + repo = .{ │ │129 + .user_id = "", │ │130 + .name = try arena.allocator().dupe(u8, rf.name), │ │131 + .description = "", │ │132 + .enable_issue = true, │ │133 + }; │ │134 + owner_name = try arena.allocator().dupe(u8, rf.owner); │ │135 + } else { │ │136 + const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │137 + const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, rf.owner, rf.name)) orelse return error.NotFound;│ │138 + repo = found.repo; │ │139 + │ │140 + // resolve the creating user so the header can show their name to the left │ │141 + // of the repo title. │ │142 + const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; │ │143 + owner_name = owner.name; │ │144 │ │124 - // resolve the creating user so the header can show their name to the left │ │125 - // of the repo title. │ │126 - const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; │ │145 + // the repo's working copy lives at <repos_dir>/<hex event id>. │ │146 + if (session.repos_dir) |repos_dir| { │ │147 + const hex = std.fmt.bytesToHex(found.event_id, .lower); │ │148 + source = .{ │ │149 + .path = try std.fs.path.join(arena.allocator(), &.{ repos_dir, &hex }), │ │150 + .repo_kind = .xit, │ │151 + }; │ │152 + } │ │153 + } │ │154 │ │155 // build files and commits first so their resolved ref (the default branch │ │156 // when the route named none) can canonicalize each tab's mirror url to the │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │157 // explicit ref it's viewing rather than leaving it bare. both resolve the │ │158 // same requested ref, so they end up viewing the same one. │ │132 - const files = try Files.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after);│ │133 - const commits = try Commits.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); │ │159 + const files = try Files.init(arena, session, source, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after); │ │160 + const commits = try Commits.init(arena, session, source, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); │ │161 │ │162 // each tab mirror carries this page's route for that tab; tabs not targeted │ │163 // by the incoming route fall back to their root/first-page route. the files │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯