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