this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 8a9c2d5e1b3778f1ea43c9bd5d325cfa72016584 243 lines 8.4 kB view raw
1const std = @import("std"); 2const vaxis = @import("vaxis"); 3const vxfw = vaxis.vxfw; 4 5const Model = struct { 6 list: std.ArrayList(vxfw.Text), 7 /// Memory owned by .arena 8 filtered: std.ArrayList(vxfw.RichText), 9 list_view: vxfw.ListView, 10 text_field: vxfw.TextField, 11 12 /// Used for filtered RichText Spans and result 13 arena: std.heap.ArenaAllocator, 14 filtered: std.ArrayList(vxfw.RichText), 15 result: []const u8, 16 17 pub fn init(gpa: std.mem.Allocator) !*Model { 18 const model = try gpa.create(Model); 19 errdefer gpa.destroy(model); 20 21 model.* = .{ 22 .list = .empty, 23 .filtered = .empty, 24 .list_view = .{ 25 .children = .{ 26 .builder = .{ 27 .userdata = model, 28 .buildFn = Model.widgetBuilder, 29 }, 30 }, 31 }, 32 .text_field = .{ 33 .buf = vxfw.TextField.Buffer.init(gpa), 34 .userdata = model, 35 .onChange = Model.onChange, 36 .onSubmit = Model.onSubmit, 37 }, 38 .result = "", 39 .arena = std.heap.ArenaAllocator.init(gpa), 40 }; 41 42 return model; 43 } 44 45 pub fn deinit(self: *Model, gpa: std.mem.Allocator) void { 46 self.arena.deinit(); 47 self.text_field.deinit(); 48 self.list.deinit(gpa); 49 gpa.destroy(self); 50 } 51 52 pub fn widget(self: *Model) vxfw.Widget { 53 return .{ 54 .userdata = self, 55 .eventHandler = Model.typeErasedEventHandler, 56 .drawFn = Model.typeErasedDrawFn, 57 }; 58 } 59 60 fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 61 const self: *Model = @ptrCast(@alignCast(ptr)); 62 switch (event) { 63 .init => { 64 // Initialize the filtered list 65 const arena = self.arena.allocator(); 66 for (self.list.items) |line| { 67 var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 68 const span: vxfw.RichText.TextSpan = .{ .text = line.text }; 69 try spans.append(arena, span); 70 try self.filtered.append(arena, .{ .text = spans.items }); 71 } 72 73 return ctx.requestFocus(self.text_field.widget()); 74 }, 75 .key_press => |key| { 76 if (key.matches('c', .{ .ctrl = true })) { 77 ctx.quit = true; 78 return; 79 } 80 return self.list_view.handleEvent(ctx, event); 81 }, 82 .focus_in => { 83 return ctx.requestFocus(self.text_field.widget()); 84 }, 85 else => {}, 86 } 87 } 88 89 fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { 90 const self: *Model = @ptrCast(@alignCast(ptr)); 91 const max = ctx.max.size(); 92 93 const list_view: vxfw.SubSurface = .{ 94 .origin = .{ .row = 2, .col = 0 }, 95 .surface = try self.list_view.draw(ctx.withConstraints( 96 ctx.min, 97 .{ .width = max.width, .height = max.height - 3 }, 98 )), 99 }; 100 101 const text_field: vxfw.SubSurface = .{ 102 .origin = .{ .row = 0, .col = 2 }, 103 .surface = try self.text_field.draw(ctx.withConstraints( 104 ctx.min, 105 .{ .width = max.width, .height = 1 }, 106 )), 107 }; 108 109 const prompt: vxfw.Text = .{ .text = "", .style = .{ .fg = .{ .index = 4 } } }; 110 111 const prompt_surface: vxfw.SubSurface = .{ 112 .origin = .{ .row = 0, .col = 0 }, 113 .surface = try prompt.draw(ctx.withConstraints(ctx.min, .{ .width = 2, .height = 1 })), 114 }; 115 116 const children = try ctx.arena.alloc(vxfw.SubSurface, 3); 117 children[0] = list_view; 118 children[1] = text_field; 119 children[2] = prompt_surface; 120 121 return .{ 122 .size = max, 123 .widget = self.widget(), 124 .buffer = &.{}, 125 .children = children, 126 }; 127 } 128 129 fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget { 130 const self: *const Model = @ptrCast(@alignCast(ptr)); 131 if (idx >= self.filtered.items.len) return null; 132 133 return self.filtered.items[idx].widget(); 134 } 135 136 fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void { 137 const ptr = maybe_ptr orelse return; 138 const self: *Model = @ptrCast(@alignCast(ptr)); 139 const arena = self.arena.allocator(); 140 self.filtered.clearAndFree(arena); 141 _ = self.arena.reset(.free_all); 142 143 const hasUpper = for (str) |b| { 144 if (std.ascii.isUpper(b)) break true; 145 } else false; 146 147 // Loop each line 148 // If our input is only lowercase, we convert the line to lowercase 149 // Iterate the input graphemes, looking for them _in order_ in the line 150 outer: for (self.list.items) |item| { 151 const tgt = if (hasUpper) 152 item.text 153 else 154 try toLower(arena, item.text); 155 156 var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; 157 var i: usize = 0; 158 var iter = vaxis.unicode.graphemeIterator(str); 159 while (iter.next()) |g| { 160 if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| { 161 const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] }; 162 const match: vxfw.RichText.TextSpan = .{ 163 .text = item.text[idx .. idx + g.len], 164 .style = .{ .fg = .{ .index = 4 }, .reverse = true }, 165 }; 166 try spans.append(arena, up_to_here); 167 try spans.append(arena, match); 168 i = idx + g.len; 169 } else continue :outer; 170 } 171 const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; 172 try spans.append(arena, up_to_here); 173 try self.filtered.append(arena, .{ .text = spans.items }); 174 } 175 self.list_view.scroll.top = 0; 176 self.list_view.scroll.offset = 0; 177 self.list_view.cursor = 0; 178 } 179 180 fn onSubmit(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext, _: []const u8) anyerror!void { 181 const ptr = maybe_ptr orelse return; 182 const self: *Model = @ptrCast(@alignCast(ptr)); 183 if (self.list_view.cursor < self.filtered.items.len) { 184 const selected = self.filtered.items[self.list_view.cursor]; 185 const arena = self.arena.allocator(); 186 var result = std.ArrayList(u8).empty; 187 for (selected.text) |span| { 188 try result.appendSlice(arena, span.text); 189 } 190 self.result = result.items; 191 } 192 ctx.quit = true; 193 } 194}; 195 196fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { 197 const lower = try arena.alloc(u8, src.len); 198 for (src, 0..) |b, i| { 199 lower[i] = std.ascii.toLower(b); 200 } 201 return lower; 202} 203 204pub fn main() !void { 205 var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){}; 206 defer _ = debug_allocator.deinit(); 207 208 const gpa = debug_allocator.allocator(); 209 210 var app = try vxfw.App.init(gpa); 211 errdefer app.deinit(); 212 213 const model = try Model.init(gpa); 214 defer model.deinit(gpa); 215 216 // Run the command 217 var fd = std.process.Child.init(&.{"fd"}, gpa); 218 fd.stdout_behavior = .Pipe; 219 fd.stderr_behavior = .Pipe; 220 var stdout = std.ArrayList(u8).empty; 221 var stderr = std.ArrayList(u8).empty; 222 defer stdout.deinit(gpa); 223 defer stderr.deinit(gpa); 224 try fd.spawn(); 225 try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000); 226 _ = try fd.wait(); 227 228 var iter = std.mem.splitScalar(u8, stdout.items, '\n'); 229 while (iter.next()) |line| { 230 if (line.len == 0) continue; 231 try model.list.append(gpa, .{ .text = line }); 232 } 233 234 try app.run(model.widget(), .{}); 235 app.deinit(); 236 237 if (model.result.len > 0) { 238 _ = try std.posix.write(std.posix.STDOUT_FILENO, model.result); 239 _ = try std.posix.write(std.posix.STDOUT_FILENO, "\n"); 240 } else { 241 std.process.exit(130); 242 } 243}