feat: add theme selection palette
Also, refactor to share palette code and make palettes scroll properly with the mouse wheel.
This commit is contained in:
parent
8c3c786357
commit
4b6c08154b
11 changed files with 675 additions and 490 deletions
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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 => {},
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
484
src/tui/mode/overlay/palette.zig
Normal file
484
src/tui/mode/overlay/palette.zig
Normal file
|
@ -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", .{});
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
65
src/tui/mode/overlay/theme_palette.zig
Normal file
65
src/tui/mode/overlay/theme_palette.zig
Normal file
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue