╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────╮ │2 const evt = @import("../event.zig");│ │3 const ui = @import("../ui.zig"); │ │4 const xit = @import("xit"); │ │5 +const rp = xit.repo; │ │6 const hash = xit.hash; │ │7 const xitui = xit.xitui; │ │8 const wgt = xitui.widget; │ ╰───────────────────────────────────────╯ ╭────────────────────────────────────────────────────╮ │11 const Grid = xitui.grid.Grid; │ │12 const Focus = xitui.focus.Focus; │ │13 const inp = @import("./input.zig"); │ │14 +const fork = @import("../fork.zig"); │ │15 │ │16 pub const Header = @import("./User/Header.zig");│ │17 pub const Settings = @import("./Settings.zig"); │ ╰────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │19 pub const Quit = @import("./Quit.zig"); │ │20 │ │21 pub const page_size = 20; // how many repos one window of the repos tab shows│ │22 + │ │23 +pub const ForkItem = struct { │ │24 + target: []const u8, │ │25 + title: []const u8, │ │26 +}; │ │27 │ │28 header: Header, │ │29 user: evt.User.Public, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │30 repos: []const evt.Repo.Record, │ │31 repos_start: usize, // the repos window this page was built with, mirrored into the url│ │32 repos_next_start: ?usize, // the `start` for the "next" row, or null on the last window│ │33 +forks: []const ForkItem, │ │34 +forks_start: usize, │ │35 +forks_next_start: ?usize, │ │36 settings: Settings, │ │37 auth: Auth, │ │38 quit: Quit, │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │42 │ │43 pub fn init( │ │44 arena: *std.heap.ArenaAllocator, │ │45 + session: *ui.Session, │ │46 haxy_moment: evt.AdminDB.HashMap(.read_only), │ │47 name: ui.RoutablePage.Array(evt.User.name_max_len),│ │37 - start: usize, │ │48 + repos_start: usize, │ │49 + forks_start: usize, │ │50 ) !Self { │ │51 const DB = evt.AdminDB; │ │52 const hash_kind = evt.admin_repo_opts.hash; │ ╰───────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │79 │ │80 // read the window [start, start+page_size) with one seek to the start│ │81 // rank, then a sequential walk. │ │70 - const end = @min(start + page_size, count); │ │71 - var repos_iter = try user_repos.iteratorFromIndex(start); │ │72 - var i = start; │ │82 + const end = @min(repos_start + page_size, count); │ │83 + var repos_iter = try user_repos.iteratorFromIndex(repos_start); │ │84 + var i = repos_start; │ │85 while (i < end) : (i += 1) { │ │86 var kv_cursor = (try repos_iter.next()) orelse break; │ │87 const kv = try kv_cursor.readKeyValuePair(); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 98 repos_next_start = if (end < count) end else null; │ │ 99 } │ │100 } │ │101 + │ │102 + var forks: std.ArrayList(ForkItem) = .empty; │ │103 + var forks_next_start: ?usize = null; │ │104 + if (try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Fork.user_id_to_fork_id_set_key))) |by_user_cursor| { │ │105 + const by_user = try DB.HashMap(.read_only).init(by_user_cursor); │ │106 + if (try by_user.getCursor(hash.hashInt(hash_kind, user_id))) |user_forks_cursor| { │ │107 + const user_forks = try DB.SortedSet(.read_only).init(user_forks_cursor); │ │108 + const repos_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Repo.record_map_key)) orelse return error.NotFound; │ │109 + const repo_records = try DB.HashMap(.read_only).init(repos_cursor); │ │110 + const count = try user_forks.count(); │ │111 + const end = @min(forks_start + page_size, count); │ │112 + var iter = try user_forks.iteratorFromIndex(forks_start); │ │113 + var i = forks_start; │ │114 + while (i < end) : (i += 1) { │ │115 + const cursor = (try iter.next()) orelse break; │ │116 + var kv_cursor = cursor; │ │117 + const kv = try kv_cursor.readKeyValuePair(); │ │118 + var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │119 + _ = try kv.key_cursor.readBytes(&order_key); │ │120 + const fork_id = order_key[@sizeOf(u64)..]; │ │121 + const record = (try evt.Fork.readById(DB, hash_kind, haxy_moment, arena, fork_id)) orelse continue; │ │122 │ │123 + const repo_cursor = try repo_records.getCursor(hash.hashInt(hash_kind, record.event.repo_id)) orelse continue; │ │124 + const target_repo = try evt.read(evt.Repo.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(repo_cursor)); │ │125 + const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, target_repo.event.user_id)) orelse continue; │ │126 + const target = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ owner.event.name, target_repo.event.name }); │ │127 + │ │128 + var title: []const u8 = "(unavailable)"; │ │129 + if (session.io) |io| if (session.repos_dir) |repos_dir| { │ │130 + const id_hex = std.fmt.bytesToHex(fork_id.*, .lower); │ │131 + const path = try fork.forkPath(arena.allocator(), repos_dir, &id_hex); │ │132 + if (rp.Repo(.xit, .{}).open(io, arena.child_allocator, .{ .path = path, .require_repo_root = true })) |opened| { │ │133 + var fork_repo = opened; │ │134 + defer fork_repo.deinit(io, arena.child_allocator); │ │135 + if (evt.currentMoment(.{}, &fork_repo)) |moment| { │ │136 + if (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, arena, fork_id)) |patch| title = patch.event.title;│ │137 + } else |_| {} │ │138 + } else |_| {} │ │139 + }; │ │140 + try forks.append(arena.allocator(), .{ .target = target, .title = title }); │ │141 + } │ │142 + forks_next_start = if (end < count) end else null; │ │143 + } │ │144 + } │ │145 + │ │146 return .{ │ │147 .header = try Header.init(arena, user.event.name), │ │148 .user = evt.project(evt.User.Public, user.event), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │149 .repos = repos.items, │ │ 94 - .repos_start = start, │ │150 + .repos_start = repos_start, │ │151 .repos_next_start = repos_next_start,│ │152 + .forks = forks.items, │ │153 + .forks_start = forks_start, │ │154 + .forks_next_start = forks_next_start,│ │155 .settings = Settings.init(), │ │156 .auth = Auth.init(), │ │157 .quit = Quit.init(), │ ╰──────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │212 try stack.children.put(allocator, list.getFocus().id, .{ .flow_box_scroll = list }); │ │213 } │ │214 │ │215 + // forks list │ │216 + { │ │217 + var list = try ui.FlowBox.Scroll.init(allocator, .{}, !session.is_terminal); │ │218 + errdefer list.deinit(allocator); │ │219 + │ │220 + var item_arena = std.heap.ArenaAllocator.init(allocator); │ │221 + defer item_arena.deinit(); │ │222 + const aa = item_arena.allocator(); │ │223 + var items: std.ArrayList(ui.FlowBox.Item) = .empty; │ │224 + if (data.forks_start > 0) │ │225 + try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/user/{s}/forks/start:{d}", .{ data.user.name, data.forks_start -| page_size }) });│ │226 + for (data.forks) |fork_item| │ │227 + try items.append(aa, .{ .text = try std.fmt.allocPrint(aa, "{s}\n{s}", .{ fork_item.target, fork_item.title }), .link = "" }); │ │228 + if (data.forks_next_start) |next_start| │ │229 + try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/user/{s}/forks/start:{d}", .{ data.user.name, next_start }) }); │ │230 + try list.setItems(allocator, items.items); │ │231 + try stack.children.put(allocator, list.getFocus().id, .{ .flow_box_scroll = list }); │ │232 + } │ │233 + │ │234 // the header has no settings tab without a login, so keep the │ │235 // stack's children 1:1 with the tabs by skipping the view too │ │236 if (session.data.user_id != null) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │275 if (header.getSelectedIndex()) |index| { │ │276 stack.getFocus().child_id = stack.children.keys()[index]; │ │277 const name = self.data.route_name; │ │200 - switch (std.meta.activeTag(stack.children.values()[index])) { │ │201 - .home_settings => self.session.data.current_page = .{ .user_settings = name }, │ │202 - .home_auth => self.session.data.current_page = .{ .user_auth = name }, │ │203 - // the quit tab is tty-only and not a route, so leave current_page │ │204 - // alone (nothing to mirror into the url). │ │205 - .quit => {}, │ │206 - // the repos list (the default tab) │ │207 - else => self.session.data.current_page = .{ .user_repos = .{ .name = name, .start = self.data.repos_start } },│ │278 + switch (index) { │ │279 + 0 => self.session.data.current_page = .{ .user_repos = .{ .name = name, .start = self.data.repos_start } }, │ │280 + 1 => self.session.data.current_page = .{ .user_forks = .{ .name = name, .start = self.data.forks_start } }, │ │281 + else => switch (std.meta.activeTag(stack.children.values()[index])) { │ │282 + .home_settings => self.session.data.current_page = .{ .user_settings = name }, │ │283 + .home_auth => self.session.data.current_page = .{ .user_auth = name }, │ │284 + // the quit tab is tty-only and not a route, so leave current_page │ │285 + // alone (nothing to mirror into the url). │ │286 + .quit => {}, │ │287 + else => {}, │ │288 + }, │ │289 } │ │290 } │ │291 try self.box.build(allocator, constraint, root_focus); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯