diff --git a/src/commands/analysis.zig b/src/commands/analysis.zig index 007ba46..269b4b9 100644 --- a/src/commands/analysis.zig +++ b/src/commands/analysis.zig @@ -86,9 +86,7 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f const label_width = fmt.analysis_label_width; const bar_width = fmt.analysis_bar_width; - try cli.setBold(out, color); - try out.print("\nPortfolio Analysis ({s})\n", .{file_path}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nPortfolio Analysis ({s})\n", .{file_path}); try out.print("========================================\n\n", .{}); // Equities vs Fixed Income summary @@ -97,9 +95,7 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f var fi_buf: [24]u8 = undefined; const eq_dollars = fmt.fmtMoneyAbs(&eq_buf, stock_pct * total_value); const fi_dollars = fmt.fmtMoneyAbs(&fi_buf, bond_pct * total_value); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Equities {d:.1}% ({s}) / Fixed Income {d:.1}% ({s})\n\n", .{ stock_pct * 100, eq_dollars, bond_pct * 100, fi_dollars }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Equities {d:.1}% ({s}) / Fixed Income {d:.1}% ({s})\n\n", .{ stock_pct * 100, eq_dollars, bond_pct * 100, fi_dollars }); } const sections = [_]struct { items: []const zfin.analysis.BreakdownItem, title: []const u8 }{ @@ -113,23 +109,18 @@ pub fn display(result: zfin.analysis.AnalysisResult, stock_pct: f64, bond_pct: f for (sections, 0..) |sec, si| { if (si > 0 and sec.items.len == 0) continue; if (si > 0) try out.print("\n", .{}); + // Bold + header color — reset at end of printFg clears both. try cli.setBold(out, color); - try cli.setFg(out, color, cli.CLR_HEADER); - try out.print("{s}\n", .{sec.title}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_HEADER, "{s}\n", .{sec.title}); try printBreakdownSection(out, sec.items, label_width, bar_width, color); } // Unclassified if (result.unclassified.len > 0) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" Unclassified (not in metadata.srf)\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_WARNING, " Unclassified (not in metadata.srf)\n", .{}); for (result.unclassified) |sym| { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s}\n", .{sym}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{sym}); } } diff --git a/src/commands/audit.zig b/src/commands/audit.zig index e30c548..282c2b2 100644 --- a/src/commands/audit.zig +++ b/src/commands/audit.zig @@ -618,20 +618,14 @@ pub fn compareSchwabSummary( } fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nSchwab Account Audit", .{}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" (brokerage is source of truth)\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nSchwab Account Audit", .{}); + try cli.printFg(out, color, cli.CLR_MUTED, " (brokerage is source of truth)\n", .{}); try out.print("========================================\n\n", .{}); // Column headers - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{ + try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>14} {s:>14} {s:>14} {s:>14}\n", .{ "Account", "PF Cash", "BR Cash", "PF Total", "BR Total", }); - try cli.reset(out, color); var grand_pf: f64 = 0; var grand_br: f64 = 0; @@ -664,21 +658,19 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o // Account label try out.print(" ", .{}); if (is_unmapped) { - try cli.setFg(out, color, cli.CLR_WARNING); + try cli.printFg(out, color, cli.CLR_WARNING, "{s:<24}", .{label}); + } else { + try out.print("{s:<24}", .{label}); } - try out.print("{s:<24}", .{label}); - if (is_unmapped) try cli.reset(out, color); // PF Cash — colored if mismatched (brokerage is truth) try out.print(" ", .{}); if (!cash_ok) { - if (r.cash_delta.? > 0) - try cli.setFg(out, color, cli.CLR_NEGATIVE) - else - try cli.setFg(out, color, cli.CLR_POSITIVE); + const rgb = if (r.cash_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE; + try cli.printFg(out, color, rgb, "{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)}); + } else { + try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)}); } - try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_cash_buf, r.portfolio_cash)}); - if (!cash_ok) try cli.reset(out, color); // BR Cash try out.print(" {s:>14}", .{br_cash_str}); @@ -686,36 +678,28 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o // PF Total — colored if not just stale prices try out.print(" ", .{}); if (!total_ok and !cash_ok) { - if (r.total_delta.? > 0) - try cli.setFg(out, color, cli.CLR_NEGATIVE) - else - try cli.setFg(out, color, cli.CLR_POSITIVE); + const rgb = if (r.total_delta.? > 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE; + try cli.printFg(out, color, rgb, "{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)}); + } else { + try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)}); } - try out.print("{s:>14}", .{fmt.fmtMoneyAbs(&pf_total_buf, r.portfolio_total)}); - if (!total_ok and !cash_ok) try cli.reset(out, color); // BR Total try out.print(" {s:>14}", .{br_total_str}); // Status if (is_unmapped) { - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" Unmapped", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_WARNING, " Unmapped", .{}); } else if (!cash_ok) { var delta_buf: [24]u8 = undefined; const d = r.cash_delta.?; const sign: []const u8 = if (d >= 0) "+" else "-"; - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_WARNING, " Cash {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }); } else if (!total_ok) { var delta_buf: [24]u8 = undefined; const d = r.total_delta.?; const sign: []const u8 = if (d >= 0) "+" else "-"; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Value {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Value {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(d)) }); } try out.print("\n", .{}); @@ -730,33 +714,24 @@ fn displaySchwabResults(results: []const SchwabAccountComparison, color: bool, o var grand_delta_buf: [24]u8 = undefined; const grand_delta = grand_br - grand_pf; - try cli.setBold(out, color); - try out.print(" Total: portfolio {s} schwab {s}", .{ + try cli.printBold(out, color, " Total: portfolio {s} schwab {s}", .{ fmt.fmtMoneyAbs(&pf_grand_buf, grand_pf), fmt.fmtMoneyAbs(&br_grand_buf, grand_br), }); - try cli.reset(out, color); if (@abs(grand_delta) < 1.0) { // no delta } else { const sign: []const u8 = if (grand_delta >= 0) "+" else "-"; - if (grand_delta >= 0) { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - } - try out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) }); - try cli.reset(out, color); + const rgb = if (grand_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE; + try cli.printFg(out, color, rgb, " delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_delta)) }); } try out.print("\n", .{}); if (discrepancy_count > 0) { - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" {d} {s} — drill down with: zfin audit --schwab \n", .{ + try cli.printFg(out, color, cli.CLR_WARNING, " {d} {s} — drill down with: zfin audit --schwab \n", .{ discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches"), }); - try cli.reset(out, color); } try out.print("\n", .{}); } @@ -1120,12 +1095,8 @@ fn displayRatioSuggestions( if (!has_header) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Ratio updates", .{}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" (for portfolio.srf)\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Ratio updates", .{}); + try cli.printFg(out, color, cli.CLR_MUTED, " (for portfolio.srf)\n", .{}); has_header = true; } @@ -1137,16 +1108,10 @@ fn displayRatioSuggestions( const drift_str = std.fmt.bufPrint(&drift_buf, "{d:.2}%", .{drift_pct}) catch "?"; try out.print(" {s:<16} ", .{lot_sym}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("ticker {s:<6}", .{price_sym}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "ticker {s:<6}", .{price_sym}); try out.print(" ratio {s} -> ", .{cur_str}); - try cli.setBold(out, color); - try out.print("{s}", .{sug_str}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" ({s} drift)\n", .{drift_str}); - try cli.reset(out, color); + try cli.printBold(out, color, "{s}", .{sug_str}); + try cli.printFg(out, color, cli.CLR_MUTED, " ({s} drift)\n", .{drift_str}); break; // One suggestion per symbol } @@ -1159,12 +1124,8 @@ fn displayRatioSuggestions( // ── Display ───────────────────────────────────────────────── fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nPortfolio Audit", .{}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" (brokerage is source of truth)\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nPortfolio Audit", .{}); + try cli.printFg(out, color, cli.CLR_MUTED, " (brokerage is source of truth)\n", .{}); try out.print("========================================\n\n", .{}); var total_portfolio: f64 = 0; @@ -1174,26 +1135,18 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io. for (results) |acct| { // Account header - try cli.setBold(out, color); if (acct.account_name.len > 0) { - try out.print(" {s}", .{acct.account_name}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" ({s}, #{s})\n", .{ acct.brokerage_name, acct.account_number }); + try cli.printBold(out, color, " {s}", .{acct.account_name}); + try cli.printFg(out, color, cli.CLR_MUTED, " ({s}, #{s})\n", .{ acct.brokerage_name, acct.account_number }); } else { - try out.print(" {s} #{s}", .{ acct.brokerage_name, acct.account_number }); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" (unmapped — add account_number to accounts.srf)\n", .{}); + try cli.printBold(out, color, " {s} #{s}", .{ acct.brokerage_name, acct.account_number }); + try cli.printFg(out, color, cli.CLR_WARNING, " (unmapped — add account_number to accounts.srf)\n", .{}); } - try cli.reset(out, color); // Column headers - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{ + try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} {s}\n", .{ "Symbol", "PF Shares", "BR Shares", "PF Price", "BR Price", "", }); - try cli.reset(out, color); for (acct.comparisons) |cmp| { var pf_shares_buf: [16]u8 = undefined; @@ -1297,57 +1250,44 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io. // Print PF Shares with color switch (shares_color) { - .positive => try cli.setFg(out, color, cli.CLR_POSITIVE), - .negative => try cli.setFg(out, color, cli.CLR_NEGATIVE), - .warning => try cli.setFg(out, color, cli.CLR_WARNING), - .normal => {}, + .positive => try cli.printFg(out, color, cli.CLR_POSITIVE, "{s:>12}", .{pf_shares_str}), + .negative => try cli.printFg(out, color, cli.CLR_NEGATIVE, "{s:>12}", .{pf_shares_str}), + .warning => try cli.printFg(out, color, cli.CLR_WARNING, "{s:>12}", .{pf_shares_str}), + .normal => try out.print("{s:>12}", .{pf_shares_str}), } - try out.print("{s:>12}", .{pf_shares_str}); - if (shares_color != .normal) try cli.reset(out, color); // Print BR Shares, prices try out.print(" {s:>12} {s:>10} {s:>10} ", .{ br_shares_str, pf_price_str, br_price_str }); // Print status switch (status_color) { - .warning => try cli.setFg(out, color, cli.CLR_WARNING), - .muted => try cli.setFg(out, color, cli.CLR_MUTED), - .none => {}, + .warning => try cli.printFg(out, color, cli.CLR_WARNING, "{s}\n", .{status}), + .muted => try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{status}), + .none => try out.print("{s}\n", .{status}), } - try out.print("{s}\n", .{status}); - if (status_color != .none) try cli.reset(out, color); } // Account totals var pf_total_buf: [24]u8 = undefined; var br_total_buf: [24]u8 = undefined; var delta_buf: [24]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} ", .{ + try cli.printFg(out, color, cli.CLR_MUTED, " {s:<24} {s:>12} {s:>12} {s:>10} {s:>10} ", .{ "", "", "", fmt.fmtMoneyAbs(&pf_total_buf, acct.portfolio_total), fmt.fmtMoneyAbs(&br_total_buf, acct.brokerage_total), }); - try cli.reset(out, color); const adj_delta = acct.total_delta - acct.option_value_delta; if (@abs(adj_delta) < 1.0) { // no delta text needed } else { const sign: []const u8 = if (adj_delta >= 0) "+" else "-"; - if (adj_delta >= 0) { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - } - try out.print("Delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(adj_delta)) }); - try cli.reset(out, color); + const rgb = if (adj_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE; + try cli.printFg(out, color, rgb, "Delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&delta_buf, @abs(adj_delta)) }); } if (@abs(acct.option_value_delta) >= 1.0) { var opt_delta_buf: [24]u8 = undefined; const opt_sign: []const u8 = if (acct.option_value_delta >= 0) "+" else "-"; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_delta_buf, @abs(acct.option_value_delta)) }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_delta_buf, @abs(acct.option_value_delta)) }); } try out.print("\n\n", .{}); @@ -1363,39 +1303,28 @@ fn displayResults(results: []const AccountComparison, color: bool, out: *std.Io. const grand_delta = total_brokerage - total_portfolio; const grand_adj_delta = grand_delta - total_option_delta; - try cli.setBold(out, color); - try out.print(" Total: portfolio {s} brokerage {s}", .{ + try cli.printBold(out, color, " Total: portfolio {s} brokerage {s}", .{ fmt.fmtMoneyAbs(&pf_grand_buf, total_portfolio), fmt.fmtMoneyAbs(&br_grand_buf, total_brokerage), }); - try cli.reset(out, color); if (@abs(grand_adj_delta) < 1.0) { // no delta text needed } else { const sign: []const u8 = if (grand_adj_delta >= 0) "+" else "-"; - if (grand_adj_delta >= 0) { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - } - try out.print(" delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_adj_delta)) }); - try cli.reset(out, color); + const rgb = if (grand_adj_delta >= 0) cli.CLR_NEGATIVE else cli.CLR_POSITIVE; + try cli.printFg(out, color, rgb, " delta {s}{s}", .{ sign, fmt.fmtMoneyAbs(&grand_delta_buf, @abs(grand_adj_delta)) }); } if (@abs(total_option_delta) >= 1.0) { var opt_grand_buf: [24]u8 = undefined; const opt_sign: []const u8 = if (total_option_delta >= 0) "+" else "-"; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_grand_buf, @abs(total_option_delta)) }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " (options {s}{s})", .{ opt_sign, fmt.fmtMoneyAbs(&opt_grand_buf, @abs(total_option_delta)) }); } try out.print("\n", .{}); if (discrepancy_count > 0) { - try cli.setFg(out, color, cli.CLR_WARNING); - try out.print(" {d} {s} to investigate\n", .{ discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches") }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_WARNING, " {d} {s} to investigate\n", .{ discrepancy_count, if (discrepancy_count == 1) @as([]const u8, "mismatch") else @as([]const u8, "mismatches") }); } try out.print("\n", .{}); } @@ -1644,9 +1573,7 @@ fn runHygieneCheck( }; defer account_map.deinit(); - try cli.setBold(out, color); - try out.print(" Portfolio hygiene\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Portfolio hygiene\n", .{}); // ── Section 1: Stale manual prices ── @@ -1665,9 +1592,7 @@ fn runHygieneCheck( if (!header_shown) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Stale manual prices (>{d} days — --stale-days to configure)\n", .{stale_days}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Stale manual prices (>{d} days — --stale-days to configure)\n", .{stale_days}); header_shown = true; } @@ -1685,18 +1610,12 @@ fn runHygieneCheck( date_str, }); const clr = stalenessColor(age_days, stale_days); - try cli.setFg(out, color, clr); - try out.print("({d} days)\n", .{@as(u32, @intCast(age_days))}); - try cli.reset(out, color); + try cli.printFg(out, color, clr, "({d} days)\n", .{@as(u32, @intCast(age_days))}); } if (!header_shown) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Stale manual prices (>{d} days)\n", .{stale_days}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print(" (none)\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Stale manual prices (>{d} days)\n", .{stale_days}); + try cli.printFg(out, color, cli.CLR_POSITIVE, " (none)\n", .{}); } } @@ -1851,34 +1770,25 @@ fn runHygieneCheck( if (!overdue_header_shown) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Accounts overdue for update (weekly default — set update_cadence in accounts.srf)\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Accounts overdue for update (weekly default — set update_cadence in accounts.srf)\n", .{}); overdue_header_shown = true; } try out.print(" {s:<32} {s:<10}", .{ acct_name, cadence.label() }); if (age_days) |ad| { const clr = stalenessColor(ad, threshold_days); - try cli.setFg(out, color, clr); - try out.print("last updated {d} days ago\n", .{@as(u32, @intCast(ad))}); + try cli.printFg(out, color, clr, "last updated {d} days ago\n", .{@as(u32, @intCast(ad))}); } else { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - try out.print("no update history found\n", .{}); + try cli.printFg(out, color, cli.CLR_NEGATIVE, "no update history found\n", .{}); } - try cli.reset(out, color); } // Display accounts updated in working copy if (updated_accounts.items.len > 0) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Accounts updated (working copy)\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Accounts updated (working copy)\n", .{}); for (updated_accounts.items) |acct| { - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print(" {s}\n", .{acct}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_POSITIVE, " {s}\n", .{acct}); } } } @@ -1915,9 +1825,7 @@ fn runHygieneCheck( // Display discovered files if (all_files.items.len > 0) { try out.print("\n", .{}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Brokerage files (last {d} hours)\n", .{audit_file_max_age_hours}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Brokerage files (last {d} hours)\n", .{audit_file_max_age_hours}); for (all_files.items) |f| { const kind_label: []const u8 = switch (f.kind) { .fidelity_csv => "fidelity", @@ -1955,9 +1863,7 @@ fn runHygieneCheck( } try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Reconciliation\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Reconciliation\n", .{}); for (all_files.items) |f| { const file_data = std.fs.cwd().readFileAlloc(allocator, f.path, 10 * 1024 * 1024) catch continue; @@ -1979,9 +1885,7 @@ fn runHygieneCheck( for (results) |r| { if (r.account_name.len > 0) acct_count += 1; } - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print(" schwab summary: {d} accounts, no discrepancies\n", .{acct_count}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_POSITIVE, " schwab summary: {d} accounts, no discrepancies\n", .{acct_count}); } }, .fidelity_csv => { @@ -1999,9 +1903,7 @@ fn runHygieneCheck( try displayResults(results, color, out); try displayRatioSuggestions(results, portfolio, prices, color, out); } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print(" fidelity: {d} accounts, no discrepancies\n", .{results.len}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_POSITIVE, " fidelity: {d} accounts, no discrepancies\n", .{results.len}); // Always show ratio suggestions even in compact mode try displayRatioSuggestions(results, portfolio, prices, color, out); } @@ -2021,9 +1923,7 @@ fn runHygieneCheck( try displayResults(results, color, out); try displayRatioSuggestions(results, portfolio, prices, color, out); } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print(" schwab: {d} accounts, no discrepancies\n", .{results.len}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_POSITIVE, " schwab: {d} accounts, no discrepancies\n", .{results.len}); try displayRatioSuggestions(results, portfolio, prices, color, out); } }, diff --git a/src/commands/compare.zig b/src/commands/compare.zig index 4d7e92d..2bd2009 100644 --- a/src/commands/compare.zig +++ b/src/commands/compare.zig @@ -399,15 +399,13 @@ fn renderCompare(out: *std.Io.Writer, color: bool, cv: view.CompareView) !void { const now_str = view.nowLabel(cv, &now_buf); // Header - try cli.setFg(out, color, cli.CLR_HEADER); try cli.setBold(out, color); - try out.print("Portfolio comparison: {s} → {s} ({d} day{s})\n", .{ + try cli.printFg(out, color, cli.CLR_HEADER, "Portfolio comparison: {s} → {s} ({d} day{s})\n", .{ then_str, now_str, cv.days_between, view.dayPlural(cv.days_between), }); - try cli.reset(out, color); try out.print("\n", .{}); // Totals line — two-color: muted "then → now", intent-colored delta/pct. diff --git a/src/commands/contributions.zig b/src/commands/contributions.zig index 3777f31..b690f22 100644 --- a/src/commands/contributions.zig +++ b/src/commands/contributions.zig @@ -728,9 +728,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co // Header try cli.setBold(out, color); - try cli.setFg(out, color, h_color); - try out.writeAll("Portfolio contributions report\n"); - try cli.reset(out, color); + try cli.printFg(out, color, h_color, "Portfolio contributions report\n", .{}); try cli.setFg(out, color, mut_color); try out.writeAll(" "); try out.writeAll(label); @@ -739,9 +737,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co // If nothing changed at all, say so explicitly and return. if (report.changes.len == 0) { - try cli.setFg(out, color, mut_color); - try out.writeAll(" No changes detected.\n"); - try cli.reset(out, color); + try cli.printFg(out, color, mut_color, " No changes detected.\n", .{}); return; } @@ -909,9 +905,7 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co // Grand totals try cli.setBold(out, color); - try cli.setFg(out, color, h_color); - try out.writeAll("Totals\n"); - try cli.reset(out, color); + try cli.printFg(out, color, h_color, "Totals\n", .{}); var buf1: [32]u8 = undefined; var buf2: [32]u8 = undefined; var buf3: [32]u8 = undefined; @@ -1040,13 +1034,9 @@ fn printSummaryCell(out: *std.Io.Writer, label: []const u8, v: f64, color: bool) var buf: [32]u8 = undefined; try out.print("{s} ", .{label}); if (v == 0) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s:>12}", .{"-"}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s:>12}", .{"-"}); } else { - try cli.setFg(out, color, cli.CLR_POSITIVE); - try out.print("{s:>12}", .{fmt.fmtMoneyAbs(&buf, v)}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_POSITIVE, "{s:>12}", .{fmt.fmtMoneyAbs(&buf, v)}); } } diff --git a/src/commands/divs.zig b/src/commands/divs.zig index 3c363ae..b26972e 100644 --- a/src/commands/divs.zig +++ b/src/commands/divs.zig @@ -28,15 +28,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io } pub fn display(dividends: []const zfin.Dividend, symbol: []const u8, current_price: ?f64, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nDividend History for {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nDividend History for {s}\n", .{symbol}); try out.print("========================================\n", .{}); if (dividends.len == 0) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" No dividends found.\n\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " No dividends found.\n\n", .{}); return; } diff --git a/src/commands/earnings.zig b/src/commands/earnings.zig index 2fd9f8d..41453ad 100644 --- a/src/commands/earnings.zig +++ b/src/commands/earnings.zig @@ -34,15 +34,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io } pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nEarnings History for {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nEarnings History for {s}\n", .{symbol}); try out.print("========================================\n", .{}); if (events.len == 0) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" No earnings data found.\n\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " No earnings data found.\n\n", .{}); return; } @@ -59,16 +55,14 @@ pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bo var row_buf: [128]u8 = undefined; const row = fmt.fmtEarningsRow(&row_buf, e); - if (row.is_future) { - try cli.setFg(out, color, cli.CLR_MUTED); - } else if (row.is_positive) { - try cli.setFg(out, color, cli.CLR_POSITIVE); - } else { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - } + const rgb = if (row.is_future) + cli.CLR_MUTED + else if (row.is_positive) + cli.CLR_POSITIVE + else + cli.CLR_NEGATIVE; - try out.print("{s}", .{row.text}); - try cli.reset(out, color); + try cli.printFg(out, color, rgb, "{s}", .{row.text}); try out.print("\n", .{}); } diff --git a/src/commands/etf.zig b/src/commands/etf.zig index 241150e..a6f7bad 100644 --- a/src/commands/etf.zig +++ b/src/commands/etf.zig @@ -24,9 +24,7 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io } pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nETF Profile: {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nETF Profile: {s}\n", .{symbol}); try out.print("========================================\n", .{}); if (profile.expense_ratio) |er| { @@ -46,9 +44,7 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o try out.print(" Inception Date: {s}\n", .{d.format(&db)}); } if (profile.leveraged) { - try cli.setFg(out, color, cli.CLR_NEGATIVE); - try out.print(" Leveraged: YES\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_NEGATIVE, " Leveraged: YES\n", .{}); } if (profile.total_holdings) |th| { try out.print(" Total Holdings: {d}\n", .{th}); @@ -57,15 +53,11 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o // Sectors if (profile.sectors) |sectors| { if (sectors.len > 0) { - try cli.setBold(out, color); - try out.print("\n Sector Allocation:\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "\n Sector Allocation:\n", .{}); for (sectors) |sec| { var title_buf: [64]u8 = undefined; const name = fmt.toTitleCase(&title_buf, sec.name); - try cli.setFg(out, color, cli.CLR_ACCENT); - try out.print(" {d:>5.1}%", .{sec.weight * 100.0}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_ACCENT, " {d:>5.1}%", .{sec.weight * 100.0}); try out.print(" {s}\n", .{name}); } } @@ -74,18 +66,14 @@ pub fn printProfile(profile: zfin.EtfProfile, symbol: []const u8, color: bool, o // Top holdings if (profile.holdings) |holdings| { if (holdings.len > 0) { - try cli.setBold(out, color); - try out.print("\n Top Holdings:\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "\n Top Holdings:\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" {s:>6} {s:>7} {s}\n", .{ "Symbol", "Weight", "Name" }); try out.print(" {s:->6} {s:->7} {s:->30}\n", .{ "", "", "" }); try cli.reset(out, color); for (holdings) |h| { if (h.symbol) |s| { - try cli.setFg(out, color, cli.CLR_ACCENT); - try out.print(" {s:>6}", .{s}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_ACCENT, " {s:>6}", .{s}); try out.print(" {d:>6.2}% {s}\n", .{ h.weight * 100.0, h.name }); } else { try out.print(" {s:>6} {d:>6.2}% {s}\n", .{ "--", h.weight * 100.0, h.name }); diff --git a/src/commands/history.zig b/src/commands/history.zig index eeea87d..66a997b 100644 --- a/src/commands/history.zig +++ b/src/commands/history.zig @@ -166,9 +166,7 @@ fn runSymbol( } pub fn displaySymbol(candles: []const zfin.Candle, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nPrice History for {s} (last 30 days)\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nPrice History for {s} (last 30 days)\n", .{symbol}); try out.print("========================================\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print("{s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{ @@ -182,11 +180,9 @@ pub fn displaySymbol(candles: []const zfin.Candle, symbol: []const u8, color: bo for (candles) |candle| { var db: [10]u8 = undefined; var vb: [32]u8 = undefined; - try cli.setGainLoss(out, color, if (candle.close >= candle.open) 1.0 else -1.0); - try out.print("{s:>12} {d:>10.2} {d:>10.2} {d:>10.2} {d:>10.2} {s:>12}\n", .{ + try cli.printGainLoss(out, color, if (candle.close >= candle.open) 1.0 else -1.0, "{s:>12} {d:>10.2} {d:>10.2} {d:>10.2} {d:>10.2} {s:>12}\n", .{ candle.date.format(&db), candle.open, candle.high, candle.low, candle.close, fmt.fmtIntCommas(&vb, candle.volume), }); - try cli.reset(out, color); } try out.print("\n{d} trading days\n\n", .{candles.len}); } @@ -288,9 +284,7 @@ pub fn renderPortfolio( resolution_override: ?timeline.Resolution, row_limit: usize, ) !void { - try cli.setBold(out, color); - try out.print("\nPortfolio Timeline — {s}\n", .{focus_metric.label()}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nPortfolio Timeline — {s}\n", .{focus_metric.label()}); try out.print("========================================\n", .{}); // ── Windows block ───────────────────────────────────────── @@ -407,9 +401,7 @@ fn renderTable( var rlabel_buf: [32]u8 = undefined; const rlabel = view.fmtResolutionLabel(&rlabel_buf, resolution_override, resolution); - try cli.setBold(out, color); - try out.print(" Recent snapshots {s}\n", .{rlabel}); - try cli.reset(out, color); + try cli.printBold(out, color, " Recent snapshots {s}\n", .{rlabel}); try cli.setFg(out, color, cli.CLR_MUTED); // Column order: Liquid → Illiquid → Net Worth (components sum to total). diff --git a/src/commands/lookup.zig b/src/commands/lookup.zig index 41ca5a8..5d41d32 100644 --- a/src/commands/lookup.zig +++ b/src/commands/lookup.zig @@ -5,9 +5,7 @@ const isCusipLike = @import("../models/portfolio.zig").isCusipLike; pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const u8, color: bool, out: *std.Io.Writer) !void { if (!isCusipLike(cusip)) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "Note: '{s}' doesn't look like a CUSIP (expected 9 alphanumeric chars with digits)\n", .{cusip}); } try cli.stderrPrint("Looking up via OpenFIGI...\n"); @@ -41,33 +39,22 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, cusip: []const pub fn display(result: zfin.CusipResult, cusip: []const u8, color: bool, out: *std.Io.Writer) !void { if (result.ticker) |ticker| { - try cli.setBold(out, color); - try out.print("{s}", .{cusip}); - try cli.reset(out, color); + try cli.printBold(out, color, "{s}", .{cusip}); try out.print(" -> ", .{}); - try cli.setFg(out, color, cli.CLR_ACCENT); - try out.print("{s}", .{ticker}); - try cli.reset(out, color); - try out.print("\n", .{}); + try cli.printFg(out, color, cli.CLR_ACCENT, "{s}\n", .{ticker}); if (result.name) |name| { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Name: {s}\n", .{name}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name}); } if (result.security_type) |st| { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Type: {s}\n", .{st}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Type: {s}\n", .{st}); } try out.print("\n To use in portfolio: ticker::{s}\n", .{ticker}); } else { try out.print("No ticker found for CUSIP '{s}'\n", .{cusip}); if (result.name) |name| { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Name: {s}\n", .{name}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Name: {s}\n", .{name}); } try out.print("\n Tip: For mutual funds, OpenFIGI often has no coverage.\n", .{}); try out.print(" Add manually: symbol::{s},ticker::XXXX,...\n", .{cusip}); diff --git a/src/commands/options.zig b/src/commands/options.zig index 8b889ab..a183b8d 100644 --- a/src/commands/options.zig +++ b/src/commands/options.zig @@ -30,9 +30,7 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, ntm: usize, color: bool, pub fn display(out: *std.Io.Writer, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void { if (chains.len == 0) return; - try cli.setBold(out, color); - try out.print("\nOptions Chain for {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nOptions Chain for {s}\n", .{symbol}); try out.print("========================================\n", .{}); if (chains[0].underlying_price) |price| { var price_buf: [24]u8 = undefined; @@ -98,9 +96,7 @@ pub fn printSection( is_calls: bool, color: bool, ) !void { - try cli.setBold(out, color); - try out.print(" {s}\n", .{label}); - try cli.reset(out, color); + try cli.printBold(out, color, " {s}\n", .{label}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" {s:>10} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}\n", .{ "Strike", "Last", "Bid", "Ask", "Volume", "OI", "IV", diff --git a/src/commands/perf.zig b/src/commands/perf.zig index 81db9e2..efb1e57 100644 --- a/src/commands/perf.zig +++ b/src/commands/perf.zig @@ -24,9 +24,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const const today = fmt.todayDate(); const month_end = today.lastDayOfPriorMonth(); - try cli.setBold(out, color); - try out.print("\nTrailing Returns for {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nTrailing Returns for {s}\n", .{symbol}); try out.print("========================================\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print("Data points: {d} (", .{c.len}); @@ -48,25 +46,19 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const // -- As-of-date returns -- { var db: [10]u8 = undefined; - try cli.setBold(out, color); - try out.print("\nAs-of {s}:\n", .{end_date.format(&db)}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nAs-of {s}:\n", .{end_date.format(&db)}); } try printReturnsTable(out, result.asof_price, if (has_divs) result.asof_total else null, color); // -- Month-end returns -- { var db: [10]u8 = undefined; - try cli.setBold(out, color); - try out.print("\nMonth-end ({s}):\n", .{month_end.format(&db)}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nMonth-end ({s}):\n", .{month_end.format(&db)}); } try printReturnsTable(out, result.me_price, if (has_divs) result.me_total else null, color); if (!has_divs) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("\nSet POLYGON_API_KEY for total returns with dividend reinvestment.\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "\nSet POLYGON_API_KEY for total returns with dividend reinvestment.\n", .{}); } // -- Risk metrics -- @@ -124,29 +116,21 @@ pub fn printReturnsTable( ); if (price_arr[i] != null) { - try cli.setGainLoss(out, color, if (row.price_positive) 1.0 else -1.0); + try cli.printGainLoss(out, color, if (row.price_positive) 1.0 else -1.0, " {s:>13}", .{row.price_str}); } else { - try cli.setFg(out, color, cli.CLR_MUTED); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13}", .{row.price_str}); } - try out.print(" {s:>13}", .{row.price_str}); - try cli.reset(out, color); if (has_total) { if (row.total_str) |ts| { - try cli.setGainLoss(out, color, if (row.price_positive) 1.0 else -1.0); - try out.print(" {s:>13}", .{ts}); - try cli.reset(out, color); + try cli.printGainLoss(out, color, if (row.price_positive) 1.0 else -1.0, " {s:>13}", .{ts}); } else { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:>13}", .{"N/A"}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13}", .{"N/A"}); } } if (row.suffix.len > 0) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s}", .{row.suffix}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s}", .{row.suffix}); } try out.print("\n", .{}); } @@ -165,9 +149,7 @@ pub fn printRiskTable(out: *std.Io.Writer, tr: zfin.risk.TrailingRisk, color: bo } if (!any) return; - try cli.setBold(out, color); - try out.print("\nRisk Metrics (monthly returns):\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nRisk Metrics (monthly returns):\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print("{s:>22} {s:>14} {s:>14} {s:>14}\n", .{ "", "Volatility", "Sharpe", "Max DD" }); @@ -181,13 +163,9 @@ pub fn printRiskTable(out: *std.Io.Writer, tr: zfin.risk.TrailingRisk, color: bo if (risk_arr[i]) |rm| { try out.print(" {d:>12.1}%", .{rm.volatility * 100.0}); try out.print(" {d:>13.2}", .{rm.sharpe}); - try cli.setFg(out, color, cli.CLR_NEGATIVE); - try out.print(" {d:>12.1}%", .{rm.max_drawdown * 100.0}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_NEGATIVE, " {d:>12.1}%", .{rm.max_drawdown * 100.0}); } else { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:>13} {s:>13} {s:>13}", .{ "—", "—", "—" }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:>13} {s:>13} {s:>13}", .{ "—", "—", "—" }); } try out.print("\n", .{}); } diff --git a/src/commands/portfolio.zig b/src/commands/portfolio.zig index 7d6def4..dddb3dc 100644 --- a/src/commands/portfolio.zig +++ b/src/commands/portfolio.zig @@ -4,11 +4,6 @@ const cli = @import("common.zig"); const fmt = cli.fmt; const views = @import("../views/portfolio_sections.zig"); -/// Map a semantic StyleIntent to CLI ANSI foreground color. -fn setIntentFg(out: *std.Io.Writer, color: bool, intent: fmt.StyleIntent) !void { - try cli.setStyleIntent(out, color, intent); -} - pub fn run(allocator: std.mem.Allocator, 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 var loaded = cli.loadPortfolio(allocator, file_path) orelse return; @@ -153,9 +148,7 @@ pub fn display( ) !void { const summary = &pf_data.summary; // Header with summary - try cli.setBold(out, color); - try out.print("\nPortfolio Summary ({s})\n", .{file_path}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nPortfolio Summary ({s})\n", .{file_path}); try out.print("========================================\n", .{}); // Summary bar @@ -165,13 +158,11 @@ pub fn display( var gl_buf: [24]u8 = undefined; const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss; try out.print(" Value: {s} Cost: {s} ", .{ fmt.fmtMoneyAbs(&val_buf, summary.total_value), fmt.fmtMoneyAbs(&cost_buf, summary.total_cost) }); - try cli.setGainLoss(out, color, summary.unrealized_gain_loss); - try out.print("Gain/Loss: {c}{s} ({d:.1}%)", .{ + try cli.printGainLoss(out, color, summary.unrealized_gain_loss, "Gain/Loss: {c}{s} ({d:.1}%)", .{ @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'), fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0, }); - try cli.reset(out, color); try out.print("\n", .{}); } @@ -182,9 +173,7 @@ pub fn display( if (lot.security_type != .stock) continue; if (lot.isOpen()) open_lots += 1 else closed_lots += 1; } - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " Lots: {d} open, {d} closed Positions: {d} symbols\n", .{ open_lots, closed_lots, positions.len }); // Historical portfolio value snapshots { @@ -306,26 +295,20 @@ pub fn display( if (!drip.st.isEmpty()) { var drip_buf: [128]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s}\n", .{fmt.fmtDripSummary(&drip_buf, "ST", drip.st)}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{fmt.fmtDripSummary(&drip_buf, "ST", drip.st)}); } if (!drip.lt.isEmpty()) { var drip_buf2: [128]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s}\n", .{fmt.fmtDripSummary(&drip_buf2, "LT", drip.lt)}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s}\n", .{fmt.fmtDripSummary(&drip_buf2, "LT", drip.lt)}); } } } } // Totals line - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:->6} {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8}\n", .{ + try cli.printFg(out, color, cli.CLR_MUTED, " {s:->6} {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8}\n", .{ "", "", "", "", "", "", "", }); - try cli.reset(out, color); { var total_mv_buf: [24]u8 = undefined; var total_gl_buf: [24]u8 = undefined; @@ -333,24 +316,20 @@ pub fn display( try out.print(" {s:>6} {s:>8} {s:>10} {s:>10} {s:>16} ", .{ "", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value), }); - try cli.setGainLoss(out, color, summary.unrealized_gain_loss); - try out.print("{c}{s:>13}", .{ + try cli.printGainLoss(out, color, summary.unrealized_gain_loss, "{c}{s:>13}", .{ @as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'), fmt.fmtMoneyAbs(&total_gl_buf, gl_abs), }); - try cli.reset(out, color); try out.print(" {s:>7}\n", .{"100.0%"}); } if (summary.realized_gain_loss != 0) { var rpl_buf: [24]u8 = undefined; const rpl_abs = if (summary.realized_gain_loss >= 0) summary.realized_gain_loss else -summary.realized_gain_loss; - try cli.setGainLoss(out, color, summary.realized_gain_loss); - try out.print("\n Realized P&L: {c}{s}\n", .{ + try cli.printGainLoss(out, color, summary.realized_gain_loss, "\n Realized P&L: {c}{s}\n", .{ @as(u8, if (summary.realized_gain_loss >= 0) '+' else '-'), fmt.fmtMoneyAbs(&rpl_buf, rpl_abs), }); - try cli.reset(out, color); } // Options section @@ -359,9 +338,7 @@ pub fn display( defer prepared_opts.deinit(); if (prepared_opts.items.len > 0) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Options\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Options\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(views.OptionsLayout.header ++ "\n", views.OptionsLayout.header_labels); try out.print(views.OptionsLayout.separator ++ "\n", views.OptionsLayout.separator_fills); @@ -374,21 +351,15 @@ pub fn display( const prem_start = po.premium_col_start; const prem_end = @min(prem_start + views.OptionsLayout.premium_w, text.len); // Pre-premium portion - try setIntentFg(out, color, po.row_style); - try out.print("{s}", .{text[0..prem_start]}); + try cli.printIntent(out, color, po.row_style, "{s}", .{text[0..prem_start]}); // Premium column - try setIntentFg(out, color, po.premium_style); - try out.print("{s}", .{text[prem_start..prem_end]}); + try cli.printIntent(out, color, po.premium_style, "{s}", .{text[prem_start..prem_end]}); // Post-premium portion (account) - try setIntentFg(out, color, po.row_style); - if (prem_end < text.len) try out.print("{s}", .{text[prem_end..]}); - try cli.reset(out, color); + if (prem_end < text.len) try cli.printIntent(out, color, po.row_style, "{s}", .{text[prem_end..]}); try out.print("\n", .{}); } // Options total - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:->30} {s:->6} {s:->12} {s:->14}\n", .{ "", "", "", "" }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:->30} {s:->6} {s:->12} {s:->14}\n", .{ "", "", "", "" }); var opt_total_buf: [24]u8 = undefined; try out.print(" {s:>30} {s:>6} {s:>12} {s:>14}\n", .{ "", "", "TOTAL", fmt.fmtMoneyAbs(&opt_total_buf, opt_total_premium), @@ -402,9 +373,7 @@ pub fn display( defer prepared_cds.deinit(); if (prepared_cds.items.len > 0) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Certificates of Deposit\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Certificates of Deposit\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(views.CDsLayout.header ++ "\n", views.CDsLayout.header_labels); try out.print(views.CDsLayout.separator ++ "\n", views.CDsLayout.separator_fills); @@ -413,14 +382,10 @@ pub fn display( var cd_section_total: f64 = 0; for (prepared_cds.items) |pc| { cd_section_total += pc.lot.shares; - try setIntentFg(out, color, pc.row_style); - try out.print("{s}\n", .{pc.text}); - try cli.reset(out, color); + try cli.printIntent(out, color, pc.row_style, "{s}\n", .{pc.text}); } // CD total - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:->12} {s:->14}\n", .{ "", "" }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:->12} {s:->14}\n", .{ "", "" }); var cd_total_buf: [24]u8 = undefined; try out.print(" {s:>12} {s:>14}\n", .{ "TOTAL", fmt.fmtMoneyAbs(&cd_total_buf, cd_section_total), @@ -431,9 +396,7 @@ pub fn display( // Cash section if (portfolio.hasType(.cash)) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Cash\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Cash\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); var cash_hdr_buf: [80]u8 = undefined; try out.print("{s}\n", .{fmt.fmtCashHeader(&cash_hdr_buf)}); @@ -449,21 +412,15 @@ pub fn display( } // Cash total var sep_buf: [80]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s}\n", .{fmt.fmtCashSep(&sep_buf)}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{fmt.fmtCashSep(&sep_buf)}); var total_buf: [80]u8 = undefined; - try cli.setBold(out, color); - try out.print("{s}\n", .{fmt.fmtCashTotal(&total_buf, portfolio.totalCash())}); - try cli.reset(out, color); + try cli.printBold(out, color, "{s}\n", .{fmt.fmtCashTotal(&total_buf, portfolio.totalCash())}); } // Illiquid assets section if (portfolio.hasType(.illiquid)) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Illiquid Assets\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Illiquid Assets\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); var il_hdr_buf: [80]u8 = undefined; try out.print("{s}\n", .{fmt.fmtIlliquidHeader(&il_hdr_buf)}); @@ -478,13 +435,9 @@ pub fn display( } // Illiquid total var il_sep_buf2: [80]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s}\n", .{fmt.fmtIlliquidSep(&il_sep_buf2)}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{fmt.fmtIlliquidSep(&il_sep_buf2)}); var il_total_buf: [80]u8 = undefined; - try cli.setBold(out, color); - try out.print("{s}\n", .{fmt.fmtIlliquidTotal(&il_total_buf, portfolio.totalIlliquid())}); - try cli.reset(out, color); + try cli.printBold(out, color, "{s}\n", .{fmt.fmtIlliquidTotal(&il_total_buf, portfolio.totalIlliquid())}); } // Net Worth (if illiquid assets exist) @@ -495,21 +448,17 @@ pub fn display( var liq_buf: [24]u8 = undefined; var il_buf: [24]u8 = undefined; try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{ + try cli.printBold(out, color, " Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{ fmt.fmtMoneyAbs(&nw_buf, net_worth), fmt.fmtMoneyAbs(&liq_buf, summary.total_value), fmt.fmtMoneyAbs(&il_buf, illiquid_total), }); - try cli.reset(out, color); } // Watchlist if (watch_symbols.len > 0) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Watchlist:\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Watchlist:\n", .{}); for (watch_symbols) |sym| { var price_str: [16]u8 = undefined; const ps: []const u8 = if (watch_prices.get(sym)) |close| @@ -530,9 +479,7 @@ pub fn display( if (tr.three_year) |metrics| { if (!any_risk) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Risk Metrics (3-Year, monthly returns):\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, " Risk Metrics (3-Year, monthly returns):\n", .{}); try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" {s:>6} {s:>10} {s:>8} {s:>10}\n", .{ "Symbol", "Volatility", "Sharpe", "Max DD", @@ -546,9 +493,7 @@ pub fn display( try out.print(" {s:>6} {d:>9.1}% {d:>8.2} ", .{ a.display_symbol, metrics.volatility * 100.0, metrics.sharpe, }); - try cli.setFg(out, color, cli.CLR_NEGATIVE); - try out.print("{d:>9.1}%", .{metrics.max_drawdown * 100.0}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_NEGATIVE, "{d:>9.1}%", .{metrics.max_drawdown * 100.0}); try out.print("\n", .{}); } } @@ -576,17 +521,11 @@ pub fn printLotRow(out: *std.Io.Writer, color: bool, lot: zfin.Lot, current_pric var lot_mv_buf: [24]u8 = undefined; const lot_mv = fmt.fmtMoneyAbs(&lot_mv_buf, lot.shares * use_price); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{ + try cli.printFg(out, color, cli.CLR_MUTED, " " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{ status_str, lot.shares, fmt.fmtMoneyAbs(&lot_price_buf, lot.open_price), "", lot_mv, }); - try cli.reset(out, color); - try cli.setGainLoss(out, color, gl); - try out.print("{s}{s:>13}", .{ lot_sign, lot_gl_money }); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:>8} {s} {s} {s}\n", .{ "", date_str, indicator, acct_col }); - try cli.reset(out, color); + try cli.printGainLoss(out, color, gl, "{s}{s:>13}", .{ lot_sign, lot_gl_money }); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:>8} {s} {s} {s}\n", .{ "", date_str, indicator, acct_col }); } // ── Tests ──────────────────────────────────────────────────── diff --git a/src/commands/projections.zig b/src/commands/projections.zig index 7645479..c5eb21e 100644 --- a/src/commands/projections.zig +++ b/src/commands/projections.zig @@ -130,14 +130,12 @@ pub fn run( const comparison = ctx.comparison; try out.print("\n", .{}); - try cli.setBold(out, color); if (resolution) |r| { var buf: [10]u8 = undefined; - try out.print("Projections (as of {s})\n", .{r.actual.format(&buf)}); + try cli.printBold(out, color, "Projections (as of {s})\n", .{r.actual.format(&buf)}); } else { - try out.print("Projections ({s})\n", .{file_path}); + try cli.printBold(out, color, "Projections ({s})\n", .{file_path}); } - try cli.reset(out, color); try out.print("========================================\n", .{}); // If auto-snapped, print a muted note so the user knows the @@ -147,24 +145,20 @@ pub fn run( const diff = r.requested.days - r.actual.days; var req_buf: [10]u8 = undefined; var act_buf: [10]u8 = undefined; - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("(requested {s}; nearest snapshot: {s}, {d} day{s} earlier)\n", .{ + try cli.printFg(out, color, cli.CLR_MUTED, "(requested {s}; nearest snapshot: {s}, {d} day{s} earlier)\n", .{ r.requested.format(&req_buf), r.actual.format(&act_buf), diff, fmt.dayPlural(diff), }); - try cli.reset(out, color); } } try out.print("\n", .{}); // Header row - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s: <32}{s: >8}{s: >9}{s: >9}{s: >10}{s: >9}\n", .{ + try cli.printFg(out, color, cli.CLR_MUTED, "{s: <32}{s: >8}{s: >9}{s: >9}{s: >10}{s: >9}\n", .{ "", "1 Year", "3 Year", "5 Year", "10 Year", "Week", }); - try cli.reset(out, color); // Build return rows via view model var spy_bufs: [5][16]u8 = undefined; @@ -208,9 +202,7 @@ pub fn run( { var buf: [16]u8 = undefined; const cell = view.fmtReturnCell(&buf, comparison.conservative_return); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s: <32}{s: >8}\n", .{ "Conservative estimate", cell.text }); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s: <32}{s: >8}\n", .{ "Conservative estimate", cell.text }); } // Target allocation note @@ -218,9 +210,7 @@ pub fn run( var note_buf: [128]u8 = undefined; if (view.fmtAllocationNote(¬e_buf, ctx.config.target_stock_pct, ctx.stock_pct)) |note| { try out.print("\n", .{}); - try cli.setStyleIntent(out, color, note.style); - try out.print("{s}\n", .{note.text}); - try cli.reset(out, color); + try cli.printIntent(out, color, note.style, "{s}\n", .{note.text}); } } @@ -230,9 +220,7 @@ pub fn run( if (ctx.data.bands[last_idx]) |b| { if (b.len >= 2) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print("Median Portfolio Value ({d}-Year, 99% withdrawal)\n\n", .{horizons[last_idx]}); - try cli.reset(out, color); + try cli.printBold(out, color, "Median Portfolio Value ({d}-Year, 99% withdrawal)\n\n", .{horizons[last_idx]}); // Synthesize candles from median values const candles = try va.alloc(zfin.Candle, b.len); @@ -267,9 +255,7 @@ pub fn run( // ── Terminal portfolio value ───────────────────────────────── try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print("Terminal Portfolio Value (nominal, at 99% withdrawal rate)\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "Terminal Portfolio Value (nominal, at 99% withdrawal rate)\n", .{}); try out.print("{s}\n", .{try view.buildHeaderRow(va, horizons, view.terminal_col_width)}); @@ -277,16 +263,12 @@ pub fn run( const p_styles = [_]view.StyleIntent{ .muted, .normal, .muted }; for (p_labels, p_styles, 0..) |plabel, pstyle, pi| { const row = try view.buildPercentileRow(va, plabel, pi, ctx.data.bands, pstyle); - try cli.setStyleIntent(out, color, row.style); - try out.print("{s}\n", .{row.text}); - try cli.reset(out, color); + try cli.printIntent(out, color, row.style, "{s}\n", .{row.text}); } // ── Safe withdrawal table ────────────────────────────────── try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print("Safe Withdrawal (FIRECalc historical simulation)\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "Safe Withdrawal (FIRECalc historical simulation)\n", .{}); // Header row try out.print("{s}\n", .{try view.buildHeaderRow(va, horizons, view.withdrawal_col_width)}); @@ -295,9 +277,7 @@ pub fn run( for (confidence_levels, 0..) |conf, ci| { const wr_rows = try view.buildWithdrawalRows(va, conf, horizons, ctx.data.withdrawals, ci); try out.print("{s}\n", .{wr_rows.amount.text}); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print("{s}\n", .{wr_rows.rate.text}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, "{s}\n", .{wr_rows.rate.text}); } // Life events summary — as-of mode uses ages-as-of-as_of; live @@ -309,14 +289,10 @@ pub fn run( const ages_ref_date = if (resolution) |r| r.actual else fmt.todayDate(); const ages = ctx.config.currentAgesAsOf(ages_ref_date); try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print("Life Events\n", .{}); - try cli.reset(out, color); + try cli.printBold(out, color, "Life Events\n", .{}); for (events) |*ev| { const line = try view.fmtEventLine(va, ev, &ages); - try cli.setStyleIntent(out, color, line.style); - try out.print("{s}\n", .{line.text}); - try cli.reset(out, color); + try cli.printIntent(out, color, line.style, "{s}\n", .{line.text}); } } } @@ -386,14 +362,12 @@ fn writeReturnRow(out: *std.Io.Writer, color: bool, row: view.ReturnRow) !void { } fn writeCell(out: *std.Io.Writer, color: bool, cell: view.ReturnCell, width: usize) !void { - try cli.setStyleIntent(out, color, cell.style); switch (width) { - 8 => try out.print("{s: >8}", .{cell.text}), - 9 => try out.print("{s: >9}", .{cell.text}), - 10 => try out.print("{s: >10}", .{cell.text}), - else => try out.print("{s}", .{cell.text}), + 8 => try cli.printIntent(out, color, cell.style, "{s: >8}", .{cell.text}), + 9 => try cli.printIntent(out, color, cell.style, "{s: >9}", .{cell.text}), + 10 => try cli.printIntent(out, color, cell.style, "{s: >10}", .{cell.text}), + else => try cli.printIntent(out, color, cell.style, "{s}", .{cell.text}), } - try cli.reset(out, color); } // ── Tests ──────────────────────────────────────────────────── diff --git a/src/commands/quote.zig b/src/commands/quote.zig index 2f13268..9dc80eb 100644 --- a/src/commands/quote.zig +++ b/src/commands/quote.zig @@ -86,9 +86,7 @@ pub fn display(allocator: std.mem.Allocator, candles: []const zfin.Candle, quote const change = price - prev_close; const pct = (change / prev_close) * 100.0; var chg_buf: [64]u8 = undefined; - try cli.setGainLoss(out, color, change); - try out.print(" Change: {s}\n", .{fmt.fmtPriceChange(&chg_buf, change, pct)}); - try cli.reset(out, color); + try cli.printGainLoss(out, color, change, " Change: {s}\n", .{fmt.fmtPriceChange(&chg_buf, change, pct)}); } } @@ -107,22 +105,16 @@ pub fn display(allocator: std.mem.Allocator, candles: []const zfin.Candle, quote // Recent history table (last 20 candles) if (candles.len > 0) { try out.print("\n", .{}); - try cli.setBold(out, color); - try out.print(" Recent History:\n", .{}); - try cli.reset(out, color); - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" {s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{ + try cli.printBold(out, color, " Recent History:\n", .{}); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:>12} {s:>10} {s:>10} {s:>10} {s:>10} {s:>12}\n", .{ "Date", "Open", "High", "Low", "Close", "Volume", }); - try cli.reset(out, color); const start_idx = if (candles.len > 20) candles.len - 20 else 0; for (candles[start_idx..]) |candle| { var row_buf: [128]u8 = undefined; const day_gain = candle.close >= candle.open; - try cli.setGainLoss(out, color, if (day_gain) 1.0 else -1.0); - try out.print("{s}\n", .{fmt.fmtCandleRow(&row_buf, candle)}); - try cli.reset(out, color); + try cli.printGainLoss(out, color, if (day_gain) 1.0 else -1.0, "{s}\n", .{fmt.fmtCandleRow(&row_buf, candle)}); } try out.print("\n {d} trading days shown\n", .{candles[start_idx..].len}); } diff --git a/src/commands/splits.zig b/src/commands/splits.zig index b237d5e..24f0f4d 100644 --- a/src/commands/splits.zig +++ b/src/commands/splits.zig @@ -22,15 +22,11 @@ pub fn run(svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io } pub fn display(splits: []const zfin.Split, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { - try cli.setBold(out, color); - try out.print("\nSplit History for {s}\n", .{symbol}); - try cli.reset(out, color); + try cli.printBold(out, color, "\nSplit History for {s}\n", .{symbol}); try out.print("========================================\n", .{}); if (splits.len == 0) { - try cli.setFg(out, color, cli.CLR_MUTED); - try out.print(" No splits found.\n\n", .{}); - try cli.reset(out, color); + try cli.printFg(out, color, cli.CLR_MUTED, " No splits found.\n\n", .{}); return; }