╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1055 if (route.parent() != current.parent() or RoutablePage.repoPageChanged(route, current) or RoutablePage.homePageChanged(route, current) or RoutablePage.userPageChanged(route, current)) return route;│ │1056 return null; │ │1057 } │ │1058 + │ │1059 +// a display author, resolved against the admin db at read time │ │1060 +pub const Author = union(enum) { │ │1061 + unknown, │ │1062 + email: []const u8, // an email no user matches │ │1063 + user_name: []const u8, │ │1064 │ │1065 + // the commit author line's email, resolved like initFromDb │ │1066 + pub fn init( │ │1067 + haxy_moment: ?evt.AdminDB.HashMap(.read_only), │ │1068 + arena: *std.heap.ArenaAllocator, │ │1069 + author_line: []const u8, │ │1070 + ) !Author { │ │1071 + return initFromDb(haxy_moment, arena, evt.authorEmail(author_line)); │ │1072 + } │ │1073 + │ │1074 + // from a record's stored author email: it resolves to the user currently │ │1075 + // holding it, else stays the email. the result is allocated in `arena`, │ │1076 + // so the email may borrow transient memory. │ │1077 + pub fn initFromDb( │ │1078 + haxy_moment: ?evt.AdminDB.HashMap(.read_only), │ │1079 + arena: *std.heap.ArenaAllocator, │ │1080 + author_email: ?[]const u8, │ │1081 + ) !Author { │ │1082 + const email = author_email orelse return .unknown; │ │1083 + if (haxy_moment) |moment| { │ │1084 + if (try evt.User.readByEmail(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, email)) |user| { │ │1085 + return .{ .user_name = user.name }; │ │1086 + } │ │1087 + } │ │1088 + return .{ .email = try arena.allocator().dupe(u8, email) }; │ │1089 + } │ │1090 +}; │ │1091 + │ │1092 +// the "a:" link to user `name`'s page. │ │1093 +pub fn userLink(page_arena: *std.heap.ArenaAllocator, name: []const u8) ![]const u8 { │ │1094 + const parsed = RoutablePage.Array(evt.User.name_max_len).from(name) orelse return error.RouteTooLong; │ │1095 + const url = try (RoutablePage{ .user_repos = .{ .name = parsed } }).toUrl(page_arena); │ │1096 + return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │1097 +} │ │1098 + │ │1099 +// a focusable " author " box showing `author`, linking to their user page │ │1100 +// when it is a known user │ │1101 +pub fn authorBox(allocator: std.mem.Allocator, page_arena: *std.heap.ArenaAllocator, author: Author) !wgt.TextBox(Widget) { │ │1102 + const text = switch (author) { │ │1103 + .unknown => "", │ │1104 + .email, .user_name => |t| t, │ │1105 + }; │ │1106 + var tb = try wgt.TextBox(Widget).init(allocator, text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none, .label = " author " }); │ │1107 + errdefer tb.deinit(allocator); │ │1108 + tb.getFocus().focusable = true; │ │1109 + switch (author) { │ │1110 + .user_name => |name| tb.getFocus().kind = .{ .custom = try userLink(page_arena, name) }, │ │1111 + else => {}, │ │1112 + } │ │1113 + return tb; │ │1114 +} │ │1115 + │ │1116 // native-TUI navigation │ │1117 pub const Nav = struct { │ │1118 root: Widget, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │176 if (session.local != null) try evt.consume(repo_kind, opened.self_repo_opts, io, gpa, opened, evt.events_ref, &.{}); │ │177 break :blk .{ │ │178 try Files.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_line), │ │179 - try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, requested_ref_or_oid, requested_ref_value, commits_content), │ │179 + try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, session.haxy_moment, repo_identity.identity, requested_ref_or_oid, requested_ref_value, commits_content),│ │180 try Refs.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, refs_kind, refs_from), │ │181 - try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, repo_identity.identity, issues_tag, issues_selected, issues_view), │ │181 + try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_tag, issues_selected, issues_view), │ │182 }; │ │183 }, │ │184 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │37 message: []const u8, // trimmed, may be multi-line │ │38 // whether `message` is only the first line of a message too big to read.│ │39 message_truncated: bool = false, │ │40 + author: ui.Author = .unknown, │ │41 hunks: []const Hunk, │ │42 // the hunk index this window starts at (0 = the first window). │ │43 window_start: usize, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │85 repo: *rp.Repo(repo_kind, repo_opts), │ │86 io: std.Io, │ │87 gpa: std.mem.Allocator, │ │88 + // the admin db's moment, for resolving author emails to user names (null│ │89 + // in local mode, which has no users) │ │90 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │91 identity: []const u8, │ │92 requested_ref_or_oid: ?ui.RoutablePage.RefOrOid, │ │93 requested_value: []const u8, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │143 .date = try formatDate(aa, md.timestamp), │ │144 .message = text, │ │145 .message_truncated = truncated, │ │146 + .author = try ui.Author.init(admin_moment, arena, md.author orelse ""),│ │147 .hunks = &.{}, │ │148 .window_start = 0, │ │149 .has_prev = false, │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │705 try self.addNavLink(allocator, inner, "← all files", commit.oid, 0, ""); │ │706 } else { │ │707 try self.addMessageBox(allocator, inner, commit, .preview); │ │708 + if (commit.author != .unknown) { │ │709 + var tb = try ui.authorBox(allocator, self.session.page_arena, commit.author); │ │710 + errdefer tb.deinit(allocator); │ │711 + try inner.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null });│ │712 + } │ │713 try self.addViewFilesLink(allocator, inner, commit.oid); │ │714 } │ │715 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────╮ │30 pub const IssueWithId = struct { │ │31 id: []const u8, │ │32 issue: evt.Issue, │ │33 + // the creation commit's author│ │34 + author: ui.Author = .unknown, │ │35 }; │ │36 │ │37 // one status's windowed listing. │ ╰───────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │117 arena: *std.heap.ArenaAllocator, │ │118 repo: *rp.Repo(repo_kind, repo_opts), │ │119 io: std.Io, │ │120 + // the admin db's moment, for resolving author emails to user names (null│ │121 + // in local mode, which has no users) │ │122 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │123 identity: []const u8, │ │124 tag: []const u8, │ │125 selected_id: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │206 }; │ │207 } │ │208 │ │204 - const open_window = try loadWindow(repo_opts.hash, arena, event_id_to_issue, open_set, open_root); │ │205 - const closed_window = try loadWindow(repo_opts.hash, arena, event_id_to_issue, closed_set, closed_root); │ │209 + const open_window = try loadWindow(repo_opts.hash, arena, admin_moment, event_id_to_issue, open_set, open_root); │ │210 + const closed_window = try loadWindow(repo_opts.hash, arena, admin_moment, event_id_to_issue, closed_set, closed_root);│ │211 │ │212 // every tag in the repo, in the tag map's sorted order. the keys are │ │213 // "tag,status", so a tag's entries are adjacent and dedup by prefix. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │270 fn loadWindow( │ │271 comptime hash_kind: hash.HashKind, │ │272 arena: *std.heap.ArenaAllocator, │ │273 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │274 event_id_to_issue: evt.EventDB(hash_kind).HashMap(.read_only),│ │275 set_maybe: ?evt.EventDB(hash_kind).SortedSet(.read_only), │ │276 root_key: ?[]const u8, │ ╰───────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯