╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │788 // to `path` ("" = every file). │ │789 fn commitsLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, oid: []const u8, start: usize, path: []const u8) ![]const u8 {│ │790 const route = data.location.commitsRoute(.object, oid, start, path, data.base_oid) orelse return error.RouteTooLong; │ │791 + const url = try route.toUrl(page_arena); │ │792 + return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │793 +} │ │794 + │ │795 +// this list's page starting at `oid`: a repo's branch or tag keeps its ref (and │ │796 +// its query) and moves `from` along, so the search box survives paging. a fork │ │797 +// or an object view roots the page at the commit itself. │ │798 +fn pageRoute(data: *const Self, oid: []const u8) ?ui.RoutablePage { │ │799 + if (data.location != .repo or data.ref_or_oid == .object) { │ │800 + return data.location.commitsRoute(.object, oid, 0, "", data.base_oid); │ │801 + } │ │802 + const route = data.location.commitsRoute(data.ref_or_oid, data.ref_or_oid_value, 0, "", data.base_oid) orelse return null; │ │803 + const paged = route.withFrom(oid) orelse return null; │ │804 + return paged.withSearch(data.search orelse return paged); │ │805 +} │ │806 + │ │807 +fn nextPageLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, oid: []const u8) ![]const u8 { │ │808 + const route = pageRoute(data, oid) orelse return error.RouteTooLong; │ │809 const url = try route.toUrl(page_arena); │ │810 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │811 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │821 // the in-page "ai:" anchor for selecting a commit with its current pane content │ │822 // and window. the href is only followed with js off. │ │823 fn commitRowLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, commit: Commit, content: Content) ![]const u8 {│ │824 + // a result roots the same results page at its commit, so the query │ │825 + // survives following the row │ │826 + if (data.search != null) { │ │827 + const route = pageRoute(data, commit.oid) orelse return error.RouteTooLong; │ │828 + return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{try route.toUrl(page_arena)}); │ │829 + } │ │830 const route = (switch (content) { │ │831 .message => data.location.commitMessageRoute(.object, commit.oid, data.base_oid), │ │832 .diff => |diff| data.location.commitsRoute(.object, commit.oid, commit.window.start, diff.path, data.base_oid), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │841 const url = try route.toUrl(page_arena); │ │842 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │843 } │ │730 - │ │731 -// the clone url shown above the log. │ │732 -pub const Header = struct { │ │733 - pub const View = struct { │ │734 - box: wgt.Box(ui.Widget), │ │735 - │ │736 - pub fn init(allocator: std.mem.Allocator, session: *ui.Session, location: ui.RoutablePage.RepoLocation) !Header.View { │ │737 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │738 - errdefer box.deinit(allocator); │ │844 │ │740 - // the spacer pushes the clone url to the right edge. │ │741 - { │ │742 - var spacer = try ui.widget.Spacer.init(allocator); │ │743 - errdefer spacer.deinit(allocator); │ │744 - try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │745 - } │ │746 - │ │747 - if (session.data.host_kind == .server) { │ │748 - if (try ui.widget.CopyableText.initClone(allocator, session, location)) |value| { │ │749 - var clone_url = value; │ │750 - errdefer clone_url.deinit(allocator); │ │751 - const min_width = clone_url.minWidth(); │ │752 - box.getFocus().child_id = clone_url.getFocus().id; │ │753 - try box.children.put(allocator, clone_url.getFocus().id, .{ .widget = .{ .copyable_text = clone_url }, .rect = null, .min_size = .{ .width = min_width, .height = 3 }, .max_size = .{ .width = min_width, .height = 3 } });│ │754 - } │ │755 - } │ │756 - │ │757 - return .{ .box = box }; │ │758 - } │ │759 - │ │760 - pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { │ │761 - self.box.deinit(allocator); │ │762 - } │ │763 - │ │764 - pub fn build(self: *Header.View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │765 - self.clearGrid(); │ │766 - try self.box.build(allocator, constraint, root_focus); │ │767 - } │ │768 - │ │769 - pub fn input(self: *Header.View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │770 - const clone_url = self.cloneUrl() orelse return; │ │771 - try clone_url.input(allocator, key, root_focus); │ │772 - } │ │773 - │ │774 - pub fn focusCloneUrl(self: *Header.View, root_focus: *Focus) bool { │ │775 - const clone_url = self.cloneUrl() orelse return false; │ │776 - root_focus.setFocus(clone_url.getFocus().id); │ │777 - return true; │ │778 - } │ │779 - │ │780 - pub fn hasCloneUrl(self: *Header.View) bool { │ │781 - return self.cloneUrl() != null; │ │782 - } │ │783 - │ │784 - fn cloneUrl(self: *Header.View) ?*ui.widget.CopyableText { │ │785 - for (self.box.children.values()) |*child| switch (child.widget) { │ │786 - .copyable_text => |*copyable_text| return copyable_text, │ │787 - else => {}, │ │788 - }; │ │789 - return null; │ │790 - } │ │791 - │ │792 - pub fn clearGrid(self: *Header.View) void { │ │793 - self.box.clearGrid(); │ │794 - } │ │795 - │ │796 - pub fn getGrid(self: Header.View) ?Grid { │ │797 - return self.box.getGrid(); │ │798 - } │ │799 - │ │800 - pub fn getFocus(self: *Header.View) *Focus { │ │801 - return self.box.getFocus(); │ │802 - } │ │803 - }; │ │804 -}; │ │805 - │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │354 │ │355 // the search box and the clone url at the top. │ │356 { │ │357 - var header_view = try Header.View.init(allocator, data, session); │ │357 + var header_view = try ui.widget.SearchHeader.init(allocator, session, data.location, " find file ", "find", data.find, data.find_available); │ │358 errdefer header_view.deinit(allocator); │ │359 - try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_files_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │359 + try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .search_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │360 } │ │361 │ │362 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │504 return &self.box.children.values()[content_index].widget.box; │ │505 } │ │506 │ │507 - fn header(self: *View) *Header.View { │ │508 - return &self.box.children.values()[header_index].widget.repo_files_header;│ │507 + fn header(self: *View) *ui.widget.SearchHeader { │ │508 + return &self.box.children.values()[header_index].widget.search_header; │ │509 } │ │510 │ │511 fn headerActive(self: *View) bool { │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │701 if (self.headerActive()) { │ │702 if (key == .arrow_down) { │ │703 root_focus.setFocus(self.contentBox().getFocus().id); │ │704 - } else { │ │705 - try self.header().input(allocator, key, root_focus); │ │704 + return; │ │705 } │ │707 - return; │ │706 + if (key == .enter) if (try self.header().submittedText(allocator)) |term| { │ │707 + defer allocator.free(term); │ │708 + return self.submit(term); │ │709 + }; │ │710 + return self.header().input(allocator, key, root_focus); │ │711 } │ │712 if (key == .arrow_up and self.contentAtTop() and self.header().focusHeader(root_focus)) return;│ │713 if (self.detailActive()) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │715 } else { │ │716 try self.listInput(key, root_focus); │ │717 } │ │718 + } │ │719 + │ │720 + // navigate to the typed term's results, pinned to the listing's ref. an│ │721 + // empty term leaves the results for the plain listing. │ │722 + fn submit(self: *View, term: []const u8) !void { │ │723 + if (term.len == 0 and self.data.find == null) return; │ │724 + const route = self.data.filesRoute("", 0) orelse return; │ │725 + try self.session.navigate(route.withFind(term) orelse return); │ │726 } │ │727 │ │728 fn listInput(self: *View, key: Key, root_focus: *Focus) !void { │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │886 │ │887 // only the header's own row sits directly below the repository header. │ │888 pub fn atTop(self: *View) bool { │ │878 - return self.headerActive() or (!self.header().hasHeaderFocus() and self.contentAtTop());│ │889 + return self.headerActive() or (!self.header().hasFocusable() and self.contentAtTop()); │ │890 } │ │891 │ │892 pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 992 } │ │ 993 return std.fmt.allocPrint(aa, " " ++ ellipsis ++ "{s} ", .{dir[start..]}); │ │ 994 } │ │ 984 - │ │ 985 -// the search box and the clone url shown above the listing. │ │ 986 -pub const Header = struct { │ │ 987 - pub const View = struct { │ │ 988 - box: wgt.Box(ui.Widget), │ │ 989 - data: *const Self, │ │ 990 - session: *ui.Session, │ │ 991 - │ │ 992 - pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !Header.View { │ │ 993 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │ 994 - errdefer box.deinit(allocator); │ │ 995 - │ │ 996 - // the search box only appears where an index can serve it. │ │ 997 - if (data.find_available) { │ │ 998 - var text_input = try wgt.TextInput.init(allocator, .{ .label = " find file ", .name = "find", .rounded_corners = true, .visible_width = 18, .render_content = session.is_terminal }); │ │ 999 - errdefer text_input.deinit(allocator); │ │1000 - text_input.getFocus().mode = .all; │ │1001 - if (data.find) |term| try text_input.setContent(allocator, term); │ │1002 - box.getFocus().child_id = text_input.getFocus().id; │ │1003 - try box.children.put(allocator, text_input.getFocus().id, .{ .widget = .{ .text_input = text_input }, .rect = null, .min_size = .{ .width = 20, .height = 3 } }); │ │1004 - } │ │1005 - │ │1006 - // the spacer pushes the clone url to the right edge. │ │1007 - { │ │1008 - var spacer = try ui.widget.Spacer.init(allocator); │ │1009 - errdefer spacer.deinit(allocator); │ │1010 - try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1011 - } │ │1012 - │ │1013 - if (session.data.host_kind == .server) { │ │1014 - if (try ui.widget.CopyableText.initClone(allocator, session, data.location)) |value| { │ │1015 - var clone_url = value; │ │1016 - errdefer clone_url.deinit(allocator); │ │1017 - const min_width = clone_url.minWidth(); │ │1018 - if (box.getFocus().child_id == null) box.getFocus().child_id = clone_url.getFocus().id; │ │1019 - try box.children.put(allocator, clone_url.getFocus().id, .{ .widget = .{ .copyable_text = clone_url }, .rect = null, .min_size = .{ .width = min_width, .height = 3 }, .max_size = .{ .width = min_width, .height = 3 } });│ │1020 - } │ │1021 - } │ │1022 - │ │1023 - return .{ .box = box, .data = data, .session = session }; │ │1024 - } │ │ 995 │ │1026 - pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { │ │1027 - self.box.deinit(allocator); │ │1028 - } │ │1029 - │ │1030 - pub fn build(self: *Header.View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │1031 - self.clearGrid(); │ │1032 - if (self.findInput()) |text_input| { │ │1033 - text_input.options.bottom_label = if (root_focus.grandchild_id == text_input.getFocus().id) " press enter " else ""; │ │1034 - } │ │1035 - try self.box.build(allocator, constraint, root_focus); │ │1036 - if (!self.session.is_terminal) { │ │1037 - if (self.findInput()) |text_input| { │ │1038 - try self.session.text_inputs.put(self.session.arena.allocator(), text_input.getFocus().id, text_input); │ │1039 - } │ │1040 - } │ │1041 - } │ │1042 - │ │1043 - pub fn input(self: *Header.View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │1044 - const current = self.box.getFocus().child_id orelse return; │ │1045 - if (self.findInput()) |text_input| { │ │1046 - if (current == text_input.getFocus().id) { │ │1047 - switch (key) { │ │1048 - .enter => return self.submit(allocator, text_input), │ │1049 - .arrow_right => if (text_input.cursor == text_input.content.items.len) { │ │1050 - if (self.cloneUrl()) |clone_url| { │ │1051 - root_focus.setFocus(clone_url.getFocus().id); │ │1052 - return; │ │1053 - } │ │1054 - }, │ │1055 - else => {}, │ │1056 - } │ │1057 - return text_input.input(allocator, key, root_focus); │ │1058 - } │ │1059 - } │ │1060 - const clone_url = self.cloneUrl() orelse return; │ │1061 - if (key == .arrow_left and clone_url.selected == 0) { │ │1062 - if (self.findInput()) |text_input| { │ │1063 - root_focus.setFocus(text_input.getFocus().id); │ │1064 - return; │ │1065 - } │ │1066 - } │ │1067 - try clone_url.input(allocator, key, root_focus); │ │1068 - } │ │1069 - │ │1070 - // navigate to the typed term's results, pinned to the listing's ref. an │ │1071 - // empty term leaves the results for the plain listing. │ │1072 - fn submit(self: *Header.View, allocator: std.mem.Allocator, text_input: *wgt.TextInput) !void { │ │1073 - const term = try text_input.text(allocator); │ │1074 - defer allocator.free(term); │ │1075 - if (term.len == 0 and self.data.find == null) return; │ │1076 - const route = self.data.filesRoute("", 0) orelse return; │ │1077 - try self.session.navigate(route.withFind(term) orelse return); │ │1078 - } │ │1079 - │ │1080 - // enter the header, preferring the search box. │ │1081 - pub fn focusHeader(self: *Header.View, root_focus: *Focus) bool { │ │1082 - if (self.findInput()) |text_input| { │ │1083 - root_focus.setFocus(text_input.getFocus().id); │ │1084 - return true; │ │1085 - } │ │1086 - const clone_url = self.cloneUrl() orelse return false; │ │1087 - root_focus.setFocus(clone_url.getFocus().id); │ │1088 - return true; │ │1089 - } │ │1090 - │ │1091 - pub fn hasHeaderFocus(self: *Header.View) bool { │ │1092 - return self.cloneUrl() != null or self.findInput() != null; │ │1093 - } │ │1094 - │ │1095 - fn cloneUrl(self: *Header.View) ?*ui.widget.CopyableText { │ │1096 - for (self.box.children.values()) |*child| switch (child.widget) { │ │1097 - .copyable_text => |*copyable_text| return copyable_text, │ │1098 - else => {}, │ │1099 - }; │ │1100 - return null; │ │1101 - } │ │1102 - │ │1103 - fn findInput(self: *Header.View) ?*wgt.TextInput { │ │1104 - for (self.box.children.values()) |*child| switch (child.widget) { │ │1105 - .text_input => |*text_input| return text_input, │ │1106 - else => {}, │ │1107 - }; │ │1108 - return null; │ │1109 - } │ │1110 - │ │1111 - pub fn clearGrid(self: *Header.View) void { │ │1112 - self.box.clearGrid(); │ │1113 - } │ │1114 - │ │1115 - pub fn getGrid(self: Header.View) ?Grid { │ │1116 - return self.box.getGrid(); │ │1117 - } │ │1118 - │ │1119 - pub fn getFocus(self: *Header.View) *Focus { │ │1120 - return self.box.getFocus(); │ │1121 - } │ │1122 - }; │ │1123 -}; │ │1124 - │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────╮ │38 user_header: ui.User.Header.View, │ │39 repo_header: ui.Repo.Header.View, │ │40 fork_header: ui.Fork.Header.View, │ │41 - repo_files_header: ui.Repo.Files.Header.View, │ │42 - repo_commits_header: ui.Repo.Commits.Header.View, │ │41 + search_header: SearchHeader, │ │42 repo_issues_header: ui.Repo.Issues.Header, │ │43 repo_patches_header: ui.Repo.Patches.Header, │ │44 repo_discussions_header: ui.Repo.Discussions.Header,│ ╰────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯