From a129c97ddd594a51579871390e361ec0e5652d93 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Sun, 30 Nov 2025 23:13:17 +0100 Subject: [PATCH] WIP: feat: render whichkey --- src/tui/WidgetList.zig | 2 +- src/tui/tui.zig | 35 +++++------------- src/tui/whichkey.zig | 84 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 src/tui/whichkey.zig diff --git a/src/tui/WidgetList.zig b/src/tui/WidgetList.zig index 67e75e1..b930b8f 100644 --- a/src/tui/WidgetList.zig +++ b/src/tui/WidgetList.zig @@ -205,7 +205,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool { fn on_render_default(_: ?*anyopaque, _: *const Widget.Theme) void {} fn render_decoration_default(self: *Self, theme: *const Widget.Theme, widget_style: *const Widget.Style) void { - return widget_style.render_decoration(self.deco_box, self.widget_type, &self.plane, theme); + widget_style.render_decoration(self.deco_box, self.widget_type, &self.plane, theme); } inline fn put_at_pos(plane: *Plane, y: usize, x: usize, egc: []const u8) void { diff --git a/src/tui/tui.zig b/src/tui/tui.zig index 9d3822f..73f6d59 100644 --- a/src/tui/tui.zig +++ b/src/tui/tui.zig @@ -297,30 +297,6 @@ fn handle_input_idle(self: *Self) void { var buf: [32]u8 = undefined; const m = tp.message.fmtbuf(&buf, .{"input_idle"}) catch return; _ = self.send_widgets(tp.self_pid(), m) catch return; - if (self.input_mode_) |mode| { - const bindings = mode.current_key_event_sequence_bindings(self.allocator) catch return; - defer self.allocator.free(bindings); - } -} - -fn render_keybind_matches(self: *Self) void { - var box = screen(); - // const anchor: Widget.Pos = .{}; - // box.y = @intCast(@max(pos.y, anchor.y) - anchor.y); - // box.x = @intCast(@max(pos.x, anchor.x) - anchor.x); - - // return if something is already rendering to the top layer - const top_layer_ = top_layer(box.to_layer()) orelse return; - const mode = self.input_mode_ orelse return; - - const bindings = mode.current_key_event_sequence_bindings(self.allocator) catch return; - defer self.allocator.free(bindings); - - for (bindings) |binding| - top_layer_.plane.print(" {f} => {s}", .{ - keybind.key_event_sequence_fmt(binding.key_events), - binding.commands[0].command, - }); } fn update_input_idle_timer(self: *Self) void { @@ -583,7 +559,11 @@ fn render(self: *Self) void { const frame = tracy.initZone(@src(), .{ .name = "tui render" }); defer frame.deinit(); self.rdr_.stdplane().erase(); - break :ret if (self.mainview_) |mv| mv.render(self.current_theme()) else false; + const continue_mainview = if (self.mainview_) |mv| mv.render(self.current_theme()) else false; + + @import("whichkey.zig").render(self.allocator, self.current_theme()); + + break :ret continue_mainview; }; if (self.top_layer_) |top_layer_| { @@ -1648,6 +1628,11 @@ pub fn top_layer(opts: renderer.Layer.Options) ?*renderer.Plane { return self.top_layer_.?.plane(); } +pub fn have_top_layer() bool { + const self = current(); + return self.top_layer_ != null; +} + fn top_layer_reset(self: *Self) void { if (self.top_layer_) |top_layer_| { top_layer_.deinit(); diff --git a/src/tui/whichkey.zig b/src/tui/whichkey.zig new file mode 100644 index 0000000..0eec96c --- /dev/null +++ b/src/tui/whichkey.zig @@ -0,0 +1,84 @@ +const std = @import("std"); +const keybind = @import("keybind"); +const command = @import("command"); + +const tui = @import("tui.zig"); +const Widget = @import("Widget.zig"); + +pub fn render(allocator: std.mem.Allocator, theme: *const Widget.Theme) void { + // return if something is already rendering to the top layer + if (tui.have_top_layer()) return; + + const mode = tui.input_mode() orelse return; + const bindings = mode.current_key_event_sequence_bindings(allocator) catch return; + defer allocator.free(bindings); + if (bindings.len == 0) return; + + var key_events_buf: [256]u8 = undefined; + const key_events = blk: { + var writer = std.Io.Writer.fixed(&key_events_buf); + writer.print("{f}", .{keybind.current_key_event_sequence_fmt()}) catch {}; + break :blk writer.buffered(); + }; + + const max_prefix_len = get_max_prefix_len(bindings) - key_events.len; + const max_description_len = get_max_description_len(bindings); + const max_len = max_prefix_len + max_description_len + 2; + + const scr = tui.screen(); + var box: Widget.Box = .{ + .h = bindings.len, + .w = max_len, + .x = scr.w -| max_len -| 4, + .y = scr.h -| bindings.len -| 2, + }; + const widget_style = tui.get_widget_style(.panel); + const deco_box = box.from_client_box(widget_style.padding); + const top_layer_ = tui.top_layer(deco_box.to_layer()) orelse return; + widget_style.render_decoration(deco_box, .panel, top_layer_, theme); + + for (bindings, 0..) |binding, y| { + var keybind_buf: [256]u8 = undefined; + const keybind_txt = blk: { + var writer = std.Io.Writer.fixed(&keybind_buf); + writer.print("{f}", .{keybind.key_event_sequence_fmt(binding.key_events)}) catch break :blk ""; + break :blk writer.buffered(); + }; + const padding = max_prefix_len + 2; + + const description = blk: { + const id = binding.commands[0].command_id orelse + command.get_id(binding.commands[0].command) orelse + break :blk binding.commands[0].command; + break :blk command.get_description(id) orelse break :blk "[n/a]"; + }; + + top_layer_.cursor_move_yx(@intCast(y), 0) catch break; + _ = top_layer_.print("{s}", .{keybind_txt[key_events.len..]}) catch {}; + + top_layer_.cursor_move_yx(@intCast(y), @intCast(padding)) catch break; + _ = top_layer_.print("{s}", .{if (description.len > 0) description else binding.commands[0].command}) catch {}; + } +} + +fn get_max_prefix_len(bindings: anytype) usize { + var max: usize = 0; + for (bindings) |binding| { + var keybind_buf: [256]u8 = undefined; + var writer = std.Io.Writer.fixed(&keybind_buf); + writer.print("{f}", .{keybind.key_event_sequence_fmt(binding.key_events)}) catch continue; + max = @max(max, writer.buffered().len); + } + return max; +} + +fn get_max_description_len(bindings: anytype) usize { + var max: usize = 0; + for (bindings) |binding| { + const id = binding.commands[0].command_id orelse command.get_id(binding.commands[0].command) orelse continue; + const description = command.get_description(id) orelse continue; + const text = if (description.len > 0) description else binding.commands[0].command; + max = @max(max, text.len); + } + return max; +}