diff --git a/src/tui.zig b/src/tui.zig index 72e59f1..04debea 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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; diff --git a/src/tui/keybinds.zig b/src/tui/keybinds.zig index ee0c8ec..565a2c9 100644 --- a/src/tui/keybinds.zig +++ b/src/tui/keybinds.zig @@ -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' } },