derive tab states from tab modules/remove file level imports

This commit is contained in:
Emil Lerch 2026-05-15 08:33:32 -07:00
parent 53f80a28db
commit 2cc1c4d05e
Signed by: lobo
GPG key ID: A7B62D657EF764F8

View file

@ -14,14 +14,24 @@ comptime {
} }
const theme = @import("tui/theme.zig"); const theme = @import("tui/theme.zig");
const chart = @import("tui/chart.zig"); const chart = @import("tui/chart.zig");
const portfolio_tab = @import("tui/portfolio_tab.zig");
const quote_tab = @import("tui/quote_tab.zig"); /// Single source of truth for tab modules. Each entry is the
const performance_tab = @import("tui/performance_tab.zig"); /// imported tab module; the field name is the tab's tag (must match
const options_tab = @import("tui/options_tab.zig"); /// the `Tab` enum variant). `TabStates` is derived from this
const earnings_tab = @import("tui/earnings_tab.zig"); /// registry at comptime adding a new tab is a single edit here
const analysis_tab = @import("tui/analysis_tab.zig"); /// (plus the matching `Tab` enum variant + label, until those are
const history_tab = @import("tui/history_tab.zig"); /// derived too).
const projections_tab = @import("tui/projections_tab.zig"); const tab_modules = .{
.portfolio = @import("tui/portfolio_tab.zig"),
.quote = @import("tui/quote_tab.zig"),
.performance = @import("tui/performance_tab.zig"),
.options = @import("tui/options_tab.zig"),
.earnings = @import("tui/earnings_tab.zig"),
.analysis = @import("tui/analysis_tab.zig"),
.history = @import("tui/history_tab.zig"),
.projections = @import("tui/projections_tab.zig"),
};
/// Comptime-generated table of single-character grapheme slices with static lifetime. /// Comptime-generated table of single-character grapheme slices with static lifetime.
/// This avoids dangling pointers from stack-allocated temporaries in draw functions. /// This avoids dangling pointers from stack-allocated temporaries in draw functions.
const ascii_g = blk: { const ascii_g = blk: {
@ -272,63 +282,27 @@ pub const ChartState = struct {
/// Per-tab state, owned by `App` and accessed as `app.states.<tab>`. /// Per-tab state, owned by `App` and accessed as `app.states.<tab>`.
/// ///
/// Each tab module defines its own `State` struct (see /// Per-tab private state aggregator, derived at comptime from the
/// `src/tui/tab_framework.zig` for the contract). This aggregator /// `tab_modules` registry. One field per registered tab; the field
/// holds one of each adding a tab means adding one field here and /// name matches the registry tag (and the `Tab` enum variant), and
/// one entry in the `tab_modules` registry below. /// the type is that tab module's `State`.
/// ///
/// Migration in progress: tabs are being moved off App's flat field /// Adding a tab is one edit: append it to `tab_modules` (and add the
/// pile into per-tab State structs one tab at a time. Tabs not yet /// matching `Tab` enum variant + `label`). `TabStates` updates
/// migrated still have their fields directly on `App`. See /// automatically.
/// `TODO.md`'s "Refactor: TUI tab framework" entry. pub const TabStates = blk: {
pub const TabStates = struct { const reg_fields = std.meta.fields(@TypeOf(tab_modules));
earnings: earnings_tab.State = .{}, var names: [reg_fields.len][]const u8 = undefined;
analysis: analysis_tab.State = .{}, var types: [reg_fields.len]type = undefined;
quote: quote_tab.State = .{}, var attrs: [reg_fields.len]std.builtin.Type.StructField.Attributes = undefined;
performance: performance_tab.State = .{}, for (reg_fields, 0..) |f, i| {
options: options_tab.State = .{}, const Module = @field(tab_modules, f.name);
history: history_tab.State = .{}, const default: Module.State = .{};
projections: projections_tab.State = .{}, names[i] = f.name;
portfolio: portfolio_tab.State = .{}, types[i] = Module.State;
}; attrs[i] = .{ .default_value_ptr = &default };
}
/// Comptime registry of all tab modules conforming to the break :blk @Struct(.auto, null, &names, &types, &attrs);
/// framework contract. Each entry pairs a registry name with the
/// imported tab module. The validator below walks this list at
/// comptime and asserts every entry conforms to the contract;
/// missing or wrong-shape decls produce build errors with the
/// full expected signature.
///
/// **This is the only place the validator is invoked.** Tab
/// modules don't have to opt in individually adding a tab here
/// is the same act as registering it for validation. If you forget
/// to add a tab here, you'd also forget the dispatcher wiring (the
/// step-3 walker will iterate this same list), so a missed entry
/// is loud at integration time.
///
/// As tabs migrate to the framework, add them here. Pre-migration
/// tabs (portfolio, history, projections) live outside the
/// registry and are dispatched ad-hoc until they migrate.
///
/// **TODO: this explicit registry is transitional.** Once all tabs
/// have migrated AND the dispatcher walker (step 3) lands, the
/// goal is for adding a tab to `tui.zig` (the `_tab` import + the
/// `TabStates` field + the `Tab` enum variant) to *automatically*
/// validate it no separate `tab_modules` literal to keep in
/// sync. The likely shape: a comptime walk over `Tab` enum
/// variants that resolves each to its `_tab` module via a name
/// convention, eliminating the parallel registry. The current
/// list-based form is a stopgap while the framework is being
/// proven out tab-by-tab.
const tab_modules = .{
.earnings = earnings_tab,
.analysis = analysis_tab,
.quote = quote_tab,
.performance = performance_tab,
.options = options_tab,
.history = history_tab,
.projections = projections_tab,
.portfolio = portfolio_tab,
}; };
comptime { comptime {
@ -466,7 +440,7 @@ pub const PortfolioData = struct {
/// and portfolio (per-account display). /// and portfolio (per-account display).
/// ///
/// **Cross-tab mutation note.** Analysis-tab refresh /// **Cross-tab mutation note.** Analysis-tab refresh
/// (`analysis_tab.tab.reload`) clears this field so the next /// (`tab_modules.analysis.tab.reload`) clears this field so the next
/// load re-reads `accounts.srf` from disk (the user may have /// load re-reads `accounts.srf` from disk (the user may have
/// edited it). Portfolio-tab consumers re-read this field on /// edited it). Portfolio-tab consumers re-read this field on
/// every render, so the clear-and-reload doesn't require a /// every render, so the clear-and-reload doesn't require a
@ -483,7 +457,7 @@ pub const PortfolioData = struct {
/// (max of each symbol's last cached candle date). Drives the /// (max of each symbol's last cached candle date). Drives the
/// "as of close on YYYY-MM-DD" line under the portfolio totals. /// "as of close on YYYY-MM-DD" line under the portfolio totals.
/// Computed as a side effect of the portfolio-prices loop in /// Computed as a side effect of the portfolio-prices loop in
/// `portfolio_tab.loadPortfolioData`; null when no symbols have /// `tab_modules.portfolio.loadPortfolioData`; null when no symbols have
/// cached candles. /// cached candles.
latest_quote_date: ?zfin.Date = null, latest_quote_date: ?zfin.Date = null,
@ -521,7 +495,7 @@ pub const App = struct {
/// Per-portfolio shared data (loaded portfolio file, summary, /// Per-portfolio shared data (loaded portfolio file, summary,
/// account map, watchlist prices, historical snapshots). See /// account map, watchlist prices, historical snapshots). See
/// `PortfolioData` above. Reloaded by /// `PortfolioData` above. Reloaded by
/// `portfolio_tab.reloadPortfolioFile` on file changes. /// `tab_modules.portfolio.reloadPortfolioFile` on file changes.
portfolio: PortfolioData = .{}, portfolio: PortfolioData = .{},
/// Captured at App init and refreshed at tab change. Using a cached /// Captured at App init and refreshed at tab change. Using a cached
/// date (rather than calling the clock on every render) keeps render /// date (rather than calling the clock on every render) keeps render
@ -642,8 +616,8 @@ pub const App = struct {
// mouse.row maps directly to content line index // mouse.row maps directly to content line index
// (same convention as portfolio click handling). // (same convention as portfolio click handling).
const content_row = @as(usize, @intCast(mouse.row)); const content_row = @as(usize, @intCast(mouse.row));
if (content_row >= portfolio_tab.account_picker_header_lines) { if (content_row >= tab_modules.portfolio.account_picker_header_lines) {
const item_idx = content_row - portfolio_tab.account_picker_header_lines; const item_idx = content_row - tab_modules.portfolio.account_picker_header_lines;
if (item_idx < total_items) { if (item_idx < total_items) {
self.account_picker_cursor = item_idx; self.account_picker_cursor = item_idx;
self.applyAccountPickerSelection(); self.applyAccountPickerSelection();
@ -950,7 +924,7 @@ pub const App = struct {
self.setStatus("As-of cleared — showing live"); self.setStatus("As-of cleared — showing live");
} }
projections_tab.tab.reload(&self.states.projections, self) catch {}; tab_modules.projections.tab.reload(&self.states.projections, self) catch {};
self.mode = .normal; self.mode = .normal;
self.input_len = 0; self.input_len = 0;
@ -1146,7 +1120,7 @@ pub const App = struct {
self.mode = .normal; self.mode = .normal;
self.states.portfolio.cursor = 0; self.states.portfolio.cursor = 0;
self.scroll_offset = 0; self.scroll_offset = 0;
portfolio_tab.rebuildPortfolioRows(&self.states.portfolio, self); tab_modules.portfolio.rebuildPortfolioRows(&self.states.portfolio, self);
if (self.states.portfolio.account_filter) |af| { if (self.states.portfolio.account_filter) |af| {
var tmp_buf: [256]u8 = undefined; var tmp_buf: [256]u8 = undefined;
@ -1195,7 +1169,7 @@ pub const App = struct {
// silently consume) these keys. This intercept runs first when // silently consume) these keys. This intercept runs first when
// the user is in the history tab so compare behavior wins. // the user is in the history tab so compare behavior wins.
if (self.active_tab == .history) { if (self.active_tab == .history) {
if (history_tab.handleCompareKey(&self.states.history, self, ctx, key)) return; if (tab_modules.history.handleCompareKey(&self.states.history, self, ctx, key)) return;
} }
// Escape: clear account filter on portfolio tab, clear as-of // Escape: clear account filter on portfolio tab, clear as-of
@ -1205,7 +1179,7 @@ pub const App = struct {
self.setAccountFilter(null); self.setAccountFilter(null);
self.states.portfolio.cursor = 0; self.states.portfolio.cursor = 0;
self.scroll_offset = 0; self.scroll_offset = 0;
portfolio_tab.rebuildPortfolioRows(&self.states.portfolio, self); tab_modules.portfolio.rebuildPortfolioRows(&self.states.portfolio, self);
self.setStatus("Filter cleared: showing all accounts"); self.setStatus("Filter cleared: showing all accounts");
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
@ -1213,7 +1187,7 @@ pub const App = struct {
self.states.projections.as_of = null; self.states.projections.as_of = null;
self.states.projections.as_of_requested = null; self.states.projections.as_of_requested = null;
self.states.projections.overlay_actuals = false; self.states.projections.overlay_actuals = false;
projections_tab.tab.reload(&self.states.projections, self) catch {}; tab_modules.projections.tab.reload(&self.states.projections, self) catch {};
self.setStatus("As-of cleared — showing live"); self.setStatus("As-of cleared — showing live");
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
@ -1233,7 +1207,7 @@ pub const App = struct {
.select_symbol => { .select_symbol => {
// 's' selects the current portfolio row's symbol as the active symbol // 's' selects the current portfolio row's symbol as the active symbol
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.Action.select_symbol); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.select_symbol);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1277,13 +1251,13 @@ pub const App = struct {
}, },
.expand_collapse => { .expand_collapse => {
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.Action.expand_collapse); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.expand_collapse);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} else if (self.active_tab == .options) { } else if (self.active_tab == .options) {
options_tab.tab.handleAction(&self.states.options, self, options_tab.Action.expand_collapse); tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.expand_collapse);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} else if (self.active_tab == .history) { } else if (self.active_tab == .history) {
history_tab.tab.handleAction(&self.states.history, self, history_tab.Action.expand_collapse); tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.expand_collapse);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1329,13 +1303,13 @@ pub const App = struct {
}, },
.collapse_all_calls => { .collapse_all_calls => {
if (self.active_tab == .options) { if (self.active_tab == .options) {
options_tab.tab.handleAction(&self.states.options, self, options_tab.Action.collapse_all_calls); tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.collapse_all_calls);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.collapse_all_puts => { .collapse_all_puts => {
if (self.active_tab == .options) { if (self.active_tab == .options) {
options_tab.tab.handleAction(&self.states.options, self, options_tab.Action.collapse_all_puts); tab_modules.options.tab.handleAction(&self.states.options, self, tab_modules.options.Action.collapse_all_puts);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1344,45 +1318,45 @@ pub const App = struct {
const n = @intFromEnum(action) - @intFromEnum(keybinds.Action.options_filter_1) + 1; const n = @intFromEnum(action) - @intFromEnum(keybinds.Action.options_filter_1) + 1;
// Route through tab.handleAction so the dispatch // Route through tab.handleAction so the dispatch
// table is consistent with the framework. Each // table is consistent with the framework. Each
// filter_N maps directly to options_tab.Action.filter_N. // filter_N maps directly to tab_modules.options.Action.filter_N.
const tab_action: options_tab.Action = @enumFromInt(@intFromEnum(options_tab.Action.filter_1) + n - 1); const tab_action: tab_modules.options.Action = @enumFromInt(@intFromEnum(tab_modules.options.Action.filter_1) + n - 1);
options_tab.tab.handleAction(&self.states.options, self, tab_action); tab_modules.options.tab.handleAction(&self.states.options, self, tab_action);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.chart_timeframe_next => { .chart_timeframe_next => {
if (self.active_tab == .quote) { if (self.active_tab == .quote) {
quote_tab.tab.handleAction(&self.states.quote, self, quote_tab.Action.chart_timeframe_next); tab_modules.quote.tab.handleAction(&self.states.quote, self, tab_modules.quote.Action.chart_timeframe_next);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.chart_timeframe_prev => { .chart_timeframe_prev => {
if (self.active_tab == .quote) { if (self.active_tab == .quote) {
quote_tab.tab.handleAction(&self.states.quote, self, quote_tab.Action.chart_timeframe_prev); tab_modules.quote.tab.handleAction(&self.states.quote, self, tab_modules.quote.Action.chart_timeframe_prev);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.history_metric_next => { .history_metric_next => {
if (self.active_tab == .history) { if (self.active_tab == .history) {
history_tab.tab.handleAction(&self.states.history, self, history_tab.Action.metric_next); tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.metric_next);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.history_resolution_next => { .history_resolution_next => {
if (self.active_tab == .history) { if (self.active_tab == .history) {
history_tab.tab.handleAction(&self.states.history, self, history_tab.Action.resolution_next); tab_modules.history.tab.handleAction(&self.states.history, self, tab_modules.history.Action.resolution_next);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.sort_col_next => { .sort_col_next => {
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.Action.sort_col_next); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.sort_col_next);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.sort_col_prev => { .sort_col_prev => {
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.Action.sort_col_prev); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.sort_col_prev);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1397,29 +1371,29 @@ pub const App = struct {
// select_symbol). The action name stays `sort_reverse` // select_symbol). The action name stays `sort_reverse`
// because portfolio was the first consumer. // because portfolio was the first consumer.
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.Action.sort_reverse); tab_modules.portfolio.tab.handleAction(&self.states.portfolio, self, tab_modules.portfolio.Action.sort_reverse);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
if (self.active_tab == .projections) { if (self.active_tab == .projections) {
projections_tab.tab.handleAction(&self.states.projections, self, projections_tab.Action.overlay_actuals); tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.overlay_actuals);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.account_filter => { .account_filter => {
if (self.active_tab == .portfolio) { if (self.active_tab == .portfolio) {
portfolio_tab.tab.handleAction(&self.states.portfolio, self, portfolio_tab.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 => { .toggle_chart => {
if (self.active_tab == .projections) { if (self.active_tab == .projections) {
projections_tab.tab.handleAction(&self.states.projections, self, projections_tab.Action.toggle_chart); tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.toggle_chart);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.toggle_events => { .toggle_events => {
if (self.active_tab == .projections) { if (self.active_tab == .projections) {
projections_tab.tab.handleAction(&self.states.projections, self, projections_tab.Action.toggle_events); tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.toggle_events);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1428,7 +1402,7 @@ pub const App = struct {
// let the same key flow to their own handlers (none // let the same key flow to their own handlers (none
// currently bind plain 'd'). // currently bind plain 'd').
if (self.active_tab == .projections) { if (self.active_tab == .projections) {
projections_tab.tab.handleAction(&self.states.projections, self, projections_tab.Action.as_of_input); tab_modules.projections.tab.handleAction(&self.states.projections, self, tab_modules.projections.Action.as_of_input);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1442,7 +1416,7 @@ pub const App = struct {
if (self.active_tab == .history) { if (self.active_tab == .history) {
const hs = &self.states.history; const hs = &self.states.history;
if (hs.compare_view == null and hs.table_row_count > 0) { if (hs.compare_view == null and hs.table_row_count > 0) {
history_tab.toggleSelectionAt(hs, self, hs.cursor); tab_modules.history.toggleSelectionAt(hs, self, hs.cursor);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
} }
@ -1451,16 +1425,16 @@ pub const App = struct {
if (self.active_tab == .history) { if (self.active_tab == .history) {
const hs = &self.states.history; const hs = &self.states.history;
if (hs.compare_view != null) { if (hs.compare_view != null) {
history_tab.clearCompareState(hs, self); tab_modules.history.clearCompareState(hs, self);
} else { } else {
history_tab.commitCompareExternal(hs, self); tab_modules.history.commitCompareExternal(hs, self);
} }
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
.compare_cancel => { .compare_cancel => {
if (self.active_tab == .history) { if (self.active_tab == .history) {
history_tab.clearCompareState(&self.states.history, self); tab_modules.history.clearCompareState(&self.states.history, self);
return ctx.consumeAndRedraw(); return ctx.consumeAndRedraw();
} }
}, },
@ -1558,19 +1532,19 @@ pub const App = struct {
self.states.quote.chart.freeCache(self.allocator); // Invalidate indicator cache self.states.quote.chart.freeCache(self.allocator); // Invalidate indicator cache
}, },
.earnings => { .earnings => {
earnings_tab.tab.reload(&self.states.earnings, self) catch {}; tab_modules.earnings.tab.reload(&self.states.earnings, self) catch {};
}, },
.options => { .options => {
options_tab.tab.reload(&self.states.options, self) catch {}; tab_modules.options.tab.reload(&self.states.options, self) catch {};
}, },
.analysis => { .analysis => {
analysis_tab.tab.reload(&self.states.analysis, self) catch {}; tab_modules.analysis.tab.reload(&self.states.analysis, self) catch {};
}, },
.history => { .history => {
history_tab.tab.reload(&self.states.history, self) catch {}; tab_modules.history.tab.reload(&self.states.history, self) catch {};
}, },
.projections => { .projections => {
projections_tab.tab.reload(&self.states.projections, self) catch {}; tab_modules.projections.tab.reload(&self.states.projections, self) catch {};
}, },
} }
self.loadTabData(); self.loadTabData();
@ -1595,34 +1569,34 @@ pub const App = struct {
pub fn loadTabData(self: *App) void { pub fn loadTabData(self: *App) void {
switch (self.active_tab) { switch (self.active_tab) {
.portfolio => { .portfolio => {
portfolio_tab.tab.activate(&self.states.portfolio, self) catch {}; tab_modules.portfolio.tab.activate(&self.states.portfolio, self) catch {};
}, },
.quote, .performance => { .quote, .performance => {
if (self.symbol.len == 0) return; if (self.symbol.len == 0) return;
performance_tab.tab.activate(&self.states.performance, self) catch {}; tab_modules.performance.tab.activate(&self.states.performance, self) catch {};
}, },
.earnings => { .earnings => {
if (self.symbol.len == 0) return; if (self.symbol.len == 0) return;
earnings_tab.tab.activate(&self.states.earnings, self) catch {}; tab_modules.earnings.tab.activate(&self.states.earnings, self) catch {};
}, },
.options => { .options => {
if (self.symbol.len == 0) return; if (self.symbol.len == 0) return;
options_tab.tab.activate(&self.states.options, self) catch {}; tab_modules.options.tab.activate(&self.states.options, self) catch {};
}, },
.analysis => { .analysis => {
analysis_tab.tab.activate(&self.states.analysis, self) catch {}; tab_modules.analysis.tab.activate(&self.states.analysis, self) catch {};
}, },
.history => { .history => {
history_tab.tab.activate(&self.states.history, self) catch {}; tab_modules.history.tab.activate(&self.states.history, self) catch {};
}, },
.projections => { .projections => {
projections_tab.tab.activate(&self.states.projections, self) catch {}; tab_modules.projections.tab.activate(&self.states.projections, self) catch {};
}, },
} }
} }
pub fn loadPortfolioData(self: *App) void { pub fn loadPortfolioData(self: *App) void {
portfolio_tab.loadPortfolioData(&self.states.portfolio, self); tab_modules.portfolio.loadPortfolioData(&self.states.portfolio, self);
} }
pub fn setStatus(self: *App, msg: []const u8) void { pub fn setStatus(self: *App, msg: []const u8) void {
@ -1643,19 +1617,19 @@ pub const App = struct {
fn deinitData(self: *App) void { fn deinitData(self: *App) void {
self.symbol_data.deinit(self.allocator); self.symbol_data.deinit(self.allocator);
earnings_tab.tab.deinit(&self.states.earnings, self); tab_modules.earnings.tab.deinit(&self.states.earnings, self);
options_tab.tab.deinit(&self.states.options, self); tab_modules.options.tab.deinit(&self.states.options, self);
portfolio_tab.tab.deinit(&self.states.portfolio, self); tab_modules.portfolio.tab.deinit(&self.states.portfolio, self);
self.account_search_matches.deinit(self.allocator); self.account_search_matches.deinit(self.allocator);
analysis_tab.tab.deinit(&self.states.analysis, self); tab_modules.analysis.tab.deinit(&self.states.analysis, self);
self.portfolio.deinit(self.allocator); self.portfolio.deinit(self.allocator);
history_tab.tab.deinit(&self.states.history, self); tab_modules.history.tab.deinit(&self.states.history, self);
projections_tab.tab.deinit(&self.states.projections, self); tab_modules.projections.tab.deinit(&self.states.projections, self);
quote_tab.tab.deinit(&self.states.quote, self); tab_modules.quote.tab.deinit(&self.states.quote, self);
} }
fn reloadPortfolioFile(self: *App) void { fn reloadPortfolioFile(self: *App) void {
portfolio_tab.reloadPortfolioFile(&self.states.portfolio, self); tab_modules.portfolio.reloadPortfolioFile(&self.states.portfolio, self);
} }
// Drawing // Drawing
@ -1753,7 +1727,7 @@ pub const App = struct {
if (self.mode == .help) { if (self.mode == .help) {
try self.drawStyledContent(ctx.arena, buf, width, height, try self.buildHelpStyledLines(ctx.arena)); try self.drawStyledContent(ctx.arena, buf, width, height, try self.buildHelpStyledLines(ctx.arena));
} else if (self.mode == .account_picker or self.mode == .account_search) { } else if (self.mode == .account_picker or self.mode == .account_search) {
try portfolio_tab.drawAccountPicker(&self.states.portfolio, self, ctx.arena, buf, width, height); try tab_modules.portfolio.drawAccountPicker(&self.states.portfolio, self, ctx.arena, buf, width, height);
} else { } else {
switch (self.active_tab) { switch (self.active_tab) {
.portfolio => try self.drawPortfolioContent(ctx.arena, buf, width, height), .portfolio => try self.drawPortfolioContent(ctx.arena, buf, width, height),
@ -1779,7 +1753,7 @@ pub const App = struct {
const start = @min(self.scroll_offset, if (lines.len > 0) lines.len - 1 else 0); const start = @min(self.scroll_offset, if (lines.len > 0) lines.len - 1 else 0);
try self.drawStyledContent(ctx.arena, buf, width, height, lines[start..]); try self.drawStyledContent(ctx.arena, buf, width, height, lines[start..]);
}, },
.projections => try projections_tab.drawContent(&self.states.projections, self, ctx, buf, width, height), .projections => try tab_modules.projections.drawContent(&self.states.projections, self, ctx, buf, width, height),
} }
} }
@ -1898,13 +1872,13 @@ pub const App = struct {
// Portfolio content // Portfolio content
fn drawPortfolioContent(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void { fn drawPortfolioContent(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void {
return portfolio_tab.drawContent(&self.states.portfolio, self, arena, buf, width, height); return tab_modules.portfolio.drawContent(&self.states.portfolio, self, arena, buf, width, height);
} }
// Options content (with cursor/scroll) // Options content (with cursor/scroll)
fn drawOptionsContent(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void { fn drawOptionsContent(self: *App, arena: std.mem.Allocator, buf: []vaxis.Cell, width: u16, height: u16) !void {
const styled_lines = try options_tab.buildStyledLines(self, arena); const styled_lines = try tab_modules.options.buildStyledLines(self, arena);
const start = @min(self.scroll_offset, if (styled_lines.len > 0) styled_lines.len - 1 else 0); const start = @min(self.scroll_offset, if (styled_lines.len > 0) styled_lines.len - 1 else 0);
try self.drawStyledContent(arena, buf, width, height, styled_lines[start..]); try self.drawStyledContent(arena, buf, width, height, styled_lines[start..]);
} }
@ -1912,29 +1886,29 @@ pub const App = struct {
// Quote tab // Quote tab
fn drawQuoteContent(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell, width: u16, height: u16) !void { fn drawQuoteContent(self: *App, ctx: vaxis.vxfw.DrawContext, buf: []vaxis.Cell, width: u16, height: u16) !void {
return quote_tab.drawContent(self, ctx, buf, width, height); return tab_modules.quote.drawContent(self, ctx, buf, width, height);
} }
// Performance tab // Performance tab
fn buildPerfStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine { fn buildPerfStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
return performance_tab.buildStyledLines(self, arena); return tab_modules.performance.buildStyledLines(self, arena);
} }
// Earnings tab // Earnings tab
fn buildEarningsStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine { fn buildEarningsStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
return earnings_tab.buildStyledLines(self, arena); return tab_modules.earnings.buildStyledLines(self, arena);
} }
// Analysis tab // Analysis tab
fn buildAnalysisStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine { fn buildAnalysisStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
return analysis_tab.buildStyledLines(self, arena); return tab_modules.analysis.buildStyledLines(self, arena);
} }
fn buildHistoryStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine { fn buildHistoryStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
return history_tab.buildStyledLines(&self.states.history, self, arena); return tab_modules.history.buildStyledLines(&self.states.history, self, arena);
} }
// Help // Help
@ -2159,14 +2133,14 @@ pub const freeWatchlist = cli.freeWatchlist;
comptime { comptime {
_ = keybinds; _ = keybinds;
_ = theme; _ = theme;
_ = portfolio_tab; _ = tab_modules.portfolio;
_ = quote_tab; _ = tab_modules.quote;
_ = performance_tab; _ = tab_modules.performance;
_ = options_tab; _ = tab_modules.options;
_ = earnings_tab; _ = tab_modules.earnings;
_ = analysis_tab; _ = tab_modules.analysis;
_ = history_tab; _ = tab_modules.history;
_ = projections_tab; _ = tab_modules.projections;
} }
/// Entry point for the interactive TUI. /// Entry point for the interactive TUI.
@ -2270,7 +2244,7 @@ pub fn run(
// History tab requires explicit init (allocator-backed hash map); // History tab requires explicit init (allocator-backed hash map);
// other tabs use field defaults. The corresponding deinit lives // other tabs use field defaults. The corresponding deinit lives
// in `App.deinitData`. // in `App.deinitData`.
try history_tab.tab.init(&app_inst.states.history, app_inst); try tab_modules.history.tab.init(&app_inst.states.history, app_inst);
if (portfolio_path) |path| { if (portfolio_path) |path| {
const file_data = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(10 * 1024 * 1024)) catch null; const file_data = std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(10 * 1024 * 1024)) catch null;
@ -2353,7 +2327,7 @@ pub fn run(
// first. Cheap (pure compute + cache reads) once prices are // first. Cheap (pure compute + cache reads) once prices are
// already in hand. // already in hand.
if (app_inst.portfolio.file != null) { if (app_inst.portfolio.file != null) {
portfolio_tab.loadPortfolioData(&app_inst.states.portfolio, app_inst); tab_modules.portfolio.loadPortfolioData(&app_inst.states.portfolio, app_inst);
} }
} }