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");
|
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 {
|
fn isSymbolSelected(self: *App) bool {
|
||||||
// Symbol is "selected" if it matches a portfolio/watchlist row the user explicitly selected with 's'
|
inline for (std.meta.fields(@TypeOf(tab_modules))) |field| {
|
||||||
if (self.active_tab != .portfolio) return false;
|
if (std.mem.eql(u8, field.name, @tagName(self.active_tab))) {
|
||||||
if (self.states.portfolio.rows.items.len == 0) return false;
|
const Module = @field(tab_modules, field.name);
|
||||||
if (self.states.portfolio.cursor >= self.states.portfolio.rows.items.len) return false;
|
if (!@hasDecl(Module.tab, "isSymbolSelected")) return false;
|
||||||
return std.mem.eql(u8, self.states.portfolio.rows.items[self.states.portfolio.cursor].symbol, self.symbol);
|
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 {
|
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 = &.{} };
|
return .{ .size = .{ .width = width, .height = 1 }, .widget = self.widget(), .buffer = buf, .children = &.{} };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default status bar: getStatus() + optional account-filter
|
// Default status bar: the App's status message, optionally
|
||||||
// suffix on the portfolio tab.
|
// 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();
|
const status_style = t.statusStyle();
|
||||||
@memset(buf, .{ .char = .{ .grapheme = " " }, .style = status_style });
|
@memset(buf, .{ .char = .{ .grapheme = " " }, .style = status_style });
|
||||||
if (self.states.portfolio.account_filter != null and self.active_tab == .portfolio) {
|
const msg = self.getStatus(ctx.arena);
|
||||||
const af = self.states.portfolio.account_filter.?;
|
const line = if (self.activeTabStatusSuffix(ctx.arena)) |suffix|
|
||||||
const msg = self.getStatus(ctx.arena);
|
std.fmt.allocPrint(ctx.arena, "{s} {s}", .{ msg, suffix }) catch msg
|
||||||
const filter_text = std.fmt.allocPrint(ctx.arena, "{s} [Account: {s}]", .{ msg, af }) catch msg;
|
else
|
||||||
for (0..@min(filter_text.len, width)) |i| {
|
msg;
|
||||||
buf[i] = .{ .char = .{ .grapheme = glyph(filter_text[i]) }, .style = status_style };
|
for (0..@min(line.len, width)) |i| {
|
||||||
}
|
buf[i] = .{ .char = .{ .grapheme = glyph(line[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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{ .size = .{ .width = width, .height = 1 }, .widget = self.widget(), .buffer = buf, .children = &.{} };
|
return .{ .size = .{ .width = width, .height = 1 }, .widget = self.widget(), .buffer = buf, .children = &.{} };
|
||||||
|
|
@ -2023,6 +2029,36 @@ pub const App = struct {
|
||||||
return null;
|
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 ─────────────────────────────────────────────────────
|
// ── Help ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn buildHelpStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
|
fn buildHelpStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
|
||||||
|
|
@ -2630,15 +2666,12 @@ pub fn run(
|
||||||
app_inst.vx_app = &vx_app;
|
app_inst.vx_app = &vx_app;
|
||||||
defer app_inst.vx_app = null;
|
defer app_inst.vx_app = null;
|
||||||
defer {
|
defer {
|
||||||
// Free any chart image before vaxis is torn down
|
// Free any per-tab Kitty chart images before vaxis is torn
|
||||||
if (app_inst.states.quote.chart.image_id) |id| {
|
// down. Each tab holding image IDs releases them via its
|
||||||
vx_app.vx.freeImage(vx_app.tty.writer(), id);
|
// optional `releaseGraphics` hook. This defer runs before
|
||||||
app_inst.states.quote.chart.image_id = null;
|
// the `vx_app = null` / `vx_app.deinit` defers above (LIFO),
|
||||||
}
|
// so `app.vx_app` is still valid inside the hooks.
|
||||||
if (app_inst.states.projections.image_id) |id| {
|
app_inst.releaseAllGraphics();
|
||||||
vx_app.vx.freeImage(vx_app.tty.writer(), id);
|
|
||||||
app_inst.states.projections.image_id = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
try vx_app.run(app_inst.widget(), .{});
|
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
|
/// Mouse handling. In account-picker mode, drives the modal
|
||||||
/// (wheel scroll, click-to-select). Otherwise: clicks on the
|
/// (wheel scroll, click-to-select). Otherwise: clicks on the
|
||||||
/// column-header row sort by that column; clicks on a data
|
/// 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));
|
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" {
|
test "ensureCursorVisible: cursor above viewport scrolls up" {
|
||||||
var state: State = .{ .cursor = 5, .header_lines = 2 };
|
var state: State = .{ .cursor = 5, .header_lines = 2 };
|
||||||
var scroll: usize = 20;
|
var scroll: usize = 20;
|
||||||
|
|
|
||||||
|
|
@ -255,6 +255,17 @@ pub const tab = struct {
|
||||||
state.* = .{};
|
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 {
|
pub fn activate(state: *State, app: *App) !void {
|
||||||
if (state.loaded) return;
|
if (state.loaded) return;
|
||||||
// Projections reads `app.portfolio.summary` and `.file`,
|
// Projections reads `app.portfolio.summary` and `.file`,
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,17 @@ pub const tab = struct {
|
||||||
state.* = .{};
|
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
|
/// On activation the Quote tab loads its shared candle data (by
|
||||||
/// delegating to performance, which owns it) and fetches the live
|
/// delegating to performance, which owns it) and fetches the live
|
||||||
/// quote that drives the headline price/change - the same
|
/// 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 handlePaste(state: *State, app: *App, text: []const u8) bool { ... }
|
||||||
//! pub fn statusOverride(state: *State, app: *App) ?framework.StatusOverride { ... }
|
//! 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
|
//! /// Optional: does this tab currently have async work in
|
||||||
//! /// flight that needs poll-driven redraws? While the ACTIVE
|
//! /// flight that needs poll-driven redraws? While the ACTIVE
|
||||||
//! /// tab answers true, the App keeps a one-shot vxfw Tick
|
//! /// 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 { ... }",
|
"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) ──
|
// ── Draw hooks (mutually exclusive, exactly one required) ──
|
||||||
//
|
//
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue