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