feat: render keyhints for keybindings that match the current keybind prefix

This commit is contained in:
CJ van den Berg 2025-11-30 15:03:42 +01:00
parent c9f43844cb
commit 8d7fe3c5fe
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
2 changed files with 121 additions and 1 deletions

116
src/tui/keyhints.zig Normal file
View file

@ -0,0 +1,116 @@
const std = @import("std");
const keybind = @import("keybind");
const command = @import("command");
const Plane = @import("renderer").Plane;
const tui = @import("tui.zig");
const Widget = @import("Widget.zig");
const widget_type: Widget.Type = .hint_window;
pub fn render_current_key_event_sequence(allocator: std.mem.Allocator, theme: *const Widget.Theme) void {
const mode = tui.input_mode() orelse return;
const bindings = mode.current_key_event_sequence_bindings(allocator) catch return;
defer allocator.free(bindings);
return render(bindings, theme);
}
pub fn render(bindings: []const keybind.Binding, theme: *const Widget.Theme) void {
// return if something is already rendering to the top layer
if (tui.have_top_layer()) return;
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 + 2;
const widget_style = tui.get_widget_style(widget_type);
const scr = tui.screen();
var box: Widget.Box = .{
.h = bindings.len,
.w = max_len,
.x = scr.w -| max_len -| 2 -| widget_style.padding.left -| widget_style.padding.right,
.y = scr.h -| bindings.len -| 1 -| widget_style.padding.top -| widget_style.padding.bottom,
};
const deco_box = box.from_client_box(widget_style.padding);
// reset position for top layer offset
box.x -= deco_box.x;
box.y -= deco_box.y;
const top_layer_ = tui.top_layer(deco_box.to_layer()) orelse return;
widget_style.render_decoration(deco_box, widget_type, top_layer_, theme);
// var plane = top_layer_;
var plane = try Plane.init(&(Widget.Box{}).opts(@typeName(@This())), top_layer_.*);
defer plane.deinit();
plane.resize_simple(@intCast(box.h), @intCast(box.w)) catch return;
plane.move_yx(@intCast(box.y), @intCast(box.x)) catch return;
const style_base = theme.editor_widget;
const style_label = theme.editor_widget;
const style_hint = if (tui.find_scope_style(theme, "entity.name")) |sty| sty.style else style_label;
plane.set_base_style(style_base);
plane.erase();
plane.home();
plane.set_style(style_label);
plane.fill(" ");
plane.home();
plane.set_style(style_hint);
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();
};
plane.cursor_move_yx(@intCast(y), 0) catch break;
_ = plane.print("{s}", .{keybind_txt[key_events.len..]}) catch {};
}
plane.set_style(style_label);
for (bindings, 0..) |binding, y| {
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]";
};
plane.cursor_move_yx(@intCast(y), @intCast(padding)) catch break;
_ = plane.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;
}

View file

@ -559,7 +559,11 @@ fn render(self: *Self) void {
const frame = tracy.initZone(@src(), .{ .name = "tui render" }); const frame = tracy.initZone(@src(), .{ .name = "tui render" });
defer frame.deinit(); defer frame.deinit();
self.rdr_.stdplane().erase(); 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("keyhints.zig").render_current_key_event_sequence(self.allocator, self.current_theme());
break :ret continue_mainview;
}; };
if (self.top_layer_) |top_layer_| { if (self.top_layer_) |top_layer_| {