╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │198 │ │199 // the "key:value" segments of a url tail, collected in any order │ │200 const Params = struct { │ │201 - const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from }; │ │201 + const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from, moment };│ │202 │ │203 // a branch:/tag:/object: param; the value stays url-encoded │ │204 const RefParam = struct { kind: RefOrOid, value: []const u8 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │244 fn start(self: Params) ?usize { │ │245 const value = self.values.get(.start) orelse return 0; │ │246 return std.fmt.parseInt(usize, value, 10) catch null; │ │247 + } │ │248 + │ │249 + // the "moment:<n>" event filter, null when absent │ │250 + fn moment(self: Params) error{BadUrl}!?u64 { │ │251 + const value = self.values.get(.moment) orelse return null; │ │252 + return std.fmt.parseInt(u64, value, 10) catch error.BadUrl; │ │253 } │ │254 │ │255 // the "line:<n>" first-line param (0 when absent), null when malformed│ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │415 view: EventsView = .active, │ │416 kind: ?evt.EventKind = null, │ │417 selected: Array(evt.event_id_size * 2) = .{}, │ │418 + // limits the list to the events one transaction wrote│ │419 + moment: ?u64 = null, │ │420 }; │ │421 │ │422 pub const ForkRoute = struct { │ ╰───────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │702 } │ │703 │ │704 // build the events list or a window rooted at one event. │ │697 - pub fn repoEventsRoute(identity: []const u8, view: EventsView, kind: ?evt.EventKind, selected: []const u8) ?RoutablePage { │ │705 + pub fn repoEventsRoute(identity: []const u8, view: EventsView, kind: ?evt.EventKind, selected: []const u8, moment: ?u64) ?RoutablePage {│ │706 if ((kind == null) != (selected.len == 0)) return null; │ │707 return .{ .repo_events = .{ │ │708 .name = Array(repo_identity_max_len).from(identity) orelse return null, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │709 .view = view, │ │710 .kind = kind, │ │711 .selected = Array(evt.event_id_size * 2).from(selected) orelse return null,│ │712 + .moment = moment, │ │713 } }; │ │714 } │ │715 │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1162 }, │ │1163 .repo_events => |e| blk: { │ │1164 const prefix = try repoUrlPrefix(arena, e.name.slice()); │ │1165 + const moment = if (e.moment) |value| try std.fmt.allocPrint(arena.allocator(), "/moment:{d}", .{value}) else ""; │ │1166 break :blk if (e.kind) |kind| │ │1157 - try std.fmt.allocPrint(arena.allocator(), "{s}/event:{s}/kind:{s}", .{ prefix, e.selected.slice(), @tagName(kind) }) │ │1167 + try std.fmt.allocPrint(arena.allocator(), "{s}/event:{s}/kind:{s}{s}", .{ prefix, e.selected.slice(), @tagName(kind), moment })│ │1168 else │ │1159 - try std.fmt.allocPrint(arena.allocator(), "{s}/events/{s}", .{ prefix, @tagName(e.view) }); │ │1169 + try std.fmt.allocPrint(arena.allocator(), "{s}/events/{s}{s}", .{ prefix, @tagName(e.view), moment }); │ │1170 }, │ │1171 .repo_settings => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/settings", .{try repoUrlPrefix(arena, name.slice())}), │ │1172 .repo_auth => |name| try std.fmt.allocPrint(arena.allocator(), "{s}/auth", .{try repoUrlPrefix(arena, name.slice())}), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1559 return if (segments.next() == null) repoUndoRoute(pair, index) else null; │ │1560 } │ │1561 if (std.mem.eql(u8, tab, "events")) { │ │1552 - const view = if (segments.next()) |word| │ │1553 - std.meta.stringToEnum(EventsView, word) orelse return null │ │1554 - else │ │1555 - .active; │ │1556 - return if (segments.next() == null) repoEventsRoute(pair, view, null, "") else null; │ │1562 + const word = params.scanTail(&segments) catch return null; │ │1563 + if (!params.only(&.{.moment})) return null; │ │1564 + const view = if (word) |value| std.meta.stringToEnum(EventsView, value) orelse return null else .active;│ │1565 + return repoEventsRoute(pair, view, null, "", params.moment() catch return null); │ │1566 } │ │1567 if (std.mem.startsWith(u8, tab, "event:")) { │ │1568 const event_id = tab["event:".len..]; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1569 const kind_segment = segments.next() orelse return null; │ │1570 if (!std.mem.startsWith(u8, kind_segment, "kind:")) return null; │ │1571 const kind = std.meta.stringToEnum(evt.EventKind, kind_segment["kind:".len..]) orelse return null;│ │1563 - if (event_id.len == 0 or segments.next() != null) return null; │ │1564 - return repoEventsRoute(pair, .active, kind, event_id); │ │1572 + if (event_id.len == 0) return null; │ │1573 + params.scanPairs(&segments) catch return null; │ │1574 + if (!params.only(&.{.moment}) or segments.next() != null) return null; │ │1575 + return repoEventsRoute(pair, .active, kind, event_id, params.moment() catch return null); │ │1576 } │ │1577 if (std.mem.eql(u8, tab, files_seg)) { │ │1578 params.scanPairs(&segments) catch return null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1701 .repo_undo => |a_u| std.mem.eql(u8, a_u.name.slice(), b.repo_undo.name.slice()) and a_u.index == b.repo_undo.index,│ │1702 .repo_events => |a_e| std.mem.eql(u8, a_e.name.slice(), b.repo_events.name.slice()) and │ │1703 a_e.view == b.repo_events.view and │ │1704 + a_e.moment == b.repo_events.moment and │ │1705 a_e.kind == b.repo_events.kind and │ │1706 std.mem.eql(u8, a_e.selected.slice(), b.repo_events.selected.slice()), │ │1707 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │2117 form_feedback: ?FormFeedback = null, │ │2118 // a transient local event-sync error │ │2119 sync_failure: ?[]const u8 = null, │ │2120 + // a failed terminal undo retained on the current tab│ │2121 + undo_failure: ?[]const u8 = null, │ │2122 current_page: RoutablePage = .default, │ │2123 // whether to render the ANSI art backdrop │ │2124 enable_ansi: bool = true, │ ╰───────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯