begin cleanup on remaining tab/app separation of concerns
This commit is contained in:
parent
0c3ddd1ffc
commit
15ae2cbf20
6 changed files with 104 additions and 96 deletions
140
src/tui.zig
140
src/tui.zig
|
|
@ -868,6 +868,29 @@ pub const App = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Dispatch to a fallible (`!void`-returning) hook on the
|
||||
/// active tab. Errors are swallowed — matches the existing
|
||||
/// `tab.activate(...) catch {}` idiom that this dispatcher
|
||||
/// replaces. Use this for `activate`/`reload`/etc. where the
|
||||
/// caller doesn't have a meaningful recovery path.
|
||||
///
|
||||
/// The contract validator already required `activate`/`reload`
|
||||
/// to exist on every tab, so the `@hasDecl` guard is mostly
|
||||
/// future-proofing for newly-added fallible hooks that aren't
|
||||
/// universally required.
|
||||
fn dispatchTry(self: *App, comptime hook_name: []const u8, args: anytype) void {
|
||||
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, hook_name)) return;
|
||||
const fn_ptr = @field(Module.tab, hook_name);
|
||||
const state_ptr = &@field(self.states, field.name);
|
||||
@call(.auto, fn_ptr, .{ state_ptr, self } ++ args) catch {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Broadcast to every tab that declares `hook_name` — not just
|
||||
/// the active tab. Tabs that don't declare it are skipped.
|
||||
/// Order is `tab_modules` declaration order; callers should not
|
||||
|
|
@ -1580,55 +1603,31 @@ pub const App = struct {
|
|||
}
|
||||
|
||||
fn refreshCurrentTab(self: *App) void {
|
||||
// Invalidate cache so the next load forces a fresh fetch
|
||||
if (self.symbol.len > 0) {
|
||||
switch (self.active_tab) {
|
||||
.quote, .performance => {
|
||||
self.svc.invalidate(self.symbol, .candles_daily);
|
||||
self.svc.invalidate(self.symbol, .dividends);
|
||||
},
|
||||
.earnings => {
|
||||
self.svc.invalidate(self.symbol, .earnings);
|
||||
},
|
||||
.options => {
|
||||
self.svc.invalidate(self.symbol, .options);
|
||||
},
|
||||
.portfolio, .analysis, .history, .projections => {},
|
||||
}
|
||||
}
|
||||
switch (self.active_tab) {
|
||||
.portfolio => {
|
||||
self.portfolio.loaded = false;
|
||||
self.freePortfolioSummary();
|
||||
},
|
||||
.quote, .performance => {
|
||||
self.states.performance.loaded = false;
|
||||
if (self.symbol_data.candles) |c| self.allocator.free(c);
|
||||
self.symbol_data.candles = null;
|
||||
if (self.symbol_data.dividends) |d| zfin.Dividend.freeSlice(self.allocator, d);
|
||||
self.symbol_data.dividends = null;
|
||||
self.states.quote.chart.dirty = true;
|
||||
self.states.quote.chart.freeCache(self.allocator); // Invalidate indicator cache
|
||||
},
|
||||
.earnings => {
|
||||
tab_modules.earnings.tab.reload(&self.states.earnings, self) catch {};
|
||||
},
|
||||
.options => {
|
||||
tab_modules.options.tab.reload(&self.states.options, self) catch {};
|
||||
},
|
||||
.analysis => {
|
||||
tab_modules.analysis.tab.reload(&self.states.analysis, self) catch {};
|
||||
},
|
||||
.history => {
|
||||
tab_modules.history.tab.reload(&self.states.history, self) catch {};
|
||||
},
|
||||
.projections => {
|
||||
tab_modules.projections.tab.reload(&self.states.projections, self) catch {};
|
||||
},
|
||||
}
|
||||
self.loadTabData();
|
||||
// Each tab's `reload` hook owns its full per-tab refresh
|
||||
// sequence: invalidate the svc cache, drop in-memory
|
||||
// state, and re-fetch via `loadData` (or whatever the
|
||||
// tab's loader is named). The framework dispatcher routes
|
||||
// to the active tab's reload; tabs that share data
|
||||
// (quote/performance) delegate via their reload bodies.
|
||||
//
|
||||
// Reload is contractually self-completing: when it
|
||||
// returns, the tab's state is fully refreshed. There's no
|
||||
// need for a follow-up `loadTabData` — `activate` would
|
||||
// see `state.loaded = true` and no-op.
|
||||
self.dispatchTry("reload", .{});
|
||||
|
||||
// After reload, fetch live quote for active symbol (costs 1 API call)
|
||||
// Live-quote re-fetch: the quote tab's freshness display
|
||||
// ("refreshed Xs ago") is driven by `states.quote.timestamp`,
|
||||
// which is independent of the candles cache. The user's
|
||||
// mental model for `r` includes "and the price ticker is
|
||||
// current as of NOW," so we hit the live-quote endpoint
|
||||
// here. Only on tabs that show a live quote.
|
||||
//
|
||||
// This is the one place where `r` reaches past the cache;
|
||||
// tab-switches that activate quote/performance don't trigger
|
||||
// it (they should keep using whatever cached price the user
|
||||
// last fetched). When live-streaming quotes ship someday,
|
||||
// this block goes away.
|
||||
switch (self.active_tab) {
|
||||
.quote, .performance => {
|
||||
if (self.symbol.len > 0) {
|
||||
|
|
@ -1645,33 +1644,12 @@ pub const App = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Activate the current tab — load any data it needs, set
|
||||
/// any per-tab UI state. Each tab's `activate` is responsible
|
||||
/// for self-gating (e.g. skipping when `app.symbol.len == 0`
|
||||
/// or when its data is already cached).
|
||||
pub fn loadTabData(self: *App) void {
|
||||
switch (self.active_tab) {
|
||||
.portfolio => {
|
||||
tab_modules.portfolio.tab.activate(&self.states.portfolio, self) catch {};
|
||||
},
|
||||
.quote, .performance => {
|
||||
if (self.symbol.len == 0) return;
|
||||
tab_modules.performance.tab.activate(&self.states.performance, self) catch {};
|
||||
},
|
||||
.earnings => {
|
||||
if (self.symbol.len == 0) return;
|
||||
tab_modules.earnings.tab.activate(&self.states.earnings, self) catch {};
|
||||
},
|
||||
.options => {
|
||||
if (self.symbol.len == 0) return;
|
||||
tab_modules.options.tab.activate(&self.states.options, self) catch {};
|
||||
},
|
||||
.analysis => {
|
||||
tab_modules.analysis.tab.activate(&self.states.analysis, self) catch {};
|
||||
},
|
||||
.history => {
|
||||
tab_modules.history.tab.activate(&self.states.history, self) catch {};
|
||||
},
|
||||
.projections => {
|
||||
tab_modules.projections.tab.activate(&self.states.projections, self) catch {};
|
||||
},
|
||||
}
|
||||
self.dispatchTry("activate", .{});
|
||||
}
|
||||
|
||||
pub fn loadPortfolioData(self: *App) void {
|
||||
|
|
@ -2244,20 +2222,6 @@ pub fn renderBrailleToStyledLines(arena: std.mem.Allocator, lines: *std.ArrayLis
|
|||
pub const loadWatchlist = cli.loadWatchlist;
|
||||
pub const freeWatchlist = cli.freeWatchlist;
|
||||
|
||||
// Force test discovery for imported TUI sub-modules
|
||||
comptime {
|
||||
_ = keybinds;
|
||||
_ = theme;
|
||||
_ = tab_modules.portfolio;
|
||||
_ = tab_modules.quote;
|
||||
_ = tab_modules.performance;
|
||||
_ = tab_modules.options;
|
||||
_ = tab_modules.earnings;
|
||||
_ = tab_modules.analysis;
|
||||
_ = tab_modules.history;
|
||||
_ = tab_modules.projections;
|
||||
}
|
||||
|
||||
/// Write the full default keymap to `out` in keys.srf format,
|
||||
/// covering both the global section and each tab's local bindings.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ pub const tab = struct {
|
|||
/// loads on first activation per symbol; subsequent activations
|
||||
/// for the same symbol short-circuit on `loaded`.
|
||||
pub fn activate(state: *State, app: *App) !void {
|
||||
if (app.symbol.len == 0) return;
|
||||
if (state.disabled) return;
|
||||
if (state.loaded) return;
|
||||
loadData(state, app);
|
||||
|
|
@ -85,9 +86,13 @@ pub const tab = struct {
|
|||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
/// Force re-fetch on user request (refresh keybind, symbol
|
||||
/// change, etc). Frees current payload + clears flags +
|
||||
/// re-runs the fetch path.
|
||||
/// change, etc). Invalidates the svc cache for earnings,
|
||||
/// frees current payload, clears flags, and re-runs the
|
||||
/// fetch path.
|
||||
pub fn reload(state: *State, app: *App) !void {
|
||||
if (app.symbol.len > 0) {
|
||||
app.svc.invalidate(app.symbol, .earnings);
|
||||
}
|
||||
// Clear every flag so loadData has the same starting
|
||||
// conditions as a fresh activation.
|
||||
if (state.data) |e| app.allocator.free(e);
|
||||
|
|
|
|||
|
|
@ -135,6 +135,9 @@ pub const tab = struct {
|
|||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
pub fn reload(state: *State, app: *App) !void {
|
||||
if (app.symbol.len > 0) {
|
||||
app.svc.invalidate(app.symbol, .options);
|
||||
}
|
||||
// Drop chains first so loadData starts clean.
|
||||
if (state.chains) |chains| {
|
||||
zfin.OptionsChain.freeSlice(app.allocator, chains);
|
||||
|
|
|
|||
|
|
@ -60,7 +60,24 @@ pub const tab = struct {
|
|||
|
||||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
/// Manual refresh: invalidate the shared svc cache for candles
|
||||
/// and dividends so the next `loadData` re-fetches from
|
||||
/// network, then drop in-memory copies and the chart cache
|
||||
/// shared with the quote tab. Quote and performance share
|
||||
/// `app.symbol_data`; quote piggybacks on this reload via its
|
||||
/// own delegating reload.
|
||||
pub fn reload(state: *State, app: *App) !void {
|
||||
if (app.symbol.len > 0) {
|
||||
app.svc.invalidate(app.symbol, .candles_daily);
|
||||
app.svc.invalidate(app.symbol, .dividends);
|
||||
}
|
||||
// The chart is rendered by the quote tab but is fed from
|
||||
// `app.symbol_data.candles` which performance owns. After
|
||||
// a refresh the next quote draw must re-render and the
|
||||
// indicator overlay cache (SMA/Bollinger/etc) must drop.
|
||||
app.states.quote.chart.dirty = true;
|
||||
app.states.quote.chart.freeCache(app.allocator);
|
||||
|
||||
state.loaded = false;
|
||||
loadData(state, app);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,8 +181,16 @@ pub const tab = struct {
|
|||
|
||||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
/// Manual refresh (r/F5): drop the cached aggregate summary
|
||||
/// and re-fetch live prices via `loadPortfolioData`. Distinct
|
||||
/// from `reloadPortfolioFile` (R), which re-reads
|
||||
/// `portfolio.srf` from disk. The framework calls this from
|
||||
/// `refreshCurrentTab`; the file-reload path has its own
|
||||
/// separate action.
|
||||
pub fn reload(state: *State, app: *App) !void {
|
||||
reloadPortfolioFile(state, app);
|
||||
app.portfolio.loaded = false;
|
||||
app.freePortfolioSummary();
|
||||
loadPortfolioData(state, app);
|
||||
}
|
||||
|
||||
pub const tick = framework.noopTick(State);
|
||||
|
|
|
|||
|
|
@ -77,20 +77,31 @@ pub const tab = struct {
|
|||
/// fetch path lives in tui.zig after the tab switches because
|
||||
/// it depends on App.svc); no-op here. Chart redraws are
|
||||
/// triggered by the dirty flag on `state.chart`.
|
||||
/// Quote and performance share `app.symbol_data` (candles +
|
||||
/// dividends). Performance owns the loader; quote piggybacks
|
||||
/// by delegating its activate to performance's. This keeps
|
||||
/// `loadTabData`'s dispatch uniform — every tab activates its
|
||||
/// own state — while preserving the historical "switching to
|
||||
/// quote populates shared candle data" behavior.
|
||||
pub fn activate(state: *State, app: *App) !void {
|
||||
_ = state;
|
||||
_ = app;
|
||||
const perf_module = @import("performance_tab.zig");
|
||||
try perf_module.tab.activate(&app.states.performance, app);
|
||||
}
|
||||
|
||||
pub const deactivate = framework.noopDeactivate(State);
|
||||
|
||||
/// Refresh: invalidate candles cache, drop the live quote,
|
||||
/// mark chart dirty so the next draw re-renders.
|
||||
/// Refresh: delegate to performance.reload, which owns the
|
||||
/// shared candle/dividend data and svc invalidation. Quote's
|
||||
/// chart-state (dirty + freeCache) is also reset by
|
||||
/// performance.reload — see the comment there for why.
|
||||
/// Quote-only state (live quote + timestamp) is reset here
|
||||
/// because performance doesn't know about it.
|
||||
pub fn reload(state: *State, app: *App) !void {
|
||||
state.live = null;
|
||||
state.timestamp = 0;
|
||||
state.chart.dirty = true;
|
||||
state.chart.freeCache(app.allocator);
|
||||
const perf_module = @import("performance_tab.zig");
|
||||
try perf_module.tab.reload(&app.states.performance, app);
|
||||
}
|
||||
|
||||
pub const tick = framework.noopTick(State);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue