╭ 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 Key = xitui.input.Key; 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.Record, 16 owner_names: []const []const u8, 17 start: usize, // the window start this page was built with, mirrored into the url 18 next_start: ?usize, // the `start` 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 start: 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.Record) = .empty; 31 var owner_names: std.ArrayList([]const u8) = .empty; 32 33 const empty: Self = .{ .repos = &.{}, .owner_names = &.{}, .start = start, .next_start = null }; 34 35 // the active repos ordered by creation (newest first); absent until the first repo 36 // exists. keyed by orderKey ([created-order][event-id]); the trailing bytes 37 // of each key are the repo event id. 38 const repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Repo.active_id_set_key)) orelse return empty; 39 const repo_id_set = try DB.SortedSet(.read_only).init(repo_id_set_cursor); 40 const count = try repo_id_set.count(); 41 42 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return empty; 43 const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); 44 45 // read the window [start, start+page_size) with one seek to the start rank, 46 // then a sequential walk 47 const end = @min(start +| page_size, count); 48 var iter = try repo_id_set.iteratorFromIndex(start); 49 var i = start; 50 while (i < end) : (i += 1) { 51 var id_cursor = (try iter.next()) orelse break; 52 const id_kv = try id_cursor.readKeyValuePair(); 53 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 54 _ = try id_kv.key_cursor.readBytes(&order_key); 55 const event_id = order_key[@sizeOf(u64)..]; 56 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue; 57 const repo_map = try DB.HashMap(.read_only).init(repo_cursor); 58 const repo_event = try evt.read(evt.Repo.Record, DB, hash_kind, arena, repo_map); 59 try repos.append(arena.allocator(), repo_event); 60 61 const owner = try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo_event.event.user_id); 62 try owner_names.append(arena.allocator(), if (owner) |o| o.event.name else ""); 63 } 64 65 return .{ 66 .repos = repos.items, 67 .owner_names = owner_names.items, 68 .start = start, 69 .next_start = if (end < count) end else null, 70 }; 71 } 72 73 pub const View = struct { 74 list: ui.widget.FlowBox.Scroll, 75 data: *const Self, 76 77 pub fn init(allocator: std.mem.Allocator, data: *const Self, web_native: bool) !View { 78 var self = blk: { 79 var list = try ui.widget.FlowBox.Scroll.init(allocator, .{}, web_native); 80 errdefer list.deinit(allocator); 81 82 break :blk View{ 83 .list = list, 84 .data = data, 85 }; 86 }; 87 errdefer self.deinit(allocator); 88 89 var arena = std.heap.ArenaAllocator.init(allocator); 90 defer arena.deinit(); 91 const aa = arena.allocator(); 92 93 // a leading "previous" row off the first window, one row per repo, then 94 // a trailing "next" row when more remain. each window row navigates to the 95 // adjacent window (full reload on web, Nav rebuild on the TUI). 96 var items: std.ArrayList(ui.widget.FlowBox.Item) = .empty; 97 if (data.start > 0) 98 try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/repos/start:{d}", .{data.start -| page_size}) }); 99 for (data.repos, 0..) |repo, i| { 100 // clicking a repo opens its page; the "a:" prefix makes the web 101 // renderer emit an <a href="/repo/alice/foo"> anchor. skip the link 102 // when the owner is unknown so it isn't a dead route. 103 const owner = data.owner_names[i]; 104 try items.append(aa, .{ 105 .text = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.event.name, repo.event.description }), 106 .link = if (owner.len > 0) try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ owner, repo.event.name }) else "", 107 }); 108 } 109 if (data.next_start) |next_start| 110 try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/repos/start:{d}", .{next_start}) }); 111 try self.list.setItems(allocator, items.items); 112 113 return self; 114 } 115 116 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 117 self.list.deinit(allocator); 118 } 119 120 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 121 try self.list.build(allocator, constraint, root_focus); 122 } 123 124 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 125 try self.list.input(allocator, key, root_focus); 126 } 127 128 pub fn clearGrid(self: *View) void { 129 self.list.clearGrid(); 130 } 131 132 pub fn getGrid(self: View) ?Grid { 133 return self.list.getGrid(); 134 } 135 136 pub fn getFocus(self: *View) *Focus { 137 return self.list.getFocus(); 138 } 139 140 pub fn atTop(self: View) bool { 141 return self.list.atTop(); 142 } 143 }; 144