migrate projections tab to new keybindings

This commit is contained in:
Emil Lerch 2026-05-15 10:59:24 -07:00
parent 4b4b954a64
commit dda56e3c97
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 29 additions and 86 deletions

View file

@ -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();
}
},
}
}

View file

@ -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 {

View file

@ -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");
},
}
}