Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248const std = @import("std");const vaxis = @import("vaxis");const vxfw = vaxis.vxfw;
const Model = struct { list: std.ArrayList(vxfw.Text), /// Memory owned by .arena filtered: std.ArrayList(vxfw.RichText), list_view: vxfw.ListView, text_field: vxfw.TextField,
/// Used for filtered RichText Spans and result arena: std.heap.ArenaAllocator, result: []const u8,
pub fn init(gpa: std.mem.Allocator) !*Model { const model = try gpa.create(Model); errdefer gpa.destroy(model);
model.* = .{ .list = .empty, .filtered = .empty, .list_view = .{ .children = .{ .builder = .{ .userdata = model, .buildFn = Model.widgetBuilder, }, }, }, .text_field = .{ .buf = .init(gpa), .userdata = model, .onChange = Model.onChange, .onSubmit = Model.onSubmit, }, .result = "", .arena = .init(gpa), };
return model; }
pub fn deinit(self: *Model, gpa: std.mem.Allocator) void { self.arena.deinit(); self.text_field.deinit(); self.list.deinit(gpa); gpa.destroy(self); }
pub fn widget(self: *Model) vxfw.Widget { return .{ .userdata = self, .eventHandler = Model.typeErasedEventHandler, .drawFn = Model.typeErasedDrawFn, }; }
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { const self: *Model = @ptrCast(@alignCast(ptr)); switch (event) { .init => { // Initialize the filtered list const arena = self.arena.allocator(); for (self.list.items) |line| { var spans: std.ArrayList(vxfw.RichText.TextSpan) = .empty; const span: vxfw.RichText.TextSpan = .{ .text = line.text }; try spans.append(arena, span); try self.filtered.append(arena, .{ .text = spans.items }); }
return ctx.requestFocus(self.text_field.widget()); }, .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { ctx.quit = true; return; } return self.list_view.handleEvent(ctx, event); }, .focus_in => { return ctx.requestFocus(self.text_field.widget()); }, else => {}, } }
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface { const self: *Model = @ptrCast(@alignCast(ptr)); const max = ctx.max.size();
const list_view: vxfw.SubSurface = .{ .origin = .{ .row = 2, .col = 0 }, .surface = try self.list_view.draw(ctx.withConstraints( ctx.min, .{ .width = max.width, .height = max.height - 3 }, )), };
const text_field: vxfw.SubSurface = .{ .origin = .{ .row = 0, .col = 2 }, .surface = try self.text_field.draw(ctx.withConstraints( ctx.min, .{ .width = max.width, .height = 1 }, )), };
const prompt: vxfw.Text = .{ .text = "", .style = .{ .fg = .{ .index = 4 } } };
const prompt_surface: vxfw.SubSurface = .{ .origin = .{ .row = 0, .col = 0 }, .surface = try prompt.draw(ctx.withConstraints(ctx.min, .{ .width = 2, .height = 1 })), };
const children = try ctx.arena.alloc(vxfw.SubSurface, 3); children[0] = list_view; children[1] = text_field; children[2] = prompt_surface;
return .{ .size = max, .widget = self.widget(), .buffer = &.{}, .children = children, }; }
fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget { const self: *const Model = @ptrCast(@alignCast(ptr)); if (idx >= self.filtered.items.len) return null;
return self.filtered.items[idx].widget(); }
fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void { const ptr = maybe_ptr orelse return; const self: *Model = @ptrCast(@alignCast(ptr)); const arena = self.arena.allocator(); self.filtered.clearAndFree(arena); _ = self.arena.reset(.free_all);
const hasUpper = for (str) |b| { if (std.ascii.isUpper(b)) break true; } else false;
// Loop each line // If our input is only lowercase, we convert the line to lowercase // Iterate the input graphemes, looking for them _in order_ in the line outer: for (self.list.items) |item| { const tgt = if (hasUpper) item.text else try toLower(arena, item.text);
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty; var i: usize = 0; var iter = vaxis.unicode.graphemeIterator(str); while (iter.next()) |g| { if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| { const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] }; const match: vxfw.RichText.TextSpan = .{ .text = item.text[idx .. idx + g.len], .style = .{ .fg = .{ .index = 4 }, .reverse = true }, }; try spans.append(arena, up_to_here); try spans.append(arena, match); i = idx + g.len; } else continue :outer; } const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; try spans.append(arena, up_to_here); try self.filtered.append(arena, .{ .text = spans.items }); } self.list_view.scroll.top = 0; self.list_view.scroll.offset = 0; self.list_view.cursor = 0; }
fn onSubmit(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext, _: []const u8) anyerror!void { const ptr = maybe_ptr orelse return; const self: *Model = @ptrCast(@alignCast(ptr)); if (self.list_view.cursor < self.filtered.items.len) { const selected = self.filtered.items[self.list_view.cursor]; const arena = self.arena.allocator(); var result = std.ArrayList(u8).empty; for (selected.text) |span| { try result.appendSlice(arena, span.text); } self.result = result.items; } ctx.quit = true; }};
fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { const lower = try arena.alloc(u8, src.len); for (src, 0..) |b, i| { lower[i] = std.ascii.toLower(b); } return lower;}
pub fn main(init: std.process.Init) !u8 { const io = init.io; const alloc = init.gpa;
const model = try Model.init(alloc); defer model.deinit(alloc);
// Run the command const fd = try std.process.run(alloc, io, .{ .argv = &.{"fd"}, }); defer alloc.free(fd.stdout); defer alloc.free(fd.stderr);
var iter = std.mem.splitScalar(u8, fd.stdout, '\n'); while (iter.next()) |line| { if (line.len == 0) continue; try model.list.append(alloc, .{ .text = line }); }
{ var buffer: [1024]u8 = undefined; var app: vxfw.App = try .init(io, alloc, init.environ_map, &buffer); defer app.deinit();
try app.run(model.widget(), .{}); }
if (model.result.len > 0) { var stdout_file: std.Io.File = .stdout(); var stdout_buffer: [1024]u8 = undefined; var stdout_writer = stdout_file.writer(io, &stdout_buffer); const stdout = &stdout_writer.interface; try stdout.writeAll(model.result); try stdout.writeByte('\n'); try stdout.flush(); return 0; } else { return 130; }}
test { std.testing.refAllDecls(@This());}