diff --git a/src/tui.zig b/src/tui.zig index f42d516..07aaf4e 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -1243,19 +1243,12 @@ pub const App = struct { return ctx.consumeAndRedraw(); } - // History-tab compare intercept. - // - // `s` / space / `c` / escape have existing global bindings - // (select_symbol, collapse_all_calls, plus the account-filter - // escape handler below) that would otherwise handle (or - // silently consume) these keys. This intercept runs first when - // the user is in the history tab so compare behavior wins. - if (self.active_tab == .history) { - if (tab_modules.history.handleCompareKey(&self.states.history, self, ctx, key)) return; - } - - // Escape: clear account filter on portfolio tab, clear as-of - // on projections tab, no-op otherwise. + // Escape: portfolio tab clears its account filter; projections + // tab clears its as-of date. Other tabs fall through to the + // global keymap (no global Esc binding) and then to the + // tab-local fallback dispatcher (e.g. history binds Esc to + // `compare_cancel`). When portfolio/projections migrate, + // their Esc handling moves into tab-local actions too. if (key.codepoint == vaxis.Key.escape) { if (self.active_tab == .portfolio and self.states.portfolio.account_filter != null) { self.setAccountFilter(null); @@ -1273,7 +1266,9 @@ pub const App = struct { self.setStatus("As-of cleared — showing live"); return ctx.consumeAndRedraw(); } - return; + // Fall through — no tab-specific handler. Esc isn't + // globally bound, so matchAction returns null and the + // tab-local fallback gets a chance. } const action = self.keymap.matchAction(key) orelse { @@ -1294,13 +1289,6 @@ pub const App = struct { self.input_len = 0; return ctx.consumeAndRedraw(); }, - .select_symbol => { - // 's' selects the current portfolio row's symbol as the active symbol - if (self.active_tab == .portfolio) { - tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.select_symbol); - return ctx.consumeAndRedraw(); - } - }, .refresh => { self.refreshCurrentTab(); return ctx.consumeAndRedraw(); @@ -1391,12 +1379,6 @@ pub const App = struct { self.reloadPortfolioFile(); return ctx.consumeAndRedraw(); }, - .collapse_all_calls => { - if (self.active_tab == .options) { - tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.collapse_all_calls); - return ctx.consumeAndRedraw(); - } - }, .collapse_all_puts => { if (self.active_tab == .options) { tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.collapse_all_puts); @@ -1414,18 +1396,6 @@ pub const App = struct { return ctx.consumeAndRedraw(); } }, - .history_metric_next => { - if (self.active_tab == .history) { - tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.metric_next); - return ctx.consumeAndRedraw(); - } - }, - .history_resolution_next => { - if (self.active_tab == .history) { - tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.resolution_next); - return ctx.consumeAndRedraw(); - } - }, .sort_col_next => { if (self.active_tab == .portfolio) { tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.sort_col_next); @@ -1484,38 +1454,6 @@ pub const App = struct { return ctx.consumeAndRedraw(); } }, - // History-tab compare actions are normally intercepted in - // `handleCompareKey` before `matchAction` runs (because the - // default 's'/'c'/space/escape key bindings belong to other - // actions). These cases exist so the switch is exhaustive - // and so future user-supplied keybindings targeting these - // action names work correctly. - .compare_select => { - if (self.active_tab == .history) { - const hs = &self.states.history; - if (hs.compare_view == null and hs.table_row_count > 0) { - tab_modules.history.toggleSelectionAt(hs, self, hs.cursor); - return ctx.consumeAndRedraw(); - } - } - }, - .compare_commit => { - if (self.active_tab == .history) { - const hs = &self.states.history; - if (hs.compare_view != null) { - tab_modules.history.clearCompareState(hs, self); - } else { - tab_modules.history.commitCompareExternal(hs, self); - } - return ctx.consumeAndRedraw(); - } - }, - .compare_cancel => { - if (self.active_tab == .history) { - tab_modules.history.clearCompareState(&self.states.history, self); - return ctx.consumeAndRedraw(); - } - }, } } diff --git a/src/tui/history_tab.zig b/src/tui/history_tab.zig index c197e8c..3b6c655 100644 --- a/src/tui/history_tab.zig +++ b/src/tui/history_tab.zig @@ -28,11 +28,7 @@ //! `src/history.zig` (snapshot IO). Compare composition is delegated //! to `src/compare.zig`; compare rendering to `src/views/compare.zig`. //! -//! Keybinds: -//! - `m` cycles chart metric (`history_metric_next`) -//! - `t` cycles resolution (`history_resolution_next`) -//! - `s` / space / `c` / Esc — compare (intercepted in `tui.zig` -//! before matchAction, see `handleCompareKey` below) +//! Keybinds: see the `Action` enum + `default_bindings` below. const std = @import("std"); const vaxis = @import("vaxis"); @@ -60,13 +56,20 @@ const StyledLine = tui.StyledLine; // (only meaningful for non-daily, non-live // rows with finer-tier breakdown). // -// `s` / space / `c` / Esc are ALSO history-local (compare-mode -// selection toggle, commit, and cancel) but are still routed via -// the legacy global `keybinds.Action.compare_*` variants and the -// pre-dispatch `handleCompareKey` intercept in tui.zig. Those are -// the next layer of the migration (scoped keymaps, TODO step 3) — -// once scoped keymaps land, both the global variants and the -// intercept disappear and these become per-tab actions. +// History tab keybinds: +// - `m` : cycle the focused metric (liquid → +// illiquid → net_worth). +// - `t` : cycle the resolution (cascading → +// daily → weekly → monthly → cascading). +// - Enter : expand/collapse the bucket at the cursor +// (only meaningful for non-daily, non-live +// rows with finer-tier breakdown). +// - `s` / space : toggle inclusion of the cursor row in +// the compare-selection set (0, 1, or 2). +// - `c` : commit compare (requires 2 rows +// selected) or exit an active compare view. +// - Esc : cancel compare view or clear pending +// selections. pub const Action = enum { /// Toggle expansion of the bucket at the cursor. No-op for @@ -76,6 +79,15 @@ pub const Action = enum { metric_next, /// Cycle the resolution: cascading → daily → weekly → monthly → cascading. resolution_next, + /// Toggle inclusion of the cursor row in the compare selection set. + /// Disabled while a compare view is up. Bound to `s` and space. + compare_select, + /// Run compare if exactly 2 rows are selected (otherwise status + /// hint), or exit an active compare view. Bound to `c`. + compare_commit, + /// Cancel compare: drop active compare view if up, else clear + /// pending selections. No-op when nothing is active. Bound to Esc. + compare_cancel, }; // ── Tab-private state ───────────────────────────────────────── @@ -156,12 +168,19 @@ pub const tab = struct { .{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } }, .{ .action = .metric_next, .key = .{ .codepoint = 'm' } }, .{ .action = .resolution_next, .key = .{ .codepoint = 't' } }, + .{ .action = .compare_select, .key = .{ .codepoint = 's' } }, + .{ .action = .compare_select, .key = .{ .codepoint = vaxis.Key.space } }, + .{ .action = .compare_commit, .key = .{ .codepoint = 'c' } }, + .{ .action = .compare_cancel, .key = .{ .codepoint = vaxis.Key.escape } }, }; pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{ .expand_collapse = "Expand/collapse bucket", .metric_next = "Cycle metric", .resolution_next = "Cycle resolution", + .compare_select = "Toggle compare selection", + .compare_commit = "Compare selected rows", + .compare_cancel = "Cancel compare", }); pub const status_hints: []const Action = &.{ @@ -201,6 +220,37 @@ pub const tab = struct { .expand_collapse => _ = toggleTierAtCursor(state, app), .metric_next => cycleMetric(state), .resolution_next => cycleResolution(state), + .compare_select => { + // Disabled while a compare view is up — Esc to return first. + if (state.compare_view != null) return; + if (state.table_row_count == 0) return; + toggleSelection(state, app, state.cursor); + }, + .compare_commit => { + // If a compare view is up, treat `c` as toggle-off + // (mirrors Esc semantics on this tab). + if (state.compare_view != null) { + clearCompareView(state, app); + clearSelections(state); + app.setStatus(""); + return; + } + commitCompare(state, app); + }, + .compare_cancel => { + // Esc semantics: drop active compare view if up, + // else clear pending selections, else no-op. + if (state.compare_view != null) { + clearCompareView(state, app); + clearSelections(state); + app.setStatus(""); + return; + } + if (selectionCount(state) > 0) { + clearSelections(state); + app.setStatus(""); + } + }, } } @@ -447,26 +497,6 @@ fn setSelectionStatus(state: *const State, app: *App) void { } } -/// Public entry for the `compare_select` action dispatched via -/// matchAction. Internally delegates to the same toggle function the -/// intercept uses. -pub fn toggleSelectionAt(state: *State, app: *App, idx: usize) void { - toggleSelection(state, app, idx); -} - -/// Public entry for the `compare_commit` action. -pub fn commitCompareExternal(state: *State, app: *App) void { - commitCompare(state, app); -} - -/// Public entry for the `compare_cancel` action or internal Esc path: -/// clear selections and any active compare view. -pub fn clearCompareState(state: *State, app: *App) void { - clearCompareView(state, app); - clearSelections(state); - app.setStatus(""); -} - // ── Compare commit ─────────────────────────────────────────── /// Attempt to run compare. Called when the user presses `c`. @@ -663,63 +693,6 @@ fn aggregateFromSummary( } } -// ── Compare key handler (intercepted from tui.zig) ─────────── - -/// Intercepted before `matchAction` runs when the active tab is -/// history. Returns true if the key was consumed. -/// -/// TODO(scoped-keymaps): once scoped keymaps land (TODO step 3), -/// this intercept goes away — `s`/space/`c`/Esc become -/// per-tab keybinds bound to local Action variants in -/// `default_bindings`, and the global keymap stops binding them. -/// The pre-dispatch interception in `tui.zig` (the call to -/// `handleCompareKey`) gets deleted alongside. -pub fn handleCompareKey(state: *State, app: *App, ctx: *vaxis.vxfw.EventContext, key: vaxis.Key) bool { - // Escape: exit compare view, or clear selections. - if (key.codepoint == vaxis.Key.escape) { - if (state.compare_view != null) { - clearCompareView(state, app); - clearSelections(state); - app.setStatus(""); - ctx.consumeAndRedraw(); - return true; - } - if (selectionCount(state) > 0) { - clearSelections(state); - app.setStatus(""); - ctx.consumeAndRedraw(); - return true; - } - return false; - } - - // 's' or space: toggle selection on the cursor row. - if (key.matches('s', .{}) or key.matches(vaxis.Key.space, .{})) { - // Disabled while the compare view is up — Esc to return first. - if (state.compare_view != null) return false; - if (state.table_row_count == 0) return false; - toggleSelection(state, app, state.cursor); - ctx.consumeAndRedraw(); - return true; - } - - // 'c': commit compare, or exit compare view if already active. - if (key.matches('c', .{})) { - if (state.compare_view != null) { - clearCompareView(state, app); - clearSelections(state); - app.setStatus(""); - ctx.consumeAndRedraw(); - return true; - } - commitCompare(state, app); - ctx.consumeAndRedraw(); - return true; - } - - return false; -} - // ── Table row model ────────────────────────────────────────── /// One row in the rendered recent-snapshots table. diff --git a/src/tui/keybinds.zig b/src/tui/keybinds.zig index b79d2f7..69a67bc 100644 --- a/src/tui/keybinds.zig +++ b/src/tui/keybinds.zig @@ -24,11 +24,9 @@ pub const Action = enum { select_next, select_prev, expand_collapse, - select_symbol, symbol_input, help, reload_portfolio, - collapse_all_calls, collapse_all_puts, options_filter_1, options_filter_2, @@ -39,17 +37,6 @@ pub const Action = enum { options_filter_7, options_filter_8, options_filter_9, - history_metric_next, - history_resolution_next, - /// History tab: toggle inclusion of the row under the cursor in the - /// compare-selection set (0, 1, or 2 rows). Defaults: 's' and space. - compare_select, - /// History tab: run compare if exactly 2 rows are selected; otherwise - /// status-bar hint. Default: 'c'. - compare_commit, - /// History tab: clear the current compare view (return to timeline) - /// or clear pending selections. Default: escape. - compare_cancel, sort_col_next, sort_col_prev, sort_reverse, @@ -179,12 +166,9 @@ pub const global_default_bindings = [_]Binding{ .{ .action = .select_prev, .key = .{ .codepoint = 'k' } }, .{ .action = .select_prev, .key = .{ .codepoint = vaxis.Key.up } }, .{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } }, - .{ .action = .select_symbol, .key = .{ .codepoint = 's' } }, - .{ .action = .select_symbol, .key = .{ .codepoint = vaxis.Key.space } }, .{ .action = .symbol_input, .key = .{ .codepoint = '/' } }, .{ .action = .help, .key = .{ .codepoint = '?' } }, .{ .action = .reload_portfolio, .key = .{ .codepoint = 'R' } }, - .{ .action = .collapse_all_calls, .key = .{ .codepoint = 'c' } }, .{ .action = .collapse_all_puts, .key = .{ .codepoint = 'p' } }, .{ .action = .options_filter_1, .key = .{ .codepoint = '1', .mods = .{ .ctrl = true } } }, .{ .action = .options_filter_2, .key = .{ .codepoint = '2', .mods = .{ .ctrl = true } } }, @@ -195,17 +179,6 @@ pub const global_default_bindings = [_]Binding{ .{ .action = .options_filter_7, .key = .{ .codepoint = '7', .mods = .{ .ctrl = true } } }, .{ .action = .options_filter_8, .key = .{ .codepoint = '8', .mods = .{ .ctrl = true } } }, .{ .action = .options_filter_9, .key = .{ .codepoint = '9', .mods = .{ .ctrl = true } } }, - .{ .action = .history_metric_next, .key = .{ .codepoint = 'm' } }, - .{ .action = .history_resolution_next, .key = .{ .codepoint = 't' } }, - // Compare bindings live AFTER the existing `s`/`c`/space bindings - // so `matchAction` (first-match-wins) returns the original action - // in non-history tabs. The history-tab intercept in `tui.zig` - // routes these keys directly to `handleCompareKey` before - // `matchAction` is consulted. - .{ .action = .compare_select, .key = .{ .codepoint = 's' } }, - .{ .action = .compare_select, .key = .{ .codepoint = vaxis.Key.space } }, - .{ .action = .compare_commit, .key = .{ .codepoint = 'c' } }, - .{ .action = .compare_cancel, .key = .{ .codepoint = vaxis.Key.escape } }, .{ .action = .sort_col_next, .key = .{ .codepoint = '>' } }, .{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } }, .{ .action = .sort_reverse, .key = .{ .codepoint = 'o' } },