this repo has no description
13
fork

Configure Feed

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

at f8b45b39501a9edfa4ebeb302d4213934e70ccb1 233 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 filtered: std.ArrayList(vxfw.RichText), 8 list_view: vxfw.ListView, 9 text_field: vxfw.TextField, 10 result: []const u8, 11 unicode_data: *const vaxis.Unicode, 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 = self.unicode_data.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 .unicode = &app.vx.unicode, 193 .userdata = model, 194 .onChange = Model.onChange, 195 .onSubmit = Model.onSubmit, 196 }, 197 .result = "", 198 .arena = std.heap.ArenaAllocator.init(allocator), 199 .unicode_data = &app.vx.unicode, 200 }; 201 defer model.text_field.deinit(); 202 defer model.list.deinit(allocator); 203 defer model.filtered.deinit(allocator); 204 defer model.arena.deinit(); 205 206 // Run the command 207 var fd = std.process.Child.init(&.{"fd"}, allocator); 208 fd.stdout_behavior = .Pipe; 209 fd.stderr_behavior = .Pipe; 210 var stdout = std.ArrayList(u8){}; 211 var stderr = std.ArrayList(u8){}; 212 defer stdout.deinit(allocator); 213 defer stderr.deinit(allocator); 214 try fd.spawn(); 215 try fd.collectOutput(allocator, &stdout, &stderr, 10_000_000); 216 _ = try fd.wait(); 217 218 var iter = std.mem.splitScalar(u8, stdout.items, '\n'); 219 while (iter.next()) |line| { 220 if (line.len == 0) continue; 221 try model.list.append(allocator, .{ .text = line }); 222 } 223 224 try app.run(model.widget(), .{}); 225 app.deinit(); 226 227 if (model.result.len > 0) { 228 _ = try std.posix.write(std.posix.STDOUT_FILENO, model.result); 229 _ = try std.posix.write(std.posix.STDOUT_FILENO, "\n"); 230 } else { 231 std.process.exit(130); 232 } 233}