╭ About.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const ui = @import("../../ui.zig"); 4 const md = @import("../../markdown.zig"); 5 const Markdown = @import("../Repo/Markdown.zig"); 6 const xit = @import("xit"); 7 const xitui = xit.xitui; 8 const wgt = xitui.widget; 9 const layout = xitui.layout; 10 const Key = xitui.input.Key; 11 const Grid = xitui.grid.Grid; 12 const Focus = xitui.focus.Focus; 13 14 // the admin repo's about page, re-read on every page build. a level 1 15 // heading on its first line becomes the header title instead. 16 title: ?[]const u8, 17 text: []const u8, 18 19 const Self = @This(); 20 21 pub fn init(arena: *std.heap.ArenaAllocator, session: *ui.Session) !Self { 22 const admin_repo = session.admin_repo orelse return error.NotFound; 23 const io = session.io orelse return error.NotFound; 24 const text = admin_repo.core.work_dir.readFileAlloc(io, evt.admin_about_path, arena.allocator(), .unlimited) catch |err| switch (err) { 25 error.FileNotFound => "", 26 else => |e| return e, 27 }; 28 const line_end = std.mem.indexOfScalar(u8, text, '\n') orelse text.len; 29 const line = std.mem.trimEnd(u8, text[0..line_end], "\r"); 30 if (line.len < 2 or line[0] != '#' or (line[1] != ' ' and line[1] != '\t')) return .{ .title = null, .text = text }; 31 const title = std.mem.trim(u8, line[2..], " \t"); 32 if (title.len == 0) return .{ .title = null, .text = text }; 33 return .{ .title = title, .text = std.mem.trimStart(u8, text[line_end..], "\r\n") }; 34 } 35 36 pub const View = struct { 37 // the scroll wrapped in a border that turns double while the page has focus 38 frame: wgt.Box(ui.Widget), 39 session: *ui.Session, 40 41 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { 42 const aa = session.page_arena.allocator(); 43 var lines: std.ArrayList([]const u8) = .empty; 44 var it = std.mem.splitScalar(u8, data.text, '\n'); 45 while (it.next()) |line| try lines.append(aa, line); 46 const doc = try md.parse(aa, lines.items); 47 48 var scroll = blk: { 49 var markdown = try Markdown.View.init(allocator, doc, null, evt.admin_about_path, session.page_arena); 50 errdefer markdown.deinit(allocator); 51 break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .markdown = markdown }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true }); 52 }; 53 errdefer scroll.deinit(allocator); 54 var frame = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = .single, .round_corners = true, .direction = .vert }); 55 errdefer frame.deinit(allocator); 56 frame.getFocus().child_id = scroll.getFocus().id; 57 try frame.children.put(allocator, scroll.getFocus().id, .{ .widget = .{ .scroll = scroll }, .rect = null, .min_size = null, .flex = .grow }); 58 return .{ .frame = frame, .session = session }; 59 } 60 61 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 62 self.frame.deinit(allocator); 63 } 64 65 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 66 const focused = if (root_focus.grandchild_id) |id| self.markdownView().owns(id) else false; 67 self.frame.options.border_style = if (focused) .double else .single; 68 try self.frame.build(allocator, constraint, root_focus); 69 } 70 71 pub fn input(self: *View, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 72 // the web scrolls natively and leaves tab to the browser 73 if (!self.session.is_terminal) return; 74 const sc = self.contentScroll(); 75 switch (key) { 76 .arrow_up => sc.y -= 1, 77 .arrow_down => sc.y += 1, 78 .page_up => sc.y -= 10, 79 .page_down => sc.y += 10, 80 .home => sc.y = 0, 81 .end => sc.y = std.math.maxInt(isize), 82 .mouse => |mouse| switch (mouse.action) { 83 .scroll => |dir| sc.y += if (dir == .up) -5 else 5, 84 else => {}, 85 }, 86 .tab, .back_tab => try self.markdownView().stepLink(allocator, root_focus, sc, key == .tab), 87 else => {}, 88 } 89 sc.clampToContent(); 90 } 91 92 pub fn clearGrid(self: *View) void { 93 self.frame.clearGrid(); 94 } 95 96 pub fn getGrid(self: View) ?Grid { 97 return self.frame.getGrid(); 98 } 99 100 pub fn getFocus(self: *View) *Focus { 101 return self.frame.getFocus(); 102 } 103 104 pub fn atTop(self: View) bool { 105 return self.frame.children.values()[0].widget.scroll.y <= 0; 106 } 107 108 fn contentScroll(self: *View) *wgt.Scroll(ui.Widget) { 109 return &self.frame.children.values()[0].widget.scroll; 110 } 111 112 fn markdownView(self: *View) *Markdown.View { 113 return &self.contentScroll().child.markdown; 114 } 115 }; 116