╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │223 - .start = 0, │ │224 - .ref_kind = null, │ │225 - .ref_value = "", │ │226 - .dir = "", │ │216 }; │ │228 - var rest: []const u8 = undefined; │ │217 if (identityElided(s)) { │ │230 - rest = if (s.len == 0) "" else s[1..]; │ │218 + return result; │ │219 } else { │ │220 var id_segments = std.mem.splitScalar(u8, s, '/'); │ │221 const owner = id_segments.next() orelse return null;│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │224 result.identity = s[0 .. owner.len + 1 + name.len]; │ │225 result.owner = owner; │ │226 result.name = name; │ │239 - rest = id_segments.rest(); │ │227 } │ │241 - var segments = std.mem.splitScalar(u8, rest, '/'); │ │242 - if (!std.mem.eql(u8, segments.next() orelse return result, files_seg)) return result;│ │243 - var params = Params{}; │ │244 - params.scanPairs(&segments) catch return result; │ │245 - result.start = params.start() orelse return result; │ │246 - const ref = (params.ref() catch return result) orelse return result; │ │247 - result.ref_kind = ref.kind; │ │248 - result.ref_value = ref.value; │ │249 - result.dir = pathValue(segments.rest()) orelse return result; │ │228 return result; │ │229 } │ │230 + }; │ │231 + │ │232 + pub const RepoFilesRoute = struct { │ │233 + name: Array(repo_identity_max_len), │ │234 + ref_kind: ?RefOrOid = null, │ │235 + ref_value: Array(ref_route_max_len) = .{}, │ │236 + path: Array(repo_route_max_len) = .{}, │ │237 + start: usize = 0, │ │238 + }; │ │239 + │ │240 + pub const RepoCommitsRoute = struct { │ │241 + name: Array(repo_identity_max_len), │ │242 + ref_or_oid: ?RefOrOid = null, │ │243 + value: Array(ref_route_max_len) = .{}, │ │244 + start: usize = 0, │ │245 }; │ │246 │ │247 // build a `.repo_files` route (a null ref_kind = the bare default-branch │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │248 // root). null if the result doesn't fit the inline name. │ │249 pub fn repoFilesRoute(identity: []const u8, ref_kind: ?RefOrOid, ref_value: []const u8, dir: []const u8, start: usize) ?RoutablePage {│ │257 - if (ref_kind == null and start == 0) return .{ .repo_files = Array(repo_route_max_len).from(identity) orelse return null }; │ │258 - var buf: [repo_route_max_len]u8 = undefined; │ │259 - var writer: std.Io.Writer = .fixed(&buf); │ │260 - writer.print("{s}/" ++ files_seg, .{identity}) catch return null; │ │261 - if (ref_kind) |kind| writer.print("/{s}:{s}", .{ @tagName(kind), ref_value }) catch return null; │ │262 - if (start != 0) writer.print("/" ++ start_seg ++ "{d}", .{start}) catch return null; │ │263 - if (dir.len != 0) writer.print("/" ++ path_seg ++ "{s}", .{dir}) catch return null; │ │264 - return .{ .repo_files = Array(repo_route_max_len).from(writer.buffered()) orelse return null }; │ │250 + if (ref_kind == null and (ref_value.len != 0 or dir.len != 0)) return null; │ │251 + if (ref_kind != null and ref_value.len == 0 and dir.len != 0) return null; │ │252 + return .{ .repo_files = .{ │ │253 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │254 + .ref_kind = ref_kind, │ │255 + .ref_value = Array(ref_route_max_len).from(ref_value) orelse return null, │ │256 + .path = Array(repo_route_max_len).from(dir) orelse return null, │ │257 + .start = start, │ │258 + } }; │ │259 } │ │260 │ │267 - // the ref/oid a `.repo_commits` route's stored string walks from (null = │ │268 - // the default branch), plus the diff window start. the value slices into `s`. │ │269 - pub const CommitsRef = struct { ref_or_oid: ?RefOrOid, value: []const u8, start: usize }; │ │270 - pub fn repoCommitsRef(s: []const u8) CommitsRef { │ │271 - var result = CommitsRef{ .ref_or_oid = null, .value = "", .start = 0 }; │ │272 - if (s.len == 0) return result; │ │273 - var segments = std.mem.splitScalar(u8, s, '/'); │ │274 - if (identityElided(s)) { │ │275 - _ = segments.next(); // the empty segment before the tail │ │276 - } else { │ │277 - _ = segments.next(); // owner │ │278 - _ = segments.next(); // name │ │279 - } │ │280 - if (!std.mem.eql(u8, segments.next() orelse "", commits_seg)) return result; │ │281 - var params = Params{}; │ │282 - params.scanPairs(&segments) catch return result; │ │283 - result.start = params.start() orelse return result; │ │284 - const ref = (params.ref() catch return result) orelse return result; │ │285 - result.ref_or_oid = ref.kind; │ │286 - result.value = ref.value; │ │287 - return result; │ │288 - } │ │289 - │ │261 // build a `.repo_commits` route (a null ref_or_oid = the default branch). │ │262 // always carries the `commits` marker so the bare route doesn't collide │ │263 // with the files root. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 pub fn repoCommitsRoute(identity: []const u8, ref_or_oid: ?RefOrOid, value: []const u8, start: usize) ?RoutablePage {│ │294 - var buf: [repo_route_max_len]u8 = undefined; │ │295 - var writer: std.Io.Writer = .fixed(&buf); │ │296 - writer.print("{s}/" ++ commits_seg, .{identity}) catch return null; │ │297 - if (ref_or_oid) |kind| writer.print("/{s}:{s}", .{ @tagName(kind), value }) catch return null; │ │298 - if (start != 0) writer.print("/" ++ start_seg ++ "{d}", .{start}) catch return null; │ │299 - return .{ .repo_commits = Array(repo_route_max_len).from(writer.buffered()) orelse return null }; │ │265 + if (ref_or_oid == null and value.len != 0) return null; │ │266 + return .{ .repo_commits = .{ │ │267 + .name = Array(repo_identity_max_len).from(identity) orelse return null, │ │268 + .ref_or_oid = ref_or_oid, │ │269 + .value = Array(ref_route_max_len).from(value) orelse return null, │ │270 + .start = start, │ │271 + } }; │ │272 } │ │273 │ │274 // build a `.repo_refs` route windowing `kind`'s column from the │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │338 }; │ │339 } │ │340 │ │369 - pub fn urlAlloc(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │341 + pub fn toUrl(self: RoutablePage, arena: *std.heap.ArenaAllocator) ![]const u8 { │ │342 return switch (self) { │ │343 .home_users => |start| if (start == 0) @as([]const u8, "/users") else try std.fmt.allocPrint(arena.allocator(), "/users/" ++ start_seg ++ "{d}", .{start}),│ │344 .home_repos => |start| if (start == 0) @as([]const u8, "/repos") else try std.fmt.allocPrint(arena.allocator(), "/repos/" ++ start_seg ++ "{d}", .{start}),│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │350 try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/repos/" ++ start_seg ++ "{d}", .{ u.name.slice(), u.start }),│ │351 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}), │ │352 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │381 - .repo_files => |name| try repoUrlPath(arena, name.slice()), │ │382 - .repo_commits => |name| try repoUrlPath(arena, name.slice()), │ │353 + .repo_files => |f| blk: { │ │354 + const prefix = try repoUrlPrefix(arena, f.name.slice()); │ │355 + if (f.ref_kind == null and f.start == 0) break :blk if (prefix.len == 0) "/" else prefix; │ │356 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │357 + try out.writer.print("{s}/" ++ files_seg, .{prefix}); │ │358 + if (f.ref_kind) |kind| if (f.ref_value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), f.ref_value.slice() });│ │359 + if (f.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{f.start}); │ │360 + if (f.path.len != 0) try out.writer.print("/" ++ path_seg ++ "{s}", .{f.path.slice()}); │ │361 + break :blk out.written(); │ │362 + }, │ │363 + .repo_commits => |c| blk: { │ │364 + const prefix = try repoUrlPrefix(arena, c.name.slice()); │ │365 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │366 + try out.writer.print("{s}/" ++ commits_seg, .{prefix}); │ │367 + if (c.ref_or_oid) |kind| if (c.value.len != 0) try out.writer.print("/{s}:{s}", .{ @tagName(kind), c.value.slice() }); │ │368 + if (c.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{c.start}); │ │369 + break :blk out.written(); │ │370 + }, │ │371 .repo_refs => |r| blk: { │ │372 const prefix = try repoUrlPrefix(arena, r.name.slice()); │ │373 break :blk if (r.from.len == 0) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │390 }; │ │391 } │ │392 │ │405 - // the url path a files/commits route's stored string (identity + tail) maps │ │406 - // to: "/repo/<string>", or the tail itself when the identity is elided │ │407 - // ("/" at the bare root). │ │408 - fn repoUrlPath(arena: *std.heap.ArenaAllocator, name: []const u8) ![]const u8 { │ │409 - if (identityElided(name)) return if (name.len == 0) "/" else name; │ │410 - return std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name}); │ │411 - } │ │412 - │ │413 - // the url prefix for a repo route's identity: "/repo/<identity>", or "" │ │414 - // when the identity is elided. │ │415 - fn repoUrlPrefix(arena: *std.heap.ArenaAllocator, identity: []const u8) ![]const u8 { │ │416 - if (identity.len == 0) return ""; │ │417 - return std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{identity}); │ │418 - } │ │419 - │ │420 - pub fn parent(self: RoutablePage) PageKind { │ │421 - return switch (self) { │ │422 - .home_users, .home_repos, .home_settings, .home_auth => .home, │ │423 - .user_repos, .user_settings, .user_auth => .user, │ │424 - .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_settings, .repo_auth => .repo,│ │425 - }; │ │426 - } │ │427 - │ │393 pub fn fromUrl(path: []const u8) ?RoutablePage { │ │394 if (std.mem.eql(u8, path, "/")) return default; │ │395 if (path.len < 2 or path[0] != '/') return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │433 if (std.mem.eql(u8, path, "/")) return repoFilesRoute("", null, "", "", 0); │ │434 if (path.len < 2 or path[0] != '/') return null; │ │435 return repoSubRoute("", path[1..]); │ │436 + } │ │437 + │ │438 + // the url prefix for a repo route's identity: "/repo/<identity>", or "" │ │439 + // when the identity is elided. │ │440 + fn repoUrlPrefix(arena: *std.heap.ArenaAllocator, identity: []const u8) ![]const u8 { │ │441 + if (identity.len == 0) return ""; │ │442 + return std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{identity}); │ │443 } │ │444 │ │445 + pub fn parent(self: RoutablePage) PageKind { │ │446 + return switch (self) { │ │447 + .home_users, .home_repos, .home_settings, .home_auth => .home, │ │448 + .user_repos, .user_settings, .user_auth => .user, │ │449 + .repo_files, .repo_commits, .repo_refs, .repo_issues, .repo_settings, .repo_auth => .repo,│ │450 + }; │ │451 + } │ │452 + │ │453 // the dir a trailing "path:<dir>" names: the raw remainder ("" when │ │454 // absent, null when malformed) │ │455 fn pathValue(rest: []const u8) ?[]const u8 { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │523 .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, │ │524 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()), │ │525 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │546 - .repo_files => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_files.slice()), │ │547 - .repo_commits => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_commits.slice()), │ │526 + .repo_files => |a_f| std.mem.eql(u8, a_f.name.slice(), b.repo_files.name.slice()) and │ │527 + a_f.ref_kind == b.repo_files.ref_kind and │ │528 + std.mem.eql(u8, a_f.ref_value.slice(), b.repo_files.ref_value.slice()) and │ │529 + std.mem.eql(u8, a_f.path.slice(), b.repo_files.path.slice()) and │ │530 + a_f.start == b.repo_files.start, │ │531 + .repo_commits => |a_c| std.mem.eql(u8, a_c.name.slice(), b.repo_commits.name.slice()) and │ │532 + a_c.ref_or_oid == b.repo_commits.ref_or_oid and │ │533 + std.mem.eql(u8, a_c.value.slice(), b.repo_commits.value.slice()) and │ │534 + a_c.start == b.repo_commits.start, │ │535 .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 std.mem.eql(u8, a_r.from.slice(), b.repo_refs.from.slice()),│ │536 .repo_issues => |a_i| std.mem.eql(u8, a_i.name.slice(), b.repo_issues.name.slice()) and │ │537 std.mem.eql(u8, a_i.tag.slice(), b.repo_issues.tag.slice()) and │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │1753 │ │1754 _ = self.arena.reset(.retain_capacity); │ │1755 const aa = self.arena.allocator(); │ │1769 - const path = self.session.data.current_page.urlAlloc(&self.arena) catch return; │ │1756 + const path = self.session.data.current_page.toUrl(&self.arena) catch return; │ │1757 const text = if (self.session.web_port) |port| │ │1758 std.fmt.allocPrint(aa, "http://localhost:{d}{s}", .{ port, path }) catch return│ │1759 else │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯