remove remaining tab-specific keybindings from global space and introduce compile-time checking for conflicts

This commit is contained in:
Emil Lerch 2026-05-15 11:43:35 -07:00
parent 9a132f89a5
commit 15a3304e19
Signed by: lobo
GPG key ID: A7B62D657EF764F8
2 changed files with 29 additions and 22 deletions

View file

@ -325,14 +325,35 @@ 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.
// Comptime check: tabs must not bind keys that are already bound
// in the global keymap. Globals always win (the dispatcher reaches
// `dispatchTabLocalKey` only when `keymap.matchAction` returns
// null), so a tab-local binding for a globally-bound key is dead
// code. Reject it loudly at compile time.
//
// This lives in `tui.zig` rather than `tab_framework.zig` to keep
// the framework decoupled from the global keymap; here both are
// already in scope.
comptime {
@setEvalBranchQuota(20000);
for (std.meta.fields(@TypeOf(tab_modules))) |field| {
const Module = @field(tab_modules, field.name);
for (Module.tab.default_bindings) |binding| {
for (keybinds.global_default_bindings) |global| {
if (binding.key.codepoint == global.key.codepoint and
std.meta.eql(binding.key.mods, global.key.mods))
{
@compileError("Tab `" ++ field.name ++ "` binds a key in `default_bindings` " ++
"that is already bound in the global keymap (`keybinds.zig`). " ++
"Tab-local bindings cannot override global keys; pick a different key " ++
"or remove the global binding. Conflicting tab action: ." ++
@tagName(binding.action) ++ "; conflicting global action: ." ++
@tagName(global.action));
}
}
}
}
}
/// Per-symbol fetched data. Owned by `App` and accessed as
/// `app.symbol_data.*`. Populated by whichever tab fetches first
@ -1299,18 +1320,6 @@ pub const App = struct {
self.moveBy(-1);
return ctx.consumeAndRedraw();
},
.expand_collapse => {
if (self.active_tab == .portfolio) {
tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.expand_collapse);
return ctx.consumeAndRedraw();
} else if (self.active_tab == .options) {
tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.expand_collapse);
return ctx.consumeAndRedraw();
} else if (self.active_tab == .history) {
tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.expand_collapse);
return ctx.consumeAndRedraw();
}
},
.scroll_down => {
const half = @max(1, self.visible_height / 2);
self.scroll_offset += half;

View file

@ -23,7 +23,6 @@ pub const Action = enum {
page_up,
select_next,
select_prev,
expand_collapse,
symbol_input,
help,
reload_portfolio,
@ -140,7 +139,6 @@ pub const global_default_bindings = [_]Binding{
.{ .action = .select_next, .key = .{ .codepoint = vaxis.Key.down } },
.{ .action = .select_prev, .key = .{ .codepoint = 'k' } },
.{ .action = .select_prev, .key = .{ .codepoint = vaxis.Key.up } },
.{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } },
.{ .action = .symbol_input, .key = .{ .codepoint = '/' } },
.{ .action = .help, .key = .{ .codepoint = '?' } },
.{ .action = .reload_portfolio, .key = .{ .codepoint = 'R' } },