migrate projections tab to new keybindings

This commit is contained in:
Emil Lerch 2026-05-15 10:59:24 -07:00
parent 4948484a46
commit b8b6dffcb5
3 changed files with 29 additions and 86 deletions

View file

@ -1243,12 +1243,12 @@ pub const App = struct {
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
// Escape: portfolio tab clears its account filter; projections // Escape: portfolio tab clears its account filter inline.
// tab clears its as-of date. Other tabs fall through to the // Other tabs fall through to the global keymap (no global
// global keymap (no global Esc binding) and then to the // Esc binding) and then to the tab-local fallback dispatcher
// tab-local fallback dispatcher (e.g. history binds Esc to // (e.g. history binds Esc to `compare_cancel`, projections
// `compare_cancel`). When portfolio/projections migrate, // binds Esc to `clear_as_of`). When portfolio migrates, its
// their Esc handling moves into tab-local actions too. // Esc handling moves into a tab-local action too.
if (key.codepoint == vaxis.Key.escape) { if (key.codepoint == vaxis.Key.escape) {
if (self.active_tab == .portfolio and self.states.portfolio.account_filter != null) { if (self.active_tab == .portfolio and self.states.portfolio.account_filter != null) {
self.setAccountFilter(null); self.setAccountFilter(null);
@ -1258,14 +1258,6 @@ pub const App = struct {
self.setStatus("Filter cleared: showing all accounts"); self.setStatus("Filter cleared: showing all accounts");
return ctx.consumeAndRedraw(); 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 // Fall through no tab-specific handler. Esc isn't
// globally bound, so matchAction returns null and the // globally bound, so matchAction returns null and the
// tab-local fallback gets a chance. // tab-local fallback gets a chance.
@ -1391,52 +1383,12 @@ pub const App = struct {
return ctx.consumeAndRedraw(); 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 => { .account_filter => {
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.open_account_picker); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.open_account_picker);
return ctx.consumeAndRedraw(); 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();
}
},
} }
} }

View file

@ -29,19 +29,7 @@ pub const Action = enum {
reload_portfolio, reload_portfolio,
sort_col_next, sort_col_next,
sort_col_prev, sort_col_prev,
sort_reverse,
account_filter, 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 { pub const KeyCombo = struct {
@ -161,14 +149,7 @@ pub const global_default_bindings = [_]Binding{
.{ .action = .reload_portfolio, .key = .{ .codepoint = 'R' } }, .{ .action = .reload_portfolio, .key = .{ .codepoint = 'R' } },
.{ .action = .sort_col_next, .key = .{ .codepoint = '>' } }, .{ .action = .sort_col_next, .key = .{ .codepoint = '>' } },
.{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } }, .{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } },
.{ .action = .sort_reverse, .key = .{ .codepoint = 'o' } },
.{ .action = .account_filter, .key = .{ .codepoint = 'a' } }, .{ .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 { pub fn defaults() KeyMap {

View file

@ -40,17 +40,16 @@ const StyledLine = tui.StyledLine;
// Tab-local action enum // Tab-local action enum
// //
// Projections tab keybinds (today routed through the legacy // Projections tab keybinds:
// global `keybinds.Action` variants and the central tui.zig // - `o` : toggle the actuals overlay on the projection chart
// switch `o` sort_reverse, `c` toggle_chart, `b` // - `v` : show/hide the percentile-band chart
// toggle_events, `d` projections_as_of_input. When scoped // - `e` : enable/disable simulated lifecycle events
// keymaps land (TODO step 3), these become genuinely tab-local // - `d` : open the as-of date input popup
// and the global enum variants for them disappear. // - Esc : clear the active as-of date (back to live view)
pub const Action = enum { pub const Action = enum {
/// Toggle the as-of-anchored actuals overlay on the projection /// Toggle the as-of-anchored actuals overlay on the projection
/// chart. No-op (with status hint) when not in as-of mode. /// chart. No-op (with status hint) when not in as-of mode.
/// Today bound to `o` via the global `sort_reverse` action.
overlay_actuals, overlay_actuals,
/// Show / hide the percentile-band chart. Toggles /// Show / hide the percentile-band chart. Toggles
/// `state.chart_visible`; doesn't reload data. /// `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 /// handled by the App; the popup commit / clear path lives in
/// tui.zig and calls `loadData` on this tab. /// tui.zig and calls `loadData` on this tab.
as_of_input, 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 // Tab-private state
@ -136,14 +138,11 @@ pub const tab = struct {
pub const label: []const u8 = "Projections"; pub const label: []const u8 = "Projections";
pub const default_bindings: []const framework.TabBinding(Action) = &.{ 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 = .overlay_actuals, .key = .{ .codepoint = 'o' } },
.{ .action = .toggle_chart, .key = .{ .codepoint = 'c' } }, .{ .action = .toggle_chart, .key = .{ .codepoint = 'v' } },
.{ .action = .toggle_events, .key = .{ .codepoint = 'b' } }, .{ .action = .toggle_events, .key = .{ .codepoint = 'e' } },
.{ .action = .as_of_input, .key = .{ .codepoint = 'd' } }, .{ .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(.{ 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_chart = "Toggle chart visibility",
.toggle_events = "Toggle lifecycle events", .toggle_events = "Toggle lifecycle events",
.as_of_input = "Set as-of date", .as_of_input = "Set as-of date",
.clear_as_of = "Clear as-of date",
}); });
pub const status_hints: []const Action = &.{ pub const status_hints: []const Action = &.{
@ -225,6 +225,16 @@ pub const tab = struct {
// No setStatus drawStatusBar replaces the whole // No setStatus drawStatusBar replaces the whole
// line with the prompt + hint when mode is .date_input. // 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");
},
} }
} }