diff --git a/build.zig b/build.zig index 2092f04..f62ca09 100644 --- a/build.zig +++ b/build.zig @@ -28,8 +28,8 @@ pub fn build(b: *std.Build) void { // Examples const Example = enum { - aio, cli, + fuzzy, image, main, nvim, @@ -38,7 +38,6 @@ pub fn build(b: *std.Build) void { vaxis, view, vt, - xev, }; const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; const example_step = b.step("example", "Run example"); diff --git a/examples/fuzzy.zig b/examples/fuzzy.zig new file mode 100644 index 0000000..21d34ee --- /dev/null +++ b/examples/fuzzy.zig @@ -0,0 +1,235 @@ +const std = @import("std"); +const vaxis = @import("vaxis"); +const vxfw = vaxis.vxfw; + +const Model = struct { + list: std.ArrayList(vxfw.Text), + filtered: std.ArrayList(vxfw.RichText), + list_view: vxfw.ListView, + text_field: vxfw.TextField, + result: []const u8, + unicode_data: *const vaxis.Unicode, + + /// Used for filtered RichText Spans + arena: std.heap.ArenaAllocator, + + 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 allocator = self.arena.allocator(); + for (self.list.items) |line| { + var spans = std.ArrayList(vxfw.RichText.TextSpan).init(allocator); + const span: vxfw.RichText.TextSpan = .{ .text = line.text }; + try spans.append(span); + try self.filtered.append(.{ .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(); + + var 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 }, + )), + }; + list_view.surface.focusable = false; + + 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(), + .focusable = true, + .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)); + self.filtered.clearAndFree(); + _ = self.arena.reset(.free_all); + const allocator = self.arena.allocator(); + + 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(allocator, item.text); + + var spans = std.ArrayList(vxfw.RichText.TextSpan).init(allocator); + var i: usize = 0; + var iter = self.unicode_data.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(up_to_here); + try spans.append(match); + i = idx + g.len; + } else continue :outer; + } + const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] }; + try spans.append(up_to_here); + try self.filtered.append(.{ .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 allocator = self.arena.allocator(); + var result: std.ArrayListUnmanaged(u8) = .{}; + for (selected.text) |span| { + try result.appendSlice(allocator, span.text); + } + self.result = result.items; + } + ctx.quit = true; + } +}; + +fn toLower(allocator: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 { + const lower = try allocator.alloc(u8, src.len); + for (src, 0..) |b, i| { + lower[i] = std.ascii.toLower(b); + } + return lower; +} + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + const allocator = gpa.allocator(); + + var app = try vxfw.App.init(allocator); + errdefer app.deinit(); + + const model = try allocator.create(Model); + defer allocator.destroy(model); + model.* = .{ + .list = std.ArrayList(vxfw.Text).init(allocator), + .filtered = std.ArrayList(vxfw.RichText).init(allocator), + .list_view = .{ + .children = .{ + .builder = .{ + .userdata = model, + .buildFn = Model.widgetBuilder, + }, + }, + }, + .text_field = .{ + .buf = vxfw.TextField.Buffer.init(allocator), + .unicode = &app.vx.unicode, + .userdata = model, + .onChange = Model.onChange, + .onSubmit = Model.onSubmit, + }, + .result = "", + .arena = std.heap.ArenaAllocator.init(allocator), + .unicode_data = &app.vx.unicode, + }; + defer model.text_field.deinit(); + defer model.list.deinit(); + defer model.filtered.deinit(); + defer model.arena.deinit(); + + // Run the command + var fd = std.process.Child.init(&.{"fd"}, allocator); + fd.stdout_behavior = .Pipe; + fd.stderr_behavior = .Pipe; + var stdout = std.ArrayList(u8).init(allocator); + var stderr = std.ArrayList(u8).init(allocator); + defer stdout.deinit(); + defer stderr.deinit(); + try fd.spawn(); + try fd.collectOutput(&stdout, &stderr, 10_000_000); + _ = try fd.wait(); + + var iter = std.mem.splitScalar(u8, stdout.items, '\n'); + while (iter.next()) |line| { + if (line.len == 0) continue; + try model.list.append(.{ .text = line }); + } + + try app.run(model.widget(), .{}); + app.deinit(); + + if (model.result.len > 0) { + const writer = std.io.getStdOut().writer(); + try writer.print("{s}\n", .{model.result}); + } else { + std.process.exit(130); + } +}