From abd56b75d37b3562934d81523602bd03bdcad4d4 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sun, 18 Jan 2026 00:02:32 +0100 Subject: [PATCH] add: search text field in preparation for fuzzy search feature --- src/content.zig | 94 +++++++++++++++++++++++++++++++++++++--------- src/main.zig | 30 ++++++++++----- src/model.zig | 2 + src/navigation.zig | 17 ++++----- 4 files changed, 106 insertions(+), 37 deletions(-) diff --git a/src/content.zig b/src/content.zig index 713cdff..1a3b28d 100644 --- a/src/content.zig +++ b/src/content.zig @@ -18,24 +18,31 @@ pub fn Website(App: type) type { fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { - .about => { - model.page.deinit(this.gpa); - model.page = .about; + .page => |page| switch (page) { + .about => { + model.page.deinit(this.gpa); + model.page = .about; - model.document.deinit(this.gpa); - model.document = .init(try std.fs.cwd().readFileAlloc(this.gpa, "./doc/about.md", std.math.maxInt(usize))); + model.document.deinit(this.gpa); + model.document = .init(try std.fs.cwd().readFileAlloc(this.gpa, "./doc/about.md", std.math.maxInt(usize))); + }, + .blog => |path| { + model.page.deinit(this.gpa); + model.document.deinit(this.gpa); + errdefer { + if (path) |p| this.gpa.free(p); + model.document = .invalidPage; + model.page = .{ .blog = null }; + } + + model.document = .init(try std.fs.cwd().readFileAlloc(this.gpa, if (path) |p| p else "./doc/blog.md", std.math.maxInt(usize))); + model.page = .{ .blog = path }; + }, }, - .blog => |path| { - model.page.deinit(this.gpa); - model.document.deinit(this.gpa); - errdefer { - if (path) |p| this.gpa.free(p); - model.document = .invalidPage; - model.page = .{ .blog = null }; - } + .key => |key| { + if (key.eql(.{ .cp = '/' })) model.searching = true; - model.document = .init(try std.fs.cwd().readFileAlloc(this.gpa, if (path) |p| p else "./doc/blog.md", std.math.maxInt(usize))); - model.page = .{ .blog = path }; + if (key.eql(.{ .cp = zterm.input.Escape })) model.searching = false; }, else => {}, } @@ -118,20 +125,66 @@ pub fn Title(App: type) type { } pub fn InfoBanner(App: type) type { - const left_text: []const u8 = "Build with zig"; - const right_text: []const u8 = "Yves Biener (@yves-biener)"; + const left_text: []const u8 = "Build using zig"; + const right_text: []const u8 = "Yves Biener"; return struct { + gpa: Allocator, + search: App.TextField, + queue: *App.Queue, + + pub fn init(gpa: Allocator, queue: *App.Queue) @This() { + return .{ + .gpa = gpa, + .search = .init(gpa, .init(.default, .default)), + .queue = queue, + }; + } + pub fn element(this: *@This()) App.Element { return .{ .ptr = this, .vtable = &.{ + .deinit = deinit, + .minSize = minSize, + .handle = handle, .content = content, }, }; } - fn content(_: *anyopaque, model: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + fn deinit(ctx: *anyopaque) void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + this.search.deinit(); + } + + fn minSize(ctx: *anyopaque, model: *const App.Model, _: zterm.Point) zterm.Point { + const this: *@This() = @ptrCast(@alignCast(ctx)); + if (!model.searching) this.search.clear(); + return .{ .y = if (model.searching) 2 else 1 }; + } + + fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + switch (event) { + .key => if (model.searching) { + const prev_len = this.search.input.items.len; + try this.search.element().handle(model, event); + const next_len = this.search.input.items.len; + + if (next_len == 0) { + model.searching = false; + return; + } + + // NOTE do not include the leading '/' in the `.search` event payload! + if (prev_len != next_len) this.queue.push(.{ .search = this.search.input.items[1..] }); + }, + else => {}, + } + } + + fn content(ctx: *anyopaque, model: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); for (0.., left_text) |idx, cp| { @@ -170,6 +223,11 @@ pub fn InfoBanner(App: type) type { cells[idx].style.emphasis = &.{.dim}; cells[idx].cp = cp; } + + if (model.searching) { + const this: *@This() = @ptrCast(@alignCast(ctx)); + try this.search.element().content(model, cells[size.x..], .{ .x = size.x, .y = 1 }); + } } }; } diff --git a/src/main.zig b/src/main.zig index d8cc8cb..1880c97 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,12 +90,9 @@ pub fn main() !void { } // footer { - var info_banner: InfoBanner = .{}; + var info_banner: InfoBanner = .init(allocator, &app.queue); const footer: App.Container = try .init(allocator, .{ - .size = .{ - .dim = .{ .y = 1 }, - .grow = .horizontal, - }, + .size = .{ .grow = .horizontal }, }, info_banner.element()); try container.append(footer); } @@ -114,7 +111,11 @@ pub fn main() !void { blog = std.mem.trimStart(u8, blog, "./"); blog = std.mem.trimEnd(u8, blog, ".md"); blog = std.mem.trimEnd(u8, blog, "/"); - app.postEvent(.{ .blog = try std.fmt.allocPrint(allocator, "./doc/{s}.md", .{blog}) }); + app.postEvent(.{ + .page = .{ + .blog = try std.fmt.allocPrint(allocator, "./doc/{s}.md", .{blog}), + }, + }); } arg_it.deinit(); @@ -137,10 +138,16 @@ pub fn main() !void { .key => |key| { if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } }) or key.eql(.{ .cp = 'q' })) app.quit(); // test if the event handling is working correctly - if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .blog = try allocator.dupe(u8, "./doc/test.md") }); + if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ + .page = .{ + .blog = try allocator.dupe(u8, "./doc/test.md"), + }, + }); + }, + .page => |page| switch (page) { + .about => log.info(ResourceRequestFormat, .{"./doc/about.md"}), + .blog => |path| log.info(ResourceRequestFormat, .{if (path) |p| p else "./doc/blog.md"}), }, - .about => log.info(ResourceRequestFormat, .{"./doc/about.md"}), - .blog => |path| log.info(ResourceRequestFormat, .{if (path) |p| p else "./doc/blog.md"}), .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), else => {}, } @@ -178,7 +185,10 @@ const zlog = @import("zlog"); const zterm = @import("zterm"); const App = zterm.App( Model, - Model.Pages, + union(enum) { + search: []const u21, + page: Model.Pages, + }, ); const contents = @import("content.zig"); diff --git a/src/model.zig b/src/model.zig index 16a94b1..d81e068 100644 --- a/src/model.zig +++ b/src/model.zig @@ -1,6 +1,8 @@ page: Pages, document: Document, +searching: bool = false, + pub fn deinit(this: *@This(), allocator: Allocator) void { this.page.deinit(allocator); this.document.deinit(allocator); diff --git a/src/navigation.zig b/src/navigation.zig index 9bff127..4be9412 100644 --- a/src/navigation.zig +++ b/src/navigation.zig @@ -1,6 +1,6 @@ -pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type { +pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Model.Pages)) type { const navigation_struct = struct { - fn navigation_fn(page: std.meta.FieldEnum(App.Event)) type { + fn navigation_fn(page: std.meta.FieldEnum(App.Model.Pages)) type { return struct { text: []const u8, highlight: bool, @@ -12,7 +12,6 @@ pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type { .highlight = switch (page) { .about => model.page == .about, .blog => model.page == .blog, - else => @compileError("NavigationButton initiated with non page `App.Event` variant"), }, .queue = queue, }; @@ -36,12 +35,13 @@ pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type { fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { const this: *@This() = @ptrCast(@alignCast(ctx)); switch (event) { - .about, .blog => this.highlight = event == page, + .page => |p| this.highlight = p == page, // TODO accept key input too? .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(switch (page) { - .about => .about, - .blog => .{ .blog = null }, - else => @compileError("The provided navigation button event to trigger is not a `App.Model.Pages` enum variation."), + .about => .{ .page = .about }, + .blog => .{ + .page = .{ .blog = null }, + }, }), else => {}, } @@ -54,8 +54,7 @@ pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type { assert(size.y == 1); for (1.., this.text) |idx, cp| { - cells[idx].style.fg = if (this.highlight) .black else .default; - cells[idx].style.bg = if (this.highlight) .green else .default; + if (this.highlight) cells[idx].style.ul = .green; cells[idx].style.emphasis = &.{.underline}; cells[idx].cp = cp; }