╭ Repos.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const ui = @import("../../ui.zig"); 4 const xit = @import("xit"); 5 const hash = xit.hash; 6 const xitui = xit.xitui; 7 const wgt = xitui.widget; 8 const layout = xitui.layout; 9 const inp = xitui.input; 10 const Grid = xitui.grid.Grid; 11 const Focus = xitui.focus.Focus; 12 13 // how many repos a page shows before the "load more" row appears. 14 pub const page_size = 20; 15 16 repos: []const evt.Repo, 17 owner_names: []const []const u8, 18 // the window start this page was built with, mirrored into the url. 19 after: usize, 20 // the `after` for the "load more" link, or null when this is the last page. 21 next_after: ?usize, 22 23 const Self = @This(); 24 25 pub fn init( 26 arena: *std.heap.ArenaAllocator, 27 haxy_moment: evt.AdminDB.HashMap(.read_only), 28 after: usize, 29 ) !Self { 30 const DB = evt.AdminDB; 31 const hash_kind = evt.admin_repo_opts.hash; 32 33 var repos: std.ArrayList(evt.Repo) = .empty; 34 var owner_names: std.ArrayList([]const u8) = .empty; 35 36 const empty: Self = .{ .repos = &.{}, .owner_names = &.{}, .after = after, .next_after = null }; 37 38 // the ordered repo-list (oldest first); absent until the first repo exists. 39 const repo_list_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "repo-list")) orelse return empty; 40 const repo_list = try DB.ArrayList(.read_only).init(repo_list_cursor); 41 const count = try repo_list.count(); 42 43 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return empty; 44 const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); 45 46 // read the window [after, after+page_size) by index — a direct O(log n) 47 // seek per entry, never scanning the whole table. ids whose repo has been 48 // deleted (tombstones) are skipped. 49 const end = @min(after + page_size, count); 50 var i = after; 51 while (i < end) : (i += 1) { 52 const id_cursor = try repo_list.getCursor(@intCast(i)) orelse continue; 53 var event_id: [evt.event_id_size]u8 = undefined; 54 _ = try id_cursor.readBytes(&event_id); 55 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, &event_id)) orelse continue; 56 const repo_map = try DB.HashMap(.read_only).init(repo_cursor); 57 const repo_event = try evt.read(evt.Repo, DB, hash_kind, arena, repo_map); 58 try repos.append(arena.allocator(), repo_event); 59 60 const owner = try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo_event.user_id); 61 try owner_names.append(arena.allocator(), if (owner) |o| o.name else ""); 62 } 63 64 return .{ 65 .repos = repos.items, 66 .owner_names = owner_names.items, 67 .after = after, 68 .next_after = if (end < count) end else null, 69 }; 70 } 71 72 pub const View = struct { 73 list: ui.FlowBox.Scroll, 74 data: *const Self, 75 76 pub fn init(allocator: std.mem.Allocator, data: *const Self) !View { 77 var self = blk: { 78 var list = try ui.FlowBox.Scroll.init(allocator, .{}); 79 errdefer list.deinit(allocator); 80 81 break :blk View{ 82 .list = list, 83 .data = data, 84 }; 85 }; 86 errdefer self.deinit(allocator); 87 88 var arena = std.heap.ArenaAllocator.init(allocator); 89 defer arena.deinit(); 90 const aa = arena.allocator(); 91 92 // one row per repo, plus a trailing "load more" row when more remain. 93 const extra: usize = if (data.next_after != null) 1 else 0; 94 const lines = try aa.alloc([]const u8, data.repos.len + extra); 95 const links = try aa.alloc([]const u8, data.repos.len + extra); 96 for (data.repos, 0..) |repo, i| { 97 lines[i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }); 98 // clicking a repo opens its page; the "a:" prefix makes the web 99 // renderer emit an <a href="/repo/alice/foo"> anchor. skip the link 100 // when the owner is unknown so it isn't a dead route. 101 const owner = data.owner_names[i]; 102 links[i] = if (owner.len > 0) try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ owner, repo.name }) else ""; 103 } 104 if (data.next_after) |next_after| { 105 // the load-more row navigates to the next window (full reload on web, 106 // Nav rebuild on the TUI), exactly like the commits "load more". 107 lines[data.repos.len] = "load more"; 108 links[data.repos.len] = try std.fmt.allocPrint(aa, "a:/repos?after={d}", .{next_after}); 109 } 110 try self.list.setItems(allocator, lines, links); 111 112 return self; 113 } 114 115 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 116 self.list.deinit(allocator); 117 } 118 119 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 120 try self.list.build(allocator, constraint, root_focus); 121 } 122 123 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 124 try self.list.input(allocator, key, root_focus); 125 } 126 127 pub fn clearGrid(self: *View) void { 128 self.list.clearGrid(); 129 } 130 131 pub fn getGrid(self: View) ?Grid { 132 return self.list.getGrid(); 133 } 134 135 pub fn getFocus(self: *View) *Focus { 136 return self.list.getFocus(); 137 } 138 139 pub fn getSelectedIndex(self: View) ?usize { 140 return self.list.getSelectedIndex(); 141 } 142 }; 143