╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │105 // the selected issue's comment window (0 = the first window). │ │106 comments_start: usize = 0, │ │107 // the comment shown in the detail pane (empty = the issue thread).│ │108 + comment: Array(evt.event_id_size * 2) = .{}, │ │109 + }, │ │110 + repo_discussions: struct { │ │111 + name: Array(repo_route_max_len), │ │112 + tag: Array(discussion_tag_route_max_len) = .{}, │ │113 + selected: Array(evt.event_id_size * 2) = .{}, │ │114 + view: DiscussionsView = .all, │ │115 + comments_start: usize = 0, │ │116 comment: Array(evt.event_id_size * 2) = .{}, │ │117 }, │ │118 repo_events: RepoEventsRoute, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 pub const RefKind = enum { branch, tag }; │ │125 │ │126 pub const IssuesView = enum { open, closed, tags, new, edit, description, new_comment, edit_comment, conflicts, resolve };│ │127 + │ │128 + pub const DiscussionsView = enum { all, tags, new, edit, description, new_comment, edit_comment }; │ │129 │ │130 pub const EventsView = enum { active, removed }; │ │131 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │216 const line_seg = @tagName(Params.ParamKey.line) ++ ":"; │ │217 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":"; │ │218 const issue_seg = "issue:"; │ │219 + const discussion_seg = "discussion:"; │ │220 const comment_seg = "comment:"; │ │221 // marks a files or commits route's trailing path: the value is the raw│ │222 // remainder of the url, so it must come last │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │237 │ │238 // a url-encoded tag can grow to three bytes per source byte. │ │239 pub const issue_tag_route_max_len = evt.Issue.tag_max_len * 3; │ │240 + pub const discussion_tag_route_max_len = evt.Discussion.tag_max_len * 3; │ │241 │ │242 // caps the resolve view's theirs: field list (a longer one doesn't route).│ │243 pub const theirs_route_max_len = 512; │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │456 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │457 .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │458 .view = .description, │ │459 + } }; │ │460 + } │ │461 + │ │462 + pub fn repoDiscussionsRoute(identity: []const u8, tag: []const u8, selected: []const u8) ?RoutablePage { │ │463 + return .{ .repo_discussions = .{ │ │464 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │465 + .tag = Array(discussion_tag_route_max_len).from(tag) orelse return null, │ │466 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │467 + } }; │ │468 + } │ │469 + │ │470 + pub fn repoDiscussionCommentsRoute(identity: []const u8, selected: []const u8, start: usize) ?RoutablePage { │ │471 + var route = repoDiscussionsRoute(identity, "", selected) orelse return null; │ │472 + route.repo_discussions.comments_start = start; │ │473 + return route; │ │474 + } │ │475 + │ │476 + pub fn repoDiscussionCommentRoute(identity: []const u8, discussion: []const u8, selected: []const u8, start: usize) ?RoutablePage {│ │477 + if (discussion.len == 0 or selected.len == 0) return null; │ │478 + var route = repoDiscussionCommentsRoute(identity, discussion, start) orelse return null; │ │479 + route.repo_discussions.comment = Array(evt.event_id_size * 2).from(selected) orelse return null; │ │480 + return route; │ │481 + } │ │482 + │ │483 + pub fn repoDiscussionCommentNewRoute(identity: []const u8, discussion: []const u8, parent_id: []const u8) ?RoutablePage { │ │484 + if (discussion.len == 0) return null; │ │485 + var route = repoDiscussionCommentsRoute(identity, discussion, 0) orelse return null; │ │486 + route.repo_discussions.comment = Array(evt.event_id_size * 2).from(parent_id) orelse return null; │ │487 + route.repo_discussions.view = .new_comment; │ │488 + return route; │ │489 + } │ │490 + │ │491 + pub fn repoDiscussionCommentEditRoute(identity: []const u8, discussion: []const u8, comment_id: []const u8) ?RoutablePage { │ │492 + if (discussion.len == 0 or comment_id.len == 0) return null; │ │493 + var route = repoDiscussionCommentRoute(identity, discussion, comment_id, 0) orelse return null; │ │494 + route.repo_discussions.view = .edit_comment; │ │495 + return route; │ │496 + } │ │497 + │ │498 + pub fn repoDiscussionsTagsRoute(identity: []const u8, tag: []const u8) ?RoutablePage { │ │499 + return .{ .repo_discussions = .{ │ │500 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │501 + .tag = Array(discussion_tag_route_max_len).from(tag) orelse return null, │ │502 + .view = .tags, │ │503 } }; │ │504 } │ │505 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │506 + pub fn repoDiscussionsNewRoute(identity: []const u8) ?RoutablePage { │ │507 + return .{ .repo_discussions = .{ │ │508 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │509 + .view = .new, │ │510 + } }; │ │511 + } │ │512 + │ │513 + pub fn repoDiscussionsEditRoute(identity: []const u8, selected: []const u8) ?RoutablePage { │ │514 + if (selected.len == 0) return null; │ │515 + return .{ .repo_discussions = .{ │ │516 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │517 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │518 + .view = .edit, │ │519 + } }; │ │520 + } │ │521 + │ │522 + pub fn repoDiscussionsDescriptionRoute(identity: []const u8, selected: []const u8) ?RoutablePage {│ │523 + if (selected.len == 0) return null; │ │524 + return .{ .repo_discussions = .{ │ │525 + .name = Array(repo_route_max_len).from(identity) orelse return null, │ │526 + .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │527 + .view = .description, │ │528 + } }; │ │529 + } │ │530 + │ │531 // build a `.repo_issues` route showing the conflicts list, rooted at the │ │532 // issue with hex event id `start` ("" = the first window). │ │533 pub fn repoIssuesConflictsRoute(identity: []const u8, start: []const u8) ?RoutablePage { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │656 else │ │657 try std.fmt.allocPrint(arena.allocator(), "{s}/issues/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(i.view), i.tag.slice() }); │ │658 }, │ │659 + .repo_discussions => |t| blk: { │ │660 + const prefix = try repoUrlPrefix(arena, t.name.slice()); │ │661 + if (t.view == .edit) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/edit", .{ prefix, t.selected.slice() }); │ │662 + if (t.view == .description) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/description", .{ prefix, t.selected.slice() }); │ │663 + if (t.view == .new_comment) break :blk if (t.comment.len == 0) │ │664 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/new", .{ prefix, t.selected.slice() }) │ │665 + else │ │666 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/" ++ comment_seg ++ "{s}/new", .{ prefix, t.selected.slice(), t.comment.slice() }); │ │667 + if (t.view == .edit_comment) break :blk try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/" ++ comment_seg ++ "{s}/edit", .{ prefix, t.selected.slice(), t.comment.slice() });│ │668 + if (t.comment.len != 0) break :blk if (t.comments_start == 0) │ │669 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/" ++ comment_seg ++ "{s}", .{ prefix, t.selected.slice(), t.comment.slice() }) │ │670 + else │ │671 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/" ++ comment_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, t.selected.slice(), t.comment.slice(), t.comments_start });│ │672 + if (t.selected.len != 0) break :blk if (t.comments_start == 0) │ │673 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}", .{ prefix, t.selected.slice() }) │ │674 + else │ │675 + try std.fmt.allocPrint(arena.allocator(), "{s}/" ++ discussion_seg ++ "{s}/" ++ start_seg ++ "{d}", .{ prefix, t.selected.slice(), t.comments_start }); │ │676 + break :blk if (t.tag.len == 0) │ │677 + try std.fmt.allocPrint(arena.allocator(), "{s}/discussions/{s}", .{ prefix, @tagName(t.view) }) │ │678 + else │ │679 + try std.fmt.allocPrint(arena.allocator(), "{s}/discussions/{s}/" ++ tag_filter_seg ++ "{s}", .{ prefix, @tagName(t.view), t.tag.slice() }); │ │680 + }, │ │681 .repo_events => |e| blk: { │ │682 const prefix = try repoUrlPrefix(arena, e.name.slice()); │ │683 break :blk if (e.kind) |kind| │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │759 .repo_commits => |*c| c.name.slice(), │ │760 .repo_refs => |*r| r.name.slice(), │ │761 .repo_issues => |*i| i.name.slice(), │ │762 + .repo_discussions => |*t| t.name.slice(), │ │763 .repo_events => |*e| e.name.slice(), │ │764 .repo_settings, .repo_auth => |*name| name.slice(),│ │765 else => null, │ ╰────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │785 return switch (self) { │ │786 .home_users, .home_repos, .home_settings, .home_auth => .home, │ │787 .user_repos, .user_settings, .user_auth => .user, │ │684 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_events, .repo_settings, .repo_auth => .repo, │ │788 + .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_discussions, .repo_events, .repo_settings, .repo_auth => .repo,│ │789 }; │ │790 } │ │791 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │855 if (!params.only(&.{.start})) return null; │ │856 return repoIssueCommentsRoute(pair, issue_id, params.start() orelse return null); │ │857 } │ │858 + if (std.mem.startsWith(u8, tab, discussion_seg)) { │ │859 + const discussion_id = tab[discussion_seg.len..]; │ │860 + if (discussion_id.len == 0) return null; │ │861 + if (segments.peek()) |tail| { │ │862 + if (std.mem.startsWith(u8, tail, comment_seg)) { │ │863 + _ = segments.next(); │ │864 + const comment_id = tail[comment_seg.len..]; │ │865 + if (comment_id.len == 0) return null; │ │866 + const word = params.scanTail(&segments) catch return null; │ │867 + if (word) |last| { │ │868 + if (!params.only(&.{})) return null; │ │869 + if (std.mem.eql(u8, last, "new")) return repoDiscussionCommentNewRoute(pair, discussion_id, comment_id); │ │870 + if (std.mem.eql(u8, last, "edit")) return repoDiscussionCommentEditRoute(pair, discussion_id, comment_id); │ │871 + return null; │ │872 + } │ │873 + if (!params.only(&.{.start})) return null; │ │874 + return repoDiscussionCommentRoute(pair, discussion_id, comment_id, params.start() orelse return null); │ │875 + } │ │876 + } │ │877 + const word = params.scanTail(&segments) catch return null; │ │878 + if (word) |tail| { │ │879 + if (std.mem.eql(u8, tail, "new")) return if (params.only(&.{})) repoDiscussionCommentNewRoute(pair, discussion_id, "") else null; │ │880 + if (std.mem.eql(u8, tail, "edit")) return if (params.only(&.{})) repoDiscussionsEditRoute(pair, discussion_id) else null; │ │881 + if (std.mem.eql(u8, tail, "description")) return if (params.only(&.{})) repoDiscussionsDescriptionRoute(pair, discussion_id) else null;│ │882 + return null; │ │883 + } │ │884 + if (!params.only(&.{.start})) return null; │ │885 + return repoDiscussionCommentsRoute(pair, discussion_id, params.start() orelse return null); │ │886 + } │ │887 if (std.mem.eql(u8, tab, "issues")) { │ │888 // the word is a list view; a filter with neither is never emitted. │ │889 const word = params.scanTail(&segments) catch return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯