╭ changed ──────────────╮ │src/ui/Repo/Threads.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1121 self.box.deinit(allocator); │ │1122 } │ │1123 │ │ 735 - fn header(self: *This) *Header { │ │1124 + fn header(self: *This) *HeaderType { │ │1125 return switch (kind) { │ │1126 .issue => &self.box.children.values()[header_index].widget.repo_issues_header, │ │1127 + .patch => &self.box.children.values()[header_index].widget.repo_patches_header, │ │1128 .discuss => &self.box.children.values()[header_index].widget.repo_discussions_header,│ │1129 else => @compileError("unsupported thread event kind"), │ │1130 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │1203 │ │1204 fn window(self: *This, index: usize) *const Window { │ │1205 if (has_conflicts and index == conflict_view_index) return &self.data.conflicts;│ │1206 + if (has_drafts and index == drafts_view_index) return &self.data.drafts; │ │1207 return self.data.window(splitStatus(index)); │ │1208 } │ │1209 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1217 // new-issue form shows). │ │1218 fn selectedSplitIndex(self: *This) ?usize { │ │1219 const idx = self.stackSelectedIndex() orelse return null; │ │ 829 - return if (idx < status_count or idx == conflict_view_index) idx else null; │ │1220 + return if (idx < status_count or (has_drafts and idx == drafts_view_index) or idx == conflict_view_index) idx else null;│ │1221 } │ │1222 │ │1223 fn detailActive(self: *This, index: usize) bool { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1280 }) │ │1281 else if (has_conflicts and index == conflict_view_index) │ │1282 conflictsRoute(self.data.identity, if (self.data.view == .conflicts) self.data.selected_id else "")│ │1283 + else if (has_drafts and index == drafts_view_index) │ │1284 + if (index == viewIndex(self.data.view) and self.data.selected_id.len != 0) │ │1285 + commentsRoute(self.data.identity, self.data.selected_id, 0) │ │1286 + else │ │1287 + draftsRoute(self.data.identity) │ │1288 else if (index == viewIndex(self.data.view) and self.data.selected_id.len != 0) │ │1289 (if (self.data.description_page) │ │1290 descriptionRoute(self.data.identity, self.data.selected_id) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │1419 inner.children.clearAndFree(allocator); │ │1420 inner.getFocus().child_id = null; │ │1421 self.author_id[index] = null; │ │1026 - self.status_button_id[index] = null; │ │1422 + self.primary_button_id[index] = null; │ │1423 │ │1028 - // the tool row: the open/close and edit buttons; the description page │ │1029 - // shows none. │ │1424 + // the tool row; the description page shows none. │ │1425 { │ │1426 const row = self.toolRow(index); │ │1427 for (row.children.values()) |*child| child.widget.deinit(allocator);│ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1437 var spacer = try ui.Spacer.init(allocator); │ │1438 errdefer spacer.deinit(allocator); │ │1439 try row.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1045 - } │ │1046 - │ │1047 - // a terminal has no file picker, so attaching is web only. the │ │1048 - // renderer covers this button with a file input, so clicking it │ │1049 - // opens the browser's own picker and posts what it chose. │ │1050 - if (!self.session.is_terminal and !entryConflicted(entry) and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1051 - const label = "add attachment"; │ │1052 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1053 - errdefer button.deinit(allocator); │ │1054 - button.getFocus().focusable = true; │ │1055 - button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1056 - try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │1057 - else │ │1058 - try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }) }; │ │1059 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1440 } │ │1061 - │ │1062 - // a conflicted issue's only action is resolving it. the button │ │1063 - // stays visible logged out; the resolve page shows the │ │1064 - // unauthorized view then. │ │1065 - if (has_conflicts and entryConflicted(entry)) { │ │1066 - const label = "resolve conflict"; │ │1067 - var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1068 - errdefer button.deinit(allocator); │ │1069 - button.getFocus().focusable = true; │ │1070 - const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1071 - button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1072 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1073 - } else { │ │1074 - // a logged-out session can't flip the status, so it gets no │ │1075 - // open/close button │ │1076 - if (has_status and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1077 - const action: []const u8 = switch (entryStatus(entry)) { │ │1078 - .open => "close", │ │1079 - .closed => "open", │ │1080 - }; │ │1081 - const route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1082 - row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), action }) }; │ │1441 │ │1084 - var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1442 + if (has_drafts and entryDraft(entry)) { │ │1443 + if (entry.revision_ready) { │ │1444 + const route = listRoute(self.data.identity, @enumFromInt(0), "", entry.id) orelse return error.RouteTooLong; │ │1445 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/post", .{try route.toUrl(self.session.page_arena)}) }; │ │1446 + var button = try wgt.TextBox(ui.Widget).init(allocator, "post", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1447 errdefer button.deinit(allocator); │ │1448 button.getFocus().focusable = true; │ │1087 - // the renderer distinguishes plain clickables from buttons │ │1088 - // that should POST to a server route by this kind. │ │1449 button.getFocus().kind = .{ .custom = "submit" }; │ │1090 - self.status_button_id[index] = button.getFocus().id; │ │1091 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } }); │ │1450 + self.primary_button_id[index] = button.getFocus().id; │ │1451 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "post".len + 2, .height = null } }); │ │1452 + } │ │1453 + if (row.children.count() > first_in_row_index) │ │1454 + row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1455 + } else { │ │1456 + // a terminal has no file picker, so attaching is web only. the │ │1457 + // renderer covers this button with a file input, so clicking it │ │1458 + // opens the browser's own picker and posts what it chose. │ │1459 + if (!self.session.is_terminal and !entryConflicted(entry) and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1460 + const label = "add attachment"; │ │1461 + var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1462 + errdefer button.deinit(allocator); │ │1463 + button.getFocus().focusable = true; │ │1464 + button.getFocus().kind = .{ .custom = if (self.data.identity.len == 0) │ │1465 + try std.fmt.allocPrint(pa, "{s}/{s}:{s}/attach", .{ ui.file_input_prefix, @tagName(kind), entry.id }) │ │1466 + else │ │1467 + try std.fmt.allocPrint(pa, "{s}/repo/{s}/{s}:{s}/attach", .{ ui.file_input_prefix, self.data.identity, @tagName(kind), entry.id }) }; │ │1468 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1469 } │ │1470 │ │1094 - // the edit button links to the issue's edit page. │ │1095 - { │ │1096 - var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1471 + // a conflicted issue's only action is resolving it. the button │ │1472 + // stays visible logged out; the resolve page shows the │ │1473 + // unauthorized view then. │ │1474 + if (has_conflicts and entryConflicted(entry)) { │ │1475 + const label = "resolve conflict"; │ │1476 + var button = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1477 errdefer button.deinit(allocator); │ │1478 button.getFocus().focusable = true; │ │1099 - const route = editRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1479 + const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1480 button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1101 - try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } }); │ │1481 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = label.len + 2, .height = null } }); │ │1482 + } else { │ │1483 + // a logged-out session can't flip the status, so it gets no │ │1484 + // open/close button │ │1485 + if (has_status and (self.session.data.is_local or self.session.data.user_id != null)) { │ │1486 + if (statusAction(entryStatus(entry))) |action| { │ │1487 + const route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1488 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), action }) }; │ │1489 + │ │1490 + var button = try wgt.TextBox(ui.Widget).init(allocator, action, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1491 + errdefer button.deinit(allocator); │ │1492 + button.getFocus().focusable = true; │ │1493 + // the renderer distinguishes plain clickables from buttons │ │1494 + // that should POST to a server route by this kind. │ │1495 + button.getFocus().kind = .{ .custom = "submit" }; │ │1496 + self.primary_button_id[index] = button.getFocus().id; │ │1497 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = action.len + 2, .height = null } });│ │1498 + } │ │1499 + } │ │1500 + │ │1501 + // the edit button links to the issue's edit page. │ │1502 + { │ │1503 + var button = try wgt.TextBox(ui.Widget).init(allocator, "edit", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1504 + errdefer button.deinit(allocator); │ │1505 + button.getFocus().focusable = true; │ │1506 + const route = editRoute(self.data.identity, entry.id) orelse return error.RouteTooLong; │ │1507 + button.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1508 + try row.children.put(allocator, button.getFocus().id, .{ .widget = .{ .text_box = button }, .rect = null, .min_size = .{ .width = "edit".len + 2, .height = null } }); │ │1509 + } │ │1510 } │ │1103 - } │ │1511 │ │1105 - if (self.session.data.is_local or self.session.data.user_id != null) { │ │1106 - var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1107 - errdefer remove.deinit(allocator); │ │1108 - remove.getFocus().focusable = true; │ │1109 - const route = removeRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1110 - remove.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1111 - try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); │ │1112 - } │ │1512 + if (self.session.data.is_local or self.session.data.user_id != null) { │ │1513 + var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │1514 + errdefer remove.deinit(allocator); │ │1515 + remove.getFocus().focusable = true; │ │1516 + const route = removeRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1517 + remove.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)}) }; │ │1518 + try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); │ │1519 + } │ │1520 │ │1114 - // the leftmost button: the attachment button when the session has │ │1115 - // one, else the resolve button on a conflicted issue, the │ │1116 - // open/close button when present, the edit button otherwise │ │1117 - row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1521 + // the leftmost button: the attachment button when the session has │ │1522 + // one, else the resolve button on a conflicted issue, the │ │1523 + // open/close button when present, the edit button otherwise │ │1524 + row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ │1525 + } │ │1526 } │ │1527 │ │1528 if (comment_page) |page| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1619 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │1620 break :blk tb.getFocus().id; │ │1621 }; │ │1622 + │ │1623 + if (has_drafts and entryDraft(entry)) { │ │1624 + if (self.data.target_branch.len != 0 and (self.session.data.clone_http_port != null or self.session.data.clone_ssh_port != null)) { │ │1625 + var push = try ui.CloneUrl.View.initPush(allocator, self.session, self.data.identity, entry.id, self.data.target_branch); │ │1626 + errdefer push.deinit(allocator); │ │1627 + try inner.children.put(allocator, push.getFocus().id, .{ .widget = .{ .clone_url = push }, .rect = null, .min_size = null }); │ │1628 + } │ │1629 + inner.getFocus().child_id = inner.children.keys()[title_child_index]; │ │1630 + const sc = self.detailScroll(index); │ │1631 + sc.x = 0; │ │1632 + sc.y = 0; │ │1633 + sc.getFocus().version +%= 1; │ │1634 + return; │ │1635 + } │ │1636 │ │1637 const parent_route = commentsRoute(self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │1638 try Attachment.appendRows(allocator, inner, self.session, self.data.identity, try parent_route.toUrl(self.session.page_arena), entry.attachments);│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1777 try self.tagsInput(index, key, root_focus); │ │1778 } else if (self.descriptionFocused(index)) { │ │1779 try self.descriptionInput(index, key, root_focus); │ │1358 - } else if (try self.attachmentInput(allocator, index, key, root_focus)) {} else { │ │1780 + } else if (try self.cloneUrlInput(allocator, index, key, root_focus)) {} else if (try self.attachmentInput(allocator, index, key, root_focus)) {} else {│ │1781 try self.commentInput(allocator, index, key, root_focus); │ │1782 + } │ │1783 + } │ │1784 + │ │1785 + fn cloneUrlInput(self: *This, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !bool { │ │1786 + const child_index = self.focusedDetailChild(index, root_focus) orelse return false; │ │1787 + const child = &self.detailInner(index).children.values()[child_index].widget; │ │1788 + const clone_url = switch (child.*) { │ │1789 + .clone_url => |*view| view, │ │1790 + else => return false, │ │1791 + }; │ │1792 + switch (key) { │ │1793 + .arrow_up => _ = self.moveDetailVertical(index, root_focus, false), │ │1794 + .arrow_down => _ = self.moveDetailVertical(index, root_focus, true), │ │1795 + .arrow_left => if (clone_url.selected == 0) │ │1796 + try self.focusList(index, root_focus) │ │1797 + else │ │1798 + try clone_url.input(allocator, key, root_focus), │ │1799 + else => try clone_url.input(allocator, key, root_focus), │ │1800 } │ │1801 + return true; │ │1802 } │ │1803 │ │1804 fn attachmentInput(self: *This, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !bool { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1823 return true; │ │1824 } │ │1825 │ │1385 - // the tool row: enter or a click on the open/close button flips the │ │1386 - // issue's status; the edit and resolve buttons are a: links the host │ │1387 - // follows. arrows cross between the buttons and to the neighboring │ │1388 - // widgets. │ │1826 + // enter or a click runs the primary action; links are handled by the host │ │1827 fn toolRowInput(self: *This, allocator: std.mem.Allocator, index: usize, key: Key, root_focus: *Focus) !void {│ │1828 const row = self.toolRow(index); │ │1829 const cur = if (row.getFocus().child_id) |cid| row.children.getIndex(cid) orelse return else return; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1392 - const on_status = if (self.status_button_id[index]) |id| row.getFocus().child_id == id else false; │ │1830 + const on_primary = if (self.primary_button_id[index]) |id| row.getFocus().child_id == id else false; │ │1831 switch (key) { │ │1832 .arrow_left => if (cur > first_in_row_index) root_focus.setFocus(row.children.keys()[cur - 1]) else try self.focusList(index, root_focus),│ │1833 .arrow_right => if (cur + 1 < row.children.count()) root_focus.setFocus(row.children.keys()[cur + 1]), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯