╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │827 │ │686 - fn focusTags(self: *View, root_focus: *Focus) !void { │ │687 - const tf = self.tagFlow() orelse return; │ │828 + fn focusTags(self: *View, index: usize, root_focus: *Focus) !void { │ │829 + const tf = self.tagFlow(index) orelse return; │ │830 if (tf.text_boxes.items.len == 0) return; │ │831 const cid = tf.focus.child_id orelse tf.text_boxes.items[0].getFocus().id;│ │690 - const index = tf.indexOfFocusId(cid) orelse 0; │ │691 - self.focusTag(tf, root_focus, index); │ │832 + const item = tf.indexOfFocusId(cid) orelse 0; │ │833 + self.focusTag(index, tf, root_focus, item); │ │834 } │ │835 │ │694 - fn focusDescription(self: *View, root_focus: *Focus) !void { │ │695 - if (self.description_id) |id| root_focus.setFocus(id); │ │836 + fn focusDescription(self: *View, index: usize, root_focus: *Focus) !void { │ │837 + if (self.description_id[index]) |id| root_focus.setFocus(id); │ │838 } │ │839 │ │840 // enter the detail pane. an empty pane (no issues) can't be entered. │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │699 - fn focusDetail(self: *View, root_focus: *Focus) !void { │ │700 - if (self.detailInner().children.count() == 0) return; │ │701 - root_focus.setFocus(self.detailOuter().getFocus().id); │ │841 + fn focusDetail(self: *View, index: usize, root_focus: *Focus) !void {│ │842 + if (self.detailInner(index).children.count() == 0) return; │ │843 + root_focus.setFocus(self.detailOuter(index).getFocus().id); │ │844 } │ │845 │ │846 // return to the list. │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │705 - fn focusList(self: *View, root_focus: *Focus) !void { │ │706 - root_focus.setFocus(self.listScroll().getFocus().id); │ │847 + fn focusList(self: *View, index: usize, root_focus: *Focus) !void {│ │848 + root_focus.setFocus(self.listScroll(index).getFocus().id); │ │849 } │ │850 │ │709 - // cross to the header tabs above the split. │ │851 + // cross to the header tabs above the stack. │ │852 fn focusHeader(self: *View, root_focus: *Focus) void { │ │853 root_focus.setFocus(self.box.children.keys()[header_index]); │ │854 } │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │874 }; │ │875 │ │876 // the "a:" navigation link for the issues page filtered to the url-encoded │ │735 -// `tag` and rooted at issue `id` within `identity` ("owner/name"). │ │877 +// `tag` and rooted at issue `id` within `identity` ("owner/name"). a rooted │ │878 +// url derives its view from the issue's status; the bare list is the open view. │ │879 fn issuesLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8, id: []const u8) ![]const u8 {│ │737 - const route = ui.RoutablePage.repoIssuesRoute(identity, tag, id) orelse return error.RouteTooLong; │ │880 + const route = ui.RoutablePage.repoIssuesRoute(identity, .open, tag, id) orelse return error.RouteTooLong; │ │881 const url = try route.urlAlloc(page_arena); │ │882 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │883 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │885 // the in-page "ai:" anchor for selecting issue `id` in `identity`'s list; the │ │886 // href is only followed with js off. │ │887 fn issueRowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, id: []const u8) ![]const u8 { │ │745 - const route = ui.RoutablePage.repoIssuesRoute(identity, "", id) orelse return error.RouteTooLong; │ │888 + const route = ui.RoutablePage.repoIssuesRoute(identity, .open, "", id) orelse return error.RouteTooLong;│ │889 const url = try route.urlAlloc(page_arena); │ │890 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); │ │891 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │892 │ │750 -// the "a:" link to the issues list filtered to `tag` (raw; encoded here). │ │751 -fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, tag: []const u8) ![]const u8 { │ │893 +// the "a:" link to `status`'s issues list filtered to `tag` (raw; encoded here). │ │894 +fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: evt.Issue.Status, tag: []const u8) ![]const u8 {│ │895 const encoded = try ui.urlEncodeRef(page_arena.allocator(), tag); │ │753 - const route = ui.RoutablePage.repoIssuesRoute(identity, encoded, "") orelse return error.RouteTooLong; │ │896 + const route = ui.RoutablePage.repoIssuesRoute(identity, status, encoded, "") orelse return error.RouteTooLong; │ │897 const url = try route.urlAlloc(page_arena); │ │898 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │899 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │903 box: wgt.Box(ui.Widget), │ │904 tab_ids: std.AutoArrayHashMapUnmanaged(usize, void), │ │905 │ │763 - // `tag` is the url-encoded tag filter ("" = unfiltered). │ │764 - pub fn init(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8, count: usize, tag: []const u8) !Header { │ │906 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { │ │907 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .hidden, .rounded_corners = true, .direction = .horiz });│ │908 errdefer box.deinit(allocator); │ │909 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │912 │ │913 const aa = session.page_arena.allocator(); │ │914 │ │773 - const results_route = ui.RoutablePage.repoIssuesRoute(identity, tag, "") orelse return error.RouteTooLong; │ │774 - const results_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try results_route.urlAlloc(session.page_arena)}); │ │775 - const tags_route = ui.RoutablePage.repoIssuesTagsRoute(identity) orelse return error.RouteTooLong; │ │776 - const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.urlAlloc(session.page_arena)}); │ │777 - │ │778 - // results tab, labeled with the listing's issue count │ │779 - { │ │780 - const label = try std.fmt.allocPrint(aa, "results ({d})", .{count}); │ │915 + // a list tab per status, labeled with its listing's issue count │ │916 + for ([_]evt.Issue.Status{ .open, .closed }) |status| { │ │917 + const route = ui.RoutablePage.repoIssuesRoute(data.identity, status, data.tag, "") orelse return error.RouteTooLong; │ │918 + const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.urlAlloc(session.page_arena)}); │ │919 + const label = try std.fmt.allocPrint(aa, "{s} ({d})", .{ @tagName(status), data.window(status).count }); │ │920 var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ │921 errdefer text_box.deinit(allocator); │ │922 text_box.getFocus().focusable = true; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │784 - text_box.getFocus().kind = .{ .custom = results_link }; │ │923 + text_box.getFocus().kind = .{ .custom = link }; │ │924 try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │925 try box.children.put(allocator, text_box.getFocus().id, .{│ │926 .widget = .{ .text_box = text_box }, │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │931 │ │932 // tags tab, labeled with the active tag filter │ │933 { │ │795 - const label = if (tag.len == 0) "tags" else blk: { │ │796 - const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, tag)); │ │934 + const tags_route = ui.RoutablePage.repoIssuesTagsRoute(data.identity, data.tag) orelse return error.RouteTooLong; │ │935 + const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.urlAlloc(session.page_arena)}); │ │936 + const label = if (data.tag.len == 0) "tags" else blk: { │ │937 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); │ │938 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); │ │939 }; │ │940 var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none });│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯