From 4297fda67af3330f07591ab95efde007c5b88337 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Tue, 19 May 2026 10:22:50 -0700 Subject: [PATCH] clean up tui tab framework --- src/tui.zig | 24 ++++---- src/tui/analysis_tab.zig | 14 ++--- src/tui/earnings_tab.zig | 22 ++++--- src/tui/history_tab.zig | 28 ++++----- src/tui/keybinds.zig | 2 +- src/tui/options_tab.zig | 28 ++++----- src/tui/performance_tab.zig | 14 ++--- src/tui/portfolio_tab.zig | 29 ++++----- src/tui/projections_tab.zig | 28 ++++----- src/tui/quote_tab.zig | 32 +++++----- src/tui/tab_framework.zig | 116 ++++++++++++++++++++---------------- 11 files changed, 169 insertions(+), 168 deletions(-) diff --git a/src/tui.zig b/src/tui.zig index b617b38..891e716 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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::,action::` 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; diff --git a/src/tui/analysis_tab.zig b/src/tui/analysis_tab.zig index eee036b..f60bfd2 100644 --- a/src/tui/analysis_tab.zig +++ b/src/tui/analysis_tab.zig @@ -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.* = .{}; diff --git a/src/tui/earnings_tab.zig b/src/tui/earnings_tab.zig index 3828749..b211d8d 100644 --- a/src/tui/earnings_tab.zig +++ b/src/tui/earnings_tab.zig @@ -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 { diff --git a/src/tui/history_tab.zig b/src/tui/history_tab.zig index cd6988c..5f60edf 100644 --- a/src/tui/history_tab.zig +++ b/src/tui/history_tab.zig @@ -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.* = .{ diff --git a/src/tui/keybinds.zig b/src/tui/keybinds.zig index ee9f179..2f0f206 100644 --- a/src/tui/keybinds.zig +++ b/src/tui/keybinds.zig @@ -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", diff --git a/src/tui/options_tab.zig b/src/tui/options_tab.zig index 60f5961..3714eea 100644 --- a/src/tui/options_tab.zig +++ b/src/tui/options_tab.zig @@ -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; diff --git a/src/tui/performance_tab.zig b/src/tui/performance_tab.zig index dc013ec..ecdec75 100644 --- a/src/tui/performance_tab.zig +++ b/src/tui/performance_tab.zig @@ -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.* = .{}; diff --git a/src/tui/portfolio_tab.zig b/src/tui/portfolio_tab.zig index c500d39..a301125 100644 --- a/src/tui/portfolio_tab.zig +++ b/src/tui/portfolio_tab.zig @@ -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; diff --git a/src/tui/projections_tab.zig b/src/tui/projections_tab.zig index cdf2ba2..5d9a77e 100644 --- a/src/tui/projections_tab.zig +++ b/src/tui/projections_tab.zig @@ -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; diff --git a/src/tui/quote_tab.zig b/src/tui/quote_tab.zig index 751e4cd..a065f0b 100644 --- a/src/tui/quote_tab.zig +++ b/src/tui/quote_tab.zig @@ -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.* = .{}; diff --git a/src/tui/tab_framework.zig b/src/tui/tab_framework.zig index 0f5f45a..f301816 100644 --- a/src/tui/tab_framework.zig +++ b/src/tui/tab_framework.zig @@ -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) ─────────────────────────