╭ Repo.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 xitui = xit.xitui; 6 const wgt = xitui.widget; 7 const layout = xitui.layout; 8 const inp = xitui.input; 9 const Grid = xitui.grid.Grid; 10 const Focus = xitui.focus.Focus; 11 12 pub const Header = @import("./Repo/Header.zig"); 13 pub const Files = @import("./Repo/Files.zig"); 14 pub const Settings = @import("./Settings.zig"); 15 pub const Auth = @import("./Auth.zig"); 16 pub const Quit = @import("./Quit.zig"); 17 18 const Array = ui.RoutablePage.Array(ui.RoutablePage.repo_route_max_len); 19 20 header: Header, 21 repo: evt.Repo, 22 files: Files, 23 settings: Settings, 24 auth: Auth, 25 quit: Quit, 26 // the full files route this page renders ("owner/name" or "owner/name/files/<dir>"), 27 // mirrored into current_page when the files tab is selected. 28 route_name: Array, 29 // "owner/name" alone, mirrored when the settings/auth tab is selected. 30 identity: Array, 31 32 const Self = @This(); 33 34 pub fn init( 35 arena: *std.heap.ArenaAllocator, 36 session: *ui.Session, 37 name: Array, 38 ) !Self { 39 const DB = evt.AdminDB; 40 const hash_kind = evt.admin_repo_opts.hash; 41 const haxy_moment = session.haxy_moment orelse return error.NoMoment; 42 43 // the route is "owner/name" or "owner/name/files/<dir>". 44 const rf = ui.RoutablePage.RepoFiles.parse(name.slice()) orelse return error.NotFound; 45 46 const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, rf.owner, rf.name)) orelse return error.NotFound; 47 const repo = found.repo; 48 49 // resolve the creating user so the header can show their name to the left 50 // of the repo title. 51 const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo.user_id)) orelse return error.NotFound; 52 53 return .{ 54 .header = try Header.init(arena, repo.name, owner.name), 55 .repo = repo, 56 // the repo's event id is the on-disk repo directory name 57 .files = try Files.init(arena, session, &found.event_id, rf.identity, rf.dir), 58 .settings = Settings.init(), 59 .auth = Auth.init(), 60 .quit = Quit.init(), 61 .route_name = name, 62 .identity = Array.from(rf.identity) orelse return error.NotFound, 63 }; 64 } 65 66 pub const View = struct { 67 box: wgt.Box(ui.Widget), 68 data: *const Self, 69 session: *ui.Session, 70 71 const header_index: usize = 0; 72 const stack_index: usize = 1; 73 74 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 75 var box = wgt.Box(ui.Widget).init(.{ .border_style = null, .rounded_corners = true, .direction = .vert }); 76 errdefer box.deinit(allocator); 77 78 // build the header first so we can grab the files-tab id for the auth 79 // view (it focuses there after login). 80 var files_tab_id: usize = undefined; 81 { 82 var header_view = try Header.View.init(allocator, &data.header, session); 83 errdefer header_view.deinit(allocator); 84 files_tab_id = header_view.tab_ids.keys()[0]; 85 try box.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_header = header_view }, .rect = null, .min_size = null }); 86 } 87 88 { 89 var stack = wgt.Stack(ui.Widget).init(); 90 errdefer stack.deinit(allocator); 91 92 // files — the default tab: the current directory's listing. 93 { 94 var files_view = try Files.View.init(allocator, &data.files, session); 95 errdefer files_view.deinit(allocator); 96 try stack.children.put(allocator, files_view.getFocus().id, .{ .repo_files = files_view }); 97 } 98 99 { 100 var settings_view = try Settings.View.init(allocator, &data.settings, session); 101 errdefer settings_view.deinit(allocator); 102 try stack.children.put(allocator, settings_view.getFocus().id, .{ .home_settings = settings_view }); 103 } 104 105 { 106 var auth_view = try Auth.View.init(allocator, &data.auth, session, files_tab_id); 107 errdefer auth_view.deinit(allocator); 108 try stack.children.put(allocator, auth_view.getFocus().id, .{ .home_auth = auth_view }); 109 } 110 111 if (session.is_terminal) { 112 var quit_view = try Quit.View.init(allocator, &data.quit, session); 113 errdefer quit_view.deinit(allocator); 114 try stack.children.put(allocator, quit_view.getFocus().id, .{ .quit = quit_view }); 115 } 116 117 try box.children.put(allocator, stack.getFocus().id, .{ .widget = .{ .stack = stack }, .rect = null, .min_size = null }); 118 } 119 120 var self = View{ .box = box, .data = data, .session = session }; 121 self.getFocus().child_id = box.children.keys()[header_index]; 122 return self; 123 } 124 125 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 126 self.box.deinit(allocator); 127 } 128 129 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 130 self.clearGrid(); 131 const header = &self.box.children.values()[header_index].widget.repo_header; 132 const stack = &self.box.children.values()[stack_index].widget.stack; 133 134 // each header tab maps 1:1 to a stack child by position. mirror the 135 // selection into current_page so the host can push the matching url; 136 // all repo tabs share the .repo parent, so this stays on the page 137 // rather than navigating. 138 if (header.getSelectedIndex()) |index| { 139 stack.getFocus().child_id = stack.children.keys()[index]; 140 switch (index) { 141 // settings/auth carry only "owner/name"; files carries this 142 // page's directory (route_name) so the url keeps the dir. 143 1 => self.session.data.current_page = .{ .repo_settings = self.data.identity }, 144 2 => self.session.data.current_page = .{ .repo_auth = self.data.identity }, 145 // the quit tab is tty-only and not a route, so leave current_page 146 // alone (nothing to mirror into the url). 147 3 => {}, 148 else => self.session.data.current_page = .{ .repo = self.data.route_name }, 149 } 150 } 151 try self.box.build(allocator, constraint, root_focus); 152 } 153 154 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 155 if (self.getFocus().child_id) |child_id| { 156 if (self.box.children.getIndex(child_id)) |current_index| { 157 const child = &self.box.children.values()[current_index].widget; 158 var index = current_index; 159 160 const Direction = enum { up, down, none }; 161 const direction: Direction = switch (key) { 162 .arrow_up => .up, 163 .arrow_down => .down, 164 .mouse => |mouse| if (mouse.action == .scroll) 165 (if (mouse.action.scroll == .up) .up else .down) 166 else 167 .none, 168 else => .none, 169 }; 170 171 switch (direction) { 172 .up => { 173 switch (child.*) { 174 .repo_header => { 175 try child.input(allocator, key, root_focus); 176 }, 177 .stack => { 178 if (child.stack.getSelected()) |selected_widget| { 179 const at_top = switch (selected_widget.*) { 180 .repo_files => |*v| v.getSelectedIndex() == 0, 181 .home_settings => |*v| v.getSelectedIndex() == 0, 182 .home_auth => |*v| v.getSelectedIndex() == 0, 183 .quit => |*v| v.getSelectedIndex() == 0, 184 else => false, 185 }; 186 if (at_top) { 187 index = header_index; 188 } else { 189 try child.input(allocator, key, root_focus); 190 } 191 } 192 }, 193 else => {}, 194 } 195 }, 196 .down => { 197 switch (child.*) { 198 .repo_header => { 199 index = stack_index; 200 }, 201 .stack => { 202 try child.input(allocator, key, root_focus); 203 }, 204 else => {}, 205 } 206 }, 207 .none => { 208 try child.input(allocator, key, root_focus); 209 }, 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