╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │852 row.getFocus().child_id = row.children.keys()[button_in_row_index]; │ │853 } │ │854 │ │824 - // the issue's title as a focusable word-wrapped text box. │ │825 - { │ │826 - var tb = try wgt.TextBox(ui.Widget).init(allocator, entry.issue.title, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word, .label = " title " }); │ │855 + if (description_page) { │ │856 + // the back link, in the title slot so the pane's input handling │ │857 + // applies to it unchanged. │ │858 + var tb = try wgt.TextBox(ui.Widget).init(allocator, "← back to issue", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │859 errdefer tb.deinit(allocator); │ │860 tb.getFocus().focusable = true; │ │861 + tb.getFocus().kind = .{ .custom = try issuesLink(self.session.page_arena, self.data.identity, entry.issue.status, "", entry.id) }; │ │862 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │830 - } │ │863 + } else { │ │864 + // the issue's title as a focusable word-wrapped text box. │ │865 + { │ │866 + var tb = try wgt.TextBox(ui.Widget).init(allocator, entry.issue.title, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word, .label = " title " });│ │867 + errdefer tb.deinit(allocator); │ │868 + tb.getFocus().focusable = true; │ │869 + try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │870 + } │ │871 │ │832 - // the issue's tags, each linking to this status's list filtered to │ │833 - // that tag. │ │834 - { │ │835 - var items: std.ArrayList(ui.TagFlow.Item) = .empty; │ │836 - defer items.deinit(allocator); │ │837 - var tag_iter = evt.Issue.tagIterator(entry.issue.tags); │ │838 - while (tag_iter.next()) |tag| { │ │839 - if (tag.len == 0) continue; │ │840 - try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, splitStatus(index), tag) }); │ │841 - } │ │842 - if (items.items.len > 0) { │ │843 - var tf = try ui.TagFlow.init(allocator); │ │844 - errdefer tf.deinit(allocator); │ │845 - try tf.setItems(allocator, items.items); │ │846 - try inner.children.put(allocator, tf.getFocus().id, .{ .widget = .{ .tag_flow = tf }, .rect = null, .min_size = null }); │ │872 + // the issue's tags, each linking to this status's list filtered to │ │873 + // that tag. │ │874 + { │ │875 + var items: std.ArrayList(ui.TagFlow.Item) = .empty; │ │876 + defer items.deinit(allocator); │ │877 + var tag_iter = evt.Issue.tagIterator(entry.issue.tags); │ │878 + while (tag_iter.next()) |tag| { │ │879 + if (tag.len == 0) continue; │ │880 + try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, splitStatus(index), tag) }); │ │881 + } │ │882 + if (items.items.len > 0) { │ │883 + var tf = try ui.TagFlow.init(allocator); │ │884 + errdefer tf.deinit(allocator); │ │885 + try tf.setItems(allocator, items.items); │ │886 + try inner.children.put(allocator, tf.getFocus().id, .{ .widget = .{ .tag_flow = tf }, .rect = null, .min_size = null }); │ │887 + } │ │888 } │ │889 } │ │890 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │850 - // the description as a focusable word-wrapped text box. │ │891 + // the description as a focusable word-wrapped text box. past the limit │ │892 + // it's cut at the last whole line and links to the /description page, │ │893 + // which shows the whole thing. │ │894 self.description_id[index] = blk: { │ │852 - const description = if (entry.issue.description.len == 0) "(no description)" else entry.issue.description; │ │853 - var tb = try wgt.TextBox(ui.Widget).init(allocator, description, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word, .label = " description " });│ │895 + const whole = entry.issue.description; │ │896 + const cut_short = !description_page and whole.len > max_description_size; │ │897 + const shown = if (cut_short) │ │898 + std.mem.trimEnd(u8, whole[0 .. std.mem.lastIndexOfScalar(u8, whole[0..max_description_size], '\n') orelse 0], " \t\r\n") │ │899 + else if (whole.len == 0) │ │900 + "(no description)" │ │901 + else │ │902 + whole; │ │903 + var tb = try wgt.TextBox(ui.Widget).init(allocator, shown, .{ │ │904 + .border_style = .single, │ │905 + .rounded_corners = true, │ │906 + .wrap_kind = .word, │ │907 + .label = " description ", │ │908 + .bottom_label = if (cut_short) " click or press enter to see more " else "", │ │909 + }); │ │910 errdefer tb.deinit(allocator); │ │911 tb.getFocus().focusable = true; │ │912 + if (cut_short) tb.getFocus().kind = .{ .custom = try descriptionLink(self.session.page_arena, self.data.identity, entry.id) }; │ │913 try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │914 break :blk tb.getFocus().id; │ │915 }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1354 if (self.description_id[index]) |id| root_focus.setFocus(id); │ │1355 } │ │1356 │ │1300 - // return to the tool row's last-focused button. │ │1357 + // return to the tool row's last-focused button (the header when the │ │1358 + // description page shows no row). │ │1359 fn focusToolRow(self: *View, index: usize, root_focus: *Focus) void { │ │1302 - const cid = self.toolRow(index).getFocus().child_id orelse return; │ │1360 + const cid = self.toolRow(index).getFocus().child_id orelse return self.focusHeader(root_focus);│ │1361 root_focus.setFocus(cid); │ │1362 } │ │1363 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1415 return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{url}); │ │1416 } │ │1417 │ │1418 +// the "a:" link to the whole-description page of issue `id` within `identity`. │ │1419 +fn descriptionLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, id: []const u8) ![]const u8 { │ │1420 + const route = ui.RoutablePage.repoIssuesDescriptionRoute(identity, id) orelse return error.RouteTooLong; │ │1421 + const url = try route.toUrl(page_arena); │ │1422 + return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │1423 +} │ │1424 + │ │1425 // the "a:" link to `status`'s issues list filtered to `tag` (raw; encoded here). │ │1426 fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: evt.Issue.Status, tag: []const u8) ![]const u8 {│ │1427 const encoded = try ui.urlEncodeRef(page_arena.allocator(), tag); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯