this repo has no description
13
fork

Configure Feed

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

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