╭ UsersAndRepos.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 rp = xit.repo; 6 const hash = xit.hash; 7 const xitui = xit.xitui; 8 const term = xitui.terminal; 9 const wgt = xitui.widget; 10 const layout = xitui.layout; 11 const inp = xitui.input; 12 const Grid = xitui.grid.Grid; 13 const Focus = xitui.focus.Focus; 14 15 users: []UserAndRepos, 16 17 const Self = @This(); 18 19 pub const UserAndRepos = struct { 20 user: evt.User, 21 repos: []const evt.Repo, 22 }; 23 24 pub fn empty() Self { 25 return .{ 26 .users = &.{}, 27 }; 28 } 29 30 pub fn init( 31 comptime repo_opts: rp.RepoOpts(.xit), 32 arena: *std.heap.ArenaAllocator, 33 repo: *rp.Repo(.xit, repo_opts), 34 ) !Self { 35 const DB = rp.Repo(.xit, repo_opts).DB; 36 37 const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); 38 39 const moment_cursor = try history.getCursor(-1) orelse return error.NotFound; 40 const moment = try DB.HashMap(.read_only).init(moment_cursor); 41 42 const last_object_id_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy-last-object-id")) orelse return error.NotFound; 43 var last_object_id: [hash.byteLen(repo_opts.hash)]u8 = undefined; 44 _ = try last_object_id_cursor.readBytes(&last_object_id); 45 46 const haxy_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, "haxy")) orelse return error.NotFound; 47 const haxy = try DB.ArrayList(.read_only).init(haxy_cursor); 48 49 const haxy_moments_cursor = try haxy.getCursor(-1) orelse return error.NotFound; 50 const haxy_moments = try DB.HashMap(.read_only).init(haxy_moments_cursor); 51 52 const haxy_moment_cursor = try haxy_moments.getCursor(hash.bytesToInt(repo_opts.hash, &last_object_id)) orelse return error.NotFound; 53 const haxy_moment = try DB.HashMap(.read_only).init(haxy_moment_cursor); 54 55 // collect users, keyed by hash(user_event_id) so we can match repos to them 56 var hash_to_index: std.AutoArrayHashMapUnmanaged(hash.HashInt(repo_opts.hash), usize) = .empty; 57 58 var user_events: std.ArrayList(evt.User) = .empty; 59 60 var repos_lists: std.ArrayList(std.ArrayList(evt.Repo)) = .empty; 61 62 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 63 const event_id_to_user = try DB.HashMap(.read_only).init(event_id_to_user_cursor); 64 65 var users_iter = try event_id_to_user.iterator(); 66 while (try users_iter.next()) |kv_cursor| { 67 const kv = try kv_cursor.readKeyValuePair(); 68 const user_map = try DB.HashMap(.read_only).init(kv.value_cursor); 69 const user_event = try evt.read(evt.User, DB, repo_opts.hash, arena, user_map); 70 71 try hash_to_index.put(arena.allocator(), kv.hash, user_events.items.len); 72 try user_events.append(arena.allocator(), user_event); 73 try repos_lists.append(arena.allocator(), .empty); 74 } 75 76 // collect repos, bucketed by their user_id 77 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 78 const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); 79 80 var repos_iter = try event_id_to_repo.iterator(); 81 while (try repos_iter.next()) |kv_cursor| { 82 const kv = try kv_cursor.readKeyValuePair(); 83 const repo_map = try DB.HashMap(.read_only).init(kv.value_cursor); 84 const repo_event = try evt.read(evt.Repo, DB, repo_opts.hash, arena, repo_map); 85 86 const user_hash = hash.hashInt(repo_opts.hash, repo_event.user_id); 87 if (hash_to_index.get(user_hash)) |user_index| { 88 try repos_lists.items[user_index].append(arena.allocator(), repo_event); 89 } 90 } 91 92 var user_repos: std.ArrayList(UserAndRepos) = .empty; 93 94 for (user_events.items, repos_lists.items) |user_event, *repos| { 95 try user_repos.append(arena.allocator(), .{ 96 .user = user_event, 97 .repos = repos.items, 98 }); 99 } 100 101 return .{ 102 .users = user_repos.items, 103 }; 104 } 105 106 pub const View = struct { 107 allocator: std.mem.Allocator, 108 box: wgt.Box(ui.Widget), 109 data: *const Self, 110 last_user_index: ?usize, 111 112 const user_list_index: usize = 0; 113 const repo_list_index: usize = 1; 114 115 pub fn init(allocator: std.mem.Allocator, data: *const Self) !View { 116 var self = blk: { 117 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .horiz }); 118 errdefer box.deinit(); 119 120 { 121 var user_list = try ui.SelectableList.init(allocator); 122 errdefer user_list.deinit(); 123 try box.children.put(allocator, user_list.getFocus().id, .{ .widget = .{ .selectable_list = user_list }, .rect = null, .min_size = .{ .width = 30, .height = null } }); 124 } 125 126 { 127 var repo_list = try ui.SelectableList.init(allocator); 128 errdefer repo_list.deinit(); 129 try box.children.put(allocator, repo_list.getFocus().id, .{ .widget = .{ .selectable_list = repo_list }, .rect = null, .min_size = .{ .width = 40, .height = null } }); 130 } 131 132 break :blk View{ 133 .allocator = allocator, 134 .box = box, 135 .data = data, 136 .last_user_index = null, 137 }; 138 }; 139 errdefer self.deinit(); 140 141 // populate the user list once at init using a temporary arena 142 // for the formatted lines (setItems dupes them). 143 var arena = std.heap.ArenaAllocator.init(allocator); 144 defer arena.deinit(); 145 const aa = arena.allocator(); 146 147 const user_lines = try aa.alloc([]const u8, data.users.len); 148 for (data.users, 0..) |uwr, i| { 149 user_lines[i] = try std.fmt.allocPrint(aa, "{s} ({s})", .{ uwr.user.name, uwr.user.display_name }); 150 } 151 const user_list = &self.box.children.values()[user_list_index].widget.selectable_list; 152 try user_list.setItems(user_lines); 153 154 self.getFocus().child_id = self.box.children.keys()[user_list_index]; 155 try self.updateRepos(); 156 157 return self; 158 } 159 160 pub fn deinit(self: *View) void { 161 self.box.deinit(); 162 } 163 164 pub fn build(self: *View, constraint: layout.Constraint, root_focus: *Focus) !void { 165 self.clearGrid(); 166 // refresh the repo list here (rather than in input) so it stays in 167 // sync regardless of how the user selection changed — including 168 // clicks that go straight through setFocus without touching input. 169 // updateRepos is a no-op when the selected user hasn't changed. 170 try self.updateRepos(); 171 try self.box.build(constraint, root_focus); 172 } 173 174 pub fn input(self: *View, key: inp.Key, root_focus: *Focus) !void { 175 if (self.getFocus().child_id) |child_id| { 176 if (self.box.children.getIndex(child_id)) |current_index| { 177 const child = &self.box.children.values()[current_index].widget; 178 179 const index = blk: { 180 switch (key) { 181 .arrow_left => { 182 if (current_index == repo_list_index) { 183 break :blk user_list_index; 184 } 185 }, 186 .arrow_right => { 187 if (current_index == user_list_index) { 188 break :blk repo_list_index; 189 } 190 }, 191 .codepoint => { 192 switch (key.codepoint) { 193 13 => { 194 if (current_index == user_list_index) { 195 break :blk repo_list_index; 196 } 197 }, 198 127, '\x1B' => { 199 if (current_index == repo_list_index) { 200 break :blk user_list_index; 201 } 202 }, 203 else => {}, 204 } 205 }, 206 else => {}, 207 } 208 try child.input(key, root_focus); 209 break :blk current_index; 210 }; 211 212 if (index != current_index) { 213 try root_focus.setFocus(self.box.children.keys()[index]); 214 } 215 } 216 } 217 } 218 219 pub fn clearGrid(self: *View) void { 220 self.box.clearGrid(); 221 } 222 223 pub fn getGrid(self: View) ?Grid { 224 return self.box.getGrid(); 225 } 226 227 pub fn getFocus(self: *View) *Focus { 228 return self.box.getFocus(); 229 } 230 231 fn updateRepos(self: *View) !void { 232 const user_list = &self.box.children.values()[user_list_index].widget.selectable_list; 233 const repo_list = &self.box.children.values()[repo_list_index].widget.selectable_list; 234 235 const user_index = user_list.getSelectedIndex(); 236 if (user_index == self.last_user_index) return; 237 self.last_user_index = user_index; 238 239 const repos = if (user_index) |i| self.data.users[i].repos else &.{}; 240 241 var arena = std.heap.ArenaAllocator.init(self.allocator); 242 defer arena.deinit(); 243 const aa = arena.allocator(); 244 245 const repo_lines = try aa.alloc([]const u8, repos.len); 246 for (repos, 0..) |repo_event, i| { 247 repo_lines[i] = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo_event.name, repo_event.description }); 248 } 249 try repo_list.setItems(repo_lines); 250 } 251 }; 252