this repo has no description
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}