╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Repo.zig ─────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
22 23 header: Header, 24 repo: evt.Repo, 25 files: Files, 26 commits: Commits, 27 refs: Refs, 28 settings: Settings, 29 auth: Auth, 30 quit: Quit, 31 // the full files route this page renders ("owner/name" or "owner/name/files/<dir>"), 32 // mirrored into current_page when the files tab is selected. 33 route_name: Array, 34 // the content window (?after=N) of that files route; mirrored alongside the name 35 // so the url keeps the query param even when focus isn't in the files view. 36 files_after: usize, 37 // the commits route this page renders ("owner/name/commits[/<oid>]"), mirrored 38 // when the commits tab is selected. 39 commits_route_name: Array, 40 // the diff window (?after=N) of that commits route; mirrored alongside the name 41 // so the url keeps the query param even when focus isn't in the commits view. 42 commits_after: usize, 43 // "owner/name" alone, mirrored when the settings/auth tab is selected. 44 identity: Array, 45 46 const Self = @This(); 47 48 pub fn init( 49 arena: *std.heap.ArenaAllocator, 50 session: *ui.Session, 51 route: ui.RoutablePage, 52 ) !Self { 53 const DB = evt.AdminDB; 54 const hash_kind = evt.admin_repo_opts.hash; 55 const haxy_moment = session.haxy_moment orelse return error.NoMoment; 56 57 // every repo route's stored string starts with "owner/name"; the files and 58 // commits routes additionally encode a ref/oid (files also a directory). 59 const name_str: []const u8 = switch (route) { 60 .repo_files => |f| f.name.slice(), 61 .repo_settings, .repo_auth => |n| n.slice(), 62 .repo_commits => |c| c.name.slice(), 63 .repo_refs => |r| r.name.slice(), 64 else => return error.UnexpectedRoute, 65 }; 66 const rf = ui.RoutablePage.RepoFiles.parse(name_str) orelse return error.NotFound; 67 const tag = std.meta.activeTag(route); 68 69 // the files and commits tabs share one ref/oid: whichever the incoming route 70 // names (it rides on the route's target tab), or the default branch when 71 // neither tab is targeted. building both views at it keeps switching tabs 72 // (even by key, without a reload) on the same ref. a null ref means the 73 // default branch (Files/Commits.init resolve it). the directory and the diff 74 // window only apply to their own tab. 75 const commits_ref = if (tag == .repo_commits) ui.RoutablePage.repoCommitsRef(name_str) else ui.RoutablePage.CommitsRef{ .ref_or_oid = null, .value = "" }; 76 const requested_ref_or_oid: ?ui.RoutablePage.RefOrOid = switch (tag) { 77 .repo_files => rf.ref_kind, 78 .repo_commits => commits_ref.ref_or_oid, 79 else => null, 80 }; 81 const requested_ref_value: []const u8 = switch (tag) { 82 .repo_files => rf.ref_value, 83 .repo_commits => commits_ref.value, 84 else => "", 85 }; 86 const files_dir = if (tag == .repo_files) rf.dir else ""; 87 // the line offset the files view's selected file content window starts at. 88 const files_after: usize = switch (route) { 89 .repo_files => |f| f.after, 90 else => 0, 91 }; 92 // how many diff hunks the commits view's selected commit shows ("load more"). 93 const commits_after: usize = switch (route) { 94 .repo_commits => |c| c.after, 95 else => 0, 96 }; 97 // the refs tab paginates one column at a time: this offset applies to 98 // `refs_kind`'s column, the other stays at its first window. 99 const refs_kind: ui.RoutablePage.RefKind = switch (route) { 100 .repo_refs => |r| r.kind, 101 else => .branch, 102 }; 103 const refs_after: usize = switch (route) { 104 .repo_refs => |r| r.after, 105 else => 0, 106 }; 107 108 const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, rf.owner, rf.name)) orelse return error.NotFound; 109 const repo = found.repo; 110 111 // resolve the creating user so the header can show their name to the left 112 // of the repo title. 113 const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; 114 115 // build files and commits first so their resolved ref (the default branch 116 // when the route named none) can canonicalize each tab's mirror url to the 117 // explicit ref it's viewing rather than leaving it bare. both resolve the 118 // same requested ref, so they end up viewing the same one. 119 const files = try Files.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after); 120 const commits = try Commits.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); 121 122 // each tab mirror carries this page's route for that tab; tabs not targeted 123 // by the incoming route fall back to their root/first-page route. the files 124 // mirror carries the selected file (when the route named one) so the url 125 // keeps it before focus enters the view. 126 const files_path = if (files.selected_file) |f| try Files.childDir(arena.allocator(), files.dir, f) else files.dir; 127 // the content window offset only applies to a selected file, so the bare 128 // directory route drops it. 129 const files_route_after = if (files.selected_file != null) files_after else 0; 130 const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files.ref_or_oid, files.ref_or_oid_value, files_path, files_route_after) orelse return error.NotFound).repo_files.name; 131 const commits_route_name = (ui.RoutablePage.repoCommitsRoute(rf.identity, commits.ref_or_oid, commits.ref_or_oid_value, commits_after) orelse return error.NotFound).repo_commits.name; 132 133 return .{ 134 // files and commits resolve the same ref, so either's serves the header, 135 // which points both tabs at it. 136 .header = try Header.init(arena, repo.name, owner.name, files.ref_or_oid, files.ref_or_oid_value), 137 .repo = repo, 138 .files = files, 139 .commits = commits, 140 .refs = try Refs.init(arena, session, &found.event_id, rf.identity, refs_kind, refs_after), 141 .settings = Settings.init(), 142 .auth = Auth.init(), 143 .quit = Quit.init(), 144 .route_name = route_name, 145 .files_after = files_route_after, 146 .commits_route_name = commits_route_name, 147 .commits_after = commits_after, 148 .identity = Array.from(rf.identity) orelse return error.NotFound, 149 }; 150 } 151 152 pub const View = struct { 153 box: wgt.Box(ui.Widget), 154 data: *const Self, 155 session: *ui.Session, 156 157 const header_index: usize = 0; 158 const stack_index: usize = 1; 159 160 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 161 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .rounded_corners = true, .direction = .vert }); 162 errdefer box.deinit(allocator); 163 164 // build the header first so we can grab the files-tab id for the auth 165 // view (it focuses there after login). 166 var files_tab_id: usize = undefined; 167 { 168 var header_view = try Header.View.init(allocator, &data.header, session); 169 errdefer header_view.deinit(allocator); 170 files_tab_id = header_view.tab_ids.keys()[0]; 171 try box.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_header = header_view }, .rect = null, .min_size = null }); 172 } 173 174 { 175 var stack = try wgt.Stack(ui.Widget).init(allocator); 176 errdefer stack.deinit(allocator); 177 178 // files — the default tab: the current directory's listing. 179 { 180 var files_view = try Files.View.init(allocator, &data.files, session); 181 errdefer files_view.deinit(allocator); 182 try stack.children.put(allocator, files_view.getFocus().id, .{ .repo_files = files_view }); 183 } 184 185 // commits — the current page of the commit log. 186 { 187 var commits_view = try Commits.View.init(allocator, &data.commits, session); 188 errdefer commits_view.deinit(allocator); 189 try stack.children.put(allocator, commits_view.getFocus().id, .{ .repo_commits = commits_view }); 190 } 191 192 // refs — the repo's branches and tags. 193 { 194 var refs_view = try Refs.View.init(allocator, &data.refs, session); 195 errdefer refs_view.deinit(allocator); 196 try stack.children.put(allocator, refs_view.getFocus().id, .{ .repo_refs = refs_view }); 197 } 198 199 { 200 var settings_view = try Settings.View.init(allocator, &data.settings, session); 201 errdefer settings_view.deinit(allocator); 202 try stack.children.put(allocator, settings_view.getFocus().id, .{ .home_settings = settings_view }); 203 } 204 205 { 206 var auth_view = try Auth.View.init(allocator, &data.auth, session, files_tab_id); 207 errdefer auth_view.deinit(allocator); 208 try stack.children.put(allocator, auth_view.getFocus().id, .{ .home_auth = auth_view }); 209 } 210 211 if (session.is_terminal) { 212 var quit_view = try Quit.View.init(allocator, &data.quit, session); 213 errdefer quit_view.deinit(allocator); 214 try stack.children.put(allocator, quit_view.getFocus().id, .{ .quit = quit_view }); 215 } 216 217 try box.children.put(allocator, stack.getFocus().id, .{ .widget = .{ .stack = stack }, .rect = null, .min_size = null }); 218 } 219 220 var self = View{ .box = box, .data = data, .session = session }; 221 self.getFocus().child_id = box.children.keys()[header_index]; 222 return self; 223 } 224 225 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 226 self.box.deinit(allocator); 227 } 228 229 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 230 self.clearGrid(); 231 const header = &self.box.children.values()[header_index].widget.repo_header; 232 const stack = &self.box.children.values()[stack_index].widget.stack; 233 234 // each header tab maps 1:1 to a stack child by position. mirror the 235 // selection into current_page so the host can push the matching url; 236 // all repo tabs share the .repo parent, so this stays on the page 237 // rather than navigating. 238 if (header.getSelectedIndex()) |index| { 239 stack.getFocus().child_id = stack.children.keys()[index]; 240 switch (std.meta.activeTag(stack.children.values()[index])) { 241 // files/commits carry this page's content route (directory / log 242 // page); settings/auth carry only "owner/name". 243 .repo_commits => self.session.data.current_page = .{ .repo_commits = .{ .name = self.data.commits_route_name, .after = self.data.commits_after } }, 244 // the refs tab mirrors this page's paginated column + offset. 245 .repo_refs => self.session.data.current_page = .{ .repo_refs = .{ .name = self.data.identity, .kind = self.data.refs.kind, .after = self.data.refs.after } }, 246 .home_settings => self.session.data.current_page = .{ .repo_settings = self.data.identity }, 247 .home_auth => self.session.data.current_page = .{ .repo_auth = self.data.identity }, 248 // the quit tab is tty-only and not a route, so leave current_page 249 // alone (nothing to mirror into the url). 250 .quit => {}, 251 // the files tab (this page's directory route) 252 else => self.session.data.current_page = .{ .repo_files = .{ .name = self.data.route_name, .after = self.data.files_after } }, 253 } 254 } 255 try self.box.build(allocator, constraint, root_focus); 256 } 257 258 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 259 if (self.getFocus().child_id) |child_id| { 260 if (self.box.children.getIndex(child_id)) |current_index| { 261 const child = &self.box.children.values()[current_index].widget; 262 var index = current_index; 263 264 const Direction = enum { up, down, none }; 265 const direction: Direction = switch (key) { 266 .arrow_up => .up, 267 .arrow_down => .down, 268 .mouse => |mouse| if (mouse.action == .scroll) 269 (if (mouse.action.scroll == .up) .up else .down) 270 else 271 .none, 272 else => .none, 273 }; 274 275 switch (direction) { 276 .up => { 277 switch (child.*) { 278 .repo_header => { 279 try child.input(allocator, key, root_focus); 280 }, 281 .stack => { 282 if (child.stack.getSelected()) |selected_widget| { 283 const at_top = switch (selected_widget.*) { 284 .repo_files => |*v| v.getSelectedIndex() == 0, 285 .repo_commits => |*v| v.getSelectedIndex() == 0, 286 .repo_refs => |*v| v.getSelectedIndex() == 0, 287 .home_settings => |*v| v.getSelectedIndex() == 0, 288 .home_auth => |*v| v.getSelectedIndex() == 0, 289 .quit => |*v| v.getSelectedIndex() == 0, 290 else => false, 291 }; 292 if (at_top) { 293 index = header_index; 294 } else { 295 try child.input(allocator, key, root_focus); 296 } 297 } 298 }, 299 else => {}, 300 } 301 }, 302 .down => { 303 switch (child.*) { 304 .repo_header => { 305 index = stack_index; 306 }, 307 .stack => { 308 try child.input(allocator, key, root_focus); 309 }, 310 else => {}, 311 } 312 }, 313 .none => { 314 try child.input(allocator, key, root_focus); 315 }, 316 } 317 318 if (index != current_index) { 319 root_focus.setFocus(self.box.children.keys()[index]); 320 } 321 } 322 } 323 } 324 325 pub fn clearGrid(self: *View) void { 326 self.box.clearGrid(); 327 } 328 329 pub fn getGrid(self: View) ?Grid { 330 return self.box.getGrid(); 331 } 332 333 pub fn getFocus(self: *View) *Focus { 334 return self.box.getFocus(); 335 } 336 }; 337