pass fetch options through TUI
All checks were successful
Generic zig build / build (push) Successful in 5m18s
Generic zig build / publish-macos (push) Successful in 12s
Generic zig build / deploy (push) Successful in 20s

This commit is contained in:
Emil Lerch 2026-06-30 11:43:40 -07:00
parent 3a1500bfbb
commit 44f940c882
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 24 additions and 8 deletions

View file

@ -501,7 +501,7 @@ fn runCli(init: std.process.Init) !u8 {
// loader resolve + union-merge the same way the CLI does.
// This is the load-bearing fix for "CLI and TUI report
// different totals" - there's exactly one code path now.
tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, resolved_theme, cmd_args, today) catch |err| switch (err) {
tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, resolved_theme, cmd_args, today, globals.refresh_policy) catch |err| switch (err) {
// tui.run already printed an actionable stderr message
// for invalid CLI args; surface as exit 1 without a
// panic / stack trace.

View file

@ -502,6 +502,12 @@ pub const App = struct {
local_tz: zfin.market.TimeZone,
config: zfin.Config,
svc: *zfin.DataService,
/// Cache-vs-network policy from the global `--refresh-data` flag,
/// mapped once at init. Threaded into every automatic data fetch
/// (the initial `portfolio.load` and each tab's `loadData`) so the
/// TUI honors `--refresh-data=never` (offline) and `=force` the same
/// way the CLI commands do. Default `.{}` is auto (respect TTL).
fetch_options: zfin.FetchOptions = .{},
keymap: keybinds.KeyMap,
theme: theme.Theme,
active_tab: Tab = .portfolio,
@ -2510,6 +2516,7 @@ pub fn run(
app_theme: theme.Theme,
args: []const []const u8,
today: zfin.Date,
refresh_policy: framework.RefreshPolicy,
) !void {
const watchlist_path: ?[]const u8 = global_watchlist_path;
@ -2593,6 +2600,7 @@ pub fn run(
.local_tz = local_tz,
.config = config,
.svc = svc,
.fetch_options = cli.fetchOptionsFromPolicy(refresh_policy),
.keymap = keymap,
.theme = app_theme,
.symbol = symbol,
@ -2684,6 +2692,8 @@ pub fn run(
.progress = symbol_progress.callback(),
.aggregate_progress = aggregate_progress.callback(),
.watchlist_syms = watch_syms.items,
.force_refresh = app_inst.fetch_options.force_refresh,
.skip_network = app_inst.fetch_options.skip_network,
}) catch |err| blk: {
std.log.scoped(.tui).warn("portfolio load failed: {t}", .{err});
break :blk null;

View file

@ -87,7 +87,10 @@ pub const tab = struct {
/// frees current payload, clears flags, and re-runs the
/// fetch path.
pub fn reload(state: *State, app: *App) !void {
if (app.symbol.len > 0) {
// Offline mode (--refresh-data=never): skip cache invalidation.
// loadData can't re-fetch, so dropping the entry would just blank
// the tab; instead re-render from the cached entry.
if (app.symbol.len > 0 and !app.fetch_options.skip_network) {
app.svc.invalidate(app.symbol, .earnings);
}
// Clear every flag so loadData has the same starting
@ -135,7 +138,7 @@ fn loadData(state: *State, app: *App) void {
state.loaded = true;
state.error_msg = null;
const result = app.svc.getEarnings(app.symbol, .{}) catch |err| {
const result = app.svc.getEarnings(app.symbol, app.fetch_options) catch |err| {
switch (err) {
zfin.DataError.NoApiKey => {
state.error_msg = "No API key. Set FMP_API_KEY (free at financialmodelingprep.com)";

View file

@ -143,7 +143,10 @@ pub const tab = struct {
pub const deactivate = framework.noopDeactivate(State);
pub fn reload(state: *State, app: *App) !void {
if (app.symbol.len > 0) {
// Offline mode (--refresh-data=never): skip cache invalidation.
// loadData can't re-fetch, so dropping the entry would just blank
// the tab; instead re-render from the cached entry.
if (app.symbol.len > 0 and !app.fetch_options.skip_network) {
app.svc.invalidate(app.symbol, .options);
}
// Drop chains first so loadData starts clean.
@ -298,7 +301,7 @@ fn loadData(state: *State, app: *App) void {
}
state.chains = null;
const result = app.svc.getOptions(app.symbol, .{}) catch |err| {
const result = app.svc.getOptions(app.symbol, app.fetch_options) catch |err| {
switch (err) {
zfin.DataError.FetchFailed => app.setStatus("CBOE fetch failed (network error)"),
else => app.setStatus("Error loading options"),

View file

@ -119,7 +119,7 @@ fn loadData(state: *State, app: *App) void {
app.symbol_data.trailing_me_price = null;
app.symbol_data.trailing_me_total = null;
const result = app.svc.getTrailingReturns(app.symbol, .{}) catch |err| {
const result = app.svc.getTrailingReturns(app.symbol, app.fetch_options) catch |err| {
switch (err) {
zfin.DataError.NoApiKey => app.setStatus("No API key. Set TIINGO_API_KEY"),
zfin.DataError.FetchFailed => app.setStatus("Fetch failed (network error or rate limit)"),
@ -155,7 +155,7 @@ fn loadData(state: *State, app: *App) void {
// Try to load ETF profile (non-fatal, won't show for non-ETFs)
if (!app.symbol_data.etf_loaded) {
app.symbol_data.etf_loaded = true;
if (app.svc.getEtfProfile(app.symbol, .{})) |etf_result| {
if (app.svc.getEtfProfile(app.symbol, app.fetch_options)) |etf_result| {
if (etf_result.data.isEtf()) {
// Take ownership of the EtfProfile data. We
// deliberately don't call etf_result.deinit

View file

@ -174,7 +174,7 @@ fn refreshLiveQuote(state: *State, app: *App, force: bool) void {
// "refreshed Xs ago" header timestamp.
const now_s = std.Io.Timestamp.now(app.io, .real).toSeconds();
if (!force and !shouldFetchLiveQuote(market.marketSession(now_s), state.live != null, now_s - state.timestamp)) return;
if (app.svc.getQuote(app.symbol, .{})) |q| {
if (app.svc.getQuote(app.symbol, app.fetch_options)) |q| {
state.live = q;
state.timestamp = now_s;
} else |err| std.log.scoped(.quote_tab).debug("{s}: live-quote fetch failed: {t}", .{ app.symbol, err });