From bb0bb64da14053f5777ae0e54bc7adfb5eba8df0 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Fri, 15 May 2026 08:54:50 -0700 Subject: [PATCH] derive tab labels --- src/tui.zig | 65 ++++++++++++++++++++++--------------- src/tui/analysis_tab.zig | 3 ++ src/tui/earnings_tab.zig | 3 ++ src/tui/history_tab.zig | 3 ++ src/tui/options_tab.zig | 3 ++ src/tui/performance_tab.zig | 3 ++ src/tui/portfolio_tab.zig | 4 +++ src/tui/projections_tab.zig | 7 ++-- src/tui/quote_tab.zig | 3 ++ src/tui/tab_framework.zig | 12 +++++++ 10 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/tui.zig b/src/tui.zig index 4ca3bce..14c146c 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -86,31 +86,44 @@ pub fn glyph(ch: u8) []const u8 { return " "; } -pub const Tab = enum { - portfolio, - quote, - performance, - options, - earnings, - analysis, - history, - projections, - - fn label(self: Tab) []const u8 { - return switch (self) { - .portfolio => " 1:Portfolio ", - .quote => " 2:Quote ", - .performance => " 3:Performance ", - .options => " 4:Options ", - .earnings => " 5:Earnings ", - .analysis => " 6:Analysis ", - .history => " 7:History ", - .projections => " 8:Projections ", - }; +/// Tab enum derived from `tab_modules` registry. Each variant +/// matches a registry field name; variant order = registry order +/// = tab-bar display order. Adding a tab requires no edit here — +/// just append to `tab_modules` and the variant appears. +pub const Tab = blk: { + const reg_fields = std.meta.fields(@TypeOf(tab_modules)); + var names: [reg_fields.len][]const u8 = undefined; + var values: [reg_fields.len]u8 = undefined; + for (reg_fields, 0..) |f, i| { + names[i] = f.name; + values[i] = @intCast(i); } + break :blk @Enum(u8, .exhaustive, &names, &values); }; -const tabs = [_]Tab{ .portfolio, .quote, .performance, .options, .earnings, .analysis, .history, .projections }; +/// Comptime lookup table of tab-bar display labels, indexed by +/// `@intFromEnum(tab)`. Each entry is `" {N}:{label} "` composed +/// from the 1-indexed registry position + the tab module's +/// `pub const label`. The format (number prefix, padding) is +/// framework policy; the bare name is owned by the tab module. +const tab_labels = blk: { + const reg_fields = std.meta.fields(@TypeOf(tab_modules)); + var arr: [reg_fields.len][]const u8 = undefined; + for (reg_fields, 0..) |f, i| { + const Module = @field(tab_modules, f.name); + arr[i] = std.fmt.comptimePrint(" {d}:{s} ", .{ i + 1, Module.tab.label }); + } + break :blk arr; +}; + +fn tabLabel(t: Tab) []const u8 { + return tab_labels[@intFromEnum(t)]; +} + +/// All tab variants in registry order. Used for tab-bar iteration +/// (rendering, hit-testing, next/prev navigation). Equivalent to +/// `std.enums.values(Tab)`; aliased for brevity at call sites. +const tabs: []const Tab = std.enums.values(Tab); pub const InputMode = enum { normal, @@ -645,7 +658,7 @@ pub const App = struct { if (mouse.row == 0) { var col: i16 = 0; for (tabs) |t| { - const lbl_len: i16 = @intCast(t.label().len); + const lbl_len: i16 = @intCast(tabLabel(t).len); if (mouse.col >= col and mouse.col < col + lbl_len) { if (self.isDisabled(t)) return; self.active_tab = t; @@ -1667,7 +1680,7 @@ pub const App = struct { var col: usize = 0; for (tabs) |t| { - const lbl = t.label(); + const lbl = tabLabel(t); const is_active = t == self.active_tab; const is_disabled = self.isDisabled(t); const tab_style: vaxis.Style = if (is_active) th.tabActiveStyle() else if (is_disabled) th.tabDisabledStyle() else inactive_style; @@ -2425,6 +2438,6 @@ test "SortDirection flip and indicator" { } test "Tab label" { - try testing.expectEqualStrings(" 1:Portfolio ", Tab.portfolio.label()); - try testing.expectEqualStrings(" 6:Analysis ", Tab.analysis.label()); + try testing.expectEqualStrings(" 1:Portfolio ", tabLabel(.portfolio)); + try testing.expectEqualStrings(" 6:Analysis ", tabLabel(.analysis)); } diff --git a/src/tui/analysis_tab.zig b/src/tui/analysis_tab.zig index 35e4fcf..f292b87 100644 --- a/src/tui/analysis_tab.zig +++ b/src/tui/analysis_tab.zig @@ -39,6 +39,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "Analysis"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{}; pub const action_labels = std.enums.EnumArray(Action, []const u8).initFill(""); pub const status_hints: []const Action = &.{}; diff --git a/src/tui/earnings_tab.zig b/src/tui/earnings_tab.zig index 6fb58d9..7308972 100644 --- a/src/tui/earnings_tab.zig +++ b/src/tui/earnings_tab.zig @@ -47,6 +47,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "Earnings"; + /// No tab-local bindings — refresh is global. Empty placeholder. pub const default_bindings: []const framework.TabBinding(Action) = &.{}; diff --git a/src/tui/history_tab.zig b/src/tui/history_tab.zig index 04dee95..c197e8c 100644 --- a/src/tui/history_tab.zig +++ b/src/tui/history_tab.zig @@ -149,6 +149,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "History"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{ .{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } }, .{ .action = .metric_next, .key = .{ .codepoint = 'm' } }, diff --git a/src/tui/options_tab.zig b/src/tui/options_tab.zig index e2b61cf..7254564 100644 --- a/src/tui/options_tab.zig +++ b/src/tui/options_tab.zig @@ -75,6 +75,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "Options"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{ .{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } }, .{ .action = .collapse_all_calls, .key = .{ .codepoint = 'c' } }, diff --git a/src/tui/performance_tab.zig b/src/tui/performance_tab.zig index 15f1510..89a90ad 100644 --- a/src/tui/performance_tab.zig +++ b/src/tui/performance_tab.zig @@ -33,6 +33,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "Performance"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{}; pub const action_labels = std.enums.EnumArray(Action, []const u8).initFill(""); pub const status_hints: []const Action = &.{}; diff --git a/src/tui/portfolio_tab.zig b/src/tui/portfolio_tab.zig index 9b90965..75d23b4 100644 --- a/src/tui/portfolio_tab.zig +++ b/src/tui/portfolio_tab.zig @@ -122,6 +122,10 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. Composed by the framework + /// into `" {N}:{label} "` where N is the registry index. + pub const label: []const u8 = "Portfolio"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{ // These are dead until scoped keymaps land — the global // keymap matches first. Declared per-tab so the help diff --git a/src/tui/projections_tab.zig b/src/tui/projections_tab.zig index 4bf9277..d9bea12 100644 --- a/src/tui/projections_tab.zig +++ b/src/tui/projections_tab.zig @@ -132,6 +132,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + 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). @@ -213,8 +216,8 @@ pub const tab = struct { freeLoaded(state, app); state.loaded = false; loadData(state, app); - const label = if (state.events_enabled) "Events enabled" else "Events disabled"; - app.setStatus(label); + const status_msg = if (state.events_enabled) "Events enabled" else "Events disabled"; + app.setStatus(status_msg); }, .as_of_input => { app.mode = .date_input; diff --git a/src/tui/quote_tab.zig b/src/tui/quote_tab.zig index 74e3931..58d0042 100644 --- a/src/tui/quote_tab.zig +++ b/src/tui/quote_tab.zig @@ -46,6 +46,9 @@ pub const tab = struct { pub const ActionT = Action; pub const StateT = State; + /// Display name for the tab bar. + pub const label: []const u8 = "Quote"; + pub const default_bindings: []const framework.TabBinding(Action) = &.{ .{ .action = .chart_timeframe_next, .key = .{ .codepoint = ']' } }, .{ .action = .chart_timeframe_prev, .key = .{ .codepoint = '[' } }, diff --git a/src/tui/tab_framework.zig b/src/tui/tab_framework.zig index 918c72a..7a6405c 100644 --- a/src/tui/tab_framework.zig +++ b/src/tui/tab_framework.zig @@ -16,6 +16,11 @@ //! pub const ActionT = Action; //! pub const StateT = State; //! +//! /// Display name for the tab bar. The framework composes +//! /// this into `" {N}:{label} "` where N is the tab's +//! /// 1-indexed registry position. +//! pub const label: []const u8 = "..."; +//! //! /// Default keybindings for this tab. //! pub const default_bindings: []const TabBinding(Action) = &.{ ... }; //! @@ -231,6 +236,13 @@ pub fn validateTabModule(comptime Module: type) void { } // ── Binding / label / hint constants ─────────────────── + expectDeclWithType( + mod_name, + tab_decl, + "label", + []const u8, + "pub const label: []const u8 = \"...\";", + ); expectDeclWithType( mod_name, tab_decl,