From 2cc1c4d05ec702ac0a007f1bf293f7be2e5bca0a Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 15 May 2026 08:33:32 -0700 Subject: [PATCH] derive tab states from tab modules/remove file level imports --- src/tui.zig | 252 +++++++++++++++++++++++----------------------------- 1 file changed, 113 insertions(+), 139 deletions(-) diff --git a/src/tui.zig b/src/tui.zig index 4a6a9df..4ca3bce 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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.`. /// -/// 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); } }