╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │513 │ │514 var issue_events: [issue_data.len]evt.EventWithId = undefined; │ │515 for (issue_data, 0..) |issue, i| { │ │516 + // the newest issue's description runs past what the detail │ │517 + // pane shows, so it shows the truncated description and its │ │518 + // link. │ │519 + const description = if (i == issue_data.len - 1) desc: { │ │520 + var desc_writer = std.Io.Writer.Allocating.init(allocator); │ │521 + defer desc_writer.deinit(); │ │522 + try desc_writer.writer.print("{s}", .{issue.description}); │ │523 + var line: usize = 0; │ │524 + while (desc_writer.written().len <= ui.Repo.Issues.max_description_size) : (line += 1) { │ │525 + try desc_writer.writer.print("\n{d} {s}", .{ line, scatter_words[line % scatter_words.len] });│ │526 + } │ │527 + break :desc try arena.allocator().dupe(u8, desc_writer.written()); │ │528 + } else issue.description; │ │529 issue_events[i] = .{ │ │530 .id = std.fmt.bytesToHex(evt.EventWithId.randomId(prng.random()), .lower), │ │531 // stepped timestamps so the issues list in a stable order │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │533 .event = .{ │ │534 .issue = .{ │ │535 .title = issue.title, │ │523 - .description = issue.description,│ │536 + .description = description, │ │537 .tags = issue.tags, │ │538 .status = issue.status, │ │539 }, │ ╰──────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │101 │ │102 pub const RefKind = enum { branch, tag }; │ │103 │ │104 - pub const IssuesView = enum { open, closed, tags, new, edit }; │ │104 + pub const IssuesView = enum { open, closed, tags, new, edit, description };│ │105 │ │106 pub const RefOrOid = enum { │ │107 branch, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │361 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │362 .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │363 .view = .edit, │ │364 + } }; │ │365 + } │ │366 + │ │367 + // build a `.repo_issues` route showing the whole description of the issue │ │368 + // with hex event id `selected`. │ │369 + pub fn repoIssuesDescriptionRoute(identity: []const u8, selected: []const u8) ?RoutablePage {│ │370 + if (selected.len == 0) return null; │ │371 + return .{ .repo_issues = .{ │ │372 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │373 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │374 + .view = .description, │ │375 } }; │ │376 } │ │377 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │453 .repo_issues => |i| blk: { │ │454 const prefix = try repoUrlPrefix(arena, i.name.slice()); │ │455 if (i.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/edit", .{ prefix, i.selected.slice() }); │ │456 + if (i.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/description", .{ prefix, i.selected.slice() });│ │457 // a selected issue's url carries its id instead of a view │ │458 // name, so it survives status changes │ │459 const word = if (i.selected.len != 0) i.selected.slice() else @tagName(i.view); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │561 return repoRefsRoute(pair, kind, ref.value); │ │562 } │ │563 if (std.mem.eql(u8, tab, "issues")) { │ │552 - // "<id>/edit" is the edit form for that issue, with no params.│ │564 + // "<id>/edit" is the edit form for that issue, with no params;│ │565 + // "<id>/description" is its whole-description page. │ │566 const after_tab = segments.rest(); │ │567 if (std.mem.endsWith(u8, after_tab, "/edit")) { │ │568 const id = after_tab[0 .. after_tab.len - "/edit".len]; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │569 if (id.len == 0 or std.mem.indexOfScalar(u8, id, '/') != null) return null;│ │570 return repoIssuesEditRoute(pair, id); │ │571 } │ │572 + if (std.mem.endsWith(u8, after_tab, "/description")) { │ │573 + const id = after_tab[0 .. after_tab.len - "/description".len]; │ │574 + if (id.len == 0 or std.mem.indexOfScalar(u8, id, '/') != null) return null;│ │575 + return repoIssuesDescriptionRoute(pair, id); │ │576 + } │ │577 // the word is a view name or an issue event id (whose status │ │578 // picks the view); a filter with neither is never emitted. │ │579 const word = params.scanTail(&segments) catch return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │530 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null });│ │531 } │ │532 │ │533 - // how much of the message a pane's box holds: the preview links to the pane │ │534 - // holding the whole thing, which is that link's destination. │ │533 + // how much of the message a pane's box holds: a truncated preview links to │ │534 + // the pane holding the whole thing, which is that link's destination. │ │535 const MessageBox = enum { preview, whole }; │ │536 │ │537 // the selected commit's message. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │14 const inp = @import("../input.zig"); │ │15 │ │16 const wasm = builtin.target.cpu.arch == .wasm32; │ │17 + │ │18 +// how much of a description the detail pane shows before linking to the│ │19 +// /description page, the same limit commit messages use. │ │20 +pub const max_description_size = 2 * 1024; │ │21 │ │22 // how many issues one window shows before a "next" link appears. │ │23 pub const page_size = 20; │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │58 closed: Window, │ │59 // the view the page shows initially (a selected issue's status overrides the route's)│ │60 view: ui.RoutablePage.IssuesView, │ │61 +// the /description page: the detail pane shows the selected issue's whole │ │62 +// description behind a back link. │ │63 +description_page: bool = false, │ │64 // every tag in the repo, in sorted order, for the tags view. │ │65 tags: []const []const u8, │ │66 // the on-disk repo this page was read from, for the terminal submit path │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯