this repo has no description
13
fork

Configure Feed

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

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