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 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}