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