╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Branches.zig ─────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
88 data: *const Self, 89 session: *ui.Session, 90 91 const about_index = 0; 92 const default_index = 1; 93 const add_index = 2; 94 95 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 96 const aa = session.page_arena.allocator(); 97 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .vert }); 98 errdefer box.deinit(allocator); 99 const route = ui.RoutablePage.repoBranchesRoute(data.identity) orelse return error.RouteTooLong; 100 const url = try route.toUrl(session.page_arena); 101 102 { 103 var about = try ui.widget.initAbout(allocator, &.{ 104 .{ .text = "the default branch is what the repo page shows and clones check out.\n" }, 105 .{ .text = "protected branches can't be deleted or force pushed.\n" }, 106 .{ .text = "owners", .style = .{ .bold = true } }, 107 .{ .text = " can push directly to them.\n" }, 108 .{ .text = "members", .style = .{ .bold = true } }, 109 .{ .text = " can only change them by merging a patch." }, 110 }); 111 errdefer about.deinit(allocator); 112 try box.children.put(allocator, about.getFocus().id, .{ .widget = .{ .box = about }, .rect = null, .min_size = null }); 113 } 114 // a failed submission keeps its text in the input it came from 115 const saved = session.formFeedback(.repo_branches); 116 try addFormRow(allocator, &box, session, try aa.print("form:{s}/default", .{url}), " default branch ", if (saved) |value| (if (value.default) value.name else data.default_branch) else data.default_branch); 117 try addFormRow(allocator, &box, session, try aa.print("form:{s}/protect", .{url}), " add a protected branch ", if (saved) |value| (if (value.default) "" else value.name) else ""); 118 119 // the branch rows follow the add row in the same box 120 for (data.names) |branch| { 121 const encoded = try ui.urlEncodeRef(aa, branch); 122 var row_route = route; 123 row_route.repo_branches.branch = ui.RoutablePage.Array(ui.RoutablePage.ref_route_max_len).from(encoded) orelse return error.RouteTooLong; 124 const row_url = try row_route.toUrl(session.page_arena); 125 126 var row_box = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); 127 errdefer row_box.deinit(allocator); 128 row_box.getFocus().kind = .{ .custom = try aa.print("form:{s}", .{row_url}) }; 129 { 130 const files_route = ui.RoutablePage.repoFilesRoute(data.identity, .branch, encoded, "", 0) orelse return error.RouteTooLong; 131 var name = try Comment.linkBox(allocator, session, branch, files_route); 132 errdefer name.deinit(allocator); 133 try row_box.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_box = name }, .rect = null, .min_size = null }); 134 } 135 { 136 var remove = try wgt.TextBox.init(allocator, "✕", .{ .border = .single, .round_corners = true, .wrap_kind = .none }); 137 errdefer remove.deinit(allocator); 138 remove.getFocus().mode = .all; 139 remove.getFocus().kind = .{ .custom = try aa.print("submit:{s}/unprotect", .{row_url}) }; 140 try row_box.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); 141 } 142 row_box.getFocus().child_id = row_box.children.keys()[0]; 143 try box.children.put(allocator, row_box.getFocus().id, .{ .widget = .{ .box = row_box }, .rect = null, .min_size = null }); 144 } 145 146 box.getFocus().child_id = box.children.keys()[about_index]; 147 var center = try ui.widget.Center.init(allocator, .{ .box = box }); 148 errdefer center.deinit(allocator); 149 return .{ 150 .scroll = try wgt.Scroll(ui.Widget).init(allocator, .{ .center = center }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }), 151 .data = data, 152 .session = session, 153 }; 154 } 155 156 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 157 self.scroll.deinit(allocator); 158 } 159 160 fn contentBox(self: *View) *wgt.Box(ui.Widget) { 161 return &self.scroll.child.center.child.box; 162 } 163 164 // a row holding one text input in its own form 165 fn addFormRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, form: []const u8, label: []const u8, content: []const u8) !void { 166 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border = null, .direction = .horiz }); 167 errdefer row.deinit(allocator); 168 row.getFocus().kind = .{ .custom = form }; 169 var name = try wgt.TextInput.init(allocator, .{ .top_label = .{ .text = label }, .name = "name", .visible_width = 30, .round_corners = true, .render_content = session.is_terminal }); 170 errdefer name.deinit(allocator); 171 name.getFocus().mode = .all; 172 try name.setContent(allocator, content); 173 row.getFocus().child_id = name.getFocus().id; 174 try row.children.put(allocator, name.getFocus().id, .{ .widget = .{ .text_input = name }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); 175 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); 176 } 177 178 fn formInput(self: *View, index: usize) *wgt.TextInput { 179 return &self.contentBox().children.values()[index].widget.box.children.values()[0].widget.text_input; 180 } 181 182 // the form row with focus, if any 183 fn activeForm(self: *View) ?usize { 184 const rows = self.contentBox(); 185 const index = rows.children.getIndex(rows.getFocus().child_id orelse return null) orelse return null; 186 return if (index == default_index or index == add_index) index else null; 187 } 188 189 fn aboutBox(self: *View) *wgt.Box(ui.Widget) { 190 return &self.contentBox().children.values()[about_index].widget.box; 191 } 192 193 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 194 self.clearGrid(); 195 try self.labelInputs(root_focus); 196 ui.widget.markAboutFocused(self.aboutBox(), root_focus); 197 // the window's height as a minimum centers rows that fit 198 try self.scroll.build(allocator, .{ 199 .min_size = .{ .width = constraint.min_size.width, .height = constraint.max_size.height orelse constraint.min_size.height }, 200 .max_size = constraint.max_size, 201 }, root_focus); 202 } 203 204 // label the inputs for this frame and register them for the web form 205 // handling. a failure labels the input it came from. 206 fn labelInputs(self: *View, root_focus: *Focus) !void { 207 const saved = self.session.formFeedback(.repo_branches); 208 for ([_]usize{ default_index, add_index }) |index| { 209 const name = self.formInput(index); 210 const default = index == default_index; 211 const failure = if (saved) |value| (if (value.default == default) value.failure else null) else null; 212 name.options.top_label.text = if (failure) |value| switch (value) { 213 .not_found => if (default) " default branch (not found) " else " add a protected branch (not found) ", 214 .already_added => " add a protected branch (already added) ", 215 .too_many => " add a protected branch (too many branches) ", 216 .too_long => if (default) " default branch (name too long) " else " add a protected branch (name too long) ", 217 } else if (default) " default branch " else " add a protected branch "; 218 name.options.bottom_label.text = if (root_focus.grandchild_id == name.getFocus().id) " press enter " else ""; 219 try self.session.text_inputs.put(self.session.arena.allocator(), name.getFocus().id, name); 220 } 221 } 222 223 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 224 // a form row keeps every key but the vertical ones, which move between rows 225 if (self.activeForm()) |index| if (inp.vertDirection(key) == .none) { 226 if (key == .enter) { 227 const name = try self.formInput(index).text(allocator); 228 defer allocator.free(name); 229 try self.act(allocator, if (index == default_index) .default else .protect, name); 230 } else try self.formInput(index).input(allocator, key, root_focus); 231 return; 232 }; 233 234 const rows = self.contentBox(); 235 const current = rows.children.getIndex(rows.getFocus().child_id orelse return) orelse return; 236 if (inp.rowDelta(key, @intCast(rows.children.count()))) |delta| { 237 ui.widget.moveRowFocus(rows, &self.scroll, root_focus, delta); 238 return; 239 } 240 switch (key) { 241 .arrow_left, .arrow_right => _ = ui.widget.moveInSelectedRow(rows, root_focus, key == .arrow_right), 242 else => { 243 // the text and the form rows sit above the rows, and a row's name is a link the host follows 244 if (current <= add_index) return; 245 const row_box = &rows.children.values()[current].widget.box; 246 const button_id = row_box.getFocus().child_id orelse return; 247 if (row_box.children.getIndex(button_id) != 1 or !inp.activated(root_focus, button_id, key)) return; 248 try self.act(allocator, .unprotect, self.data.names[current - add_index - 1]); 249 }, 250 } 251 } 252 253 // the terminal performs the action itself; the web posts the form instead 254 fn act(self: *View, allocator: std.mem.Allocator, action: Action, branch: []const u8) !void { 255 if (comptime wasm) return; 256 const io = self.session.io orelse return; 257 const users_dir = self.session.users_dir orelse return; 258 const actor = (try self.session.authorize(self.data.identity, .owner, null)) orelse return; 259 if (try perform(io, allocator, users_dir, actor, self.data.identity, action, branch)) |failure| { 260 self.session.data.form_feedback = .{ .repo_branches = .{ .failure = failure, .name = try self.session.arena.allocator().dupe(u8, branch), .default = action == .default } }; 261 return; 262 } 263 try self.session.navigate(ui.RoutablePage.repoBranchesRoute(self.data.identity) orelse return error.RouteTooLong); 264 } 265 266 pub fn clearGrid(self: *View) void { 267 self.scroll.clearGrid(); 268 } 269 270 pub fn getGrid(self: View) ?Grid { 271 return self.scroll.getGrid(); 272 } 273 274 pub fn getFocus(self: *View) *Focus { 275 return self.scroll.getFocus(); 276 } 277 278 // up leaves the view from the explanatory text 279 pub fn atTop(self: *View) bool { 280 return self.contentBox().getFocus().child_id == self.aboutBox().getFocus().id; 281 } 282 }; 283