╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │308 - const entry_idx = selectedFileIndex(data) orelse readmeIndex(data.entries); │ │404 + const entry_idx = selectedFileIndex(data) orelse if (data.find == null) readmeIndex(data.entries) else null;│ │405 sel = if (entry_idx) |i| i + base else 0; │ │406 list_box.getFocus().child_id = list_box.children.keys()[sel]; │ │407 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │469 box.getFocus().child_id = box.children.keys()[list_index]; │ │470 try outer.children.put(allocator, box.getFocus().id, .{ .widget = .{ .box = box }, .rect = null, .min_size = null });│ │471 │ │376 - // focus lives in the split; the banner isn't focusable. │ │377 - outer.getFocus().child_id = outer.children.keys()[content_index]; │ │472 + // focus lives in the split, except that search results start in the │ │473 + // search box so the term can be refined right away. │ │474 + outer.getFocus().child_id = outer.children.keys()[if (data.find != null) header_index else content_index]; │ │475 return .{ .box = outer, .data = data, .session = session, .shown_index = null }; │ │476 } │ │477 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │381 - fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, link: []const u8) !void { │ │382 - var row = try wgt.TextBox.init(allocator, label, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none }); │ │478 + fn addRow(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), label: []const u8, bottom_label: []const u8, link: []const u8) !void { │ │479 + var row = try wgt.TextBox.init(allocator, label, .{ .border_style = .hidden, .rounded_corners = true, .wrap_kind = .none, .bottom_label = bottom_label });│ │480 errdefer row.deinit(allocator); │ │481 row.getFocus().mode = .all; │ │482 if (link.len != 0) row.getFocus().kind = .{ .custom = link }; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │706 } │ │707 return; │ │708 } │ │612 - if (key == .arrow_up and self.contentAtTop() and self.header().focusCloneUrl(root_focus)) return;│ │709 + if (key == .arrow_up and self.contentAtTop() and self.header().focusHeader(root_focus)) return; │ │710 if (self.detailActive()) { │ │711 try self.detailInput(key, root_focus); │ │712 } else { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │873 return lb.children.getIndex(cid) == 0; │ │874 } │ │875 │ │779 - // only the clone-url row sits directly below the repository header. │ │876 + // only the header's own row sits directly below the repository header. │ │877 pub fn atTop(self: *View) bool { │ │781 - return self.headerActive() or (!self.header().hasCloneUrl() and self.contentAtTop()); │ │878 + return self.headerActive() or (!self.header().hasHeaderFocus() and self.contentAtTop());│ │879 } │ │880 │ │784 - pub fn focusCloneUrl(self: *View, root_focus: *Focus) bool { │ │785 - return self.header().focusCloneUrl(root_focus); │ │881 + pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ │882 + return self.header().focusHeader(root_focus); │ │883 } │ │884 }; │ │885 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 961 return dir[0..slash]; │ │ 962 } │ │ 963 │ │ 867 -// the "<ref_or_oid> <value>" banner shown above the listing. │ │ 868 -pub const Header = struct { │ │ 869 - content: []const u8, │ │ 964 +// a search result's bottom label: the directory it sits in, empty at the root. │ │ 965 +// a directory too wide for the list column is clipped from the left. │ │ 966 +fn resultDirLabel(aa: std.mem.Allocator, path: []const u8) ![]const u8 { │ │ 967 + const slash = std.mem.lastIndexOfScalar(u8, path, '/') orelse return ""; │ │ 968 + const dir = path[0..slash]; │ │ 969 + // the row's border and the label's padding take two columns each │ │ 970 + const max_width = View.list_max_width - 4; │ │ 971 + var width = try xitui.width.displayWidth(dir); │ │ 972 + if (width <= max_width) return std.fmt.allocPrint(aa, " {s} ", .{dir}); │ │ 973 │ │ 871 - // `value` arrives url-encoded, so decode it for display. │ │ 872 - pub fn init(aa: std.mem.Allocator, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Header { │ │ 873 - const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, value)); │ │ 874 - return .{ │ │ 875 - .content = try std.fmt.allocPrint(aa, "{s} {s}", .{ @tagName(ref_or_oid), decoded }), │ │ 876 - }; │ │ 974 + const ellipsis = ".."; │ │ 975 + var iter = (try std.unicode.Utf8View.init(dir)).iterator(); │ │ 976 + var start: usize = 0; │ │ 977 + while (width > max_width - ellipsis.len) { │ │ 978 + const slice = iter.nextCodepointSlice() orelse break; │ │ 979 + width -= xitui.width.cellWidth(try std.unicode.utf8Decode(slice)); │ │ 980 + start += slice.len; │ │ 981 } │ │ 982 + return std.fmt.allocPrint(aa, " " ++ ellipsis ++ "{s} ", .{dir[start..]}); │ │ 983 +} │ │ 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), │ │ 881 - data: *const Header, │ │ 989 + data: *const Self, │ │ 990 + session: *ui.Session, │ │ 991 │ │ 883 - pub fn init(allocator: std.mem.Allocator, data: *const Header, session: *ui.Session, location: ui.RoutablePage.RepoLocation) !Header.View { │ │ 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) { │ │ 888 - if (try ui.widget.CopyableText.initClone(allocator, session, location)) |value| { │ │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(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 892 - box.getFocus().child_id = clone_url.getFocus().id; │ │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 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 896 - │ │ 897 - var spacer = try ui.widget.Spacer.init(allocator); │ │ 898 - errdefer spacer.deinit(allocator); │ │ 899 - try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │1022 │ │ 901 - var text_box = try wgt.TextBox.init(allocator, data.content, .{ .border_style = .hidden, .wrap_kind = .none }); │ │ 902 - errdefer text_box.deinit(allocator); │ │ 903 - try box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null, .flex = .shrink });│ │ 904 - │ │ 905 - return .{ .box = box, .data = data }; │ │1023 + return .{ .box = box, .data = data, .session = session }; │ │1024 } │ │1025 │ │1026 pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯