add function for tab local key dispatch and ctrl+f/ctrl+b keybinds

This commit is contained in:
Emil Lerch 2026-05-15 09:40:02 -07:00
parent bb0bb64da1
commit 126ad53fd5
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 54 additions and 4 deletions

View file

@ -325,6 +325,15 @@ comptime {
}
}
// TODO(scoped-keybinds): comptime validator that tabs don't bind
// keys already bound in the global keymap. Skipped in commit 1
// because tabs (portfolio/options/history) currently declare Enter
// as a tab-local binding for `expand_collapse` while Enter remains
// globally bound. The validator gets added at the END of the
// migration (after commit 8 removes the global Enter binding) so
// it doesn't block intermediate commits. Runtime check on user
// keys.srf still rejects conflicts at TUI startup.
/// Per-symbol fetched data. Owned by `App` and accessed as
/// `app.symbol_data.*`. Populated by whichever tab fetches first
/// (typically the perf or quote tab); consumed by every tab that
@ -799,6 +808,37 @@ pub const App = struct {
return false;
}
/// Tab-local keybind dispatch. Walks the active tab's
/// `default_bindings` looking for a key match. On match,
/// invokes `tab.handleAction(state, app, action)` and returns
/// `true`. Returns `false` if no binding matched.
///
/// Called as a fallback AFTER the global keymap; under the
/// "globals always win" rule, tabs are forbidden (by validator
/// at comptime, by user-config check at runtime) from binding
/// keys that are already global, so a key reaching here is by
/// definition not a global keybind.
///
/// Adding a tab-local action: declare it in the tab's `Action`
/// enum, bind it in `default_bindings`, and `handleAction` runs
/// it. No edit to `tui.zig` required.
fn dispatchTabLocalKey(self: *App, key: vaxis.Key) bool {
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
const Module = @field(tab_modules, field.name);
for (Module.tab.default_bindings) |binding| {
if (key.matches(binding.key.codepoint, binding.key.mods)) {
const state_ptr = &@field(self.states, field.name);
Module.tab.handleAction(state_ptr, self, binding.action);
return true;
}
}
return false;
}
}
return false;
}
/// Outcome of a single keypress in an input-mode buffer (symbol
/// input, date input, etc.). Returned by `handleInputBuffer` so
/// the per-mode caller only needs to wire up the `committed`
@ -1207,7 +1247,15 @@ pub const App = struct {
return;
}
const action = self.keymap.matchAction(key) orelse return;
const action = self.keymap.matchAction(key) orelse {
// No global binding matched. Fall back to tab-local
// dispatch the active tab may bind this key in its
// `default_bindings`. Globals win (no overlap allowed,
// enforced by validator + user-config check), so
// reaching here means the key is purely tab-local.
if (self.dispatchTabLocalKey(key)) return ctx.consumeAndRedraw();
return;
};
switch (action) {
.quit => {
ctx.quit = true;

View file

@ -101,7 +101,7 @@ pub const KeyMap = struct {
// Defaults
const default_bindings = [_]Binding{
pub const global_default_bindings = [_]Binding{
.{ .action = .quit, .key = .{ .codepoint = 'q' } },
.{ .action = .quit, .key = .{ .codepoint = 'c', .mods = .{ .ctrl = true } } },
.{ .action = .refresh, .key = .{ .codepoint = 'r' } },
@ -125,7 +125,9 @@ const default_bindings = [_]Binding{
.{ .action = .scroll_top, .key = .{ .codepoint = 'g' } },
.{ .action = .scroll_bottom, .key = .{ .codepoint = 'G' } },
.{ .action = .page_down, .key = .{ .codepoint = vaxis.Key.page_down } },
.{ .action = .page_down, .key = .{ .codepoint = 'f', .mods = .{ .ctrl = true } } },
.{ .action = .page_up, .key = .{ .codepoint = vaxis.Key.page_up } },
.{ .action = .page_up, .key = .{ .codepoint = 'b', .mods = .{ .ctrl = true } } },
.{ .action = .select_next, .key = .{ .codepoint = 'j' } },
.{ .action = .select_next, .key = .{ .codepoint = vaxis.Key.down } },
.{ .action = .select_prev, .key = .{ .codepoint = 'k' } },
@ -173,7 +175,7 @@ const default_bindings = [_]Binding{
};
pub fn defaults() KeyMap {
return .{ .bindings = &default_bindings };
return .{ .bindings = &global_default_bindings };
}
// SRF serialization
@ -323,7 +325,7 @@ pub fn printDefaults(io: std.Io) !void {
try out.writeAll("# F1-F12, insert, delete\n");
try out.writeAll("# Multiple lines with the same action = multiple bindings.\n");
for (default_bindings) |b| {
for (global_default_bindings) |b| {
var key_buf: [32]u8 = undefined;
const key_str = formatKeyCombo(b.key, &key_buf) orelse continue;
try out.print("action::{s},key::{s}\n", .{ @tagName(b.action), key_str });