╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │31 repo: Repo, │ │32 │ │33 pub fn init(arena: *std.heap.ArenaAllocator, session: *Session, route: RoutablePage) !Page { │ │34 - const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │34 + // the repo page can build without a moment in local mode; the home and │ │35 + // user pages always read from the admin db. │ │36 return switch (route.parent()) { │ │36 - .home => .{ .home = try Home.init(arena, haxy_moment, switch (route) { │ │37 + .home => .{ .home = try Home.init(arena, session.haxy_moment orelse return error.NoMoment, switch (route) {│ │38 .home_users => |after| after, │ │39 else => 0, │ │40 }, switch (route) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │42 else => 0, │ │43 }) }, │ │44 .user => switch (route) { │ │44 - .user_repos => |u| .{ .user = try User.init(arena, haxy_moment, u.name, u.after) }, │ │45 - .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name, 0) }, │ │45 + .user_repos => |u| .{ .user = try User.init(arena, session.haxy_moment orelse return error.NoMoment, u.name, u.after) }, │ │46 + .user_settings, .user_auth => |name| .{ .user = try User.init(arena, session.haxy_moment orelse return error.NoMoment, name, 0) },│ │47 else => return error.UnexpectedRoute, │ │48 }, │ │49 .repo => switch (route) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │269 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │270 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │271 }; │ │272 + } │ │273 + │ │274 + // the url with its leading "/repo/<owner>/<name>" elided, for local mode │ │275 + // (every url there points at the one local repo). non-repo routes serialize │ │276 + // in full. │ │277 + pub fn urlAllocLocal(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │278 + const full = try self.urlAlloc(arena); │ │279 + const name_str: []const u8 = switch (self) { │ │280 + .repo_files => |f| f.name.slice(), │ │281 + .repo_commits => |c| c.name.slice(), │ │282 + .repo_refs => |r| r.name.slice(), │ │283 + .repo_issues => |i| i.name.slice(), │ │284 + .repo_settings, .repo_auth => |n| n.slice(), │ │285 + else => return full, │ │286 + }; │ │287 + const identity = (RepoFiles.parse(name_str) orelse return full).identity; │ │288 + const rest = full[repo_segment.len + identity.len ..]; │ │289 + if (rest.len == 0) return "/"; │ │290 + if (rest[0] == '/') return rest; │ │291 + return try std.fmt.allocPrint(arena.allocator(), "/{s}", .{rest}); │ │292 } │ │293 │ │294 pub fn parent(self: RoutablePage) PageKind { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │584 return out.written(); │ │585 } │ │586 │ │566 -// a resolved ref/oid for a repo opened with `repo_opts` │ │567 -pub fn ResolvedRefOrOid(comptime repo_opts: rp.RepoOpts(.xit)) type { │ │587 +// a resolved ref/oid for a repo opened with `repo_kind`/`repo_opts` │ │588 +pub fn ResolvedRefOrOid(comptime repo_kind: rp.RepoKind, comptime repo_opts: rp.RepoOpts(repo_kind)) type {│ │589 return struct { │ │590 pub const hex_len = hash.hexLen(repo_opts.hash); │ │591 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │605 // the url-encoded form (as it appears in the route). null when it doesn't│ │606 // resolve (an unknown branch/tag, or a malformed/unknown oid). │ │607 pub fn init( │ │587 - repo: *rp.Repo(.xit, repo_opts), │ │608 + repo: *rp.Repo(repo_kind, repo_opts), │ │609 io: std.Io, │ │610 aa: std.mem.Allocator, │ │611 requested_ref_or_oid: ?RoutablePage.RefOrOid, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │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); │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯