diff --git a/src/tui/Menu.zig b/src/tui/Menu.zig index 90244e8..dd7a55e 100644 --- a/src/tui/Menu.zig +++ b/src/tui/Menu.zig @@ -196,13 +196,18 @@ pub fn State(ctx_type: type) type { } pub fn activate_selected(self: *Self) void { - const selected = self.selected orelse return; + const button = self.get_selected() orelse return; + button.opts.on_click(&button.opts.ctx, button); + } + + pub fn get_selected(self: *Self) ?*button_type { + const selected = self.selected orelse return null; self.selected_active = true; const pos = selected + self.header_count; - if (pos < self.menu.widgets.items.len) { - const button = self.menu.widgets.items[pos].widget.dynamic_cast(button_type) orelse return; - button.opts.on_click(&button.opts.ctx, button); - } + return if (pos < self.menu.widgets.items.len) + self.menu.widgets.items[pos].widget.dynamic_cast(button_type) + else + null; } }; } diff --git a/src/tui/home.zig b/src/tui/home.zig index d8ac0d6..9653d7e 100644 --- a/src/tui/home.zig +++ b/src/tui/home.zig @@ -42,6 +42,7 @@ pub fn create(a: std.mem.Allocator, parent: Widget) !Widget { try self.menu.add_item_with_handler("Open recent project ·(wip)·· :r", menu_action_open_recent_project); try self.menu.add_item_with_handler("Show/Run commands ·········· :p", menu_action_show_commands); try self.menu.add_item_with_handler("Open config file ··········· :c", menu_action_open_config); + try self.menu.add_item_with_handler("Change theme ··············· :t", menu_action_change_theme); try self.menu.add_item_with_handler("Quit/Close ················· :q", menu_action_quit); self.menu.resize(.{ .y = 15, .x = 9, .w = 32 }); command.executeName("enter_mode", command.Context.fmt(.{"home"})) catch {}; @@ -126,6 +127,10 @@ fn menu_action_open_config(_: **Menu.State(*Self), _: *Button.State(*Menu.State( command.executeName("open_config", .{}) catch {}; } +fn menu_action_change_theme(_: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { + command.executeName("change_theme", .{}) catch {}; +} + fn menu_action_quit(_: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { command.executeName("quit", .{}) catch {}; } diff --git a/src/tui/mode/input/flow.zig b/src/tui/mode/input/flow.zig index 842e2ca..27e58b2 100644 --- a/src/tui/mode/input/flow.zig +++ b/src/tui/mode/input/flow.zig @@ -234,6 +234,7 @@ fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) !void { 'U' => self.cmd("delete_to_begin", .{}), 'K' => self.cmd("delete_to_end", .{}), 'D' => self.cmd("move_cursor_next_match", .{}), + 'T' => self.cmd("change_theme", .{}), else => {}, }, else => {}, @@ -389,6 +390,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "select_right", "S-right" }, .{ "select_scroll_down", "C-S-down" }, .{ "select_scroll_up", "C-S-up" }, + .{ "change_theme", "C-k C-t" }, .{ "select_up", "S-up" }, .{ "select_word_left", "C-S-left" }, .{ "select_word_right", "C-S-right" }, diff --git a/src/tui/mode/input/home.zig b/src/tui/mode/input/home.zig index 9b90c71..71853f0 100644 --- a/src/tui/mode/input/home.zig +++ b/src/tui/mode/input/home.zig @@ -14,6 +14,7 @@ const Self = @This(); a: std.mem.Allocator, f: usize = 0, +leader: ?struct { keypress: u32, modifiers: u32 } = null, pub fn create(a: std.mem.Allocator) !tui.Mode { const self: *Self = try a.create(Self); @@ -53,6 +54,7 @@ fn mapEvent(self: *Self, evtype: u32, keypress: u32, modifiers: u32) tp.result { fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress; + if (self.leader) |_| return self.mapFollower(keynormal, modifiers); return switch (modifiers) { mod.CTRL => switch (keynormal) { 'F' => self.sheeran(), @@ -63,6 +65,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'E' => self.cmd("open_recent", .{}), 'P' => self.cmd("open_command_palette", .{}), '/' => self.cmd("open_help", .{}), + 'K' => self.leader = .{ .keypress = keynormal, .modifiers = modifiers }, else => {}, }, mod.CTRL | mod.SHIFT => switch (keynormal) { @@ -94,6 +97,7 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { 'r' => self.msg("open recent project not implemented"), 'p' => self.cmd("open_command_palette", .{}), 'c' => self.cmd("open_config", .{}), + 't' => self.cmd("change_theme", .{}), 'q' => self.cmd("quit", .{}), key.F01 => self.cmd("open_help", .{}), @@ -111,6 +115,24 @@ fn mapPress(self: *Self, keypress: u32, modifiers: u32) tp.result { }; } +fn mapFollower(self: *Self, keypress: u32, modifiers: u32) !void { + defer self.leader = null; + const ldr = if (self.leader) |leader| leader else return; + return switch (ldr.modifiers) { + mod.CTRL => switch (ldr.keypress) { + 'K' => switch (modifiers) { + mod.CTRL => switch (keypress) { + 'T' => self.cmd("change_theme", .{}), + else => {}, + }, + else => {}, + }, + else => {}, + }, + else => {}, + }; +} + fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { try command.executeName(name_, ctx); } @@ -146,6 +168,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "quit", "q, C-q, C-w" }, .{ "quit_without_saving", "C-S-q" }, .{ "restart", "C-S-r" }, + .{ "change_theme", "t, C-k C-t" }, .{ "theme_next", "F10" }, .{ "theme_prev", "F9" }, .{ "toggle_inputview", "F12, A-i" }, diff --git a/src/tui/mode/input/vim/insert.zig b/src/tui/mode/input/vim/insert.zig index bc6ebc8..4a8dfd5 100644 --- a/src/tui/mode/input/vim/insert.zig +++ b/src/tui/mode/input/vim/insert.zig @@ -236,6 +236,7 @@ fn mapFollower(self: *Self, keypress: u32, _: u32, modifiers: u32) !void { 'U' => self.cmd("delete_to_begin", .{}), 'K' => self.cmd("delete_to_end", .{}), 'D' => self.cmd("move_cursor_next_match", .{}), + 'T' => self.cmd("change_theme", .{}), else => {}, }, else => {}, diff --git a/src/tui/mode/input/vim/normal.zig b/src/tui/mode/input/vim/normal.zig index 6b13bc3..05c33cd 100644 --- a/src/tui/mode/input/vim/normal.zig +++ b/src/tui/mode/input/vim/normal.zig @@ -308,6 +308,7 @@ fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) !void { 'U' => self.cmd("delete_to_begin", .{}), 'K' => self.cmd("delete_to_end", .{}), 'D' => self.cmd("move_cursor_next_match", .{}), + 'T' => self.cmd("change_theme", .{}), else => {}, }, else => {}, @@ -580,6 +581,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "select_right", "S-right" }, .{ "select_scroll_down", "C-S-down" }, .{ "select_scroll_up", "C-S-up" }, + .{ "change_theme", "C-k C-t" }, .{ "select_up", "S-up" }, .{ "select_word_left", "C-S-left" }, .{ "select_word_right", "C-S-right" }, diff --git a/src/tui/mode/input/vim/visual.zig b/src/tui/mode/input/vim/visual.zig index c2daf80..6aca3b0 100644 --- a/src/tui/mode/input/vim/visual.zig +++ b/src/tui/mode/input/vim/visual.zig @@ -306,6 +306,7 @@ fn mapFollower(self: *Self, keypress: u32, egc: u32, modifiers: u32) !void { 'U' => self.cmd("delete_to_begin", .{}), 'K' => self.cmd("delete_to_end", .{}), 'D' => self.cmd("move_cursor_next_match", .{}), + 'T' => self.cmd("change_theme", .{}), else => {}, }, else => {}, @@ -540,6 +541,7 @@ const hints = tui.KeybindHints.initComptime(.{ .{ "select_right", "S-right" }, .{ "select_scroll_down", "C-S-down" }, .{ "select_scroll_up", "C-S-up" }, + .{ "change_theme", "C-k C-t" }, .{ "select_up", "S-up" }, .{ "select_word_left", "C-S-left" }, .{ "select_word_right", "C-S-right" }, diff --git a/src/tui/mode/overlay/command_palette.zig b/src/tui/mode/overlay/command_palette.zig index e1b1256..d850f3a 100644 --- a/src/tui/mode/overlay/command_palette.zig +++ b/src/tui/mode/overlay/command_palette.zig @@ -1,449 +1,76 @@ const std = @import("std"); -const tp = @import("thespian"); -const log = @import("log"); const cbor = @import("cbor"); -const fuzzig = @import("fuzzig"); +const tp = @import("thespian"); const root = @import("root"); -const Plane = @import("renderer").Plane; -const key = @import("renderer").input.key; -const mod = @import("renderer").input.modifier; -const event_type = @import("renderer").input.event_type; -const ucs32_to_utf8 = @import("renderer").ucs32_to_utf8; - -const tui = @import("../../tui.zig"); const command = @import("../../command.zig"); -const EventHandler = @import("../../EventHandler.zig"); -const WidgetList = @import("../../WidgetList.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"); -const scrollbar_v = @import("../../scrollbar_v.zig"); -const Self = @This(); -const max_menu_width = 80; +pub const Type = @import("palette.zig").Create(@This()); -a: std.mem.Allocator, -menu: *Menu.State(*Self), -inputbox: *InputBox.State(*Self), -logger: log.Logger, -longest: usize = 0, -palette_commands: command.Collection(cmds) = undefined, -commands: std.ArrayList(Command) = undefined, -hints: ?*const tui.KeybindHints = null, -longest_hint: usize = 0, +pub const label = "Search commands"; -items: usize = 0, -view_rows: usize, -view_pos: usize = 0, -total_items: usize = 0, - -const Command = struct { +pub const Entry = struct { name: []const u8, id: command.ID, used_time: i64, }; -pub fn create(a: std.mem.Allocator) !tui.Mode { - const mv = if (tui.current().mainview.dynamic_cast(mainview)) |mv_| mv_ else return error.NotFound; - const self: *Self = try a.create(Self); - self.* = .{ - .a = a, - .menu = try Menu.create(*Self, a, tui.current().mainview, .{ - .ctx = self, - .on_render = on_render_menu, - .on_resize = on_resize_menu, - .on_scroll = EventHandler.bind(self, Self.on_scroll), - }), - .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 commands", - }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, - .hints = if (tui.current().input_mode) |m| m.keybind_hints else null, - .view_rows = get_view_rows(tui.current().screen()), - .commands = std.ArrayList(Command).init(a), - }; - self.menu.scrollbar.?.style_factory = scrollbar_style; - if (self.hints) |hints| { - for (hints.values()) |val| - self.longest_hint = @max(self.longest_hint, val.len); - } +pub fn load_entries(palette: *Type) !void { for (command.commands.items) |cmd_| if (cmd_) |p| { - (self.commands.addOne() catch @panic("oom")).* = .{ + (palette.entries.addOne() catch @panic("oom")).* = .{ .name = p.name, .id = p.id, .used_time = 0, }; }; - self.restore_state() catch {}; - self.sort_by_used_time(); - try self.palette_commands.init(self); - try self.start_query(); - try mv.floating_views.add(self.menu.container_widget); - return .{ - .handler = EventHandler.to_owned(self), - .name = "󱊒 command", - .description = "command", - }; } -pub fn deinit(self: *Self) void { - self.palette_commands.deinit(); - self.commands.deinit(); - tui.current().message_filters.remove_ptr(self); - if (tui.current().mainview.dynamic_cast(mainview)) |mv| - mv.floating_views.remove(self.menu.container_widget); - self.logger.deinit(); - self.a.destroy(self); +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.a); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.name); + try cbor.writeValue(writer, entry.id); + try cbor.writeValue(writer, if (palette.hints) |hints| hints.get(entry.name) orelse "" else ""); + if (matches) |matches_| + try cbor.writeValue(writer, matches_); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; } -fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.Style { - return if (sb.active) - .{ .fg = theme.scrollbar_active.fg, .bg = theme.editor_widget.bg } - else if (sb.hover) - .{ .fg = theme.scrollbar_hover.fg, .bg = theme.editor_widget.bg } - else - .{ .fg = theme.scrollbar.fg, .bg = theme.editor_widget.bg }; -} - -fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { - const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; - const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; - button.plane.set_base_style(" ", style_base); - button.plane.erase(); - button.plane.home(); - var command_name: []const u8 = undefined; - var keybind_hint: []const u8 = undefined; - var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes - if (!(cbor.matchString(&iter, &command_name) catch false)) - command_name = "#ERROR#"; - var command_id: command.ID = undefined; - if (!(cbor.matchValue(&iter, cbor.extract(&command_id)) catch false)) - command_id = 0; - if (!(cbor.matchString(&iter, &keybind_hint) catch false)) - keybind_hint = ""; - button.plane.set_style(style_keybind); - const pointer = if (selected) "⏵" else " "; - _ = button.plane.print("{s}", .{pointer}) catch {}; - button.plane.set_style(style_base); - _ = button.plane.print("{s} ", .{command_name}) catch {}; - button.plane.set_style(style_keybind); - _ = button.plane.print_aligned_right(0, "{s} ", .{keybind_hint}) catch {}; - var index: usize = 0; - var len = cbor.decodeArrayHeader(&iter) catch return false; - while (len > 0) : (len -= 1) { - if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { - render_cell(&button.plane, 0, index + 1, theme.editor_match) catch break; - } else break; - } - return false; -} - -fn render_cell(plane: *Plane, y: usize, x: usize, style: Widget.Theme.Style) !void { - plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; - var cell = plane.cell_init(); - _ = plane.at_cursor_cell(&cell) catch return; - cell.set_style(style); - _ = plane.putc(&cell) catch {}; -} - -fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { - self.do_resize(); - self.start_query() catch {}; -} - -fn do_resize(self: *Self) void { - const screen = tui.current().screen(); - const w = @min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint; - const x = if (screen.w > w) (screen.w - w) / 2 else 0; - self.view_rows = get_view_rows(screen); - const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); - self.menu.container.resize(.{ .y = 0, .x = x, .w = w, .h = h }); - self.update_scrollbar(); -} - -fn get_view_rows(screen: Widget.Box) usize { - var h = screen.h; - if (h > 0) h = h / 5 * 4; - return h; -} - -fn menu_action_execute_command(menu: **Menu.State(*Self), button: *Button.State(*Menu.State(*Self))) void { +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { var command_name: []const u8 = undefined; var command_id: command.ID = undefined; var iter = button.opts.label; if (!(cbor.matchString(&iter, &command_name) catch false)) return; if (!(cbor.matchValue(&iter, cbor.extract(&command_id)) catch false)) return; - menu.*.opts.ctx.update_used_time(command_id); + update_used_time(menu.*.opts.ctx, command_id); tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); tp.self_pid().send(.{ "cmd", command_name, .{} }) catch |e| menu.*.opts.ctx.logger.err("navigate", e); } -fn on_scroll(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!void { - if (try m.match(.{ "scroll_to", tp.extract(&self.view_pos) })) { - self.start_query() catch |e| return tp.exit_error(e, @errorReturnTrace()); - } -} - -fn update_scrollbar(self: *Self) void { - self.menu.scrollbar.?.set(@intCast(self.total_items), @intCast(self.view_rows), @intCast(self.view_pos)); -} - -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; - var text: []const u8 = undefined; - - if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) { - self.mapEvent(evtype, keypress, egc, modifiers) catch |e| return tp.exit_error(e, @errorReturnTrace()); - } else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { - self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace()); - } - return false; -} - -fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) !void { - return switch (evtype) { - event_type.PRESS => self.mapPress(keypress, egc, modifiers), - event_type.REPEAT => self.mapPress(keypress, egc, modifiers), - event_type.RELEASE => self.mapRelease(keypress, modifiers), - else => {}, - }; -} - -fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) !void { - const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress; - return switch (modifiers) { - mod.CTRL => switch (keynormal) { - 'J' => self.cmd("toggle_panel", .{}), - 'Q' => self.cmd("quit", .{}), - 'W' => self.cmd("close_file", .{}), - 'P' => self.cmd("command_palette_menu_up", .{}), - 'N' => self.cmd("command_palette_menu_down", .{}), - 'V' => self.cmd("system_paste", .{}), - 'C' => self.cmd("exit_overlay_mode", .{}), - 'G' => self.cmd("exit_overlay_mode", .{}), - key.ESC => self.cmd("exit_overlay_mode", .{}), - key.UP => self.cmd("command_palette_menu_up", .{}), - key.DOWN => self.cmd("command_palette_menu_down", .{}), - key.PGUP => self.cmd("command_palette_menu_pageup", .{}), - key.PGDOWN => self.cmd("command_palette_menu_pagedown", .{}), - key.ENTER => self.cmd("command_palette_menu_activate", .{}), - key.BACKSPACE => self.delete_word(), - else => {}, - }, - mod.CTRL | mod.SHIFT => switch (keynormal) { - 'P' => self.cmd("command_palette_menu_down", .{}), - 'Q' => self.cmd("quit_without_saving", .{}), - 'W' => self.cmd("close_file_without_saving", .{}), - 'R' => self.cmd("restart", .{}), - 'L' => self.cmd_async("toggle_panel"), - 'I' => self.cmd_async("toggle_inputview"), - else => {}, - }, - mod.ALT | mod.SHIFT => switch (keynormal) { - 'P' => self.cmd("command_palette_menu_down", .{}), - else => {}, - }, - mod.ALT => switch (keynormal) { - 'P' => self.cmd("command_palette_menu_up", .{}), - 'L' => self.cmd("toggle_panel", .{}), - 'I' => self.cmd("toggle_inputview", .{}), - else => {}, - }, - mod.SHIFT => switch (keypress) { - else => if (!key.synthesized_p(keypress)) - self.insert_code_point(egc) - else {}, - }, - 0 => switch (keypress) { - key.F09 => self.cmd("theme_prev", .{}), - key.F10 => self.cmd("theme_next", .{}), - key.F11 => self.cmd("toggle_panel", .{}), - key.F12 => self.cmd("toggle_inputview", .{}), - key.ESC => self.cmd("exit_overlay_mode", .{}), - key.UP => self.cmd("command_palette_menu_up", .{}), - key.DOWN => self.cmd("command_palette_menu_down", .{}), - key.PGUP => self.cmd("command_palette_menu_pageup", .{}), - key.PGDOWN => self.cmd("command_palette_menu_pagedown", .{}), - key.ENTER => self.cmd("command_palette_menu_activate", .{}), - key.BACKSPACE => self.delete_code_point(), - else => if (!key.synthesized_p(keypress)) - self.insert_code_point(egc) - else {}, - }, - else => {}, - }; -} - -fn mapRelease(self: *Self, keypress: u32, _: u32) !void { - return switch (keypress) { - key.LCTRL, key.RCTRL => if (self.menu.selected orelse 0 > 0) return self.cmd("command_palette_menu_activate", .{}), - else => {}, - }; -} - -fn start_query(self: *Self) !void { - self.items = 0; - self.menu.reset_items(); - self.menu.selected = null; - for (self.commands.items) |cmd_| - self.longest = @max(self.longest, cmd_.name.len); - - if (self.inputbox.text.items.len == 0) { - self.total_items = 0; - var pos: usize = 0; - for (self.commands.items) |cmd_| { - defer self.total_items += 1; - defer pos += 1; - if (pos < self.view_pos) continue; - if (self.items < self.view_rows) - try self.add_item(cmd_.name, cmd_.id, null); - } - } else { - _ = try self.query_commands(self.inputbox.text.items); - } - self.menu.select_down(); - self.do_resize(); -} - -fn query_commands(self: *Self, query: []const u8) error{OutOfMemory}!usize { - var searcher = try fuzzig.Ascii.init( - self.a, - self.longest, // haystack max size - self.longest, // needle max size - .{ .case_sensitive = false }, - ); - defer searcher.deinit(); - - const Match = struct { - name: []const u8, - id: command.ID, - score: i32, - matches: []const usize, - }; - var matches = std.ArrayList(Match).init(self.a); - - for (self.commands.items) |cmd_| { - const match = searcher.scoreMatches(cmd_.name, query); - if (match.score) |score| { - (try matches.addOne()).* = .{ - .name = cmd_.name, - .id = cmd_.id, - .score = score, - .matches = try self.a.dupe(usize, match.matches), - }; - } - } - if (matches.items.len == 0) return 0; - +fn sort_by_used_time(palette: *Type) void { const less_fn = struct { - fn less_fn(_: void, lhs: Match, rhs: Match) bool { - return lhs.score > rhs.score; - } - }.less_fn; - std.mem.sort(Match, matches.items, {}, less_fn); - - var pos: usize = 0; - self.total_items = 0; - for (matches.items) |match| { - defer self.total_items += 1; - defer pos += 1; - if (pos < self.view_pos) continue; - if (self.items < self.view_rows) - try self.add_item(match.name, match.id, match.matches); - } - return matches.items.len; -} - -fn add_item(self: *Self, name: []const u8, id: command.ID, matches: ?[]const usize) !void { - var label = std.ArrayList(u8).init(self.a); - defer label.deinit(); - const writer = label.writer(); - try cbor.writeValue(writer, name); - try cbor.writeValue(writer, id); - try cbor.writeValue(writer, if (self.hints) |hints| hints.get(name) orelse "" else ""); - if (matches) |matches_| - try cbor.writeValue(writer, matches_); - try self.menu.add_item_with_handler(label.items, menu_action_execute_command); - self.items += 1; -} - -fn delete_word(self: *Self) !void { - 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; - self.view_pos = 0; - return self.start_query(); -} - -fn delete_code_point(self: *Self) !void { - 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; - } - self.view_pos = 0; - return self.start_query(); -} - -fn insert_code_point(self: *Self, c: u32) !void { - var buf: [6]u8 = undefined; - const bytes = try ucs32_to_utf8(&[_]u32{c}, &buf); - try self.inputbox.text.appendSlice(buf[0..bytes]); - self.inputbox.cursor = self.inputbox.text.items.len; - self.view_pos = 0; - return self.start_query(); -} - -fn insert_bytes(self: *Self, bytes: []const u8) !void { - try self.inputbox.text.appendSlice(bytes); - self.inputbox.cursor = self.inputbox.text.items.len; - self.view_pos = 0; - return self.start_query(); -} - -fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { - try command.executeName(name_, ctx); -} - -fn msg(_: *Self, text: []const u8) tp.result { - return tp.self_pid().send(.{ "log", "home", text }); -} - -fn cmd_async(_: *Self, name_: []const u8) tp.result { - return tp.self_pid().send(.{ "cmd", name_ }); -} - -fn sort_by_used_time(self: *Self) void { - const less_fn = struct { - fn less_fn(_: void, lhs: Command, rhs: Command) bool { + fn less_fn(_: void, lhs: Entry, rhs: Entry) bool { return lhs.used_time > rhs.used_time; } }.less_fn; - std.mem.sort(Command, self.commands.items, {}, less_fn); + std.mem.sort(Entry, palette.entries.items, {}, less_fn); } -fn update_used_time(self: *Self, id: command.ID) void { - self.set_used_time(id, std.time.milliTimestamp()); - self.write_state() catch {}; +fn update_used_time(palette: *Type, id: command.ID) void { + set_used_time(palette, id, std.time.milliTimestamp()); + write_state(palette) catch {}; } -fn set_used_time(self: *Self, id: command.ID, used_time: i64) void { - for (self.commands.items) |*cmd_| if (cmd_.id == id) { +fn set_used_time(palette: *Type, id: command.ID, used_time: i64) void { + for (palette.entries.items) |*cmd_| if (cmd_.id == id) { cmd_.used_time = used_time; return; }; } -fn write_state(self: *Self) !void { +fn write_state(palette: *Type) !void { var state_file_buffer: [std.fs.max_path_bytes]u8 = undefined; const state_file = try std.fmt.bufPrint(&state_file_buffer, "{s}/{s}", .{ try root.get_state_dir(), "commands" }); var file = try std.fs.createFileAbsolute(state_file, .{ .truncate = true }); @@ -452,7 +79,7 @@ fn write_state(self: *Self) !void { defer buffer.flush() catch {}; const writer = buffer.writer(); - for (self.commands.items) |cmd_| { + for (palette.entries.items) |cmd_| { if (cmd_.used_time == 0) continue; try cbor.writeArrayHeader(writer, 2); try cbor.writeValue(writer, cmd_.name); @@ -460,7 +87,7 @@ fn write_state(self: *Self) !void { } } -fn restore_state(self: *Self) !void { +pub fn restore_state(palette: *Type) !void { var state_file_buffer: [std.fs.max_path_bytes]u8 = undefined; const state_file = try std.fmt.bufPrint(&state_file_buffer, "{s}/{s}", .{ try root.get_state_dir(), "commands" }); const a = std.heap.c_allocator; @@ -486,59 +113,7 @@ fn restore_state(self: *Self) !void { else => return e, }) { const id = command.getId(name) orelse continue; - self.set_used_time(id, used_time); + set_used_time(palette, id, used_time); } + sort_by_used_time(palette); } - -const cmds = struct { - pub const Target = Self; - const Ctx = command.Context; - const Result = command.Result; - - pub fn command_palette_menu_down(self: *Self, _: Ctx) Result { - if (self.menu.selected) |selected| { - if (selected == self.view_rows - 1) { - self.view_pos += 1; - try self.start_query(); - self.menu.select_last(); - return; - } - } - self.menu.select_down(); - } - - pub fn command_palette_menu_up(self: *Self, _: Ctx) Result { - if (self.menu.selected) |selected| { - if (selected == 0 and self.view_pos > 0) { - self.view_pos -= 1; - try self.start_query(); - self.menu.select_first(); - return; - } - } - self.menu.select_up(); - } - - pub fn command_palette_menu_pagedown(self: *Self, _: Ctx) Result { - if (self.total_items > self.view_rows) { - self.view_pos += self.view_rows; - if (self.view_pos > self.total_items - self.view_rows) - self.view_pos = self.total_items - self.view_rows; - } - try self.start_query(); - self.menu.select_last(); - } - - pub fn command_palette_menu_pageup(self: *Self, _: Ctx) Result { - if (self.view_pos > self.view_rows) - self.view_pos -= self.view_rows - else - self.view_pos = 0; - try self.start_query(); - self.menu.select_first(); - } - - pub fn command_palette_menu_activate(self: *Self, _: Ctx) Result { - self.menu.activate_selected(); - } -}; diff --git a/src/tui/mode/overlay/palette.zig b/src/tui/mode/overlay/palette.zig new file mode 100644 index 0000000..2457ac3 --- /dev/null +++ b/src/tui/mode/overlay/palette.zig @@ -0,0 +1,484 @@ +const std = @import("std"); +const tp = @import("thespian"); +const log = @import("log"); +const cbor = @import("cbor"); +const fuzzig = @import("fuzzig"); + +const Plane = @import("renderer").Plane; +const key = @import("renderer").input.key; +const mod = @import("renderer").input.modifier; +const event_type = @import("renderer").input.event_type; +const ucs32_to_utf8 = @import("renderer").ucs32_to_utf8; + +const tui = @import("../../tui.zig"); +const command = @import("../../command.zig"); +const EventHandler = @import("../../EventHandler.zig"); +const WidgetList = @import("../../WidgetList.zig"); +const Button = @import("../../Button.zig"); +const InputBox = @import("../../InputBox.zig"); +const Widget = @import("../../Widget.zig"); +const mainview = @import("../../mainview.zig"); +const scrollbar_v = @import("../../scrollbar_v.zig"); + +pub const Menu = @import("../../Menu.zig"); + +const max_menu_width = 80; + +pub fn Create(options: type) type { + return struct { + a: std.mem.Allocator, + menu: *Menu.State(*Self), + inputbox: *InputBox.State(*Self), + logger: log.Logger, + longest: usize = 0, + commands: command.Collection(cmds) = undefined, + entries: std.ArrayList(Entry) = undefined, + hints: ?*const tui.KeybindHints = null, + longest_hint: usize = 0, + + items: usize = 0, + view_rows: usize, + view_pos: usize = 0, + total_items: usize = 0, + + const Entry = options.Entry; + const Self = @This(); + + pub const MenuState = Menu.State(*Self); + pub const ButtonState = Button.State(*Menu.State(*Self)); + + pub fn create(a: std.mem.Allocator) !tui.Mode { + const mv = if (tui.current().mainview.dynamic_cast(mainview)) |mv_| mv_ else return error.NotFound; + const self: *Self = try a.create(Self); + self.* = .{ + .a = a, + .menu = try Menu.create(*Self, a, tui.current().mainview, .{ + .ctx = self, + .on_render = on_render_menu, + .on_resize = on_resize_menu, + .on_scroll = EventHandler.bind(self, Self.on_scroll), + .on_click4 = mouse_click_button4, + .on_click5 = mouse_click_button5, + }), + .logger = log.logger(@typeName(Self)), + .inputbox = (try self.menu.add_header(try InputBox.create(*Self, self.a, self.menu.menu.parent, .{ + .ctx = self, + .label = options.label, + }))).dynamic_cast(InputBox.State(*Self)) orelse unreachable, + .hints = if (tui.current().input_mode) |m| m.keybind_hints else null, + .view_rows = get_view_rows(tui.current().screen()), + .entries = std.ArrayList(Entry).init(a), + }; + self.menu.scrollbar.?.style_factory = scrollbar_style; + if (self.hints) |hints| { + for (hints.values()) |val| + self.longest_hint = @max(self.longest_hint, val.len); + } + try options.load_entries(self); + if (@hasDecl(options, "restore_state")) + options.restore_state(self) catch {}; + try self.commands.init(self); + try self.start_query(); + try mv.floating_views.add(self.menu.container_widget); + return .{ + .handler = EventHandler.to_owned(self), + .name = "󱊒 command", + .description = "command", + }; + } + + pub fn deinit(self: *Self) void { + self.commands.deinit(); + self.entries.deinit(); + tui.current().message_filters.remove_ptr(self); + if (tui.current().mainview.dynamic_cast(mainview)) |mv| + mv.floating_views.remove(self.menu.container_widget); + self.logger.deinit(); + self.a.destroy(self); + } + + fn scrollbar_style(sb: *scrollbar_v, theme: *const Widget.Theme) Widget.Theme.Style { + return if (sb.active) + .{ .fg = theme.scrollbar_active.fg, .bg = theme.editor_widget.bg } + else if (sb.hover) + .{ .fg = theme.scrollbar_hover.fg, .bg = theme.editor_widget.bg } + else + .{ .fg = theme.scrollbar.fg, .bg = theme.editor_widget.bg }; + } + + fn on_render_menu(_: *Self, button: *Button.State(*Menu.State(*Self)), theme: *const Widget.Theme, selected: bool) bool { + const style_base = if (button.active) theme.editor_cursor else if (button.hover or selected) theme.editor_selection else theme.editor_widget; + const style_keybind = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_base; + button.plane.set_base_style(" ", style_base); + button.plane.erase(); + button.plane.home(); + var command_name: []const u8 = undefined; + var keybind_hint: []const u8 = undefined; + var iter = button.opts.label; // label contains cbor, first the file name, then multiple match indexes + if (!(cbor.matchString(&iter, &command_name) catch false)) + command_name = "#ERROR#"; + var command_id: command.ID = undefined; + if (!(cbor.matchValue(&iter, cbor.extract(&command_id)) catch false)) + command_id = 0; + if (!(cbor.matchString(&iter, &keybind_hint) catch false)) + keybind_hint = ""; + button.plane.set_style(style_keybind); + const pointer = if (selected) "⏵" else " "; + _ = button.plane.print("{s}", .{pointer}) catch {}; + button.plane.set_style(style_base); + _ = button.plane.print("{s} ", .{command_name}) catch {}; + button.plane.set_style(style_keybind); + _ = button.plane.print_aligned_right(0, "{s} ", .{keybind_hint}) catch {}; + var index: usize = 0; + var len = cbor.decodeArrayHeader(&iter) catch return false; + while (len > 0) : (len -= 1) { + if (cbor.matchValue(&iter, cbor.extract(&index)) catch break) { + render_cell(&button.plane, 0, index + 1, theme.editor_match) catch break; + } else break; + } + return false; + } + + fn render_cell(plane: *Plane, y: usize, x: usize, style: Widget.Theme.Style) !void { + plane.cursor_move_yx(@intCast(y), @intCast(x)) catch return; + var cell = plane.cell_init(); + _ = plane.at_cursor_cell(&cell) catch return; + cell.set_style(style); + _ = plane.putc(&cell) catch {}; + } + + fn on_resize_menu(self: *Self, _: *Menu.State(*Self), _: Widget.Box) void { + self.do_resize(); + self.start_query() catch {}; + } + + fn do_resize(self: *Self) void { + const screen = tui.current().screen(); + const w = @min(self.longest, max_menu_width) + 2 + 1 + self.longest_hint; + const x = if (screen.w > w) (screen.w - w) / 2 else 0; + self.view_rows = get_view_rows(screen); + const h = @min(self.items + self.menu.header_count, self.view_rows + self.menu.header_count); + self.menu.container.resize(.{ .y = 0, .x = x, .w = w, .h = h }); + self.update_scrollbar(); + } + + fn get_view_rows(screen: Widget.Box) usize { + var h = screen.h; + if (h > 0) h = h / 5 * 4; + return h; + } + + fn on_scroll(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!void { + if (try m.match(.{ "scroll_to", tp.extract(&self.view_pos) })) { + self.start_query() catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + } + + fn update_scrollbar(self: *Self) void { + self.menu.scrollbar.?.set(@intCast(self.total_items), @intCast(self.view_rows), @intCast(self.view_pos)); + } + + fn mouse_click_button4(menu: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { + const self = &menu.*.opts.ctx.*; + if (self.view_pos < Menu.scroll_lines) { + self.view_pos = 0; + } else { + self.view_pos -= Menu.scroll_lines; + } + self.update_scrollbar(); + self.start_query() catch {}; + } + + fn mouse_click_button5(menu: **Menu.State(*Self), _: *Button.State(*Menu.State(*Self))) void { + const self = &menu.*.opts.ctx.*; + if (self.view_pos < @max(self.total_items, self.view_rows) - self.view_rows) + self.view_pos += Menu.scroll_lines; + self.update_scrollbar(); + self.start_query() catch {}; + } + + 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; + var text: []const u8 = undefined; + + if (try m.match(.{ "I", tp.extract(&evtype), tp.extract(&keypress), tp.extract(&egc), tp.string, tp.extract(&modifiers) })) { + self.mapEvent(evtype, keypress, egc, modifiers) catch |e| return tp.exit_error(e, @errorReturnTrace()); + } else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { + self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace()); + } + return false; + } + + fn mapEvent(self: *Self, evtype: u32, keypress: u32, egc: u32, modifiers: u32) !void { + return switch (evtype) { + event_type.PRESS => self.mapPress(keypress, egc, modifiers), + event_type.REPEAT => self.mapPress(keypress, egc, modifiers), + event_type.RELEASE => self.mapRelease(keypress, modifiers), + else => {}, + }; + } + + fn mapPress(self: *Self, keypress: u32, egc: u32, modifiers: u32) !void { + const keynormal = if ('a' <= keypress and keypress <= 'z') keypress - ('a' - 'A') else keypress; + return switch (modifiers) { + mod.CTRL => switch (keynormal) { + 'J' => self.cmd("toggle_panel", .{}), + 'Q' => self.cmd("quit", .{}), + 'W' => self.cmd("close_file", .{}), + 'P' => self.cmd("palette_menu_up", .{}), + 'N' => self.cmd("palette_menu_down", .{}), + 'V' => self.cmd("system_paste", .{}), + 'C' => self.cmd("palette_menu_cancel", .{}), + 'G' => self.cmd("palette_menu_cancel", .{}), + key.ESC => self.cmd("palette_menu_cancel", .{}), + key.UP => self.cmd("palette_menu_up", .{}), + key.DOWN => self.cmd("palette_menu_down", .{}), + key.PGUP => self.cmd("palette_menu_pageup", .{}), + key.PGDOWN => self.cmd("palette_menu_pagedown", .{}), + key.ENTER => self.cmd("palette_menu_activate", .{}), + key.BACKSPACE => self.delete_word(), + else => {}, + }, + mod.CTRL | mod.SHIFT => switch (keynormal) { + 'P' => self.cmd("palette_menu_down", .{}), + 'Q' => self.cmd("quit_without_saving", .{}), + 'W' => self.cmd("close_file_without_saving", .{}), + 'R' => self.cmd("restart", .{}), + 'L' => self.cmd_async("toggle_panel"), + 'I' => self.cmd_async("toggle_inputview"), + else => {}, + }, + mod.ALT | mod.SHIFT => switch (keynormal) { + 'P' => self.cmd("palette_menu_down", .{}), + else => {}, + }, + mod.ALT => switch (keynormal) { + 'P' => self.cmd("palette_menu_up", .{}), + 'L' => self.cmd("toggle_panel", .{}), + 'I' => self.cmd("toggle_inputview", .{}), + else => {}, + }, + mod.SHIFT => switch (keypress) { + else => if (!key.synthesized_p(keypress)) + self.insert_code_point(egc) + else {}, + }, + 0 => switch (keypress) { + key.F09 => self.cmd("theme_prev", .{}), + key.F10 => self.cmd("theme_next", .{}), + key.F11 => self.cmd("toggle_panel", .{}), + key.F12 => self.cmd("toggle_inputview", .{}), + key.ESC => self.cmd("palette_menu_cancel", .{}), + key.UP => self.cmd("palette_menu_up", .{}), + key.DOWN => self.cmd("palette_menu_down", .{}), + key.PGUP => self.cmd("palette_menu_pageup", .{}), + key.PGDOWN => self.cmd("palette_menu_pagedown", .{}), + key.ENTER => self.cmd("palette_menu_activate", .{}), + key.BACKSPACE => self.delete_code_point(), + else => if (!key.synthesized_p(keypress)) + self.insert_code_point(egc) + else {}, + }, + else => {}, + }; + } + + fn mapRelease(self: *Self, keypress: u32, _: u32) !void { + return switch (keypress) { + key.LCTRL, key.RCTRL => if (self.menu.selected orelse 0 > 0) return self.cmd("palette_menu_activate", .{}), + else => {}, + }; + } + + fn start_query(self: *Self) !void { + self.items = 0; + self.menu.reset_items(); + self.menu.selected = null; + for (self.entries.items) |entry| + self.longest = @max(self.longest, entry.name.len); + + if (self.inputbox.text.items.len == 0) { + self.total_items = 0; + var pos: usize = 0; + for (self.entries.items) |*entry| { + defer self.total_items += 1; + defer pos += 1; + if (pos < self.view_pos) continue; + if (self.items < self.view_rows) + try options.add_menu_entry(self, entry, null); + } + } else { + _ = try self.query_entries(self.inputbox.text.items); + } + self.menu.select_down(); + self.do_resize(); + tui.current().refresh_hover(); + self.selection_updated(); + } + + fn query_entries(self: *Self, query: []const u8) error{OutOfMemory}!usize { + var searcher = try fuzzig.Ascii.init( + self.a, + self.longest, // haystack max size + self.longest, // needle max size + .{ .case_sensitive = false }, + ); + defer searcher.deinit(); + + const Match = struct { + entry: *Entry, + score: i32, + matches: []const usize, + }; + + var matches = std.ArrayList(Match).init(self.a); + + for (self.entries.items) |*entry| { + const match = searcher.scoreMatches(entry.name, query); + if (match.score) |score| + (try matches.addOne()).* = .{ + .entry = entry, + .score = score, + .matches = try self.a.dupe(usize, match.matches), + }; + } + if (matches.items.len == 0) return 0; + + const less_fn = struct { + fn less_fn(_: void, lhs: Match, rhs: Match) bool { + return lhs.score > rhs.score; + } + }.less_fn; + std.mem.sort(Match, matches.items, {}, less_fn); + + var pos: usize = 0; + self.total_items = 0; + for (matches.items) |*match| { + defer self.total_items += 1; + defer pos += 1; + if (pos < self.view_pos) continue; + if (self.items < self.view_rows) + try options.add_menu_entry(self, match.entry, match.matches); + } + return matches.items.len; + } + + fn delete_word(self: *Self) !void { + 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; + self.view_pos = 0; + return self.start_query(); + } + + fn delete_code_point(self: *Self) !void { + 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; + } + self.view_pos = 0; + return self.start_query(); + } + + fn insert_code_point(self: *Self, c: u32) !void { + var buf: [6]u8 = undefined; + const bytes = try ucs32_to_utf8(&[_]u32{c}, &buf); + try self.inputbox.text.appendSlice(buf[0..bytes]); + self.inputbox.cursor = self.inputbox.text.items.len; + self.view_pos = 0; + return self.start_query(); + } + + fn insert_bytes(self: *Self, bytes: []const u8) !void { + try self.inputbox.text.appendSlice(bytes); + self.inputbox.cursor = self.inputbox.text.items.len; + self.view_pos = 0; + return self.start_query(); + } + + fn cmd(_: *Self, name_: []const u8, ctx: command.Context) tp.result { + try command.executeName(name_, ctx); + } + + fn msg(_: *Self, text: []const u8) tp.result { + return tp.self_pid().send(.{ "log", "home", text }); + } + + fn cmd_async(_: *Self, name_: []const u8) tp.result { + return tp.self_pid().send(.{ "cmd", name_ }); + } + + fn selection_updated(self: *Self) void { + if (@hasDecl(options, "updated")) + options.updated(self, self.menu.get_selected()) catch {}; + } + + const cmds = struct { + pub const Target = Self; + const Ctx = command.Context; + const Result = command.Result; + + pub fn palette_menu_down(self: *Self, _: Ctx) Result { + if (self.menu.selected) |selected| { + if (selected == self.view_rows - 1) { + self.view_pos += 1; + try self.start_query(); + self.menu.select_last(); + return; + } + } + self.menu.select_down(); + self.selection_updated(); + } + + pub fn palette_menu_up(self: *Self, _: Ctx) Result { + if (self.menu.selected) |selected| { + if (selected == 0 and self.view_pos > 0) { + self.view_pos -= 1; + try self.start_query(); + self.menu.select_first(); + return; + } + } + self.menu.select_up(); + self.selection_updated(); + } + + pub fn palette_menu_pagedown(self: *Self, _: Ctx) Result { + if (self.total_items > self.view_rows) { + self.view_pos += self.view_rows; + if (self.view_pos > self.total_items - self.view_rows) + self.view_pos = self.total_items - self.view_rows; + } + try self.start_query(); + self.menu.select_last(); + self.selection_updated(); + } + + pub fn palette_menu_pageup(self: *Self, _: Ctx) Result { + if (self.view_pos > self.view_rows) + self.view_pos -= self.view_rows + else + self.view_pos = 0; + try self.start_query(); + self.menu.select_first(); + self.selection_updated(); + } + + pub fn palette_menu_activate(self: *Self, _: Ctx) Result { + self.menu.activate_selected(); + } + + pub fn palette_menu_cancel(self: *Self, _: Ctx) Result { + if (@hasDecl(options, "cancel")) try options.cancel(self); + try self.cmd("exit_overlay_mode", .{}); + } + }; + }; +} diff --git a/src/tui/mode/overlay/theme_palette.zig b/src/tui/mode/overlay/theme_palette.zig new file mode 100644 index 0000000..0a052c6 --- /dev/null +++ b/src/tui/mode/overlay/theme_palette.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const cbor = @import("cbor"); +const tp = @import("thespian"); + +const Widget = @import("../../Widget.zig"); +const tui = @import("../../tui.zig"); + +pub const Type = @import("palette.zig").Create(@This()); + +pub const label = "Search themes"; + +pub const Entry = struct { + name: []const u8, +}; + +pub const Match = struct { + name: []const u8, + score: i32, + matches: []const usize, +}; + +var previous_theme: ?[]const u8 = null; +pub fn load_entries(palette: *Type) !void { + previous_theme = tui.current().theme.name; + for (Widget.themes) |theme| { + (palette.entries.addOne() catch @panic("oom")).* = .{ + .name = theme.name, + }; + } +} + +pub fn add_menu_entry(palette: *Type, entry: *Entry, matches: ?[]const usize) !void { + var value = std.ArrayList(u8).init(palette.a); + defer value.deinit(); + const writer = value.writer(); + try cbor.writeValue(writer, entry.name); + try cbor.writeValue(writer, if (palette.hints) |hints| hints.get(entry.name) orelse "" else ""); + if (matches) |matches_| + try cbor.writeValue(writer, matches_); + try palette.menu.add_item_with_handler(value.items, select); + palette.items += 1; +} + +fn select(menu: **Type.MenuState, button: *Type.ButtonState) void { + var name: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &name) catch false)) return; + tp.self_pid().send(.{ "cmd", "exit_overlay_mode" }) catch |e| menu.*.opts.ctx.logger.err("theme_palette", e); + tp.self_pid().send(.{ "cmd", "set_theme", .{name} }) catch |e| menu.*.opts.ctx.logger.err("theme_palette", e); +} + +pub fn updated(palette: *Type, button_: ?*Type.ButtonState) !void { + const button = button_ orelse return cancel(palette); + var name: []const u8 = undefined; + var iter = button.opts.label; + if (!(cbor.matchString(&iter, &name) catch false)) return; + tp.self_pid().send(.{ "cmd", "set_theme", .{name} }) catch |e| palette.logger.err("theme_palette upated", e); +} + +pub fn cancel(palette: *Type) !void { + if (previous_theme) |name| if (!std.mem.eql(u8, name, tui.current().theme.name)) { + previous_theme = null; + tp.self_pid().send(.{ "cmd", "set_theme", .{name} }) catch |e| palette.logger.err("theme_palette cancel", e); + }; +} diff --git a/src/tui/tui.zig b/src/tui/tui.zig index faac039..cbad2ca 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -41,6 +41,8 @@ input_listeners: EventHandler.List, keyboard_focus: ?Widget = null, mini_mode: ?MiniModeState = null, hover_focus: ?*Widget = null, +last_hover_x: c_int = 0, +last_hover_y: c_int = 0, commands: Commands = undefined, logger: log.Logger, drag_source: ?*Widget = null, @@ -481,25 +483,10 @@ fn send_mouse(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) _ = self.input_listeners.send(from, m) catch {}; if (self.keyboard_focus) |w| { _ = try w.send(from, m); - } else if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| { - if (if (self.hover_focus) |h| h != w else true) { - var buf: [256]u8 = undefined; - if (self.hover_focus) |h| { - if (self.is_live_widget_ptr(h)) - _ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace())); - } - self.hover_focus = w; - _ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e, @errorReturnTrace())); - } - _ = try w.send(from, m); - } else { - if (self.hover_focus) |h| { - var buf: [256]u8 = undefined; - if (self.is_live_widget_ptr(h)) - _ = try h.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", false }) catch |e| return tp.exit_error(e, @errorReturnTrace())); - } - self.hover_focus = null; + return; } + if (try self.update_hover(y, x)) |w| + _ = try w.send(from, m); } fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.message) tp.result { @@ -508,7 +495,15 @@ fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.mess if (self.keyboard_focus) |w| { _ = try w.send(from, m); return; - } else if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| { + } + _ = try self.update_hover(y, x); + if (self.drag_source) |w| _ = try w.send(from, m); +} + +fn update_hover(self: *Self, y: c_int, x: c_int) !?*Widget { + self.last_hover_y = y; + self.last_hover_x = x; + if (self.find_coord_widget(@intCast(y), @intCast(x))) |w| { if (if (self.hover_focus) |h| h != w else true) { var buf: [256]u8 = undefined; if (self.hover_focus) |h| { @@ -518,10 +513,11 @@ fn send_mouse_drag(self: *Self, y: c_int, x: c_int, from: tp.pid_ref, m: tp.mess self.hover_focus = w; _ = try w.send(tp.self_pid(), tp.message.fmtbuf(&buf, .{ "H", true }) catch |e| return tp.exit_error(e, @errorReturnTrace())); } + return w; } else { try self.clear_hover_focus(); + return null; } - if (self.drag_source) |w| _ = try w.send(from, m); } fn clear_hover_focus(self: *Self) tp.result { @@ -533,10 +529,22 @@ fn clear_hover_focus(self: *Self) tp.result { self.hover_focus = null; } +pub fn refresh_hover(self: *Self) void { + self.clear_hover_focus() catch return; + _ = self.update_hover(self.last_hover_y, self.last_hover_x) catch {}; +} + pub fn save_config(self: *const Self) !void { try root.write_config(self.config, self.a); } +fn enter_overlay_mode(self: *Self, mode: type) command.Result { + if (self.mini_mode) |_| try cmds.exit_mini_mode(self, .{}); + if (self.input_mode_outer) |_| try cmds.exit_overlay_mode(self, .{}); + self.input_mode_outer = self.input_mode; + self.input_mode = try mode.create(self.a); +} + const cmds = struct { pub const Target = Self; const Ctx = command.Context; @@ -552,6 +560,20 @@ const cmds = struct { root.exit(99); } + pub fn set_theme(self: *Self, ctx: Ctx) Result { + var name: []const u8 = undefined; + if (!try ctx.args.match(.{tp.extract(&name)})) + return tp.exit_error(error.InvalidArgument, null); + self.theme = get_theme_by_name(name) orelse { + self.logger.print("theme not found: {s}", .{name}); + return; + }; + self.config.theme = self.theme.name; + self.rdr.set_terminal_style(self.theme.editor); + self.logger.print("theme: {s}", .{self.theme.description}); + try self.save_config(); + } + pub fn theme_next(self: *Self, _: Ctx) Result { self.theme = get_next_theme_by_name(self.theme.name); self.config.theme = self.theme.name; @@ -612,17 +634,15 @@ const cmds = struct { } pub fn open_command_palette(self: *Self, _: Ctx) Result { - if (self.mini_mode) |_| try exit_mini_mode(self, .{}); - if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{}); - self.input_mode_outer = self.input_mode; - self.input_mode = try @import("mode/overlay/command_palette.zig").create(self.a); + return self.enter_overlay_mode(@import("mode/overlay/command_palette.zig").Type); } pub fn open_recent(self: *Self, _: Ctx) Result { - if (self.mini_mode) |_| try exit_mini_mode(self, .{}); - if (self.input_mode_outer) |_| try exit_overlay_mode(self, .{}); - self.input_mode_outer = self.input_mode; - self.input_mode = try @import("mode/overlay/open_recent.zig").create(self.a); + return self.enter_overlay_mode(@import("mode/overlay/open_recent.zig")); + } + + pub fn change_theme(self: *Self, _: Ctx) Result { + return self.enter_overlay_mode(@import("mode/overlay/theme_palette.zig").Type); } pub fn exit_overlay_mode(self: *Self, _: Ctx) Result { @@ -729,6 +749,7 @@ pub fn need_render() void { pub fn resize(self: *Self) void { self.mainview.resize(self.screen()); + self.refresh_hover(); need_render(); }