this repo has no description
13
fork

Configure Feed

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

vxfw: add fuzzy finder example

+236 -2
+1 -2
build.zig
··· 28 28 29 29 // Examples 30 30 const Example = enum { 31 - aio, 32 31 cli, 32 + fuzzy, 33 33 image, 34 34 main, 35 35 nvim, ··· 38 38 vaxis, 39 39 view, 40 40 vt, 41 - xev, 42 41 }; 43 42 const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; 44 43 const example_step = b.step("example", "Run example");
+235
examples/fuzzy.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + const vxfw = vaxis.vxfw; 4 + 5 + const 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).init(allocator); 32 + const span: vxfw.RichText.TextSpan = .{ .text = line.text }; 33 + try spans.append(span); 34 + try self.filtered.append(.{ .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 + var 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 + list_view.surface.focusable = false; 65 + 66 + const text_field: vxfw.SubSurface = .{ 67 + .origin = .{ .row = 0, .col = 2 }, 68 + .surface = try self.text_field.draw(ctx.withConstraints( 69 + ctx.min, 70 + .{ .width = max.width, .height = 1 }, 71 + )), 72 + }; 73 + 74 + const prompt: vxfw.Text = .{ .text = "", .style = .{ .fg = .{ .index = 4 } } }; 75 + 76 + const prompt_surface: vxfw.SubSurface = .{ 77 + .origin = .{ .row = 0, .col = 0 }, 78 + .surface = try prompt.draw(ctx.withConstraints(ctx.min, .{ .width = 2, .height = 1 })), 79 + }; 80 + 81 + const children = try ctx.arena.alloc(vxfw.SubSurface, 3); 82 + children[0] = list_view; 83 + children[1] = text_field; 84 + children[2] = prompt_surface; 85 + 86 + return .{ 87 + .size = max, 88 + .widget = self.widget(), 89 + .focusable = true, 90 + .buffer = &.{}, 91 + .children = children, 92 + }; 93 + } 94 + 95 + fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget { 96 + const self: *const Model = @ptrCast(@alignCast(ptr)); 97 + if (idx >= self.filtered.items.len) return null; 98 + 99 + return self.filtered.items[idx].widget(); 100 + } 101 + 102 + fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void { 103 + const ptr = maybe_ptr orelse return; 104 + const self: *Model = @ptrCast(@alignCast(ptr)); 105 + self.filtered.clearAndFree(); 106 + _ = self.arena.reset(.free_all); 107 + const allocator = self.arena.allocator(); 108 + 109 + const hasUpper = for (str) |b| { 110 + if (std.ascii.isUpper(b)) break true; 111 + } else false; 112 + 113 + // Loop each line 114 + // If our input is only lowercase, we convert the line to lowercase 115 + // Iterate the input graphemes, looking for them _in order_ in the line 116 + outer: for (self.list.items) |item| { 117 + const tgt = if (hasUpper) 118 + item.text 119 + else 120 + try toLower(allocator, item.text); 121 + 122 + var spans = std.ArrayList(vxfw.RichText.TextSpan).init(allocator); 123 + var i: usize = 0; 124 + var iter = self.unicode_data.graphemeIterator(str); 125 + while (iter.next()) |g| { 126 + if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| { 127 + const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] }; 128 + const match: vxfw.RichText.TextSpan = .{ 129 + .text = item.text[idx .. idx + g.len], 130 + .style = .{ .fg = .{ .index = 4 }, .reverse = true }, 131 + }; 132 + try spans.append(up_to_here); 133 + try spans.append(match); 134 + i = idx + g.len; 135 + } else continue :outer; 136 + } 137 + const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; 138 + try spans.append(up_to_here); 139 + try self.filtered.append(.{ .text = spans.items }); 140 + } 141 + self.list_view.scroll.top = 0; 142 + self.list_view.scroll.offset = 0; 143 + self.list_view.cursor = 0; 144 + } 145 + 146 + fn onSubmit(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext, _: []const u8) anyerror!void { 147 + const ptr = maybe_ptr orelse return; 148 + const self: *Model = @ptrCast(@alignCast(ptr)); 149 + if (self.list_view.cursor < self.filtered.items.len) { 150 + const selected = self.filtered.items[self.list_view.cursor]; 151 + const allocator = self.arena.allocator(); 152 + var result: std.ArrayListUnmanaged(u8) = .{}; 153 + for (selected.text) |span| { 154 + try result.appendSlice(allocator, span.text); 155 + } 156 + self.result = result.items; 157 + } 158 + ctx.quit = true; 159 + } 160 + }; 161 + 162 + fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { 163 + const lower = try allocator.alloc(u8, src.len); 164 + for (src, 0..) |b, i| { 165 + lower[i] = std.ascii.toLower(b); 166 + } 167 + return lower; 168 + } 169 + 170 + pub fn main() !void { 171 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 172 + defer _ = gpa.deinit(); 173 + 174 + const allocator = gpa.allocator(); 175 + 176 + var app = try vxfw.App.init(allocator); 177 + errdefer app.deinit(); 178 + 179 + const model = try allocator.create(Model); 180 + defer allocator.destroy(model); 181 + model.* = .{ 182 + .list = std.ArrayList(vxfw.Text).init(allocator), 183 + .filtered = std.ArrayList(vxfw.RichText).init(allocator), 184 + .list_view = .{ 185 + .children = .{ 186 + .builder = .{ 187 + .userdata = model, 188 + .buildFn = Model.widgetBuilder, 189 + }, 190 + }, 191 + }, 192 + .text_field = .{ 193 + .buf = vxfw.TextField.Buffer.init(allocator), 194 + .unicode = &app.vx.unicode, 195 + .userdata = model, 196 + .onChange = Model.onChange, 197 + .onSubmit = Model.onSubmit, 198 + }, 199 + .result = "", 200 + .arena = std.heap.ArenaAllocator.init(allocator), 201 + .unicode_data = &app.vx.unicode, 202 + }; 203 + defer model.text_field.deinit(); 204 + defer model.list.deinit(); 205 + defer model.filtered.deinit(); 206 + defer model.arena.deinit(); 207 + 208 + // Run the command 209 + var fd = std.process.Child.init(&.{"fd"}, allocator); 210 + fd.stdout_behavior = .Pipe; 211 + fd.stderr_behavior = .Pipe; 212 + var stdout = std.ArrayList(u8).init(allocator); 213 + var stderr = std.ArrayList(u8).init(allocator); 214 + defer stdout.deinit(); 215 + defer stderr.deinit(); 216 + try fd.spawn(); 217 + try fd.collectOutput(&stdout, &stderr, 10_000_000); 218 + _ = try fd.wait(); 219 + 220 + var iter = std.mem.splitScalar(u8, stdout.items, '\n'); 221 + while (iter.next()) |line| { 222 + if (line.len == 0) continue; 223 + try model.list.append(.{ .text = line }); 224 + } 225 + 226 + try app.run(model.widget(), .{}); 227 + app.deinit(); 228 + 229 + if (model.result.len > 0) { 230 + const writer = std.io.getStdOut().writer(); 231 + try writer.print("{s}\n", .{model.result}); 232 + } else { 233 + std.process.exit(130); 234 + } 235 + }