vxfw: add fuzzy finder example

This commit is contained in:
Tim Culverhouse 2024-10-30 14:45:56 -05:00
parent 118c5566d7
commit cec9e5814f
2 changed files with 236 additions and 2 deletions

View file

@ -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");

235
examples/fuzzy.zig Normal file
View file

@ -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);
}
}