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