ai: provide fetch progress info when tui starts
This commit is contained in:
parent
bfeac82c51
commit
089e81df54
3 changed files with 160 additions and 73 deletions
|
|
@ -79,6 +79,50 @@ pub fn stderrRateLimitWait(wait_seconds: u64, color: bool) !void {
|
|||
try out.flush();
|
||||
}
|
||||
|
||||
/// Progress callback for loadPrices that prints to stderr.
|
||||
/// Shared between the CLI portfolio command and TUI pre-fetch.
|
||||
pub const LoadProgress = struct {
|
||||
svc: *zfin.DataService,
|
||||
color: bool,
|
||||
/// Offset added to index for display (e.g. stock count when loading watch symbols).
|
||||
index_offset: usize,
|
||||
/// Grand total across all loadPrices calls (stocks + watch).
|
||||
grand_total: usize,
|
||||
|
||||
fn onProgress(ctx: *anyopaque, index: usize, _: usize, symbol: []const u8, status: zfin.DataService.SymbolStatus) void {
|
||||
const self: *LoadProgress = @ptrCast(@alignCast(ctx));
|
||||
const display_idx = self.index_offset + index + 1;
|
||||
switch (status) {
|
||||
.fetching => {
|
||||
// Show rate-limit wait before the fetch
|
||||
if (self.svc.estimateWaitSeconds()) |w| {
|
||||
if (w > 0) stderrRateLimitWait(w, self.color) catch {};
|
||||
}
|
||||
stderrProgress(symbol, " (fetching)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.cached => {
|
||||
stderrProgress(symbol, " (cached)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.fetched => {
|
||||
// Already showed "(fetching)" — no extra line needed
|
||||
},
|
||||
.failed_used_stale => {
|
||||
stderrProgress(symbol, " FAILED (using cached)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.failed => {
|
||||
stderrProgress(symbol, " FAILED", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn callback(self: *LoadProgress) zfin.DataService.ProgressCallback {
|
||||
return .{
|
||||
.context = @ptrCast(self),
|
||||
.on_progress = onProgress,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
||||
test "setFg emits ANSI when color enabled" {
|
||||
|
|
|
|||
|
|
@ -3,49 +3,6 @@ const zfin = @import("../root.zig");
|
|||
const cli = @import("common.zig");
|
||||
const fmt = cli.fmt;
|
||||
|
||||
/// CLI progress context for loadPrices callback.
|
||||
const CliProgress = struct {
|
||||
svc: *zfin.DataService,
|
||||
color: bool,
|
||||
/// Offset added to index for display (e.g. stock count when loading watch symbols).
|
||||
index_offset: usize,
|
||||
/// Grand total across all loadPrices calls (stocks + watch).
|
||||
grand_total: usize,
|
||||
|
||||
fn onProgress(ctx: *anyopaque, index: usize, _: usize, symbol: []const u8, status: zfin.DataService.SymbolStatus) void {
|
||||
const self: *CliProgress = @ptrCast(@alignCast(ctx));
|
||||
const display_idx = self.index_offset + index + 1;
|
||||
switch (status) {
|
||||
.fetching => {
|
||||
// Show rate-limit wait before the fetch
|
||||
if (self.svc.estimateWaitSeconds()) |w| {
|
||||
if (w > 0) cli.stderrRateLimitWait(w, self.color) catch {};
|
||||
}
|
||||
cli.stderrProgress(symbol, " (fetching)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.cached => {
|
||||
cli.stderrProgress(symbol, " (cached)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.fetched => {
|
||||
// Already showed "(fetching)" — no extra line needed
|
||||
},
|
||||
.failed_used_stale => {
|
||||
cli.stderrProgress(symbol, " FAILED (using cached)", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
.failed => {
|
||||
cli.stderrProgress(symbol, " FAILED", display_idx, self.grand_total, self.color) catch {};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(self: *CliProgress) zfin.DataService.ProgressCallback {
|
||||
return .{
|
||||
.context = @ptrCast(self),
|
||||
.on_progress = onProgress,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataService, file_path: []const u8, watchlist_path: ?[]const u8, force_refresh: bool, color: bool, out: *std.Io.Writer) !void {
|
||||
// Load portfolio from SRF file
|
||||
const data = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch |err| {
|
||||
|
|
@ -104,7 +61,7 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, svc: *zfin.DataSer
|
|||
}
|
||||
|
||||
// Progress callback for per-symbol output
|
||||
var progress_ctx = CliProgress{
|
||||
var progress_ctx = cli.LoadProgress{
|
||||
.svc = svc,
|
||||
.color = color,
|
||||
.index_offset = 0,
|
||||
|
|
|
|||
144
src/tui.zig
144
src/tui.zig
|
|
@ -2,6 +2,7 @@ const std = @import("std");
|
|||
const vaxis = @import("vaxis");
|
||||
const zfin = @import("root.zig");
|
||||
const fmt = zfin.format;
|
||||
const cli = @import("commands/common.zig");
|
||||
const keybinds = @import("tui/keybinds.zig");
|
||||
const theme_mod = @import("tui/theme.zig");
|
||||
const chart_mod = @import("tui/chart.zig");
|
||||
|
|
@ -234,6 +235,7 @@ const App = struct {
|
|||
portfolio_sort_field: PortfolioSortField = .symbol, // current sort column
|
||||
portfolio_sort_dir: SortDirection = .asc, // current sort direction
|
||||
watchlist_prices: ?std.StringHashMap(f64) = null, // cached watchlist prices (no disk I/O during render)
|
||||
prefetched_prices: ?std.StringHashMap(f64) = null, // prices loaded before TUI starts (with stderr progress)
|
||||
|
||||
// Options navigation (inline expand/collapse like portfolio)
|
||||
options_cursor: usize = 0, // selected row in flattened options view
|
||||
|
|
@ -1017,34 +1019,6 @@ const App = struct {
|
|||
self.portfolio_loaded = true;
|
||||
self.freePortfolioSummary();
|
||||
|
||||
// Fetch data for watchlist symbols so they have prices to display
|
||||
// (from both the separate watchlist file and watch lots in the portfolio)
|
||||
if (self.watchlist_prices) |*wp| wp.clearRetainingCapacity() else {
|
||||
self.watchlist_prices = std.StringHashMap(f64).init(self.allocator);
|
||||
}
|
||||
var wp = &(self.watchlist_prices.?);
|
||||
if (self.watchlist) |wl| {
|
||||
for (wl) |sym| {
|
||||
const result = self.svc.getCandles(sym) catch continue;
|
||||
defer self.allocator.free(result.data);
|
||||
if (result.data.len > 0) {
|
||||
wp.put(sym, result.data[result.data.len - 1].close) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (self.portfolio) |pf| {
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.lot_type == .watch) {
|
||||
const sym = lot.priceSymbol();
|
||||
const result = self.svc.getCandles(sym) catch continue;
|
||||
defer self.allocator.free(result.data);
|
||||
if (result.data.len > 0) {
|
||||
wp.put(sym, result.data[result.data.len - 1].close) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pf = self.portfolio orelse return;
|
||||
|
||||
const positions = pf.positions(self.allocator) catch {
|
||||
|
|
@ -1068,7 +1042,57 @@ const App = struct {
|
|||
var fetch_count: usize = 0;
|
||||
var stale_count: usize = 0;
|
||||
var failed_syms: [8][]const u8 = undefined;
|
||||
{
|
||||
|
||||
if (self.prefetched_prices) |*pp| {
|
||||
// Use pre-fetched prices from before TUI started (first load only)
|
||||
// Move stock prices into the working map
|
||||
for (syms) |sym| {
|
||||
if (pp.get(sym)) |price| {
|
||||
prices.put(sym, price) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// Extract watchlist prices
|
||||
if (self.watchlist_prices) |*wp| wp.clearRetainingCapacity() else {
|
||||
self.watchlist_prices = std.StringHashMap(f64).init(self.allocator);
|
||||
}
|
||||
var wp = &(self.watchlist_prices.?);
|
||||
var pp_iter = pp.iterator();
|
||||
while (pp_iter.next()) |entry| {
|
||||
if (!prices.contains(entry.key_ptr.*)) {
|
||||
wp.put(entry.key_ptr.*, entry.value_ptr.*) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
pp.deinit();
|
||||
self.prefetched_prices = null;
|
||||
} else {
|
||||
// Live fetch (refresh path) — fetch watchlist first, then stock prices
|
||||
if (self.watchlist_prices) |*wp| wp.clearRetainingCapacity() else {
|
||||
self.watchlist_prices = std.StringHashMap(f64).init(self.allocator);
|
||||
}
|
||||
var wp = &(self.watchlist_prices.?);
|
||||
if (self.watchlist) |wl| {
|
||||
for (wl) |sym| {
|
||||
const result = self.svc.getCandles(sym) catch continue;
|
||||
defer self.allocator.free(result.data);
|
||||
if (result.data.len > 0) {
|
||||
wp.put(sym, result.data[result.data.len - 1].close) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.lot_type == .watch) {
|
||||
const sym = lot.priceSymbol();
|
||||
const result = self.svc.getCandles(sym) catch continue;
|
||||
defer self.allocator.free(result.data);
|
||||
if (result.data.len > 0) {
|
||||
wp.put(sym, result.data[result.data.len - 1].close) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch stock prices with TUI status-bar progress
|
||||
const TuiProgress = struct {
|
||||
app: *App,
|
||||
failed: *[8][]const u8,
|
||||
|
|
@ -3705,6 +3729,68 @@ pub fn run(allocator: std.mem.Allocator, config: zfin.Config, args: []const []co
|
|||
app_inst.active_tab = .quote;
|
||||
}
|
||||
|
||||
// Pre-fetch portfolio prices before TUI starts, with stderr progress.
|
||||
// This runs while the terminal is still in normal mode so output is visible.
|
||||
if (app_inst.portfolio) |pf| {
|
||||
const syms = pf.stockSymbols(allocator) catch null;
|
||||
defer if (syms) |s| allocator.free(s);
|
||||
|
||||
// Collect watchlist symbols
|
||||
var watch_syms: std.ArrayList([]const u8) = .empty;
|
||||
defer watch_syms.deinit(allocator);
|
||||
{
|
||||
var seen = std.StringHashMap(void).init(allocator);
|
||||
defer seen.deinit();
|
||||
if (syms) |ss| for (ss) |s| seen.put(s, {}) catch {};
|
||||
if (app_inst.watchlist) |wl| {
|
||||
for (wl) |sym_w| {
|
||||
if (!seen.contains(sym_w)) {
|
||||
seen.put(sym_w, {}) catch {};
|
||||
watch_syms.append(allocator, sym_w) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
for (pf.lots) |lot| {
|
||||
if (lot.lot_type == .watch and !seen.contains(lot.priceSymbol())) {
|
||||
seen.put(lot.priceSymbol(), {}) catch {};
|
||||
watch_syms.append(allocator, lot.priceSymbol()) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stock_count = if (syms) |ss| ss.len else 0;
|
||||
const total_count = stock_count + watch_syms.items.len;
|
||||
|
||||
if (total_count > 0) {
|
||||
var prices = std.StringHashMap(f64).init(allocator);
|
||||
|
||||
var progress = cli.LoadProgress{
|
||||
.svc = svc,
|
||||
.color = true,
|
||||
.index_offset = 0,
|
||||
.grand_total = total_count,
|
||||
};
|
||||
|
||||
if (syms) |ss| {
|
||||
const result = svc.loadPrices(ss, &prices, false, progress.callback());
|
||||
progress.index_offset = stock_count;
|
||||
|
||||
if (result.fetched_count > 0 or result.fail_count > 0) {
|
||||
var msg_buf: [256]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&msg_buf, "Loaded {d} symbols ({d} cached, {d} fetched, {d} failed)\n", .{ ss.len, result.cached_count, result.fetched_count, result.fail_count }) catch "Done loading\n";
|
||||
cli.stderrPrint(msg) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
// Load watchlist prices
|
||||
if (watch_syms.items.len > 0) {
|
||||
_ = svc.loadPrices(watch_syms.items, &prices, false, progress.callback());
|
||||
}
|
||||
|
||||
app_inst.prefetched_prices = prices;
|
||||
}
|
||||
}
|
||||
|
||||
defer if (app_inst.portfolio) |*pf| pf.deinit();
|
||||
defer freeWatchlist(allocator, app_inst.watchlist);
|
||||
defer app_inst.deinitData();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue