╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │63 user_settings: Array(evt.User.name_max_len), │ │64 user_auth: Array(evt.User.name_max_len), │ │65 repo: Array(repo_route_max_len), │ │66 - repo_commits: Array(repo_route_max_len), │ │66 + repo_commits: struct { name: Array(repo_route_max_len), after: usize = 0 },│ │67 repo_settings: Array(repo_route_max_len), │ │68 repo_auth: Array(repo_route_max_len), │ │69 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │123 // build a `.repo_commits` route for "owner/name" (identity) at `start_oid` │ │124 // ("" = the first page). always carries the `commits` marker so page 1 │ │125 // ("owner/name/commits") doesn't collide with the files root. │ │126 - pub fn repoCommitsRoute(identity: []const u8, start_oid: []const u8) ?RoutablePage { │ │126 + pub fn repoCommitsRoute(identity: []const u8, start_oid: []const u8, after: usize) ?RoutablePage {│ │127 var buf: [repo_route_max_len]u8 = undefined; │ │128 const s = if (start_oid.len == 0) │ │129 std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg, .{identity}) catch return null │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │130 else │ │131 std.fmt.bufPrint(&buf, "{s}/" ++ commits_seg ++ "/{s}", .{ identity, start_oid }) catch return null; │ │132 - return .{ .repo_commits = Array(repo_route_max_len).from(s) orelse return null }; │ │132 + return .{ .repo_commits = .{ .name = Array(repo_route_max_len).from(s) orelse return null, .after = after } };│ │133 } │ │134 │ │135 // an inline, owned array of data. keeping it in the route (rather than a │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │170 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}),│ │171 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │172 .repo => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │173 - // the stored string already carries the "commits[/oid]" suffix. │ │174 - .repo_commits => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ │173 + .repo_commits => |c| if (c.after == 0) │ │174 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{c.name.slice()}) │ │175 + else │ │176 + try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}?after={d}", .{ c.name.slice(), c.after }), │ │177 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/settings", .{name.slice()}),│ │178 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}/auth", .{name.slice()}), │ │179 inline else => |_, tag| url(tag), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │188 }; │ │189 } │ │190 │ │189 - pub fn fromUrl(path: []const u8) ?RoutablePage { │ │191 + // parse a path plus its raw query string │ │192 + pub fn fromUrl(path: []const u8, query: ?[]const u8) ?RoutablePage {│ │193 + const after = uintParam(query, "after") orelse 0; │ │194 if (std.mem.eql(u8, path, "/")) return default; │ │195 if (std.mem.eql(u8, path, "/users")) return .home_users; │ │196 if (std.mem.eql(u8, path, "/repos")) return .home_repos; │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │230 if (std.mem.eql(u8, sub, "auth")) return .{ .repo_auth = Array(repo_route_max_len).from(pair) orelse return null }; │ │231 if (std.mem.eql(u8, sub, files_seg)) return repoFilesRoute(pair, ""); // trailing /files == root │ │232 if (std.mem.startsWith(u8, sub, files_seg ++ "/")) return repoFilesRoute(pair, sub[files_seg.len + 1 ..]); │ │229 - if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, ""); // trailing /commits == page 1 │ │230 - if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) return repoCommitsRoute(pair, sub[commits_seg.len + 1 ..]); │ │233 + if (std.mem.eql(u8, sub, commits_seg)) return repoCommitsRoute(pair, "", after); // trailing /commits == page 1 │ │234 + if (std.mem.startsWith(u8, sub, commits_seg ++ "/")) return repoCommitsRoute(pair, sub[commits_seg.len + 1 ..], after);│ │235 return null; // unknown sub-path │ │236 } │ │237 return repoFilesRoute(pair, ""); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │238 + } │ │239 + return null; │ │240 + } │ │241 + │ │242 + // read a "key=<uint>" value from a raw "a=1&b=2" query string. │ │243 + fn uintParam(query: ?[]const u8, key: []const u8) ?usize { │ │244 + const qs = query orelse return null; │ │245 + var it = std.mem.splitScalar(u8, qs, '&'); │ │246 + while (it.next()) |pair| { │ │247 + const eq = std.mem.indexOfScalar(u8, pair, '=') orelse continue; │ │248 + if (std.mem.eql(u8, pair[0..eq], key)) │ │249 + return std.fmt.parseInt(usize, pair[eq + 1 ..], 10) catch null;│ │250 } │ │251 return null; │ │252 } │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │258 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()), │ │259 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │260 .repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo.slice()), │ │245 - .repo_commits => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_commits.slice()), │ │261 + .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,│ │262 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │263 .repo_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_auth.slice()), │ │264 else => true, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │268 // the content a repo route renders: which repo, plus the files directory and │ │269 // commits page. tab switches keep this fixed (the page already holds every │ │270 // tab's data); a files-directory or commits-page change alters it. │ │255 - const RepoContent = struct { identity: []const u8, files_dir: []const u8, commits_start: []const u8 }; │ │271 + const RepoContent = struct { identity: []const u8, files_dir: []const u8, commits_start: []const u8, commits_after: usize };│ │272 │ │273 fn repoContent(r: RoutablePage) ?RepoContent { │ │274 const name = switch (r) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │259 - .repo, .repo_commits, .repo_settings, .repo_auth => |n| n.slice(),│ │275 + .repo, .repo_settings, .repo_auth => |n| n.slice(), │ │276 + .repo_commits => |c| c.name.slice(), │ │277 else => return null, │ │278 }; │ │279 const rf = RepoFiles.parse(name) orelse return null; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯