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 chart = @import("tui/chart.zig");
const portfolio_tab = @import("tui/portfolio_tab.zig");
const quote_tab = @import("tui/quote_tab.zig");
const performance_tab = @import("tui/performance_tab.zig");
const options_tab = @import("tui/options_tab.zig");
const earnings_tab = @import("tui/earnings_tab.zig");
const analysis_tab = @import("tui/analysis_tab.zig");
const history_tab = @import("tui/history_tab.zig");
const projections_tab = @import("tui/projections_tab.zig");
/// Single source of truth for tab modules. Each entry is the
/// imported tab module; the field name is the tab's tag (must match
/// the `Tab` enum variant). `TabStates` is derived from this
/// registry at comptime adding a new tab is a single edit here
/// (plus the matching `Tab` enum variant + label, until those are
/// derived too).
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.
/// This avoids dangling pointers from stack-allocated temporaries in draw functions.
const ascii_g = blk: {
@ -272,63 +282,27 @@ pub const ChartState = struct {
/// Per-tab state, owned by `App` and accessed as `app.states.<tab>`.
///
/// Each tab module defines its own `State` struct (see
/// `src/tui/tab_framework.zig` for the contract). This aggregator
/// holds one of each adding a tab means adding one field here and
/// one entry in the `tab_modules` registry below.
/// Per-tab private state aggregator, derived at comptime from the
/// `tab_modules` registry. One field per registered tab; the field
/// name matches the registry tag (and the `Tab` enum variant), and
/// the type is that tab module's `State`.
///
/// Migration in progress: tabs are being moved off App's flat field
/// pile into per-tab State structs one tab at a time. Tabs not yet
/// migrated still have their fields directly on `App`. See
/// `TODO.md`'s "Refactor: TUI tab framework" entry.
pub const TabStates = struct {
earnings: earnings_tab.State = .{},
analysis: analysis_tab.State = .{},
quote: quote_tab.State = .{},
performance: performance_tab.State = .{},
options: options_tab.State = .{},
history: history_tab.State = .{},
projections: projections_tab.State = .{},
portfolio: portfolio_tab.State = .{},
};
/// Comptime registry of all tab modules conforming to the
/// 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,
/// Adding a tab is one edit: append it to `tab_modules` (and add the
/// matching `Tab` enum variant + `label`). `TabStates` updates
/// automatically.
pub const TabStates = blk: {
const reg_fields = std.meta.fields(@TypeOf(tab_modules));
var names: [reg_fields.len][]const u8 = undefined;
var types: [reg_fields.len]type = undefined;
var attrs: [reg_fields.len]std.builtin.Type.StructField.Attributes = undefined;
for (reg_fields, 0..) |f, i| {
const Module = @field(tab_modules, f.name);
const default: Module.State = .{};
names[i] = f.name;
types[i] = Module.State;
attrs[i] = .{ .default_value_ptr = &default };
}
break :blk @Struct(.auto, null, &names, &types, &attrs);
};
comptime {
@ -466,7 +440,7 @@ pub const PortfolioData = struct {
/// and portfolio (per-account display).
///
/// **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
/// edited it). Portfolio-tab consumers re-read this field on
/// 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
/// "as of close on YYYY-MM-DD" line under the portfolio totals.
/// 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.
latest_quote_date: ?zfin.Date = null,
@ -521,7 +495,7 @@ pub const App = struct {
/// Per-portfolio shared data (loaded portfolio file, summary,
/// account map, watchlist prices, historical snapshots). See
/// `PortfolioData` above. Reloaded by
/// `portfolio_tab.reloadPortfolioFile` on file changes.
/// `tab_modules.portfolio.reloadPortfolioFile` on file changes.
portfolio: PortfolioData = .{},
/// Captured at App init and refreshed at tab change. Using a cached
/// 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
// (same convention as portfolio click handling).
const content_row = @as(usize, @intCast(mouse.row));
if (content_row >= portfolio_tab.account_picker_header_lines) {
const item_idx = content_row - portfolio_tab.account_picker_header_lines;
if (content_row >= tab_modules.portfolio.account_picker_header_lines) {
const item_idx = content_row - tab_modules.portfolio.account_picker_header_lines;
if (item_idx < total_items) {
self.account_picker_cursor = item_idx;
self.applyAccountPickerSelection();
@ -950,7 +924,7 @@ pub const App = struct {
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.input_len = 0;
@ -1146,7 +1120,7 @@ pub const App = struct {
self.mode = .normal;
self.states.portfolio.cursor = 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| {
var tmp_buf: [256]u8 = undefined;
@ -1195,7 +1169,7 @@ pub const App = struct {
// 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 (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
@ -1205,7 +1179,7 @@ pub const App = struct {
self.setAccountFilter(null);
self.states.portfolio.cursor = 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");
return ctx.consumeAndRedraw();
}
@ -1213,7 +1187,7 @@ pub const App = struct {
self.states.projections.as_of = null;
self.states.projections.as_of_requested = null;
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");
return ctx.consumeAndRedraw();
}
@ -1233,7 +1207,7 @@ pub const App = struct {
.select_symbol => {
// 's' selects the current portfolio row's symbol as the active symbol
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();
}
},
@ -1277,13 +1251,13 @@ pub const App = struct {
},
.expand_collapse => {
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();
} 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();
} 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();
}
},
@ -1329,13 +1303,13 @@ pub const App = struct {
},
.collapse_all_calls => {
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();
}
},
.collapse_all_puts => {
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();
}
},
@ -1344,45 +1318,45 @@ pub const App = struct {
const n = @intFromEnum(action) - @intFromEnum(keybinds.Action.options_filter_1) + 1;
// Route through tab.handleAction so the dispatch
// table is consistent with the framework. Each
// filter_N maps directly to options_tab.Action.filter_N.
const tab_action: options_tab.Action = @enumFromInt(@intFromEnum(options_tab.Action.filter_1) + n - 1);
options_tab.tab.handleAction(&self.states.options, self, tab_action);
// filter_N maps directly to tab_modules.options.Action.filter_N.
const tab_action: tab_modules.options.Action = @enumFromInt(@intFromEnum(tab_modules.options.Action.filter_1) + n - 1);
tab_modules.options.tab.handleAction(&self.states.options, self, tab_action);
return ctx.consumeAndRedraw();
}
},
.chart_timeframe_next => {
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();
}
},
.chart_timeframe_prev => {
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();
}
},
.history_metric_next => {
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();
}
},
.history_resolution_next => {
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();
}
},
.sort_col_next => {
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();
}
},
.sort_col_prev => {
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();
}
},
@ -1397,29 +1371,29 @@ pub const App = struct {
// select_symbol). The action name stays `sort_reverse`
// because portfolio was the first consumer.
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();
}
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();
}
},
.account_filter => {
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();
}
},
.toggle_chart => {
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();
}
},
.toggle_events => {
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();
}
},
@ -1428,7 +1402,7 @@ pub const App = struct {
// let the same key flow to their own handlers (none
// currently bind plain 'd').
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();
}
},
@ -1442,7 +1416,7 @@ pub const App = struct {
if (self.active_tab == .history) {
const hs = &self.states.history;
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();
}
}
@ -1451,16 +1425,16 @@ pub const App = struct {
if (self.active_tab == .history) {
const hs = &self.states.history;
if (hs.compare_view != null) {
history_tab.clearCompareState(hs, self);
tab_modules.history.clearCompareState(hs, self);
} else {
history_tab.commitCompareExternal(hs, self);
tab_modules.history.commitCompareExternal(hs, self);
}
return ctx.consumeAndRedraw();
}
},
.compare_cancel => {
if (self.active_tab == .history) {
history_tab.clearCompareState(&self.states.history, self);
tab_modules.history.clearCompareState(&self.states.history, self);
return ctx.consumeAndRedraw();
}
},
@ -1558,19 +1532,19 @@ pub const App = struct {
self.states.quote.chart.freeCache(self.allocator); // Invalidate indicator cache
},
.earnings => {
earnings_tab.tab.reload(&self.states.earnings, self) catch {};
tab_modules.earnings.tab.reload(&self.states.earnings, self) catch {};
},
.options => {
options_tab.tab.reload(&self.states.options, self) catch {};
tab_modules.options.tab.reload(&self.states.options, self) catch {};
},
.analysis => {
analysis_tab.tab.reload(&self.states.analysis, self) catch {};
tab_modules.analysis.tab.reload(&self.states.analysis, self) catch {};
},
.history => {
history_tab.tab.reload(&self.states.history, self) catch {};
tab_modules.history.tab.reload(&self.states.history, self) catch {};
},
.projections => {
projections_tab.tab.reload(&self.states.projections, self) catch {};
tab_modules.projections.tab.reload(&self.states.projections, self) catch {};
},
}
self.loadTabData();
@ -1595,34 +1569,34 @@ pub const App = struct {
pub fn loadTabData(self: *App) void {
switch (self.active_tab) {
.portfolio => {
portfolio_tab.tab.activate(&self.states.portfolio, self) catch {};
tab_modules.portfolio.tab.activate(&self.states.portfolio, self) catch {};
},
.quote, .performance => {
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 => {
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 => {
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_tab.tab.activate(&self.states.analysis, self) catch {};
tab_modules.analysis.tab.activate(&self.states.analysis, self) catch {};
},
.history => {
history_tab.tab.activate(&self.states.history, self) catch {};
tab_modules.history.tab.activate(&self.states.history, self) catch {};
},
.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 {
portfolio_tab.loadPortfolioData(&self.states.portfolio, self);
tab_modules.portfolio.loadPortfolioData(&self.states.portfolio, self);
}
pub fn setStatus(self: *App, msg: []const u8) void {
@ -1643,19 +1617,19 @@ pub const App = struct {
fn deinitData(self: *App) void {
self.symbol_data.deinit(self.allocator);
earnings_tab.tab.deinit(&self.states.earnings, self);
options_tab.tab.deinit(&self.states.options, self);
portfolio_tab.tab.deinit(&self.states.portfolio, self);
tab_modules.earnings.tab.deinit(&self.states.earnings, self);
tab_modules.options.tab.deinit(&self.states.options, self);
tab_modules.portfolio.tab.deinit(&self.states.portfolio, self);
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);
history_tab.tab.deinit(&self.states.history, self);
projections_tab.tab.deinit(&self.states.projections, self);
quote_tab.tab.deinit(&self.states.quote, self);
tab_modules.history.tab.deinit(&self.states.history, self);
tab_modules.projections.tab.deinit(&self.states.projections, self);
tab_modules.quote.tab.deinit(&self.states.quote, self);
}
fn reloadPortfolioFile(self: *App) void {
portfolio_tab.reloadPortfolioFile(&self.states.portfolio, self);
tab_modules.portfolio.reloadPortfolioFile(&self.states.portfolio, self);
}
// Drawing
@ -1753,7 +1727,7 @@ pub const App = struct {
if (self.mode == .help) {
try self.drawStyledContent(ctx.arena, buf, width, height, try self.buildHelpStyledLines(ctx.arena));
} 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 {
switch (self.active_tab) {
.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);
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
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)
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);
try self.drawStyledContent(arena, buf, width, height, styled_lines[start..]);
}
@ -1912,29 +1886,29 @@ pub const App = struct {
// Quote tab
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
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
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
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 {
return history_tab.buildStyledLines(&self.states.history, self, arena);
return tab_modules.history.buildStyledLines(&self.states.history, self, arena);
}
// Help
@ -2159,14 +2133,14 @@ pub const freeWatchlist = cli.freeWatchlist;
comptime {
_ = keybinds;
_ = theme;
_ = portfolio_tab;
_ = quote_tab;
_ = performance_tab;
_ = options_tab;
_ = earnings_tab;
_ = analysis_tab;
_ = history_tab;
_ = projections_tab;
_ = tab_modules.portfolio;
_ = tab_modules.quote;
_ = tab_modules.performance;
_ = tab_modules.options;
_ = tab_modules.earnings;
_ = tab_modules.analysis;
_ = tab_modules.history;
_ = tab_modules.projections;
}
/// Entry point for the interactive TUI.
@ -2270,7 +2244,7 @@ pub fn run(
// History tab requires explicit init (allocator-backed hash map);
// other tabs use field defaults. The corresponding deinit lives
// 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| {
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
// already in hand.
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);
}
}