From 744c6012a7379c735bf531baec4a630ff11cdfe3 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Thu, 28 Mar 2024 22:18:23 +0100 Subject: [PATCH] feat: add search box to open recent file mode --- src/tui/mode/overlay/open_recent.zig | 98 +++++++++++++++++++++++----- src/tui/tui.zig | 3 + 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/src/tui/mode/overlay/open_recent.zig b/src/tui/mode/overlay/open_recent.zig index bb96d7e..1fce36d 100644 --- a/src/tui/mode/overlay/open_recent.zig +++ b/src/tui/mode/overlay/open_recent.zig @@ -8,6 +8,7 @@ const command = @import("../../command.zig"); const EventHandler = @import("../../EventHandler.zig"); const MessageFilter = @import("../../MessageFilter.zig"); const Button = @import("../../Button.zig"); +const InputBox = @import("../../InputBox.zig"); const Menu = @import("../../Menu.zig"); const Widget = @import("../../Widget.zig"); const mainview = @import("../../mainview.zig"); @@ -19,8 +20,11 @@ const max_recent_files: usize = 25; a: std.mem.Allocator, f: usize = 0, menu: *Menu.State(*Self), +inputbox: *InputBox.State(*Self), logger: log.Logger, -count: usize = 0, +query_pending: bool = false, +need_reset: bool = false, +need_select_first: bool = true, longest: usize = 0, commands: Commands = undefined, @@ -31,9 +35,14 @@ pub fn create(a: std.mem.Allocator) !tui.Mode { .a = a, .menu = try Menu.create(*Self, a, tui.current().mainview, .{ .ctx = self, .on_render = on_render_menu, .on_resize = on_resize_menu }), .logger = log.logger(@typeName(Self)), + .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.a, self.menu.menu.parent, .{ + .ctx = self, + .label = "Search files by name", + }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, }; try self.commands.init(self); try tui.current().message_filters.add(MessageFilter.bind(self, receive_project_manager)); + self.query_pending = true; try project_manager.request_recent_files(max_recent_files); self.menu.resize(.{ .y = 0, .x = 25, .w = 32 }); try mv.floating_views.add(self.menu.menu_widget); @@ -87,14 +96,22 @@ fn receive_project_manager(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit fn process_project_manager(self: *Self, m: tp.message) tp.result { var file_name: []const u8 = undefined; + var query: []const u8 = undefined; if (try m.match(.{ "PRJ", "recent", tp.extract(&file_name) })) { - if (self.count < max_recent_files) { - self.count += 1; - self.longest = @max(self.longest, file_name.len); - self.menu.add_item_with_handler(file_name, menu_action_open_file) catch |e| return tp.exit_error(e); - self.menu.resize(.{ .y = 0, .x = 25, .w = @min(self.longest, 80) + 2 }); - tui.need_render(); + if (self.need_reset) self.reset_results(); + self.longest = @max(self.longest, file_name.len); + self.menu.add_item_with_handler(file_name, menu_action_open_file) catch |e| return tp.exit_error(e); + self.menu.resize(.{ .y = 0, .x = 25, .w = @min(self.longest, 80) + 2 }); + if (self.need_select_first) { + self.menu.select_down(); + self.need_select_first = false; } + tui.need_render(); + } else if (try m.match(.{ "PRJ", "recent_done", tp.extract(&query) })) { + self.query_pending = false; + self.need_reset = true; + if (!std.mem.eql(u8, self.inputbox.text.items, query)) + try self.start_query(); } else { self.logger.err("receive", tp.unexpected(m)); } @@ -103,24 +120,25 @@ fn process_project_manager(self: *Self, m: tp.message) tp.result { pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool { var evtype: u32 = undefined; var keypress: u32 = undefined; + var egc: u32 = undefined; var modifiers: u32 = undefined; - if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.any, tp.string, tp.extract(&modifiers) })) { - try self.mapEvent(evtype, keypress, modifiers); + if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) { + try self.mapEvent(evtype, keypress, egc, modifiers); } return false; } -fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result { +fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) tp.result { return switch (evtype) { - nc.event_type.PRESS => self.mapPress(keypress, modifiers), - nc.event_type.REPEAT => self.mapPress(keypress, modifiers), + nc.event_type.PRESS => self.mapPress(keypress, egc, modifiers), + nc.event_type.REPEAT => self.mapPress(keypress, egc, modifiers), nc.event_type.RELEASE => self.mapRelease(keypress, modifiers), else => {}, }; } -fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { +fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) tp.result { const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress; return switch (modifiers) { nc.mod.CTRL => switch (keynormal) { @@ -128,10 +146,13 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'Q' => self.cmd("quit", .{}), 'W' => self.cmd("close_file", .{}), 'E' => self.cmd("open_recent_menu_down", .{}), + 'P' => self.cmd("open_recent_menu_up", .{}), + 'N' => self.cmd("open_recent_menu_down", .{}), nc.key.ESC => self.cmd("exit_overlay_mode", .{}), nc.key.UP => self.cmd("open_recent_menu_up", .{}), nc.key.DOWN => self.cmd("open_recent_menu_down", .{}), nc.key.ENTER => self.cmd("open_recent_menu_activate", .{}), + nc.key.BACKSPACE => self.delete_word(), else => {}, }, nc.mod.CTRL | nc.mod.SHIFT => switch (keynormal) { @@ -147,6 +168,11 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'I' => self.cmd("toggle_inputview", .{}), else => {}, }, + nc.mod.SHIFT => switch (keypress) { + else => if (!nc.key.synthesized_p(keypress)) + self.insert_code_point(egc) + else {}, + }, 0 => switch (keypress) { nc.key.F09 => self.cmd("theme_prev", .{}), nc.key.F10 => self.cmd("theme_next", .{}), @@ -156,7 +182,10 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { nc.key.UP => self.cmd("open_recent_menu_up", .{}), nc.key.DOWN => self.cmd("open_recent_menu_down", .{}), nc.key.ENTER => self.cmd("open_recent_menu_activate", .{}), - else => {}, + nc.key.BACKSPACE => self.delete_code_point(), + else => if (!nc.key.synthesized_p(keypress)) + self.insert_code_point(egc) + else {}, }, else => {}, }; @@ -164,11 +193,50 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { fn mapRelease(self: *Self, keypress: u32, _: u32) tp.result { return switch (keypress) { - nc.key.LCTRL, nc.key.RCTRL => self.cmd("open_recent_menu_activate", .{}), + nc.key.LCTRL, nc.key.RCTRL => if (self.menu.selected orelse 0 > 0) return self.cmd("open_recent_menu_activate", .{}), else => {}, }; } +fn reset_results(self: *Self) void { + self.need_reset = false; + self.menu.reset_items(); + self.menu.selected = null; + self.need_select_first = true; +} + +fn start_query(self: *Self) tp.result { + if (self.query_pending) return; + self.query_pending = true; + try project_manager.query_recent_files(max_recent_files, self.inputbox.text.items); +} + +fn delete_word(self: *Self) tp.result { + if (std.mem.lastIndexOfAny(u8, self.inputbox.text.items, "/\\. -_")) |pos| { + self.inputbox.text.shrinkRetainingCapacity(pos); + } else { + self.inputbox.text.shrinkRetainingCapacity(0); + } + self.inputbox.cursor = self.inputbox.text.items.len; + return self.start_query(); +} + +fn delete_code_point(self: *Self) tp.result { + if (self.inputbox.text.items.len > 0) { + self.inputbox.text.shrinkRetainingCapacity(self.inputbox.text.items.len - 1); + self.inputbox.cursor = self.inputbox.text.items.len; + } + return self.start_query(); +} + +fn insert_code_point(self: *Self, c: u32) tp.result { + var buf: [6]u8 = undefined; + const bytes = nc.ucs32_to_utf8(&[_]u32{c}, &buf) catch |e| return tp.exit_error(e); + self.inputbox.text.appendSlice(buf[0..bytes]) catch |e| return tp.exit_error(e); + self.inputbox.cursor = self.inputbox.text.items.len; + return self.start_query(); +} + fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { try command.executeName(name_, ctx); } diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 09ac098..8e93055 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -299,6 +299,9 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) tp.result { if (try m.match(.{ "exit", "timeout_error", 125, "Operation aborted." })) return; + if (try m.match(.{ "PRJ", tp.more })) // drop late project manager query responses + return; + return tp.unexpected(m); }