╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 // user pages always read from the admin db. │ │72 return switch (route.parent()) { │ │73 .home => .{ .home = try Home.init(arena, session, session.haxy_moment orelse return error.NoMoment, switch (route) {│ │74 - .home_users => |start| start, │ │75 - else => 0, │ │74 + .home_users => |u| u, │ │75 + else => .{}, │ │76 }) }, │ │77 .user => blk: { │ │78 const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 break :blk switch (route) { │ │80 - .user_repos => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, u.start, 0) }, │ │81 - .user_forks => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, 0, u.start) }, │ │82 - .user_settings, .user_auth => |name| .{ .user = try User.init(arena, session, haxy_moment, name, 0, 0) }, │ │80 + .user_repos => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, u.start, u.search.slice(), 0) },│ │81 + .user_forks => |u| .{ .user = try User.init(arena, session, haxy_moment, u.name, 0, "", u.start) }, │ │82 + .user_settings, .user_auth => |name| .{ .user = try User.init(arena, session, haxy_moment, name, 0, "", 0) }, │ │83 else => return error.UnexpectedRoute, │ │84 }; │ │85 }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯