clean up tui tab framework

This commit is contained in:
Emil Lerch 2026-05-19 10:22:50 -07:00
parent 3d176daaaa
commit 4297fda67a
Signed by: lobo
GPG key ID: A7B62D657EF764F8
11 changed files with 169 additions and 168 deletions

View file

@ -107,7 +107,7 @@ const tab_labels = blk: {
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 });
arr[i] = std.fmt.comptimePrint(" {d}:{s} ", .{ i + 1, Module.meta.label });
}
break :blk arr;
};
@ -285,7 +285,7 @@ comptime {
@setEvalBranchQuota(20000);
for (std.meta.fields(@TypeOf(tab_modules))) |field| {
const Module = @field(tab_modules, field.name);
for (Module.tab.default_bindings) |binding| {
for (Module.meta.default_bindings) |binding| {
for (keybinds.global_default_bindings) |global| {
if (binding.key.codepoint == global.key.codepoint and
std.meta.eql(binding.key.mods, global.key.mods))
@ -838,7 +838,7 @@ pub const App = struct {
}
// No user overrides use the tab's default_bindings.
for (Module.tab.default_bindings) |binding| {
for (Module.meta.default_bindings) |binding| {
if (key.matches(binding.key.codepoint, binding.key.mods)) {
Module.tab.handleAction(state_ptr, self, binding.action);
return true;
@ -898,7 +898,7 @@ pub const App = struct {
}
// No overrides read from the tab's default_bindings.
for (Module.tab.default_bindings) |binding| {
for (Module.meta.default_bindings) |binding| {
if (!std.mem.eql(u8, @tagName(binding.action), action_tag_name)) continue;
var key_buf: [32]u8 = undefined;
const s = keybinds.formatKeyCombo(binding.key, &key_buf) orelse continue;
@ -1487,11 +1487,11 @@ pub const App = struct {
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
const Module = @field(tab_modules, field.name);
for (Module.tab.status_hints) |hint_action| {
for (Module.meta.status_hints) |hint_action| {
const action_name = @tagName(hint_action);
const keys = try self.keysForTabAction(arena, field.name, action_name);
if (keys.len == 0) continue;
const label = Module.tab.action_labels.get(hint_action);
const label = Module.meta.action_labels.get(hint_action);
if (label.len == 0) continue;
try fragments.append(arena, .{ .key = keys[0], .label = label });
}
@ -1806,7 +1806,7 @@ pub const App = struct {
const tab_actions = comptime std.enums.values(Module.Action);
inline for (tab_actions) |action| {
const action_name = @tagName(action);
const label = Module.tab.action_labels.get(action);
const label = Module.meta.action_labels.get(action);
if (label.len > 0) {
const keys = try self.keysForTabAction(arena, field.name, action_name);
const keys_str = if (keys.len == 0)
@ -2016,12 +2016,12 @@ fn writeDefaultKeys(out: *std.Io.Writer) !void {
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
const Module = @field(tab_modules, field.name);
if (Module.tab.default_bindings.len == 0) continue;
if (Module.meta.default_bindings.len == 0) continue;
const heading = "Tab: " ++ field.name;
try keybinds.printSectionHeader(out, heading);
for (Module.tab.default_bindings) |binding| {
for (Module.meta.default_bindings) |binding| {
try keybinds.printScopedBinding(
out,
field.name,
@ -2495,7 +2495,7 @@ test "writeDefaultKeys: every registered tab with default_bindings has a section
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
const Module = @field(tab_modules, field.name);
if (Module.tab.default_bindings.len == 0) continue;
if (Module.meta.default_bindings.len == 0) continue;
const heading = "# ── Tab: " ++ field.name ++ " ──";
if (std.mem.indexOf(u8, out, heading) == null) {
@ -2504,7 +2504,7 @@ test "writeDefaultKeys: every registered tab with default_bindings has a section
}
// And every binding for that tab must show up as a `scope::<tab>,action::<name>` line.
inline for (Module.tab.default_bindings) |binding| {
inline for (Module.meta.default_bindings) |binding| {
const needle = "scope::" ++ field.name ++ ",action::" ++ @tagName(binding.action);
if (std.mem.indexOf(u8, out, needle) == null) {
std.debug.print("missing binding line: {s}\n", .{needle});
@ -2523,7 +2523,7 @@ test "writeDefaultKeys: tab sections appear in tab_modules declaration order" {
var prev_pos: usize = 0;
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
const Module = @field(tab_modules, field.name);
if (Module.tab.default_bindings.len == 0) continue;
if (Module.meta.default_bindings.len == 0) continue;
const heading = "# ── Tab: " ++ field.name ++ " ──";
const pos = std.mem.indexOf(u8, out, heading) orelse return error.MissingTabSection;

View file

@ -35,17 +35,17 @@ pub const State = struct {
// Tab framework contract
pub const meta: framework.TabMeta(Action) = .{
.label = "Analysis",
.default_bindings = &.{},
.action_labels = std.enums.EnumArray(Action, []const u8).initFill(""),
.status_hints = &.{},
};
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 = &.{};
pub fn init(state: *State, app: *App) !void {
_ = app;
state.* = .{};

View file

@ -43,22 +43,20 @@ pub const State = struct {
// Tab framework contract
pub const meta: framework.TabMeta(Action) = .{
.label = "Earnings",
// No tab-local bindings refresh is global. Empty placeholder.
.default_bindings = &.{},
// One label per Action variant also empty.
.action_labels = std.enums.EnumArray(Action, []const u8).initFill(""),
// Status-line hints empty.
.status_hints = &.{},
};
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) = &.{};
/// One label per Action variant also empty.
pub const action_labels = std.enums.EnumArray(Action, []const u8).initFill("");
/// Status-line hints empty.
pub const status_hints: []const Action = &.{};
/// One-time construction. State already has zero-initialized
/// defaults via field defaults; nothing to allocate up front.
pub fn init(state: *State, app: *App) !void {

View file

@ -157,14 +157,9 @@ pub const State = struct {
// Tab framework contract
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) = &.{
pub const meta: framework.TabMeta(Action) = .{
.label = "History",
.default_bindings = &.{
.{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } },
.{ .action = .metric_next, .key = .{ .codepoint = 'm' } },
.{ .action = .resolution_next, .key = .{ .codepoint = 't' } },
@ -172,21 +167,24 @@ pub const tab = struct {
.{ .action = .compare_select, .key = .{ .codepoint = vaxis.Key.space } },
.{ .action = .compare_commit, .key = .{ .codepoint = 'c' } },
.{ .action = .compare_cancel, .key = .{ .codepoint = vaxis.Key.escape } },
};
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
},
.action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.expand_collapse = "Expand/collapse bucket",
.metric_next = "Cycle metric",
.resolution_next = "Cycle resolution",
.compare_select = "Toggle compare selection",
.compare_commit = "Compare selected rows",
.compare_cancel = "Cancel compare",
});
pub const status_hints: []const Action = &.{
}),
.status_hints = &.{
.metric_next,
.resolution_next,
};
},
};
pub const tab = struct {
pub const ActionT = Action;
pub const StateT = State;
pub fn init(state: *State, app: *App) !void {
state.* = .{

View file

@ -151,7 +151,7 @@ pub fn defaults() KeyMap {
/// Display labels for each global Action variant. Used by the help
/// overlay and status-line hint to render human-readable names
/// alongside the resolved key bindings. Parallel in shape to each
/// tab module's own `tab.action_labels`.
/// tab module's own `meta.action_labels`.
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.quit = "Quit",
.refresh = "Refresh",

View file

@ -82,14 +82,9 @@ pub const State = struct {
// Tab framework contract
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) = &.{
pub const meta: framework.TabMeta(Action) = .{
.label = "Options",
.default_bindings = &.{
.{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } },
.{ .action = .collapse_all_calls, .key = .{ .codepoint = 'c' } },
.{ .action = .collapse_all_puts, .key = .{ .codepoint = 'p' } },
@ -102,9 +97,8 @@ pub const tab = struct {
.{ .action = .filter_7, .key = .{ .codepoint = '7', .mods = .{ .ctrl = true } } },
.{ .action = .filter_8, .key = .{ .codepoint = '8', .mods = .{ .ctrl = true } } },
.{ .action = .filter_9, .key = .{ .codepoint = '9', .mods = .{ .ctrl = true } } },
};
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
},
.action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.expand_collapse = "Expand/collapse row",
.collapse_all_calls = "Toggle all calls",
.collapse_all_puts = "Toggle all puts",
@ -117,12 +111,16 @@ pub const tab = struct {
.filter_7 = "Filter +/- 7 NTM",
.filter_8 = "Filter +/- 8 NTM",
.filter_9 = "Filter +/- 9 NTM",
});
pub const status_hints: []const Action = &.{
}),
.status_hints = &.{
.collapse_all_calls,
.collapse_all_puts,
};
},
};
pub const tab = struct {
pub const ActionT = Action;
pub const StateT = State;
pub fn init(state: *State, app: *App) !void {
_ = app;

View file

@ -29,17 +29,17 @@ pub const State = struct {
// Tab framework contract
pub const meta: framework.TabMeta(Action) = .{
.label = "Performance",
.default_bindings = &.{},
.action_labels = std.enums.EnumArray(Action, []const u8).initFill(""),
.status_hints = &.{},
};
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 = &.{};
pub fn init(state: *State, app: *App) !void {
_ = app;
state.* = .{};

View file

@ -251,15 +251,9 @@ pub const State = struct {
// Tab framework contract
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) = &.{
pub const meta: framework.TabMeta(Action) = .{
.label = "Portfolio",
.default_bindings = &.{
.{ .action = .expand_collapse, .key = .{ .codepoint = vaxis.Key.enter } },
.{ .action = .sort_col_next, .key = .{ .codepoint = '>' } },
.{ .action = .sort_col_prev, .key = .{ .codepoint = '<' } },
@ -268,9 +262,8 @@ pub const tab = struct {
.{ .action = .clear_account_filter, .key = .{ .codepoint = vaxis.Key.escape } },
.{ .action = .select_symbol, .key = .{ .codepoint = 's' } },
.{ .action = .select_symbol, .key = .{ .codepoint = vaxis.Key.space } },
};
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
},
.action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.expand_collapse = "Expand/collapse position",
.sort_col_next = "Sort: next column",
.sort_col_prev = "Sort: previous column",
@ -278,14 +271,18 @@ pub const tab = struct {
.open_account_picker = "Filter by account",
.clear_account_filter = "Clear account filter",
.select_symbol = "Select symbol",
});
pub const status_hints: []const Action = &.{
}),
.status_hints = &.{
.sort_col_prev,
.sort_col_next,
.sort_reverse,
.open_account_picker,
};
},
};
pub const tab = struct {
pub const ActionT = Action;
pub const StateT = State;
pub fn init(state: *State, app: *App) !void {
_ = app;

View file

@ -198,14 +198,9 @@ pub const Modal = enum {
// Tab framework contract
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) = &.{
pub const meta: framework.TabMeta(Action) = .{
.label = "Projections",
.default_bindings = &.{
.{ .action = .overlay_actuals, .key = .{ .codepoint = 'o' } },
.{ .action = .toggle_chart, .key = .{ .codepoint = 'v' } },
.{ .action = .toggle_events, .key = .{ .codepoint = 'e' } },
@ -213,9 +208,8 @@ pub const tab = struct {
.{ .action = .clear_as_of, .key = .{ .codepoint = vaxis.Key.escape } },
.{ .action = .toggle_convergence, .key = .{ .codepoint = 'c' } },
.{ .action = .toggle_return_backtest, .key = .{ .codepoint = 'b' } },
};
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
},
.action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.overlay_actuals = "Toggle actuals overlay",
.toggle_chart = "Toggle chart visibility",
.toggle_events = "Toggle lifecycle events",
@ -223,15 +217,19 @@ pub const tab = struct {
.clear_as_of = "Clear as-of date",
.toggle_convergence = "Toggle convergence sub-view",
.toggle_return_backtest = "Toggle return back-test sub-view",
});
pub const status_hints: []const Action = &.{
}),
.status_hints = &.{
.toggle_chart,
.toggle_events,
.as_of_input,
.toggle_convergence,
.toggle_return_backtest,
};
},
};
pub const tab = struct {
pub const ActionT = Action;
pub const StateT = State;
pub fn init(state: *State, app: *App) !void {
_ = app;

View file

@ -98,27 +98,25 @@ pub const State = struct {
// Tab framework contract
pub const meta: framework.TabMeta(Action) = .{
.label = "Quote",
.default_bindings = &.{
.{ .action = .chart_timeframe_next, .key = .{ .codepoint = ']' } },
.{ .action = .chart_timeframe_prev, .key = .{ .codepoint = '[' } },
},
.action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.chart_timeframe_next = "Chart: next timeframe",
.chart_timeframe_prev = "Chart: previous timeframe",
}),
.status_hints = &.{
.chart_timeframe_next,
},
};
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 = '[' } },
};
pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{
.chart_timeframe_next = "Chart: next timeframe",
.chart_timeframe_prev = "Chart: previous timeframe",
});
pub const status_hints: []const Action = &.{
.chart_timeframe_next,
};
pub fn init(state: *State, app: *App) !void {
_ = app;
state.* = .{};

View file

@ -12,25 +12,17 @@
//! pub const Action = enum { /* tab-local keybind actions */ };
//! pub const State = struct { /* tab-private state */ };
//!
//! pub const meta: framework.TabMeta(Action) = .{
//! .label = "...",
//! .default_bindings = &.{ ... },
//! .action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... }),
//! .status_hints = &.{ ... },
//! };
//!
//! pub const tab = struct {
//! 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) = &.{ ... };
//!
//! /// One label per Action variant for the help overlay.
//! pub const action_labels =
//! std.enums.EnumArray(Action, []const u8).init(.{ ... });
//!
//! /// Subset of actions that show in the status-line hint.
//! pub const status_hints: []const Action = &.{ ... };
//!
//! // Lifecycle hooks (required)
//! pub fn init(state: *State, app: *App) !void { ... }
//! pub fn deinit(state: *State, app: *App) void { ... }
@ -91,13 +83,20 @@
//! };
//! ```
//!
//! Tabs without keybind actions ship with `Action = enum {}` and
//! empty `default_bindings` / `action_labels` / `status_hints`.
//! No implicit defaults the contract is fully explicit for
//! action-related fields and lifecycle hooks. The event hooks
//! (`handleKey`, `handleMouse`, `handlePaste`) and context-change
//! hooks (`onSymbolChange`) are the exception: their absence means
//! "this tab doesn't process that event class."
//! `meta` carries the four declarative fields; `tab` carries the
//! function hooks. Functions can't be bundled into a struct value,
//! so the data/behavior split is a Zig-language concession rather
//! than a stylistic preference. The split mirrors `commands/framework.zig`'s
//! `Meta` for command modules.
//!
//! Tabs without keybind actions ship with `Action = enum {}`,
//! `default_bindings = &.{}`, `action_labels =
//! std.enums.EnumArray(Action, []const u8).initFill("")`, and
//! `status_hints = &.{}`. No implicit defaults the contract is
//! fully explicit for action-related fields and lifecycle hooks.
//! The event hooks (`handleKey`, `handleMouse`, `handlePaste`) and
//! context-change hooks (`onSymbolChange`) are the exception:
//! their absence means "this tab doesn't process that event class."
//!
//! Lifecycle hooks that aren't meaningful for a given tab can use
//! the `noop*` factory helpers below to inherit no-op
@ -133,6 +132,41 @@ pub fn TabBinding(comptime ActionT: type) type {
};
}
/// Per-tab declarative metadata. Mirrors `commands/framework.zig`'s
/// `Meta`: every tab module declares
/// `pub const meta: framework.TabMeta(Action) = .{ ... };` with all
/// fields populated. The compiler enforces the contract via the
/// field types; `validateTabModule` reduces to a single
/// `expectDeclWithType` check on the declaration's type.
///
/// Function hooks (init, deinit, activate, ..., handleAction, the
/// optional event hooks) live on the separate `pub const tab =
/// struct { ... };` namespace because Zig structs can't carry
/// `pub fn` declarations as values. The split is deliberate: data
/// in `meta`, behavior in `tab`.
///
/// Generic over `ActionT` so `default_bindings`, `action_labels`,
/// and `status_hints` can be type-checked at comptime against the
/// tab's own Action enum.
pub fn TabMeta(comptime ActionT: type) type {
return struct {
/// Display name for the tab bar. The framework composes
/// this into `" {N}:{label} "` where N is the tab's
/// 1-indexed registry position.
label: []const u8,
/// Default keybindings for this tab. Tabs without
/// keybind actions ship with `&.{}`.
default_bindings: []const TabBinding(ActionT),
/// One label per Action variant for the help overlay.
/// Tabs without keybind actions use
/// `std.enums.EnumArray(Action, []const u8).init(.{})`.
action_labels: std.enums.EnumArray(ActionT, []const u8),
/// Subset of actions that show in the status-line hint.
/// Tabs without status hints use `&.{}`.
status_hints: []const ActionT,
};
}
/// Returned by a tab's optional `statusOverride` hook to take
/// over the status-bar row while a tab-internal modal is active
/// (account picker, date input, etc).
@ -268,38 +302,18 @@ pub fn validateTabModule(comptime Module: type) void {
@compileError("Tab module `" ++ mod_name ++ "`: `tab` is missing `pub const StateT = State;`");
}
// Binding / label / hint constants
// meta (top-level decl, struct value)
// One check covers all four declarative fields. Field
// types are enforced by `TabMeta(Action)`'s declaration;
// missing fields surface as Zig's native "missing field"
// comptime errors pointing at the call site.
validator.expectDeclWithType(
"Tab module",
mod_name,
tab_decl,
"label",
[]const u8,
"pub const label: []const u8 = \"...\";",
);
validator.expectDeclWithType(
"Tab module",
mod_name,
tab_decl,
"default_bindings",
[]const TabBinding(Action),
"pub const default_bindings: []const TabBinding(Action) = &.{ ... };",
);
validator.expectDeclWithType(
"Tab module",
mod_name,
tab_decl,
"action_labels",
std.enums.EnumArray(Action, []const u8),
"pub const action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... });",
);
validator.expectDeclWithType(
"Tab module",
mod_name,
tab_decl,
"status_hints",
[]const Action,
"pub const status_hints: []const Action = &.{ ... };",
Module,
"meta",
TabMeta(Action),
"pub const meta: framework.TabMeta(Action) = .{ .label = \"...\", .default_bindings = &.{ ... }, .action_labels = std.enums.EnumArray(Action, []const u8).init(.{ ... }), .status_hints = &.{ ... } };",
);
// Lifecycle hooks (required)