╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Files.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
776 const lb = self.listBox(); 777 const cid = lb.getFocus().child_id orelse return null; 778 return lb.children.getIndex(cid); 779 } 780 }; 781 782 // join `lines` into one string, each prefixed with its 1-based file line number 783 // (right-aligned in a column wide enough for the last number, then a space), 784 // like the diffs view. `start` is the window's 0-based first line, so numbers 785 // continue across paginated windows. 786 fn numberedContent(arena: std.mem.Allocator, lines: []const []const u8, start: usize) ![]const u8 { 787 if (lines.len == 0) return ""; 788 const width = std.fmt.count("{d}", .{start + lines.len}); 789 const indent = try arena.alloc(u8, width); 790 @memset(indent, ' '); 791 792 var out: std.ArrayList(u8) = .empty; 793 for (lines, 0..) |line, i| { 794 if (i != 0) try out.append(arena, '\n'); 795 const num_str = try std.fmt.allocPrint(arena, "{d}", .{start + i + 1}); 796 try out.appendSlice(arena, indent[0 .. width - num_str.len]); 797 try out.appendSlice(arena, num_str); 798 try out.append(arena, ' '); 799 try out.appendSlice(arena, line); 800 } 801 return out.toOwnedSlice(arena); 802 } 803 804 // the index of the "README"/"README.md" file entry (case-insensitive), or null 805 // if none. used to pick the default selection at the repo root. 806 fn readmeIndex(entries: []const Entry) ?usize { 807 for (entries, 0..) |entry, i| { 808 if (!entry.is_dir and isReadme(entry.name)) return i; 809 } 810 return null; 811 } 812 813 fn isReadme(name: []const u8) bool { 814 return std.ascii.eqlIgnoreCase(name, "README") or std.ascii.eqlIgnoreCase(name, "README.md"); 815 } 816 817 // the index of the entry the route named (data.selected_file), or null if none. 818 fn selectedFileIndex(data: *const Self) ?usize { 819 const name = data.selected_file orelse return null; 820 for (data.entries, 0..) |entry, i| { 821 if (!entry.is_dir and std.mem.eql(u8, entry.name, name)) return i; 822 } 823 return null; 824 } 825 826 // an "a:" link to the files route at `path`, pinned to the listing's ref, so 827 // following it navigates to that directory's listing. 828 fn dirLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, path: []const u8) ![]const u8 { 829 const route = ui.RoutablePage.repoFilesRoute(data.identity, data.ref_or_oid, data.ref_or_oid_value, path, 0) orelse return error.RouteTooLong; 830 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{try route.toUrl(page_arena)}); 831 } 832 833 // a link to the file's route. a preloaded file gets an "ai:" link 834 // (crossPageLink ignores it, so a click selects the row in place and shows its 835 // contents); an unloaded file gets an "a:" link, so activating it reloads the 836 // page with that file selected and its content read. 837 fn fileLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, path: []const u8, loaded: bool) ![]const u8 { 838 const route = ui.RoutablePage.repoFilesRoute(data.identity, data.ref_or_oid, data.ref_or_oid_value, path, 0) orelse return error.RouteTooLong; 839 const prefix = if (loaded) "ai:" else "a:"; 840 return std.fmt.allocPrint(page_arena.allocator(), "{s}{s}", .{ prefix, try route.toUrl(page_arena) }); 841 } 842 843 // `dir`/`name`, or just `name` at the root. 844 pub fn childDir(arena: std.mem.Allocator, dir: []const u8, name: []const u8) ![]const u8 { 845 return if (dir.len == 0) name else std.fmt.allocPrint(arena, "{s}/{s}", .{ dir, name }); 846 } 847 848 // `dir` with its last segment removed ("" when `dir` has a single segment). 849 fn parentDir(dir: []const u8) []const u8 { 850 const slash = std.mem.lastIndexOfScalar(u8, dir, '/') orelse return ""; 851 return dir[0..slash]; 852 } 853 854 // the "viewing <ref_or_oid> <value>" banner shown above the listing. 855 pub const Header = struct { 856 content: []const u8, 857 858 pub fn init(aa: std.mem.Allocator, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Header { 859 return .{ 860 .content = try std.fmt.allocPrint(aa, "viewing {s} {s}", .{ @tagName(ref_or_oid), value }), 861 }; 862 } 863 864 pub const View = struct { 865 box: wgt.Box(ui.Widget), 866 data: *const Header, 867 868 pub fn init(allocator: std.mem.Allocator, data: *const Header, session: *ui.Session) !Header.View { 869 _ = session; 870 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 871 errdefer box.deinit(allocator); 872 873 var text_box = try wgt.TextBox(ui.Widget).init(allocator, data.content, .{ .border_style = .hidden, .wrap_kind = .none }); 874 errdefer text_box.deinit(allocator); 875 try box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null }); 876 877 return .{ .box = box, .data = data }; 878 } 879 880 pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { 881 self.box.deinit(allocator); 882 } 883 884 pub fn build(self: *Header.View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 885 self.clearGrid(); 886 try self.box.build(allocator, constraint, root_focus); 887 } 888 889 pub fn input(self: *Header.View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 890 _ = self; 891 _ = allocator; 892 _ = key; 893 _ = root_focus; 894 } 895 896 pub fn clearGrid(self: *Header.View) void { 897 self.box.clearGrid(); 898 } 899 900 pub fn getGrid(self: Header.View) ?Grid { 901 return self.box.getGrid(); 902 } 903 904 pub fn getFocus(self: *Header.View) *Focus { 905 return self.box.getFocus(); 906 } 907 }; 908 }; 909