╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯