╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Commits.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
595 chosen = id; 596 if (!prefer_last) break; // first visible 597 } 598 if (chosen) |id| try root_focus.setFocus(id); 599 } 600 601 fn focusedIsLoadMore(self: *View, root_focus: *Focus) bool { 602 const id = self.load_more_id orelse return false; 603 return root_focus.grandchild_id == id; 604 } 605 606 // keep the diff scroll within its content, using the last build's grids. 607 fn clampDiffScroll(self: *View) void { 608 const sc = self.diffScroll(); 609 const vp = sc.grid orelse return; 610 const content = sc.child.box.grid orelse return; 611 const max_y: isize = if (content.size.height > vp.size.height) @intCast(content.size.height - vp.size.height) else 0; 612 const max_x: isize = if (content.size.width > vp.size.width) @intCast(content.size.width - vp.size.width) else 0; 613 sc.y = std.math.clamp(sc.y, 0, max_y); 614 sc.x = std.math.clamp(sc.x, 0, max_x); 615 } 616 617 // enter the diff pane by focusing the hunk that was focused last (the box's 618 // child_id remembers it across the trip to the commit list); fall back to the 619 // first hunk on a fresh diff. the host arrives here on right-arrow or Enter 620 // from the list. a diff with no hunks can't be entered. 621 fn focusDiff(self: *View, root_focus: *Focus) !void { 622 const inner = self.diffInner(); 623 if (inner.children.count() == 0) return; 624 const target = inner.getFocus().child_id orelse inner.children.keys()[0]; 625 try root_focus.setFocus(target); 626 if (root_focus.grandchild_id == target) return; 627 // the diff pane wasn't laid out last build (too narrow to show beside 628 // the list), so its hunks aren't in the focus tree yet. select the pane 629 // at the box level, and the hunk will be focused after the next build. 630 self.box.getFocus().child_id = self.diffOuter().getFocus().id; 631 } 632 633 fn focusList(self: *View, root_focus: *Focus) !void { 634 const lb = self.listBox(); 635 const id = lb.getFocus().child_id orelse (if (lb.children.count() > 0) lb.children.keys()[0] else return); 636 try root_focus.setFocus(id); 637 } 638 639 fn moveSelection(self: *View, root_focus: *Focus, delta: isize) !void { 640 const lb = self.listBox(); 641 const keys = lb.children.keys(); 642 if (keys.len == 0) return; 643 const cur_id = lb.getFocus().child_id orelse return; 644 const cur: isize = @intCast(lb.children.getIndex(cur_id) orelse return); 645 const last: isize = @intCast(keys.len - 1); 646 const next: usize = @intCast(std.math.clamp(cur + delta, 0, last)); 647 if (next == @as(usize, @intCast(cur))) return; 648 try root_focus.setFocus(keys[next]); 649 if (lb.children.values()[next].rect) |rect| self.listScroll().scrollToRect(rect); 650 } 651 652 // render the next budget of hunks for the displayed commit and append them. 653 // the TUI reopens the repo here; the wasm path has no repo and the web host 654 // drives "load more" via the GET endpoint instead, so the repo code is 655 // comptime-elided from the wasm build. 656 fn loadMore(self: *View, allocator: std.mem.Allocator, root_focus: *Focus) !void { 657 // the wasm build has no repo; it drives "load more" via the GET endpoint. 658 if (!builtin.cpu.arch.isWasm()) try self.loadMoreNative(allocator, root_focus); 659 } 660 661 fn loadMoreNative(self: *View, allocator: std.mem.Allocator, root_focus: *Focus) !void { 662 const io = self.session.io orelse return; 663 const repos_dir = self.session.repos_dir orelse return; 664 const sel = self.diffed_index orelse return; 665 const commit = self.data.commits[sel]; 666 if (commit.oid.len != xit.hash.hexLen(.sha1)) return; 667 668 const aa = self.session.page_arena.allocator(); 669 const gpa = self.session.page_arena.child_allocator; 670 const repo_path = try std.fs.path.join(aa, &.{ repos_dir, self.data.repo_dir_hex }); 671 var repo = rp.Repo(.xit, .{}).open(io, gpa, .{ .path = repo_path }) catch return; 672 defer repo.deinit(io, gpa); 673 674 var oid: [xit.hash.hexLen(.sha1)]u8 = undefined; 675 @memcpy(&oid, commit.oid); 676 const rendered = renderCommitDiff(io, gpa, aa, &repo, oid, self.shown_hunks, diff_budget) catch return; 677 678 const inner = self.diffInner(); 679 if (self.load_more_id) |id| { 680 if (inner.children.fetchOrderedRemove(id)) |kv| { 681 var w = kv.value.widget; 682 w.deinit(allocator); 683 } 684 self.load_more_id = null; 685 } 686 // focus the last already-shown hunk (the one just above where "load 687 // more" was). it's already in the focus tree from the last build, so the 688 // focus lands; the freshly-appended hunks aren't in the tree until the 689 // next build, so focusing one here would silently no-op. the user scrolls 690 // down into the new hunks from here. 691 const shown = inner.children.count(); // old hunk count ("load more" removed) 692 for (rendered.hunks) |hunk| try self.addHunkBox(allocator, inner, hunk.lines); 693 self.shown_hunks = rendered.shown_hunks; 694 self.has_more = rendered.has_more; 695 if (rendered.has_more) try self.addLoadMore(allocator, inner, commit.oid, rendered.shown_hunks); 696 if (shown > 0) try root_focus.setFocus(inner.children.keys()[shown - 1]); 697 } 698 699 pub fn clearGrid(self: *View) void { 700 self.box.clearGrid(); 701 } 702 703 pub fn getGrid(self: View) ?Grid { 704 return self.box.getGrid(); 705 } 706 707 pub fn getFocus(self: *View) *Focus { 708 return self.box.getFocus(); 709 } 710 711 // for the parent's "scroll up at the top jumps to the header" check. in the 712 // diff pane that means the first hunk is focused and the diff can't scroll up 713 // any further; otherwise up-arrow stays in the pane (scrolling or moving 714 // between hunks). when the list holds focus, report its selection directly. 715 pub fn getSelectedIndex(self: *View) ?usize { 716 if (self.diffActive()) { 717 const inner = self.diffInner(); 718 const cid = inner.getFocus().child_id orelse return 1; 719 const first_focused = inner.children.count() > 0 and cid == inner.children.keys()[0]; 720 return if (first_focused and self.diffScroll().y == 0) 0 else 1; 721 } 722 const lb = self.listBox(); 723 const cid = lb.getFocus().child_id orelse return null; 724 return lb.children.getIndex(cid); 725 } 726 }; 727 728 // the "a:" link for the commits page starting at `start_oid` within `identity`. 729 fn commitsPageLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, start_oid: []const u8) ![]const u8 { 730 const route = ui.RoutablePage.repoCommitsRoute(identity, start_oid) orelse return error.RouteTooLong; 731 const url = try route.urlAlloc(page_arena); 732 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); 733 } 734