╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │108 // updates the url) rather than navigating away. │ │109 pub const RoutablePage = union(enum) { │ │110 home_about, │ │111 - home_users: usize, // 0 = first page │ │111 + home_users: HomeUsersRoute, │ │112 home_settings, │ │113 home_auth, │ │114 - user_repos: struct { name: Array(evt.User.name_max_len), start: usize = 0 },│ │114 + user_repos: struct { │ │115 + name: Array(evt.User.name_max_len), │ │116 + start: usize = 0, │ │117 + // the url-encoded name prefix the list is narrowed to ("" = none) │ │118 + search: Array(search_route_max_len) = .{}, │ │119 + }, │ │120 user_forks: struct { name: Array(evt.User.name_max_len), start: usize = 0 },│ │121 user_settings: Array(evt.User.name_max_len), │ │122 user_auth: Array(evt.User.name_max_len), │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │273 } │ │274 return result; │ │275 } │ │276 + }; │ │277 + │ │278 + pub const HomeUsersRoute = struct { │ │279 + start: usize = 0, │ │280 + // the url-encoded name prefix the list is narrowed to ("" = none)│ │281 + search: Array(search_route_max_len) = .{}, │ │282 }; │ │283 │ │284 const user_segment = "/user/"; │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │602 return route; │ │603 } │ │604 │ │594 - // carry a search query on a commits or thread-list route, url-encoding it │ │605 + // carry a search query on a searchable route, url-encoding it │ │606 // into the route. │ │607 pub fn withSearch(self: RoutablePage, query: []const u8) ?RoutablePage { │ │608 const search = encodeParam(search_route_max_len, query) orelse return null;│ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │630 var route = self orelse return null; │ │631 const search = Array(search_route_max_len).from(value) orelse return null;│ │632 switch (route) { │ │633 + .home_users => |*u| u.search = search, │ │634 + .user_repos => |*u| u.search = search, │ │635 .repo_commits => |*c| c.search = search, │ │636 .repo_refs => |*r| r.search = search, │ │637 .repo_issues => |*i| i.search = search, │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1027 pub fn toUrl(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │1028 return switch (self) { │ │1029 .home_about => "/", │ │1017 - .home_users => |start| if (start == 0) @as([]const u8, "/users") else try std.fmt.allocPrint(arena.allocator(), "/users/" ++ start_seg ++ "{d}", .{start}),│ │1030 + .home_users => |u| blk: { │ │1031 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1032 + try out.writer.writeAll("/users"); │ │1033 + try writeListWindow(&out.writer, u.search.slice(), u.start); │ │1034 + break :blk out.written(); │ │1035 + }, │ │1036 .home_settings => "/settings", │ │1037 .home_auth => "/auth", │ │1020 - .user_repos => |u| if (u.start == 0) │ │1021 - try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/repos", .{u.name.slice()}) │ │1022 - else │ │1023 - try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/repos/" ++ start_seg ++ "{d}", .{ u.name.slice(), u.start }), │ │1038 + .user_repos => |u| blk: { │ │1039 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1040 + try out.writer.print(user_segment ++ "{s}/repos", .{u.name.slice()}); │ │1041 + try writeListWindow(&out.writer, u.search.slice(), u.start); │ │1042 + break :blk out.written(); │ │1043 + }, │ │1044 .user_forks => |u| if (u.start == 0) │ │1045 try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/forks", .{u.name.slice()}) │ │1046 else │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1262 if (path.len < 2 or path[0] != '/') return null; │ │1263 var segments = std.mem.splitScalar(u8, path[1..], '/'); │ │1264 const first = segments.next() orelse return null; │ │1245 - if (std.mem.eql(u8, first, "users")) return .{ .home_users = listStart(&segments) orelse return null }; │ │1265 + if (std.mem.eql(u8, first, "users")) { │ │1266 + const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1267 + return withEncodedSearch(.{ .home_users = .{ .start = params.start() orelse return null } }, params.values.get(.search) orelse "");│ │1268 + } │ │1269 if (std.mem.eql(u8, first, "settings")) return if (segments.next() == null) .home_settings else null; │ │1270 if (std.mem.eql(u8, first, "auth")) return if (segments.next() == null) .home_auth else null; │ │1248 - // "user/<name>[/repos[/start:<n>]|/settings|/auth]" │ │1271 + // "user/<name>[/repos[/search:<s>][/start:<n>]|/settings|/auth]" │ │1272 if (std.mem.eql(u8, first, "user")) { │ │1273 const name = segments.next() orelse return null; │ │1274 if (name.len == 0) return null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1275 const parsed = Array(evt.User.name_max_len).from(name) orelse return null; // name too long │ │1276 const sub = segments.next() orelse return .{ .user_repos = .{ .name = parsed } }; │ │1254 - if (std.mem.eql(u8, sub, "repos")) return .{ .user_repos = .{ .name = parsed, .start = listStart(&segments) orelse return null } }; │ │1277 + if (std.mem.eql(u8, sub, "repos")) { │ │1278 + const params = listParams(&segments, &.{ .start, .search }) orelse return null; │ │1279 + return withEncodedSearch(.{ .user_repos = .{ .name = parsed, .start = params.start() orelse return null } }, params.values.get(.search) orelse "");│ │1280 + } │ │1281 if (std.mem.eql(u8, sub, "forks")) return .{ .user_forks = .{ .name = parsed, .start = listStart(&segments) orelse return null } }; │ │1282 if (std.mem.eql(u8, sub, "settings")) return if (segments.next() == null) .{ .user_settings = parsed } else null; │ │1283 if (std.mem.eql(u8, sub, "auth")) return if (segments.next() == null) .{ .user_auth = parsed } else null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1441 │ │1442 // a list route's tail: at most a "start:<n>" param, null on anything else │ │1443 fn listStart(segments: *std.mem.SplitIterator(u8, .scalar)) ?usize { │ │1444 + const params = listParams(segments, &.{.start}) orelse return null; │ │1445 + return params.start(); │ │1446 + } │ │1447 + │ │1448 + // a list route's tail of `allowed` params, null on anything else │ │1449 + fn listParams(segments: *std.mem.SplitIterator(u8, .scalar), allowed: []const Params.ParamKey) ?Params {│ │1450 var params = Params{}; │ │1451 const word = params.scanTail(segments) catch return null; │ │1420 - if (word != null or !params.only(&.{.start})) return null; │ │1421 - return params.start(); │ │1452 + if (word != null or !params.only(allowed)) return null; │ │1453 + return params; │ │1454 + } │ │1455 + │ │1456 + // a searchable list url's search and window segments, each optional │ │1457 + fn writeListWindow(writer: *std.Io.Writer, search: []const u8, start: usize) !void { │ │1458 + if (search.len != 0) try writer.print("/" ++ search_seg ++ "{s}", .{search}); │ │1459 + if (start != 0) try writer.print("/" ++ start_seg ++ "{d}", .{start}); │ │1460 } │ │1461 │ │1462 // parse the sub-path after a repo route's identity ("" for a local │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1719 pub fn eql(a: RoutablePage, b: RoutablePage) bool { │ │1720 if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; │ │1721 return switch (a) { │ │1684 - .home_users => |a_start| a_start == b.home_users, │ │1685 - .user_repos => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_repos.name.slice()) and a_u.start == b.user_repos.start, │ │1722 + .home_users => |a_u| a_u.start == b.home_users.start and std.mem.eql(u8, a_u.search.slice(), b.home_users.search.slice()), │ │1723 + .user_repos => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_repos.name.slice()) and a_u.start == b.user_repos.start and std.mem.eql(u8, a_u.search.slice(), b.user_repos.search.slice()),│ │1724 .user_forks => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_forks.name.slice()) and a_u.start == b.user_forks.start, │ │1725 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()), │ │1726 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1826 } │ │1827 │ │1828 // true when `a` and `b` are the same user paginated to a different repos │ │1791 - // window. switching between a user's tabs is in-page, so │ │1792 - // only a changed `start` on the repos list navigates. │ │1829 + // window or search. switching between a user's tabs is in-page, so only a│ │1830 + // changed `start` or search on the same list navigates. │ │1831 pub fn userPageChanged(a: RoutablePage, b: RoutablePage) bool { │ │1832 return switch (a) { │ │1833 .user_repos => |aa| switch (b) { │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯