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