╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │373 // copied by their text boxes. │ │374 const aa = session.page_arena.allocator(); │ │375 if (data.dir.len != 0) { │ │290 - try addRow(allocator, &list_box, "..", try dirLink(session.page_arena, data, parentDir(data.dir))); │ │376 + try addRow(allocator, &list_box, "..", "", try dirLink(session.page_arena, data, parentDir(data.dir)));│ │377 } │ │378 for (data.entries) |entry| { │ │379 const path = try childDir(aa, data.dir, entry.name); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │294 - const label = if (entry.is_dir) try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) else entry.name;│ │380 + // a search result shows its file name, with its directory │ │381 + // in the bottom label. │ │382 + const label = if (data.find != null) │ │383 + std.fs.path.basenamePosix(entry.name) │ │384 + else if (entry.is_dir) │ │385 + try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) │ │386 + else │ │387 + entry.name; │ │388 + const bottom_label = if (data.find != null) try resultDirLabel(aa, entry.name) else ""; │ │389 const link = if (entry.is_dir) │ │390 try dirLink(session.page_arena, data, path) │ │391 else │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │392 try fileLink(session.page_arena, data, path, entry.loaded, lineNumber(entry.window_start));│ │299 - try addRow(allocator, &list_box, label, link); │ │393 + try addRow(allocator, &list_box, label, bottom_label, link); │ │394 } │ │395 + if (data.capped) try addRow(allocator, &list_box, "(list truncated)", "", ""); │ │396 // select the file the route named, else prefer a README so its │ │397 // contents greet the visitor in the detail pane, else the first │ │398 // row. a non-root directory has a leading ".." row, so the entry │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │304 - // index is offset by one there. │ │399 + // index is offset by one there. search results have no README│ │400 + // default: the term decides what matters. │ │401 var sel: usize = 0; │ │402 if (list_box.children.count() > 0) { │ │403 const base: usize = if (data.dir.len != 0) 1 else 0; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯