clean remaining tui.zig "reach-ins"
This commit is contained in:
parent
bfabd66866
commit
2bd49af8f3
5 changed files with 165 additions and 28 deletions
89
src/tui.zig
89
src/tui.zig
|
|
@ -1617,12 +1617,21 @@ pub const App = struct {
|
|||
return self.appPredicate(t, "isDisabled");
|
||||
}
|
||||
|
||||
/// Whether the App's active symbol is the user-selected row in the
|
||||
/// active tab - drives the `*` marker on the tab bar. Dispatches to
|
||||
/// the active tab's optional `isSymbolSelected` hook (tabs without
|
||||
/// it default to "not selected"); no App-level reach into any
|
||||
/// specific tab's state.
|
||||
fn isSymbolSelected(self: *App) bool {
|
||||
// Symbol is "selected" if it matches a portfolio/watchlist row the user explicitly selected with 's'
|
||||
if (self.active_tab != .portfolio) return false;
|
||||
if (self.states.portfolio.rows.items.len == 0) return false;
|
||||
if (self.states.portfolio.cursor >= self.states.portfolio.rows.items.len) return false;
|
||||
return std.mem.eql(u8, self.states.portfolio.rows.items[self.states.portfolio.cursor].symbol, self.symbol);
|
||||
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);
|
||||
if (!@hasDecl(Module.tab, "isSymbolSelected")) return false;
|
||||
const state_ptr = &@field(self.states, field.name);
|
||||
return Module.tab.isSymbolSelected(state_ptr, self);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
fn drawContent(self: *App, ctx: vaxis.vxfw.DrawContext, width: u16, height: u16) !vaxis.vxfw.Surface {
|
||||
|
|
@ -1986,22 +1995,19 @@ pub const App = struct {
|
|||
return .{ .size = .{ .width = width, .height = 1 }, .widget = self.widget(), .buffer = buf, .children = &.{} };
|
||||
}
|
||||
|
||||
// Default status bar: getStatus() + optional account-filter
|
||||
// suffix on the portfolio tab.
|
||||
// Default status bar: the App's status message, optionally
|
||||
// annotated by the active tab's `statusSuffix` hook (e.g.
|
||||
// portfolio's account filter). No App-level reach into any
|
||||
// specific tab's state.
|
||||
const status_style = t.statusStyle();
|
||||
@memset(buf, .{ .char = .{ .grapheme = " " }, .style = status_style });
|
||||
if (self.states.portfolio.account_filter != null and self.active_tab == .portfolio) {
|
||||
const af = self.states.portfolio.account_filter.?;
|
||||
const msg = self.getStatus(ctx.arena);
|
||||
const filter_text = std.fmt.allocPrint(ctx.arena, "{s} [Account: {s}]", .{ msg, af }) catch msg;
|
||||
for (0..@min(filter_text.len, width)) |i| {
|
||||
buf[i] = .{ .char = .{ .grapheme = glyph(filter_text[i]) }, .style = status_style };
|
||||
}
|
||||
} else {
|
||||
const msg = self.getStatus(ctx.arena);
|
||||
for (0..@min(msg.len, width)) |i| {
|
||||
buf[i] = .{ .char = .{ .grapheme = glyph(msg[i]) }, .style = status_style };
|
||||
}
|
||||
const msg = self.getStatus(ctx.arena);
|
||||
const line = if (self.activeTabStatusSuffix(ctx.arena)) |suffix|
|
||||
std.fmt.allocPrint(ctx.arena, "{s} {s}", .{ msg, suffix }) catch msg
|
||||
else
|
||||
msg;
|
||||
for (0..@min(line.len, width)) |i| {
|
||||
buf[i] = .{ .char = .{ .grapheme = glyph(line[i]) }, .style = status_style };
|
||||
}
|
||||
|
||||
return .{ .size = .{ .width = width, .height = 1 }, .widget = self.widget(), .buffer = buf, .children = &.{} };
|
||||
|
|
@ -2023,6 +2029,36 @@ pub const App = struct {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Call the active tab's `statusSuffix` hook (when declared) to get
|
||||
/// an annotation appended to the default status message (e.g.
|
||||
/// portfolio's account filter). Comptime-walks `tab_modules`;
|
||||
/// returns null when the active tab declares no suffix.
|
||||
fn activeTabStatusSuffix(self: *App, arena: std.mem.Allocator) ?[]const u8 {
|
||||
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);
|
||||
if (!@hasDecl(Module.tab, "statusSuffix")) return null;
|
||||
const state_ptr = &@field(self.states, field.name);
|
||||
return Module.tab.statusSuffix(state_ptr, self, arena);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Release every tab's transmitted Kitty graphics via the optional
|
||||
/// `releaseGraphics` hook. Called once at App teardown (from the
|
||||
/// run-scope defer) while `app.vx_app` is still valid - tabs without
|
||||
/// graphics simply omit the hook.
|
||||
fn releaseAllGraphics(self: *App) void {
|
||||
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||
const Module = @field(tab_modules, field.name);
|
||||
if (@hasDecl(Module.tab, "releaseGraphics")) {
|
||||
const state_ptr = &@field(self.states, field.name);
|
||||
Module.tab.releaseGraphics(state_ptr, self);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Help ─────────────────────────────────────────────────────
|
||||
|
||||
fn buildHelpStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
|
||||
|
|
@ -2630,15 +2666,12 @@ pub fn run(
|
|||
app_inst.vx_app = &vx_app;
|
||||
defer app_inst.vx_app = null;
|
||||
defer {
|
||||
// Free any chart image before vaxis is torn down
|
||||
if (app_inst.states.quote.chart.image_id) |id| {
|
||||
vx_app.vx.freeImage(vx_app.tty.writer(), id);
|
||||
app_inst.states.quote.chart.image_id = null;
|
||||
}
|
||||
if (app_inst.states.projections.image_id) |id| {
|
||||
vx_app.vx.freeImage(vx_app.tty.writer(), id);
|
||||
app_inst.states.projections.image_id = null;
|
||||
}
|
||||
// Free any per-tab Kitty chart images before vaxis is torn
|
||||
// down. Each tab holding image IDs releases them via its
|
||||
// optional `releaseGraphics` hook. This defer runs before
|
||||
// the `vx_app = null` / `vx_app.deinit` defers above (LIFO),
|
||||
// so `app.vx_app` is still valid inside the hooks.
|
||||
app_inst.releaseAllGraphics();
|
||||
}
|
||||
try vx_app.run(app_inst.widget(), .{});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -561,6 +561,30 @@ pub const tab = struct {
|
|||
};
|
||||
}
|
||||
|
||||
/// Whether the App's active symbol is the row under the cursor -
|
||||
/// drives the `*` marker the tab bar shows next to the symbol.
|
||||
/// Active-tab hook; the App consults it generically rather than
|
||||
/// reaching into portfolio state itself.
|
||||
pub fn isSymbolSelected(state: *State, app: *App) bool {
|
||||
if (state.cursor >= state.rows.items.len) return false;
|
||||
return std.mem.eql(u8, state.rows.items[state.cursor].symbol, app.symbol);
|
||||
}
|
||||
|
||||
/// Status-bar suffix: when an account filter is active, annotate
|
||||
/// the default status line with it (the App appends this to
|
||||
/// `getStatus()`). Null when no filter is set.
|
||||
pub fn statusSuffix(state: *State, app: *App, arena: std.mem.Allocator) ?[]const u8 {
|
||||
_ = app;
|
||||
return formatAccountSuffix(arena, state.account_filter);
|
||||
}
|
||||
|
||||
/// Format the account-filter status suffix (or null when unset).
|
||||
/// Split from `statusSuffix` so it's unit-testable without an App.
|
||||
fn formatAccountSuffix(arena: std.mem.Allocator, account_filter: ?[]const u8) ?[]const u8 {
|
||||
const af = account_filter orelse return null;
|
||||
return std.fmt.allocPrint(arena, "[Account: {s}]", .{af}) catch null;
|
||||
}
|
||||
|
||||
/// Mouse handling. In account-picker mode, drives the modal
|
||||
/// (wheel scroll, click-to-select). Otherwise: clicks on the
|
||||
/// column-header row sort by that column; clicks on a data
|
||||
|
|
@ -2489,6 +2513,18 @@ test "matchesAccountFilter: with filter, null account fails" {
|
|||
try testing.expect(!matchesAccountFilter(&state, null));
|
||||
}
|
||||
|
||||
test "statusSuffix: formats the active account filter, null when unset" {
|
||||
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena_state.deinit();
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
const s = tab.formatAccountSuffix(arena, "Sample IRA");
|
||||
try testing.expect(s != null);
|
||||
try testing.expectEqualStrings("[Account: Sample IRA]", s.?);
|
||||
|
||||
try testing.expect(tab.formatAccountSuffix(arena, null) == null);
|
||||
}
|
||||
|
||||
test "ensureCursorVisible: cursor above viewport scrolls up" {
|
||||
var state: State = .{ .cursor = 5, .header_lines = 2 };
|
||||
var scroll: usize = 20;
|
||||
|
|
|
|||
|
|
@ -255,6 +255,17 @@ pub const tab = struct {
|
|||
state.* = .{};
|
||||
}
|
||||
|
||||
/// Release the transmitted Kitty projection-chart image before
|
||||
/// vaxis is torn down. Called across all tabs at App teardown while
|
||||
/// `app.vx_app` is still valid (distinct from `deinit`, which runs
|
||||
/// after vaxis is gone).
|
||||
pub fn releaseGraphics(state: *State, app: *App) void {
|
||||
if (state.image_id) |id| {
|
||||
if (app.vx_app) |va| va.vx.freeImage(va.tty.writer(), id);
|
||||
state.image_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(state: *State, app: *App) !void {
|
||||
if (state.loaded) return;
|
||||
// Projections reads `app.portfolio.summary` and `.file`,
|
||||
|
|
|
|||
|
|
@ -169,6 +169,17 @@ pub const tab = struct {
|
|||
state.* = .{};
|
||||
}
|
||||
|
||||
/// Release the transmitted Kitty chart image before vaxis is torn
|
||||
/// down. The App calls this across all tabs at teardown while
|
||||
/// `app.vx_app` is still valid - distinct from `deinit`, which runs
|
||||
/// after vaxis is already gone.
|
||||
pub fn releaseGraphics(state: *State, app: *App) void {
|
||||
if (state.chart.image_id) |id| {
|
||||
if (app.vx_app) |va| va.vx.freeImage(va.tty.writer(), id);
|
||||
state.chart.image_id = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// On activation the Quote tab loads its shared candle data (by
|
||||
/// delegating to performance, which owns it) and fetches the live
|
||||
/// quote that drives the headline price/change - the same
|
||||
|
|
|
|||
|
|
@ -54,6 +54,22 @@
|
|||
//! pub fn handlePaste(state: *State, app: *App, text: []const u8) bool { ... }
|
||||
//! pub fn statusOverride(state: *State, app: *App) ?framework.StatusOverride { ... }
|
||||
//!
|
||||
//! /// Optional: a short suffix appended to the App's default
|
||||
//! /// status message (e.g. portfolio's "[Account: <filter>]").
|
||||
//! /// Allocate the returned slice in `arena`. Active tab only.
|
||||
//! pub fn statusSuffix(state: *State, app: *App, arena: std.mem.Allocator) ?[]const u8 { ... }
|
||||
//!
|
||||
//! /// Optional: is the App's active symbol the user-selected
|
||||
//! /// row in this tab? Drives the tab-bar `*` marker. Active
|
||||
//! /// tab only; tabs without it default to "not selected".
|
||||
//! pub fn isSymbolSelected(state: *State, app: *App) bool { ... }
|
||||
//!
|
||||
//! /// Optional: release any transmitted Kitty graphics (chart
|
||||
//! /// images) before vaxis is torn down. Called across ALL tabs
|
||||
//! /// at App teardown while `app.vx_app` is still valid - distinct
|
||||
//! /// from `deinit`, which runs after vaxis is already gone.
|
||||
//! pub fn releaseGraphics(state: *State, app: *App) void { ... }
|
||||
//!
|
||||
//! /// Optional: does this tab currently have async work in
|
||||
//! /// flight that needs poll-driven redraws? While the ACTIVE
|
||||
//! /// tab answers true, the App keeps a one-shot vxfw Tick
|
||||
|
|
@ -498,6 +514,36 @@ pub fn validateTabModule(comptime Module: type) void {
|
|||
"pub fn wantsPollTick(state: *State, app: *App) bool { ... }",
|
||||
);
|
||||
}
|
||||
if (@hasDecl(tab_decl, "isSymbolSelected")) {
|
||||
validator.expectFn(
|
||||
"Tab module",
|
||||
mod_name,
|
||||
tab_decl,
|
||||
"isSymbolSelected",
|
||||
fn (*State, *App) bool,
|
||||
"pub fn isSymbolSelected(state: *State, app: *App) bool { ... }",
|
||||
);
|
||||
}
|
||||
if (@hasDecl(tab_decl, "statusSuffix")) {
|
||||
validator.expectFn(
|
||||
"Tab module",
|
||||
mod_name,
|
||||
tab_decl,
|
||||
"statusSuffix",
|
||||
fn (*State, *App, std.mem.Allocator) ?[]const u8,
|
||||
"pub fn statusSuffix(state: *State, app: *App, arena: std.mem.Allocator) ?[]const u8 { ... }",
|
||||
);
|
||||
}
|
||||
if (@hasDecl(tab_decl, "releaseGraphics")) {
|
||||
validator.expectFn(
|
||||
"Tab module",
|
||||
mod_name,
|
||||
tab_decl,
|
||||
"releaseGraphics",
|
||||
fn (*State, *App) void,
|
||||
"pub fn releaseGraphics(state: *State, app: *App) void { ... }",
|
||||
);
|
||||
}
|
||||
|
||||
// ── Draw hooks (mutually exclusive, exactly one required) ──
|
||||
//
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue