this repo has no description
13
fork

Configure Feed

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

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