╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ UsersAndRepos.zig ────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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