derive tab labels

This commit is contained in:
Emil Lerch 2026-05-15 08:54:50 -07:00
parent 2cc1c4d05e
commit bb0bb64da1
Signed by: lobo
GPG key ID: A7B62D657EF764F8
10 changed files with 78 additions and 28 deletions

View file

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

View file

@ -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 = &.{};

View file

@ -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) = &.{};

View file

@ -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' } },

View file

@ -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' } },

View file

@ -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 = &.{};

View file

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

View file

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

View file

@ -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 = '[' } },

View file

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