╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │75 repo_files: struct { name: Array(repo_route_max_len), after: usize = 0 }, │ │76 repo_commits: struct { name: Array(repo_route_max_len), after: usize = 0 }, │ │77 repo_refs: struct { name: Array(repo_route_max_len), kind: RefKind = .branch, after: usize = 0 }, │ │78 - repo_issues: struct { name: Array(repo_route_max_len), selected: Array(evt.event_id_size * 2) = .{} },│ │78 + repo_issues: struct { │ │79 + name: Array(repo_route_max_len), │ │80 + // the tag the list is filtered to, url-encoded ("" = unfiltered). │ │81 + tag: Array(issue_tag_route_max_len) = .{}, │ │82 + selected: Array(evt.event_id_size * 2) = .{}, │ │83 + }, │ │84 repo_settings: Array(repo_route_max_len), │ │85 repo_auth: Array(repo_route_max_len), │ │86 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │109 // mean the files root of the repo's default branch. │ │110 pub const repo_route_max_len = 1024; │ │111 │ │112 + // a url-encoded tag can grow to three bytes per source byte. │ │113 + pub const issue_tag_route_max_len = evt.Issue.tag_max_len * 3; │ │114 + │ │115 // the parts of a `.repo_files` route. all slices point into the route's │ │116 // stored string. ref_kind is null (and ref_value/dir empty) for the bare│ │117 // default-branch root; dir is "" at a ref's root. │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 return .{ .repo_refs = .{ .name = Array(repo_route_max_len).from(identity) orelse return null, .kind = kind, .after = after } };│ │209 } │ │210 │ │203 - // build a `.repo_issues` route for "owner/name" (identity) rooted at the │ │204 - // issue with hex event id `selected` ("" = the first window). │ │205 - pub fn repoIssuesRoute(identity: []const u8, selected: []const u8) ?RoutablePage { │ │211 + // build a `.repo_issues` route for "owner/name" (identity) filtered to the │ │212 + // url-encoded `tag` ("" = unfiltered) and rooted at the issue with hex │ │213 + // event id `selected` ("" = the first window). │ │214 + pub fn repoIssuesRoute(identity: []const u8, tag: []const u8, selected: []const u8) ?RoutablePage { │ │215 return .{ .repo_issues = .{ │ │216 .name = Array(repo_route_max_len).from(identity) orelse return null, │ │217 + .tag = Array(issue_tag_route_max_len).from(tag) orelse return null, │ │218 .selected = Array(evt.event_id_size * 2).from(selected) orelse return null, │ │219 } }; │ │220 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/refs", .{r.name.slice()}) │ │265 else │ │266 try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/refs?kind={s}&after={d}", .{ r.name.slice(), @tagName(r.kind), r.after }), │ │257 - .repo_issues => |i| if (i.selected.len == 0) │ │258 - try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues", .{i.name.slice()}) │ │267 + .repo_issues => |i| if (i.tag.len == 0) │ │268 + (if (i.selected.len == 0) │ │269 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues", .{i.name.slice()}) │ │270 + else │ │271 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues/{s}", .{ i.name.slice(), i.selected.slice() })) │ │272 + else if (i.selected.len == 0) │ │273 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues/tag/{s}", .{ i.name.slice(), i.tag.slice() }) │ │274 else │ │260 - try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues/{s}", .{ i.name.slice(), i.selected.slice() }), │ │275 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/issues/tag/{s}/{s}", .{ i.name.slice(), i.tag.slice(), i.selected.slice() }),│ │276 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}), │ │277 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │278 }; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │330 .kind = refsKind(query), │ │331 .after = after, │ │332 } }; │ │318 - if (std.mem.eql(u8, sub, "issues")) return repoIssuesRoute(pair, ""); │ │319 - if (std.mem.startsWith(u8, sub, "issues/")) { │ │320 - // "issues/<issue event id>" │ │321 - const id = sub["issues/".len..]; │ │322 - if (id.len == 0) return null; │ │323 - return repoIssuesRoute(pair, id); │ │333 + if (std.mem.eql(u8, sub, "issues") or std.mem.startsWith(u8, sub, "issues/")) { │ │334 + // "issues[/tag/<tag>][/<issue event id>]" │ │335 + var segments = std.mem.splitScalar(u8, sub, '/'); │ │336 + _ = segments.next(); // "issues" │ │337 + var tag_value: []const u8 = ""; │ │338 + var id: []const u8 = ""; │ │339 + if (segments.next()) |segment| { │ │340 + if (std.mem.eql(u8, segment, "tag")) { │ │341 + tag_value = segments.next() orelse return null; │ │342 + if (tag_value.len == 0) return null; │ │343 + id = segments.next() orelse ""; │ │344 + } else { │ │345 + if (segment.len == 0) return null; │ │346 + id = segment; │ │347 + } │ │348 + } │ │349 + if (segments.next() != null) return null; │ │350 + return repoIssuesRoute(pair, tag_value, id); │ │351 } │ │352 if (std.mem.eql(u8, sub, "settings")) return .{ .repo_settings = Array(repo_route_max_len).from(pair) orelse return null };│ │353 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │416 .repo_files => |a_f| std.mem.eql(u8, a_f.name.slice(), b.repo_files.name.slice()) and a_f.after == b.repo_files.after, │ │417 .repo_commits => |a_c| std.mem.eql(u8, a_c.name.slice(), b.repo_commits.name.slice()) and a_c.after == b.repo_commits.after, │ │418 .repo_refs => |a_r| std.mem.eql(u8, a_r.name.slice(), b.repo_refs.name.slice()) and a_r.kind == b.repo_refs.kind and a_r.after == b.repo_refs.after, │ │392 - .repo_issues => |a_i| std.mem.eql(u8, a_i.name.slice(), b.repo_issues.name.slice()) and std.mem.eql(u8, a_i.selected.slice(), b.repo_issues.selected.slice()),│ │419 + .repo_issues => |a_i| std.mem.eql(u8, a_i.name.slice(), b.repo_issues.name.slice()) and │ │420 + std.mem.eql(u8, a_i.tag.slice(), b.repo_issues.tag.slice()) and │ │421 + std.mem.eql(u8, a_i.selected.slice(), b.repo_issues.selected.slice()), │ │422 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │423 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │424 else => true, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────╮ │491 .repo_issues => |i| { │ │492 try jw.objectField("name"); │ │493 try jw.write(i.name.slice()); │ │494 + try jw.objectField("tag"); │ │495 + try jw.write(i.tag.slice()); │ │496 try jw.objectField("selected"); │ │497 try jw.write(i.selected.slice());│ │498 }, │ ╰──────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │528 } │ │529 │ │530 pub fn jsonParse(allocator: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !RoutablePage { │ │500 - const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null, after: usize = 0, ref_kind: ?[]const u8 = null, selected: ?[]const u8 = null }; │ │531 + const Helper = struct { kind: std.meta.Tag(RoutablePage), name: ?[]const u8 = null, after: usize = 0, ref_kind: ?[]const u8 = null, tag: ?[]const u8 = null, selected: ?[]const u8 = null };│ │532 const helper = try std.json.innerParse(Helper, allocator, source, options); │ │533 const parseName = struct { │ │534 // errors must stay within std.json's ParseError set; ValueTooLong │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │554 } }, │ │555 .repo_issues => .{ .repo_issues = .{ │ │556 .name = try parseName(repo_route_max_len, helper.name), │ │557 + .tag = Array(issue_tag_route_max_len).from(helper.tag orelse "") orelse return error.ValueTooLong, │ │558 .selected = Array(evt.event_id_size * 2).from(helper.selected orelse "") orelse return error.ValueTooLong,│ │559 } }, │ │560 .repo_settings => .{ .repo_settings = try parseName(repo_route_max_len, helper.name) }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │1106 stack: wgt.Stack(Widget), │ │1107 flow_box: FlowBox, │ │1108 flow_box_scroll: FlowBox.Scroll,│ │1109 + tag_flow: TagFlow, │ │1110 spacer: Spacer, │ │1111 center: Center, │ │1112 background: AnsiBackground, │ ╰──────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯