diff --git a/src/tui.zig b/src/tui.zig index 3b5b183..c9edafd 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -1243,12 +1243,12 @@ pub const App = struct { return ctx.consumeAndRedraw(); } - // 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. + // Escape: portfolio tab clears its account filter inline. + // 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`, projections + // binds Esc to `clear_as_of`). When portfolio migrates, its + // Esc handling moves into a tab-local action too. if (key.codepoint == vaxis.Key.escape) { if (self.active_tab == .portfolio and self.states.portfolio.account_filter != null) { self.setAccountFilter(null); @@ -1258,14 +1258,6 @@ pub const App = struct { self.setStatus("Filter cleared: showing all accounts"); return ctx.consumeAndRedraw(); } - if (self.active_tab == .projections and self.states.projections.as_of != null) { - self.states.projections.as_of = null; - self.states.projections.as_of_requested = null; - self.states.projections.overlay_actuals = false; - tab_modules.projections.tab.reload(&self.states.projections, self) catch {}; - self.setStatus("As-of cleared — showing live"); - return ctx.consumeAndRedraw(); - } // Fall through — no tab-specific handler. Esc isn't // globally bound, so matchAction returns null and the // tab-local fallback gets a chance. @@ -1391,52 +1383,12 @@ pub const App = struct { return ctx.consumeAndRedraw(); } }, - .sort_reverse => { - // The `o` keybind dual-dispatches by active tab: - // - portfolio → flip the sort direction - // - projections → toggle the actuals-overlay - // `matchAction` is first-match-wins so we can't have - // separate Action variants share a codepoint; routing - // via the active tab in the handler is the project's - // existing pattern (see also `s` → compare_select / - // select_symbol). The action name stays `sort_reverse` - // because portfolio was the first consumer. - if (self.active_tab == .portfolio) { - tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.sort_reverse); - return ctx.consumeAndRedraw(); - } - if (self.active_tab == .projections) { - tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.overlay_actuals); - return ctx.consumeAndRedraw(); - } - }, .account_filter => { if (self.active_tab == .portfolio) { tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.open_account_picker); return ctx.consumeAndRedraw(); } }, - .toggle_chart => { - if (self.active_tab == .projections) { - tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.toggle_chart); - return ctx.consumeAndRedraw(); - } - }, - .toggle_events => { - if (self.active_tab == .projections) { - tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.toggle_events); - return ctx.consumeAndRedraw(); - } - }, - .projections_as_of_input => { - // Only meaningful on the projections tab. Other tabs - // let the same key flow to their own handlers (none - // currently bind plain 'd'). - if (self.active_tab == .projections) { - tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.as_of_input); - return ctx.consumeAndRedraw(); - } - }, } } diff --git a/src/tui/keybinds.zig b/src/tui/keybinds.zig index 62526c6..8c5a6f3 100644 --- a/src/tui/keybinds.zig +++ b/src/tui/keybinds.zig @@ -29,19 +29,7 @@ pub const Action = enum { reload_portfolio, sort_col_next, sort_col_prev, - sort_reverse, account_filter, - toggle_chart, - toggle_events, - /// Projections tab: open the as-of date input popup. Default: 'd'. - /// Accepts YYYY-MM-DD, N[WMQY] shortcuts (1W, 1M, 3M, 1Q, 1Y), or 'live'. - /// Empty input + Enter returns to live. See `parseAsOfDate` in - /// `src/commands/common.zig`. - /// - /// To return to live without opening the popup, press Esc on the - /// projections tab while an as-of date is active. That path is - /// intercepted directly in `tui.zig` — no separate keybind action. - projections_as_of_input, }; pub const KeyCombo = struct { @@ -161,14 +149,7 @@ pub const global_default_bindings = [_]Binding{ .{ .action = .reload_portfolio, .key = .{ .codepoint = 'R' } }, .{ .action = .sort_col_next, .key = .{ .codepoint = '>' } }, .{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } }, - .{ .action = .sort_reverse, .key = .{ .codepoint = 'o' } }, .{ .action = .account_filter, .key = .{ .codepoint = 'a' } }, - .{ .action = .toggle_chart, .key = .{ .codepoint = 'v' } }, - .{ .action = .toggle_events, .key = .{ .codepoint = 'e' } }, - // Projections-tab date-picker popup. `d` opens the popup; to - // clear an active as-of date, press Esc while on the projections - // tab (intercepted in `tui.zig` before `matchAction`). - .{ .action = .projections_as_of_input, .key = .{ .codepoint = 'd' } }, }; pub fn defaults() KeyMap { diff --git a/src/tui/projections_tab.zig b/src/tui/projections_tab.zig index d9bea12..9bbe6ea 100644 --- a/src/tui/projections_tab.zig +++ b/src/tui/projections_tab.zig @@ -40,17 +40,16 @@ const StyledLine = tui.StyledLine; // ── Tab-local action enum ───────────────────────────────────── // -// Projections tab keybinds (today routed through the legacy -// global `keybinds.Action` variants and the central tui.zig -// switch — `o` → sort_reverse, `c` → toggle_chart, `b` → -// toggle_events, `d` → projections_as_of_input. When scoped -// keymaps land (TODO step 3), these become genuinely tab-local -// and the global enum variants for them disappear. +// Projections tab keybinds: +// - `o` : toggle the actuals overlay on the projection chart +// - `v` : show/hide the percentile-band chart +// - `e` : enable/disable simulated lifecycle events +// - `d` : open the as-of date input popup +// - Esc : clear the active as-of date (back to live view) pub const Action = enum { /// Toggle the as-of-anchored actuals overlay on the projection /// chart. No-op (with status hint) when not in as-of mode. - /// Today bound to `o` via the global `sort_reverse` action. overlay_actuals, /// Show / hide the percentile-band chart. Toggles /// `state.chart_visible`; doesn't reload data. @@ -63,6 +62,9 @@ pub const Action = enum { /// handled by the App; the popup commit / clear path lives in /// tui.zig and calls `loadData` on this tab. as_of_input, + /// Clear the active as-of date and return to the live view. + /// No-op when no as-of date is set. Bound to Esc. + clear_as_of, }; // ── Tab-private state ───────────────────────────────────────── @@ -136,14 +138,11 @@ pub const tab = struct { pub const label: []const u8 = "Projections"; pub const default_bindings: []const framework.TabBinding(Action) = &.{ - // Today's keybinds are in the global keymap (sort_reverse, - // toggle_chart, toggle_events, projections_as_of_input). - // These per-tab declarations become authoritative when - // scoped keymaps land. .{ .action = .overlay_actuals, .key = .{ .codepoint = 'o' } }, - .{ .action = .toggle_chart, .key = .{ .codepoint = 'c' } }, - .{ .action = .toggle_events, .key = .{ .codepoint = 'b' } }, + .{ .action = .toggle_chart, .key = .{ .codepoint = 'v' } }, + .{ .action = .toggle_events, .key = .{ .codepoint = 'e' } }, .{ .action = .as_of_input, .key = .{ .codepoint = 'd' } }, + .{ .action = .clear_as_of, .key = .{ .codepoint = vaxis.Key.escape } }, }; pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{ @@ -151,6 +150,7 @@ pub const tab = struct { .toggle_chart = "Toggle chart visibility", .toggle_events = "Toggle lifecycle events", .as_of_input = "Set as-of date", + .clear_as_of = "Clear as-of date", }); pub const status_hints: []const Action = &.{ @@ -225,6 +225,16 @@ pub const tab = struct { // No setStatus — drawStatusBar replaces the whole // line with the prompt + hint when mode is .date_input. }, + .clear_as_of => { + // No-op when no as-of date is set. Returns to the + // live view by clearing as-of state and reloading. + if (state.as_of == null) return; + state.as_of = null; + state.as_of_requested = null; + state.overlay_actuals = false; + tab.reload(state, app) catch {}; + app.setStatus("As-of cleared — showing live"); + }, } }