clean up format.zig/remove it from exported module

This commit is contained in:
Emil Lerch 2026-03-10 15:13:20 -07:00
parent feb1fe21f0
commit d187664494
Signed by: lobo
GPG key ID: A7B62D657EF764F8
10 changed files with 101 additions and 136 deletions

View file

@ -188,7 +188,7 @@ pub fn printBreakdownSection(out: *std.Io.Writer, items: []const zfin.analysis.B
if (color) try fmt.ansiSetFg(out, cli.CLR_ACCENT[0], cli.CLR_ACCENT[1], cli.CLR_ACCENT[2]); if (color) try fmt.ansiSetFg(out, cli.CLR_ACCENT[0], cli.CLR_ACCENT[1], cli.CLR_ACCENT[2]);
try out.writeAll(bar); try out.writeAll(bar);
if (color) try fmt.ansiReset(out); if (color) try fmt.ansiReset(out);
try out.print(" {d:>5.1}% {s}\n", .{ pct, fmt.fmtMoney(&val_buf, item.value) }); try out.print(" {d:>5.1}% {s}\n", .{ pct, fmt.fmtMoneyAbs(&val_buf, item.value) });
} }
} }

View file

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const zfin = @import("../root.zig"); const zfin = @import("../root.zig");
pub const fmt = zfin.format; pub const fmt = @import("../format.zig");
// Default CLI colors (match TUI default Monokai theme) // Default CLI colors (match TUI default Monokai theme)
pub const CLR_POSITIVE = [3]u8{ 0x7f, 0xd8, 0x8f }; // gains (TUI .positive) pub const CLR_POSITIVE = [3]u8{ 0x7f, 0xd8, 0x8f }; // gains (TUI .positive)

View file

@ -31,10 +31,10 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
return; return;
} }
try display(out, allocator, ch, symbol, ntm, color); try display(out, ch, symbol, ntm, color);
} }
pub fn display(out: *std.Io.Writer, allocator: std.mem.Allocator, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void { pub fn display(out: *std.Io.Writer, chains: []const zfin.OptionsChain, symbol: []const u8, ntm: usize, color: bool) !void {
if (chains.len == 0) return; if (chains.len == 0) return;
try cli.setBold(out, color); try cli.setBold(out, color);
@ -43,7 +43,7 @@ pub fn display(out: *std.Io.Writer, allocator: std.mem.Allocator, chains: []cons
try out.print("========================================\n", .{}); try out.print("========================================\n", .{});
if (chains[0].underlying_price) |price| { if (chains[0].underlying_price) |price| {
var price_buf: [24]u8 = undefined; var price_buf: [24]u8 = undefined;
try out.print("Underlying: {s} {d} expiration(s) +/- {d} strikes NTM\n", .{ fmt.fmtMoney(&price_buf, price), chains.len, ntm }); try out.print("Underlying: {s} {d} expiration(s) +/- {d} strikes NTM\n", .{ fmt.fmtMoneyAbs(&price_buf, price), chains.len, ntm });
} else { } else {
try out.print("{d} expiration(s) available\n", .{chains.len}); try out.print("{d} expiration(s) available\n", .{chains.len});
} }
@ -78,10 +78,10 @@ pub fn display(out: *std.Io.Writer, allocator: std.mem.Allocator, chains: []cons
try out.print("\n", .{}); try out.print("\n", .{});
// Print calls // Print calls
try printSection(out, allocator, "CALLS", chain.calls, atm_price, ntm, true, color); try printSection(out, "CALLS", chain.calls, atm_price, ntm, true, color);
try out.print("\n", .{}); try out.print("\n", .{});
// Print puts // Print puts
try printSection(out, allocator, "PUTS", chain.puts, atm_price, ntm, false, color); try printSection(out, "PUTS", chain.puts, atm_price, ntm, false, color);
} else { } else {
try cli.setFg(out, color, if (is_monthly) cli.CLR_HEADER else cli.CLR_MUTED); try cli.setFg(out, color, if (is_monthly) cli.CLR_HEADER else cli.CLR_MUTED);
try out.print("{s} ({d} calls, {d} puts)", .{ try out.print("{s} ({d} calls, {d} puts)", .{
@ -98,7 +98,6 @@ pub fn display(out: *std.Io.Writer, allocator: std.mem.Allocator, chains: []cons
pub fn printSection( pub fn printSection(
out: *std.Io.Writer, out: *std.Io.Writer,
allocator: std.mem.Allocator,
label: []const u8, label: []const u8,
contracts: []const zfin.OptionContract, contracts: []const zfin.OptionContract,
atm_price: f64, atm_price: f64,
@ -122,8 +121,8 @@ pub fn printSection(
for (filtered) |c| { for (filtered) |c| {
const itm = if (is_calls) c.strike <= atm_price else c.strike >= atm_price; const itm = if (is_calls) c.strike <= atm_price else c.strike >= atm_price;
const prefix: []const u8 = if (itm) " |" else " "; const prefix: []const u8 = if (itm) " |" else " ";
const line = try fmt.fmtContractLine(allocator, prefix, c); var contract_buf: [128]u8 = undefined;
defer allocator.free(line); const line = fmt.fmtContractLine(&contract_buf, prefix, c);
try out.print("{s}\n", .{line}); try out.print("{s}\n", .{line});
} }
} }
@ -139,7 +138,7 @@ test "printSection shows header and contracts" {
.{ .contract_type = .call, .strike = 150.0, .expiration = .{ .days = 20100 }, .bid = 5.0, .ask = 5.50, .last_price = 5.25 }, .{ .contract_type = .call, .strike = 150.0, .expiration = .{ .days = 20100 }, .bid = 5.0, .ask = 5.50, .last_price = 5.25 },
.{ .contract_type = .call, .strike = 155.0, .expiration = .{ .days = 20100 }, .bid = 2.0, .ask = 2.50, .last_price = 2.25 }, .{ .contract_type = .call, .strike = 155.0, .expiration = .{ .days = 20100 }, .bid = 2.0, .ask = 2.50, .last_price = 2.25 },
}; };
try printSection(&w, gpa.allocator(), "CALLS", &calls, 152.0, 8, true, false); try printSection(&w, "CALLS", &calls, 152.0, 8, true, false);
const out = w.buffered(); const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "CALLS") != null); try std.testing.expect(std.mem.indexOf(u8, out, "CALLS") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "Strike") != null); try std.testing.expect(std.mem.indexOf(u8, out, "Strike") != null);
@ -156,7 +155,7 @@ test "display shows chain header no color" {
const chains = [_]zfin.OptionsChain{ const chains = [_]zfin.OptionsChain{
.{ .underlying_symbol = "SPY", .underlying_price = 500.0, .expiration = .{ .days = 20100 }, .calls = &calls, .puts = &puts }, .{ .underlying_symbol = "SPY", .underlying_price = 500.0, .expiration = .{ .days = 20100 }, .calls = &calls, .puts = &puts },
}; };
try display(&w, gpa.allocator(), &chains, "SPY", 8, false); try display(&w, &chains, "SPY", 8, false);
const out = w.buffered(); const out = w.buffered();
try std.testing.expect(std.mem.indexOf(u8, out, "Options Chain for SPY") != null); try std.testing.expect(std.mem.indexOf(u8, out, "Options Chain for SPY") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "1 expiration(s)") != null); try std.testing.expect(std.mem.indexOf(u8, out, "1 expiration(s)") != null);

View file

@ -41,7 +41,7 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
} }
try cli.reset(out, color); try cli.reset(out, color);
var close_buf: [24]u8 = undefined; var close_buf: [24]u8 = undefined;
try out.print(")\nLatest close: {s}\n", .{fmt.fmtMoney(&close_buf, c[c.len - 1].close)}); try out.print(")\nLatest close: {s}\n", .{fmt.fmtMoneyAbs(&close_buf, c[c.len - 1].close)});
const has_divs = result.asof_total != null; const has_divs = result.asof_total != null;

View file

@ -234,12 +234,12 @@ pub fn display(
var cost_buf: [24]u8 = undefined; var cost_buf: [24]u8 = undefined;
var gl_buf: [24]u8 = undefined; var gl_buf: [24]u8 = undefined;
const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss; 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.fmtMoney(&val_buf, summary.total_value), fmt.fmtMoney(&cost_buf, summary.total_cost) }); 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 cli.setGainLoss(out, color, summary.unrealized_gain_loss);
if (summary.unrealized_gain_loss >= 0) { if (summary.unrealized_gain_loss >= 0) {
try out.print("Gain/Loss: +{s} ({d:.1}%)", .{ fmt.fmtMoney(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); try out.print("Gain/Loss: +{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 });
} else { } else {
try out.print("Gain/Loss: -{s} ({d:.1}%)", .{ fmt.fmtMoney(&gl_buf, gl_abs), summary.unrealized_return * 100.0 }); try out.print("Gain/Loss: -{s} ({d:.1}%)", .{ fmt.fmtMoneyAbs(&gl_buf, gl_abs), summary.unrealized_return * 100.0 });
} }
try cli.reset(out, color); try cli.reset(out, color);
try out.print("\n", .{}); try out.print("\n", .{});
@ -311,7 +311,7 @@ pub fn display(
var price_buf2: [24]u8 = undefined; var price_buf2: [24]u8 = undefined;
var gl_val_buf: [24]u8 = undefined; var gl_val_buf: [24]u8 = undefined;
const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss; const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss;
const gl_money = fmt.fmtMoney(&gl_val_buf, gl_abs); const gl_money = fmt.fmtMoneyAbs(&gl_val_buf, gl_abs);
const sign: []const u8 = if (a.unrealized_gain_loss >= 0) "+" else "-"; const sign: []const u8 = if (a.unrealized_gain_loss >= 0) "+" else "-";
// Date + ST/LT for single-lot positions // Date + ST/LT for single-lot positions
@ -328,10 +328,10 @@ pub fn display(
if (a.is_manual_price) try cli.setFg(out, color, cli.CLR_WARNING); if (a.is_manual_price) try cli.setFg(out, color, cli.CLR_WARNING);
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} ", .{ try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} ", .{
a.display_symbol, a.shares, fmt.fmtMoney2(&cost_buf2, a.avg_cost), a.display_symbol, a.shares, fmt.fmtMoneyAbs(&cost_buf2, a.avg_cost),
}); });
try out.print("{s:>10}", .{fmt.fmtMoney2(&price_buf2, a.current_price)}); try out.print("{s:>10}", .{fmt.fmtMoneyAbs(&price_buf2, a.current_price)});
try out.print(" {s:>16} ", .{fmt.fmtMoney(&mv_buf, a.market_value)}); try out.print(" {s:>16} ", .{fmt.fmtMoneyAbs(&mv_buf, a.market_value)});
try cli.setGainLoss(out, color, a.unrealized_gain_loss); try cli.setGainLoss(out, color, a.unrealized_gain_loss);
try out.print("{s}{s:>13}", .{ sign, gl_money }); try out.print("{s}{s:>13}", .{ sign, gl_money });
if (a.is_manual_price) { if (a.is_manual_price) {
@ -388,7 +388,7 @@ pub fn display(
try out.print(" ST: {d} DRIP lots, {d:.1} shares, avg {s} ({s} to {s})\n", .{ try out.print(" ST: {d} DRIP lots, {d:.1} shares, avg {s} ({s} to {s})\n", .{
drip.st.lot_count, drip.st.lot_count,
drip.st.shares, drip.st.shares,
fmt.fmtMoney2(&avg_buf, drip.st.avgCost()), fmt.fmtMoneyAbs(&avg_buf, drip.st.avgCost()),
if (drip.st.first_date) |d| d.format(&d1_buf)[0..7] else "?", if (drip.st.first_date) |d| d.format(&d1_buf)[0..7] else "?",
if (drip.st.last_date) |d| d.format(&d2_buf)[0..7] else "?", if (drip.st.last_date) |d| d.format(&d2_buf)[0..7] else "?",
}); });
@ -402,7 +402,7 @@ pub fn display(
try out.print(" LT: {d} DRIP lots, {d:.1} shares, avg {s} ({s} to {s})\n", .{ try out.print(" LT: {d} DRIP lots, {d:.1} shares, avg {s} ({s} to {s})\n", .{
drip.lt.lot_count, drip.lt.lot_count,
drip.lt.shares, drip.lt.shares,
fmt.fmtMoney2(&avg_buf2, drip.lt.avgCost()), fmt.fmtMoneyAbs(&avg_buf2, drip.lt.avgCost()),
if (drip.lt.first_date) |d| d.format(&d1_buf2)[0..7] else "?", if (drip.lt.first_date) |d| d.format(&d1_buf2)[0..7] else "?",
if (drip.lt.last_date) |d| d.format(&d2_buf2)[0..7] else "?", if (drip.lt.last_date) |d| d.format(&d2_buf2)[0..7] else "?",
}); });
@ -423,13 +423,13 @@ pub fn display(
var total_gl_buf: [24]u8 = undefined; var total_gl_buf: [24]u8 = undefined;
const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss; const gl_abs = if (summary.unrealized_gain_loss >= 0) summary.unrealized_gain_loss else -summary.unrealized_gain_loss;
try out.print(" {s:>6} {s:>8} {s:>10} {s:>10} {s:>16} ", .{ try out.print(" {s:>6} {s:>8} {s:>10} {s:>10} {s:>16} ", .{
"", "", "", "TOTAL", fmt.fmtMoney(&total_mv_buf, summary.total_value), "", "", "", "TOTAL", fmt.fmtMoneyAbs(&total_mv_buf, summary.total_value),
}); });
try cli.setGainLoss(out, color, summary.unrealized_gain_loss); try cli.setGainLoss(out, color, summary.unrealized_gain_loss);
if (summary.unrealized_gain_loss >= 0) { if (summary.unrealized_gain_loss >= 0) {
try out.print("+{s:>13}", .{fmt.fmtMoney(&total_gl_buf, gl_abs)}); try out.print("+{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)});
} else { } else {
try out.print("-{s:>13}", .{fmt.fmtMoney(&total_gl_buf, gl_abs)}); try out.print("-{s:>13}", .{fmt.fmtMoneyAbs(&total_gl_buf, gl_abs)});
} }
try cli.reset(out, color); try cli.reset(out, color);
try out.print(" {s:>7}\n", .{"100.0%"}); try out.print(" {s:>7}\n", .{"100.0%"});
@ -440,9 +440,9 @@ pub fn display(
const rpl_abs = if (summary.realized_gain_loss >= 0) summary.realized_gain_loss else -summary.realized_gain_loss; 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 cli.setGainLoss(out, color, summary.realized_gain_loss);
if (summary.realized_gain_loss >= 0) { if (summary.realized_gain_loss >= 0) {
try out.print("\n Realized P&L: +{s}\n", .{fmt.fmtMoney(&rpl_buf, rpl_abs)}); try out.print("\n Realized P&L: +{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)});
} else { } else {
try out.print("\n Realized P&L: -{s}\n", .{fmt.fmtMoney(&rpl_buf, rpl_abs)}); try out.print("\n Realized P&L: -{s}\n", .{fmt.fmtMoneyAbs(&rpl_buf, rpl_abs)});
} }
try cli.reset(out, color); try cli.reset(out, color);
} }
@ -475,8 +475,8 @@ pub fn display(
try out.print(" {s:<30} {d:>6.0} {s:>12} {s:>14} {s}\n", .{ try out.print(" {s:<30} {d:>6.0} {s:>12} {s:>14} {s}\n", .{
lot.symbol, lot.symbol,
qty, qty,
fmt.fmtMoney2(&cost_per_buf, cost_per), fmt.fmtMoneyAbs(&cost_per_buf, cost_per),
fmt.fmtMoney(&total_cost_buf, total_cost_opt), fmt.fmtMoneyAbs(&total_cost_buf, total_cost_opt),
acct, acct,
}); });
} }
@ -486,7 +486,7 @@ pub fn display(
try cli.reset(out, color); try cli.reset(out, color);
var opt_total_buf: [24]u8 = undefined; var opt_total_buf: [24]u8 = undefined;
try out.print(" {s:>30} {s:>6} {s:>12} {s:>14}\n", .{ try out.print(" {s:>30} {s:>6} {s:>12} {s:>14}\n", .{
"", "", "TOTAL", fmt.fmtMoney(&opt_total_buf, opt_total_cost), "", "", "TOTAL", fmt.fmtMoneyAbs(&opt_total_buf, opt_total_cost),
}); });
} }
@ -530,7 +530,7 @@ pub fn display(
const note_display = if (note_str.len > 50) note_str[0..50] else note_str; const note_display = if (note_str.len > 50) note_str[0..50] else note_str;
try out.print(" {s:<12} {s:>14} {s:>7} {s:>10} {s}\n", .{ try out.print(" {s:<12} {s:>14} {s:>7} {s:>10} {s}\n", .{
lot.symbol, lot.symbol,
fmt.fmtMoney(&face_buf, lot.shares), fmt.fmtMoneyAbs(&face_buf, lot.shares),
rate_str, rate_str,
mat_str, mat_str,
note_display, note_display,
@ -542,7 +542,7 @@ pub fn display(
try cli.reset(out, color); try cli.reset(out, color);
var cd_total_buf: [24]u8 = undefined; var cd_total_buf: [24]u8 = undefined;
try out.print(" {s:>12} {s:>14}\n", .{ try out.print(" {s:>12} {s:>14}\n", .{
"TOTAL", fmt.fmtMoney(&cd_total_buf, cd_section_total), "TOTAL", fmt.fmtMoneyAbs(&cd_total_buf, cd_section_total),
}); });
} }
@ -615,9 +615,9 @@ pub fn display(
try out.print("\n", .{}); try out.print("\n", .{});
try cli.setBold(out, color); try cli.setBold(out, color);
try out.print(" Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{ try out.print(" Net Worth: {s} (Liquid: {s} Illiquid: {s})\n", .{
fmt.fmtMoney(&nw_buf, net_worth), fmt.fmtMoneyAbs(&nw_buf, net_worth),
fmt.fmtMoney(&liq_buf, summary.total_value), fmt.fmtMoneyAbs(&liq_buf, summary.total_value),
fmt.fmtMoney(&il_buf, illiquid_total), fmt.fmtMoneyAbs(&il_buf, illiquid_total),
}); });
try cli.reset(out, color); try cli.reset(out, color);
} }
@ -631,7 +631,7 @@ pub fn display(
for (watch_symbols) |sym| { for (watch_symbols) |sym| {
var price_str: [16]u8 = undefined; var price_str: [16]u8 = undefined;
const ps: []const u8 = if (watch_prices.get(sym)) |close| const ps: []const u8 = if (watch_prices.get(sym)) |close|
fmt.fmtMoney2(&price_str, close) fmt.fmtMoneyAbs(&price_str, close)
else else
"--"; "--";
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps }); try out.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps });
@ -693,15 +693,15 @@ pub fn printLotRow(out: *std.Io.Writer, color: bool, lot: zfin.Lot, current_pric
const gl = lot.shares * (use_price - lot.open_price); const gl = lot.shares * (use_price - lot.open_price);
var lot_gl_buf: [24]u8 = undefined; var lot_gl_buf: [24]u8 = undefined;
const lot_gl_abs = if (gl >= 0) gl else -gl; const lot_gl_abs = if (gl >= 0) gl else -gl;
const lot_gl_money = fmt.fmtMoney(&lot_gl_buf, lot_gl_abs); const lot_gl_money = fmt.fmtMoneyAbs(&lot_gl_buf, lot_gl_abs);
const lot_sign: []const u8 = if (gl >= 0) "+" else "-"; const lot_sign: []const u8 = if (gl >= 0) "+" else "-";
var lot_mv_buf: [24]u8 = undefined; var lot_mv_buf: [24]u8 = undefined;
const lot_mv = fmt.fmtMoney(&lot_mv_buf, lot.shares * use_price); const lot_mv = fmt.fmtMoneyAbs(&lot_mv_buf, lot.shares * use_price);
try cli.setFg(out, color, cli.CLR_MUTED); 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 out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} ", .{
status_str, lot.shares, fmt.fmtMoney2(&lot_price_buf, lot.open_price), "", lot_mv, status_str, lot.shares, fmt.fmtMoneyAbs(&lot_price_buf, lot.open_price), "", lot_mv,
}); });
try cli.reset(out, color); try cli.reset(out, color);
try cli.setGainLoss(out, color, gl); try cli.setGainLoss(out, color, gl);

View file

@ -53,10 +53,10 @@ pub fn display(allocator: std.mem.Allocator, candles: []const zfin.Candle, quote
try cli.setBold(out, color); try cli.setBold(out, color);
if (quote) |q| { if (quote) |q| {
var price_buf: [24]u8 = undefined; var price_buf: [24]u8 = undefined;
try out.print("\n{s} {s}\n", .{ symbol, fmt.fmtMoney(&price_buf, q.price) }); try out.print("\n{s} {s}\n", .{ symbol, fmt.fmtMoneyAbs(&price_buf, q.price) });
} else if (candles.len > 0) { } else if (candles.len > 0) {
var price_buf: [24]u8 = undefined; var price_buf: [24]u8 = undefined;
try out.print("\n{s} {s} (close)\n", .{ symbol, fmt.fmtMoney(&price_buf, candles[candles.len - 1].close) }); try out.print("\n{s} {s} (close)\n", .{ symbol, fmt.fmtMoneyAbs(&price_buf, candles[candles.len - 1].close) });
} else { } else {
try out.print("\n{s}\n", .{symbol}); try out.print("\n{s}\n", .{symbol});
} }

View file

@ -1,6 +1,6 @@
//! Shared formatting utilities used by both CLI and TUI. //! Shared formatting utilities used by both CLI and TUI.
//! //!
//! Number formatting (fmtMoney, fmtIntCommas, etc.), financial helpers //! Number formatting (fmtMoneyAbs, fmtIntCommas, etc.), financial helpers
//! (capitalGainsIndicator, filterNearMoney), and braille chart computation. //! (capitalGainsIndicator, filterNearMoney), and braille chart computation.
const std = @import("std"); const std = @import("std");
@ -52,7 +52,7 @@ pub fn fmtCashHeader(buf: []u8) []const u8 {
/// Returns a slice of `buf`. /// Returns a slice of `buf`.
pub fn fmtCashRow(buf: []u8, account: []const u8, amount: f64, note: ?[]const u8) []const u8 { pub fn fmtCashRow(buf: []u8, account: []const u8, amount: f64, note: ?[]const u8) []const u8 {
var money_buf: [24]u8 = undefined; var money_buf: [24]u8 = undefined;
const money = fmtMoney(&money_buf, amount); const money = fmtMoneyAbs(&money_buf, amount);
const w = cash_acct_width; const w = cash_acct_width;
// " {name:<w} {money:>14} {note}" // " {name:<w} {money:>14} {note}"
const prefix = " "; const prefix = " ";
@ -102,7 +102,7 @@ pub fn fmtCashSep(buf: []u8) []const u8 {
/// Format the cash total row. /// Format the cash total row.
pub fn fmtCashTotal(buf: []u8, total: f64) []const u8 { pub fn fmtCashTotal(buf: []u8, total: f64) []const u8 {
var money_buf: [24]u8 = undefined; var money_buf: [24]u8 = undefined;
const money = fmtMoney(&money_buf, total); const money = fmtMoneyAbs(&money_buf, total);
const w = cash_acct_width; const w = cash_acct_width;
var pos: usize = 0; var pos: usize = 0;
@memcpy(buf[0..2], " "); @memcpy(buf[0..2], " ");
@ -157,19 +157,16 @@ pub fn fmtIlliquidRow(buf: []u8, name: []const u8, value: f64, note: ?[]const u8
} }
/// Format the illiquid total separator line. /// Format the illiquid total separator line.
pub fn fmtIlliquidSep(buf: []u8) []const u8 { pub const fmtIlliquidSep = fmtCashSep;
return fmtCashSep(buf);
}
/// Format the illiquid total row. /// Format the illiquid total row.
pub fn fmtIlliquidTotal(buf: []u8, total: f64) []const u8 { pub const fmtIlliquidTotal = fmtCashTotal;
return fmtCashTotal(buf, total);
}
// Number formatters // Number formatters
/// Format a dollar amount with commas and 2 decimals: $1,234.56 /// Format a dollar amount with commas and 2 decimals: $1,234.56
pub fn fmtMoney(buf: []u8, amount: f64) []const u8 { /// Always returns the absolute value callers handle sign display.
pub fn fmtMoneyAbs(buf: []u8, amount: f64) []const u8 {
const cents = @as(i64, @intFromFloat(@round(amount * 100.0))); const cents = @as(i64, @intFromFloat(@round(amount * 100.0)));
const abs_cents = if (cents < 0) @as(u64, @intCast(-cents)) else @as(u64, @intCast(cents)); const abs_cents = if (cents < 0) @as(u64, @intCast(-cents)) else @as(u64, @intCast(cents));
const dollars = abs_cents / 100; const dollars = abs_cents / 100;
@ -213,11 +210,6 @@ pub fn fmtMoney(buf: []u8, amount: f64) []const u8 {
return buf[0..len]; return buf[0..len];
} }
/// Format price with 2 decimals (no commas, for per-share prices): $185.23
pub fn fmtMoney2(buf: []u8, amount: f64) []const u8 {
return std.fmt.bufPrint(buf, "${d:.2}", .{amount}) catch "$?";
}
/// Format an integer with commas (e.g. 1234567 -> "1,234,567"). /// Format an integer with commas (e.g. 1234567 -> "1,234,567").
pub fn fmtIntCommas(buf: []u8, value: u64) []const u8 { pub fn fmtIntCommas(buf: []u8, value: u64) []const u8 {
var tmp: [32]u8 = undefined; var tmp: [32]u8 = undefined;
@ -252,13 +244,13 @@ pub fn fmtTimeAgo(buf: []u8, timestamp: i64) []const u8 {
const delta = now - timestamp; const delta = now - timestamp;
if (delta < 0) return "just now"; if (delta < 0) return "just now";
if (delta < 60) return "just now"; if (delta < 60) return "just now";
if (delta < 3600) { if (delta < std.time.s_per_hour) {
return std.fmt.bufPrint(buf, "{d}m ago", .{@as(u64, @intCast(@divFloor(delta, 60)))}) catch "?"; return std.fmt.bufPrint(buf, "{d}m ago", .{@as(u64, @intCast(@divFloor(delta, 60)))}) catch "?";
} }
if (delta < 86400) { if (delta < std.time.s_per_day) {
return std.fmt.bufPrint(buf, "{d}h ago", .{@as(u64, @intCast(@divFloor(delta, 3600)))}) catch "?"; return std.fmt.bufPrint(buf, "{d}h ago", .{@as(u64, @intCast(@divFloor(delta, std.time.s_per_hour)))}) catch "?";
} }
return std.fmt.bufPrint(buf, "{d}d ago", .{@as(u64, @intCast(@divFloor(delta, 86400)))}) catch "?"; return std.fmt.bufPrint(buf, "{d}d ago", .{@as(u64, @intCast(@divFloor(delta, std.time.s_per_day)))}) catch "?";
} }
/// Format large numbers with T/B/M suffixes (e.g. "1.5B", "45.6M"). /// Format large numbers with T/B/M suffixes (e.g. "1.5B", "45.6M").
@ -370,7 +362,7 @@ pub fn toTitleCase(buf: []u8, s: []const u8) []const u8 {
} }
/// Format an options contract line: strike + last + bid + ask + volume + OI + IV. /// Format an options contract line: strike + last + bid + ask + volume + OI + IV.
pub fn fmtContractLine(alloc: std.mem.Allocator, prefix: []const u8, c: OptionContract) ![]const u8 { pub fn fmtContractLine(buf: []u8, prefix: []const u8, c: OptionContract) []const u8 {
var last_buf: [12]u8 = undefined; var last_buf: [12]u8 = undefined;
const last_str = if (c.last_price) |p| std.fmt.bufPrint(&last_buf, "{d:>10.2}", .{p}) catch "--" else "--"; const last_str = if (c.last_price) |p| std.fmt.bufPrint(&last_buf, "{d:>10.2}", .{p}) catch "--" else "--";
var bid_buf: [12]u8 = undefined; var bid_buf: [12]u8 = undefined;
@ -384,9 +376,9 @@ pub fn fmtContractLine(alloc: std.mem.Allocator, prefix: []const u8, c: OptionCo
var iv_buf: [10]u8 = undefined; var iv_buf: [10]u8 = undefined;
const iv_str = if (c.implied_volatility) |iv| std.fmt.bufPrint(&iv_buf, "{d:>6.1}%", .{iv * 100.0}) catch "--" else "--"; const iv_str = if (c.implied_volatility) |iv| std.fmt.bufPrint(&iv_buf, "{d:>6.1}%", .{iv * 100.0}) catch "--" else "--";
return std.fmt.allocPrint(alloc, "{s}{d:>10.2} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}", .{ return std.fmt.bufPrint(buf, "{s}{d:>10.2} {s:>10} {s:>10} {s:>10} {s:>10} {s:>8} {s:>8}", .{
prefix, c.strike, last_str, bid_str, ask_str, vol_str, oi_str, iv_str, prefix, c.strike, last_str, bid_str, ask_str, vol_str, oi_str, iv_str,
}); }) catch "";
} }
// Portfolio helpers // Portfolio helpers
@ -449,8 +441,6 @@ pub fn aggregateDripLots(lots: []const Lot) DripAggregation {
return result; return result;
} }
// Color helpers
// Shared rendering helpers (CLI + TUI) // Shared rendering helpers (CLI + TUI)
/// Layout constants for analysis breakdown views. /// Layout constants for analysis breakdown views.
@ -465,7 +455,7 @@ pub fn fmtGainLoss(buf: []u8, pnl: f64) GainLossResult {
const positive = pnl >= 0; const positive = pnl >= 0;
const abs_val = if (positive) pnl else -pnl; const abs_val = if (positive) pnl else -pnl;
var money_buf: [24]u8 = undefined; var money_buf: [24]u8 = undefined;
const money = fmtMoney(&money_buf, abs_val); const money = fmtMoneyAbs(&money_buf, abs_val);
const sign: []const u8 = if (positive) "+" else "-"; const sign: []const u8 = if (positive) "+" else "-";
const text = std.fmt.bufPrint(buf, "{s}{s}", .{ sign, money }) catch "?"; const text = std.fmt.bufPrint(buf, "{s}{s}", .{ sign, money }) catch "?";
return .{ .text = text, .positive = positive }; return .{ .text = text, .positive = positive };
@ -694,18 +684,14 @@ pub const BrailleChart = struct {
return self.patterns[row * self.n_cols + col]; return self.patterns[row * self.n_cols + col];
} }
/// Format a date as "MMM DD" or "MMM 'YY" depending on whether it's the same year as `ref_year`. /// Format a date as "MMM DD" for the braille chart x-axis.
/// The year context is already visible in the surrounding CLI/TUI interface.
/// Returns the number of bytes written. /// Returns the number of bytes written.
pub fn fmtShortDate(date: Date, buf: *[7]u8) []const u8 { pub fn fmtShortDate(date: Date, buf: *[7]u8) []const u8 {
const months = [_][]const u8{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; const months = [_][]const u8{ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
const m = date.month(); const m = date.month();
const d = date.day(); const d = date.day();
const y = date.year();
const mon = if (m >= 1 and m <= 12) months[m - 1] else "???"; const mon = if (m >= 1 and m <= 12) months[m - 1] else "???";
// Use "MMM DD 'YY" is too long (10 chars). Use "MMM 'YY" (7 chars) for year context,
// or "MMM DD" (6 chars) for day-level precision. We'll use "MMM DD" for compactness
// and add the year as a separate concern if dates span multiple years.
// Actually let's just use "YYYY-MM-DD" is too long. "Mon DD" is 6 chars.
buf[0] = mon[0]; buf[0] = mon[0];
buf[1] = mon[1]; buf[1] = mon[1];
buf[2] = mon[2]; buf[2] = mon[2];
@ -716,8 +702,6 @@ pub const BrailleChart = struct {
buf[4] = '0'; buf[4] = '0';
} }
buf[5] = '0' + d % 10; buf[5] = '0' + d % 10;
// If we want to show year when it differs, store in extra chars:
_ = y;
return buf[0..6]; return buf[0..6];
} }
@ -922,28 +906,19 @@ pub fn ansiReset(out: anytype) !void {
// Tests // Tests
test "fmtMoney" { test "fmtMoneyAbs" {
var buf: [24]u8 = undefined; var buf: [24]u8 = undefined;
try std.testing.expectEqualStrings("$0.00", fmtMoney(&buf, 0)); try std.testing.expectEqualStrings("$0.00", fmtMoneyAbs(&buf, 0));
try std.testing.expectEqualStrings("$1.23", fmtMoney(&buf, 1.23)); try std.testing.expectEqualStrings("$1.23", fmtMoneyAbs(&buf, 1.23));
try std.testing.expectEqualStrings("$1,234.56", fmtMoney(&buf, 1234.56)); try std.testing.expectEqualStrings("$1,234.56", fmtMoneyAbs(&buf, 1234.56));
try std.testing.expectEqualStrings("$1,234,567.89", fmtMoney(&buf, 1234567.89)); try std.testing.expectEqualStrings("$1,234,567.89", fmtMoneyAbs(&buf, 1234567.89));
} }
test "fmtMoney negative" { test "fmtMoneyAbs negative" {
// Negative amounts: the function uses abs(cents) so the sign is lost // Returns absolute value callers handle sign display.
// (implementation detail: no minus sign is produced, result is same as positive)
var buf: [24]u8 = undefined; var buf: [24]u8 = undefined;
// Verify it doesn't crash on negative input const result = fmtMoneyAbs(&buf, -1234.56);
const result = fmtMoney(&buf, -1234.56); try std.testing.expectEqualStrings("$1,234.56", result);
try std.testing.expect(result.len > 0);
}
test "fmtMoney2" {
var buf: [24]u8 = undefined;
try std.testing.expectEqualStrings("$185.23", fmtMoney2(&buf, 185.23));
try std.testing.expectEqualStrings("$0.00", fmtMoney2(&buf, 0.0));
try std.testing.expectEqualStrings("$0.50", fmtMoney2(&buf, 0.5));
} }
test "fmtIntCommas" { test "fmtIntCommas" {
@ -1264,7 +1239,7 @@ test "computeBrailleChart insufficient data" {
} }
test "fmtContractLine" { test "fmtContractLine" {
const alloc = std.testing.allocator; var buf: [128]u8 = undefined;
const contract = OptionContract{ const contract = OptionContract{
.strike = 150.0, .strike = 150.0,
.contract_type = .call, .contract_type = .call,
@ -1276,22 +1251,20 @@ test "fmtContractLine" {
.open_interest = 5678, .open_interest = 5678,
.implied_volatility = 0.25, .implied_volatility = 0.25,
}; };
const line = try fmtContractLine(alloc, "C ", contract); const line = fmtContractLine(&buf, "C ", contract);
defer alloc.free(line);
try std.testing.expect(std.mem.indexOf(u8, line, "150.00") != null); try std.testing.expect(std.mem.indexOf(u8, line, "150.00") != null);
try std.testing.expect(std.mem.indexOf(u8, line, "5.25") != null); try std.testing.expect(std.mem.indexOf(u8, line, "5.25") != null);
try std.testing.expect(std.mem.indexOf(u8, line, "1234") != null); try std.testing.expect(std.mem.indexOf(u8, line, "1234") != null);
} }
test "fmtContractLine null fields" { test "fmtContractLine null fields" {
const alloc = std.testing.allocator; var buf: [128]u8 = undefined;
const contract = OptionContract{ const contract = OptionContract{
.strike = 200.0, .strike = 200.0,
.contract_type = .put, .contract_type = .put,
.expiration = Date.fromYmd(2024, 6, 21), .expiration = Date.fromYmd(2024, 6, 21),
}; };
const line = try fmtContractLine(alloc, "P ", contract); const line = fmtContractLine(&buf, "P ", contract);
defer alloc.free(line);
try std.testing.expect(std.mem.indexOf(u8, line, "200.00") != null); try std.testing.expect(std.mem.indexOf(u8, line, "200.00") != null);
// Null fields should show "--" // Null fields should show "--"
try std.testing.expect(std.mem.indexOf(u8, line, "--") != null); try std.testing.expect(std.mem.indexOf(u8, line, "--") != null);

View file

@ -81,7 +81,7 @@ pub fn main() !u8 {
for (args[1..]) |arg| { for (args[1..]) |arg| {
if (std.mem.eql(u8, arg, "--no-color")) no_color_flag = true; if (std.mem.eql(u8, arg, "--no-color")) no_color_flag = true;
} }
const color = zfin.format.shouldUseColor(no_color_flag); const color = @import("format.zig").shouldUseColor(no_color_flag);
var config = zfin.Config.fromEnv(allocator); var config = zfin.Config.fromEnv(allocator);
defer config.deinit(); defer config.deinit();

View file

@ -18,9 +18,6 @@
//! //!
//! For portfolio workflows, load a Portfolio from an SRF file and pass //! For portfolio workflows, load a Portfolio from an SRF file and pass
//! it through `risk` and `performance` for analytics. //! it through `risk` and `performance` for analytics.
//!
//! The `format` module contains shared rendering helpers used by both
//! the CLI commands and TUI.
// Data Models // Data Models
@ -89,12 +86,6 @@ pub const analysis = @import("analytics/analysis.zig");
/// Sector/industry/country classification for enriched securities. /// Sector/industry/country classification for enriched securities.
pub const classification = @import("models/classification.zig"); pub const classification = @import("models/classification.zig");
// Formatting
/// Shared rendering helpers (money formatting, charts, earnings rows, bars)
/// used by both CLI commands and TUI.
pub const format = @import("format.zig");
// Service Layer // Service Layer
/// High-level data service: orchestrates providers, caching, and fallback logic. /// High-level data service: orchestrates providers, caching, and fallback logic.

View file

@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const vaxis = @import("vaxis"); const vaxis = @import("vaxis");
const zfin = @import("root.zig"); const zfin = @import("root.zig");
const fmt = zfin.format; const fmt = @import("format.zig");
const cli = @import("commands/common.zig"); const cli = @import("commands/common.zig");
const keybinds = @import("tui/keybinds.zig"); const keybinds = @import("tui/keybinds.zig");
const theme_mod = @import("tui/theme.zig"); const theme_mod = @import("tui/theme.zig");
@ -2035,10 +2035,10 @@ const App = struct {
var val_buf: [24]u8 = undefined; var val_buf: [24]u8 = undefined;
var cost_buf: [24]u8 = undefined; var cost_buf: [24]u8 = undefined;
var gl_buf: [24]u8 = undefined; var gl_buf: [24]u8 = undefined;
const val_str = fmt.fmtMoney(&val_buf, s.total_value); const val_str = fmt.fmtMoneyAbs(&val_buf, s.total_value);
const cost_str = fmt.fmtMoney(&cost_buf, s.total_cost); const cost_str = fmt.fmtMoneyAbs(&cost_buf, s.total_cost);
const gl_abs = if (s.unrealized_gain_loss >= 0) s.unrealized_gain_loss else -s.unrealized_gain_loss; const gl_abs = if (s.unrealized_gain_loss >= 0) s.unrealized_gain_loss else -s.unrealized_gain_loss;
const gl_str = fmt.fmtMoney(&gl_buf, gl_abs); const gl_str = fmt.fmtMoneyAbs(&gl_buf, gl_abs);
const summary_text = try std.fmt.allocPrint(arena, " Value: {s} Cost: {s} Gain/Loss: {s}{s} ({d:.1}%)", .{ const summary_text = try std.fmt.allocPrint(arena, " Value: {s} Cost: {s} Gain/Loss: {s}{s} ({d:.1}%)", .{
val_str, cost_str, if (s.unrealized_gain_loss >= 0) @as([]const u8, "+") else @as([]const u8, "-"), gl_str, s.unrealized_return * 100.0, val_str, cost_str, if (s.unrealized_gain_loss >= 0) @as([]const u8, "+") else @as([]const u8, "-"), gl_str, s.unrealized_return * 100.0,
}); });
@ -2060,9 +2060,9 @@ const App = struct {
var nw_buf: [24]u8 = undefined; var nw_buf: [24]u8 = undefined;
var il_buf: [24]u8 = undefined; var il_buf: [24]u8 = undefined;
const nw_text = try std.fmt.allocPrint(arena, " Net Worth: {s} (Liquid: {s} Illiquid: {s})", .{ const nw_text = try std.fmt.allocPrint(arena, " Net Worth: {s} (Liquid: {s} Illiquid: {s})", .{
fmt.fmtMoney(&nw_buf, net_worth), fmt.fmtMoneyAbs(&nw_buf, net_worth),
val_str, val_str,
fmt.fmtMoney(&il_buf, illiquid_total), fmt.fmtMoneyAbs(&il_buf, illiquid_total),
}); });
try lines.append(arena, .{ .text = nw_text, .style = th.headerStyle() }); try lines.append(arena, .{ .text = nw_text, .style = th.headerStyle() });
} }
@ -2141,18 +2141,18 @@ const App = struct {
const pnl_pct = if (a.cost_basis > 0) (a.unrealized_gain_loss / a.cost_basis) * 100.0 else @as(f64, 0); const pnl_pct = if (a.cost_basis > 0) (a.unrealized_gain_loss / a.cost_basis) * 100.0 else @as(f64, 0);
var gl_val_buf: [24]u8 = undefined; var gl_val_buf: [24]u8 = undefined;
const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss; const gl_abs = if (a.unrealized_gain_loss >= 0) a.unrealized_gain_loss else -a.unrealized_gain_loss;
const gl_money = fmt.fmtMoney(&gl_val_buf, gl_abs); const gl_money = fmt.fmtMoneyAbs(&gl_val_buf, gl_abs);
var pnl_buf: [20]u8 = undefined; var pnl_buf: [20]u8 = undefined;
const pnl_str = if (a.unrealized_gain_loss >= 0) const pnl_str = if (a.unrealized_gain_loss >= 0)
std.fmt.bufPrint(&pnl_buf, "+{s}", .{gl_money}) catch "?" std.fmt.bufPrint(&pnl_buf, "+{s}", .{gl_money}) catch "?"
else else
std.fmt.bufPrint(&pnl_buf, "-{s}", .{gl_money}) catch "?"; std.fmt.bufPrint(&pnl_buf, "-{s}", .{gl_money}) catch "?";
var mv_buf: [24]u8 = undefined; var mv_buf: [24]u8 = undefined;
const mv_str = fmt.fmtMoney(&mv_buf, a.market_value); const mv_str = fmt.fmtMoneyAbs(&mv_buf, a.market_value);
var cost_buf2: [24]u8 = undefined; var cost_buf2: [24]u8 = undefined;
const cost_str = fmt.fmtMoney2(&cost_buf2, a.avg_cost); const cost_str = fmt.fmtMoneyAbs(&cost_buf2, a.avg_cost);
var price_buf2: [24]u8 = undefined; var price_buf2: [24]u8 = undefined;
const price_str = fmt.fmtMoney2(&price_buf2, a.current_price); const price_str = fmt.fmtMoneyAbs(&price_buf2, a.current_price);
// Date + ST/LT: show for single-lot, blank for multi-lot // Date + ST/LT: show for single-lot, blank for multi-lot
var pos_date_buf: [10]u8 = undefined; var pos_date_buf: [10]u8 = undefined;
@ -2233,17 +2233,17 @@ const App = struct {
const gl = lot.shares * (use_price - lot.open_price); const gl = lot.shares * (use_price - lot.open_price);
lot_positive = gl >= 0; lot_positive = gl >= 0;
var lot_gl_money_buf: [24]u8 = undefined; var lot_gl_money_buf: [24]u8 = undefined;
const lot_gl_money = fmt.fmtMoney(&lot_gl_money_buf, if (gl >= 0) gl else -gl); const lot_gl_money = fmt.fmtMoneyAbs(&lot_gl_money_buf, if (gl >= 0) gl else -gl);
lot_gl_str = try std.fmt.allocPrint(arena, "{s}{s}", .{ lot_gl_str = try std.fmt.allocPrint(arena, "{s}{s}", .{
if (gl >= 0) @as([]const u8, "+") else @as([]const u8, "-"), lot_gl_money, if (gl >= 0) @as([]const u8, "+") else @as([]const u8, "-"), lot_gl_money,
}); });
var lot_mv_buf: [24]u8 = undefined; var lot_mv_buf: [24]u8 = undefined;
lot_mv_str = try std.fmt.allocPrint(arena, "{s}", .{fmt.fmtMoney(&lot_mv_buf, lot.shares * use_price)}); lot_mv_str = try std.fmt.allocPrint(arena, "{s}", .{fmt.fmtMoneyAbs(&lot_mv_buf, lot.shares * use_price)});
} }
} }
var price_str2: [24]u8 = undefined; var price_str2: [24]u8 = undefined;
const lot_price_str = fmt.fmtMoney2(&price_str2, lot.open_price); const lot_price_str = fmt.fmtMoneyAbs(&price_str2, lot.open_price);
const status_str: []const u8 = if (lot.isOpen()) "open" else "closed"; const status_str: []const u8 = if (lot.isOpen()) "open" else "closed";
const indicator = fmt.capitalGainsIndicator(lot.open_date); const indicator = fmt.capitalGainsIndicator(lot.open_date);
const lot_date_col = try std.fmt.allocPrint(arena, "{s} {s}", .{ date_str, indicator }); const lot_date_col = try std.fmt.allocPrint(arena, "{s} {s}", .{ date_str, indicator });
@ -2265,7 +2265,7 @@ const App = struct {
.watchlist => { .watchlist => {
var price_str3: [16]u8 = undefined; var price_str3: [16]u8 = undefined;
const ps: []const u8 = if (self.watchlist_prices) |wp| const ps: []const u8 = if (self.watchlist_prices) |wp|
(if (wp.get(row.symbol)) |p| fmt.fmtMoney2(&price_str3, p) else "--") (if (wp.get(row.symbol)) |p| fmt.fmtMoneyAbs(&price_str3, p) else "--")
else else
"--"; "--";
const star2: []const u8 = if (is_active_sym) "* " else " "; const star2: []const u8 = if (is_active_sym) "* " else " ";
@ -2306,8 +2306,8 @@ const App = struct {
const text = try std.fmt.allocPrint(arena, " {s:<30} {d:>6.0} {s:>12} {s:>14} {s}", .{ const text = try std.fmt.allocPrint(arena, " {s:<30} {d:>6.0} {s:>12} {s:>14} {s}", .{
lot.symbol, lot.symbol,
qty, qty,
fmt.fmtMoney2(&cost_buf3, cost_per), fmt.fmtMoneyAbs(&cost_buf3, cost_per),
fmt.fmtMoney(&total_buf, total_cost), fmt.fmtMoneyAbs(&total_buf, total_cost),
acct_col2, acct_col2,
}); });
const row_style2 = if (is_cursor) th.selectStyle() else th.contentStyle(); const row_style2 = if (is_cursor) th.selectStyle() else th.contentStyle();
@ -2331,7 +2331,7 @@ const App = struct {
const acct_col3: []const u8 = lot.account orelse ""; const acct_col3: []const u8 = lot.account orelse "";
const text = try std.fmt.allocPrint(arena, " {s:<12} {s:>14} {s:>7} {s:>10} {s} {s}", .{ const text = try std.fmt.allocPrint(arena, " {s:<12} {s:>14} {s:>7} {s:>10} {s} {s}", .{
lot.symbol, lot.symbol,
fmt.fmtMoney(&face_buf, lot.shares), fmt.fmtMoneyAbs(&face_buf, lot.shares),
rate_str, rate_str,
mat_str, mat_str,
note_display, note_display,
@ -2348,7 +2348,7 @@ const App = struct {
const arrow3: []const u8 = if (self.cash_expanded) "v " else "> "; const arrow3: []const u8 = if (self.cash_expanded) "v " else "> ";
const text = try std.fmt.allocPrint(arena, " {s}Total Cash {s:>14}", .{ const text = try std.fmt.allocPrint(arena, " {s}Total Cash {s:>14}", .{
arrow3, arrow3,
fmt.fmtMoney(&cash_buf, total_cash), fmt.fmtMoneyAbs(&cash_buf, total_cash),
}); });
const row_style4 = if (is_cursor) th.selectStyle() else th.contentStyle(); const row_style4 = if (is_cursor) th.selectStyle() else th.contentStyle();
try lines.append(arena, .{ .text = text, .style = row_style4 }); try lines.append(arena, .{ .text = text, .style = row_style4 });
@ -2370,7 +2370,7 @@ const App = struct {
const arrow4: []const u8 = if (self.illiquid_expanded) "v " else "> "; const arrow4: []const u8 = if (self.illiquid_expanded) "v " else "> ";
const text = try std.fmt.allocPrint(arena, " {s}Total Illiquid {s:>14}", .{ const text = try std.fmt.allocPrint(arena, " {s}Total Illiquid {s:>14}", .{
arrow4, arrow4,
fmt.fmtMoney(&illiquid_buf, total_illiquid), fmt.fmtMoneyAbs(&illiquid_buf, total_illiquid),
}); });
const row_style6 = if (is_cursor) th.selectStyle() else th.contentStyle(); const row_style6 = if (is_cursor) th.selectStyle() else th.contentStyle();
try lines.append(arena, .{ .text = text, .style = row_style6 }); try lines.append(arena, .{ .text = text, .style = row_style6 });
@ -2396,7 +2396,7 @@ const App = struct {
label_str, label_str,
row.drip_lot_count, row.drip_lot_count,
row.drip_shares, row.drip_shares,
fmt.fmtMoney2(&drip_avg_buf, row.drip_avg_cost), fmt.fmtMoneyAbs(&drip_avg_buf, row.drip_avg_cost),
drip_d1, drip_d1,
drip_d2, drip_d2,
}); });
@ -2697,7 +2697,7 @@ const App = struct {
if (row >= height) continue; if (row >= height) continue;
var lbl_buf: [16]u8 = undefined; var lbl_buf: [16]u8 = undefined;
const lbl = fmt.fmtMoney2(&lbl_buf, price_val); const lbl = fmt.fmtMoneyAbs(&lbl_buf, price_val);
const start_idx = row * @as(usize, width) + label_col; const start_idx = row * @as(usize, width) + label_col;
for (lbl, 0..) |ch, ci| { for (lbl, 0..) |ch, ci| {
const idx = start_idx + ci; const idx = start_idx + ci;
@ -2794,7 +2794,7 @@ const App = struct {
if (quote_data) |q| { if (quote_data) |q| {
// No candle data but have a quote - show it // No candle data but have a quote - show it
var qclose_buf: [24]u8 = undefined; var qclose_buf: [24]u8 = undefined;
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Price: {s}", .{fmt.fmtMoney(&qclose_buf, q.close)}), .style = th.contentStyle() }); try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Price: {s}", .{fmt.fmtMoneyAbs(&qclose_buf, q.close)}), .style = th.contentStyle() });
{ {
var chg_buf: [64]u8 = undefined; var chg_buf: [64]u8 = undefined;
const change_style = if (q.change >= 0) th.positiveStyle() else th.negativeStyle(); const change_style = if (q.change >= 0) th.positiveStyle() else th.negativeStyle();
@ -2881,7 +2881,7 @@ const App = struct {
var col1 = Column.init(); var col1 = Column.init();
col1.width = 30; col1.width = 30;
try col1.add(arena, try std.fmt.allocPrint(arena, " Date: {s}", .{latest.date.format(&date_buf)}), th.contentStyle()); try col1.add(arena, try std.fmt.allocPrint(arena, " Date: {s}", .{latest.date.format(&date_buf)}), th.contentStyle());
try col1.add(arena, try std.fmt.allocPrint(arena, " Price: {s}", .{fmt.fmtMoney(&close_buf, price)}), th.contentStyle()); try col1.add(arena, try std.fmt.allocPrint(arena, " Price: {s}", .{fmt.fmtMoneyAbs(&close_buf, price)}), th.contentStyle());
try col1.add(arena, try std.fmt.allocPrint(arena, " Open: ${d:.2}", .{if (quote_data) |q| q.open else latest.open}), th.mutedStyle()); try col1.add(arena, try std.fmt.allocPrint(arena, " Open: ${d:.2}", .{if (quote_data) |q| q.open else latest.open}), th.mutedStyle());
try col1.add(arena, try std.fmt.allocPrint(arena, " High: ${d:.2}", .{if (quote_data) |q| q.high else latest.high}), th.mutedStyle()); try col1.add(arena, try std.fmt.allocPrint(arena, " High: ${d:.2}", .{if (quote_data) |q| q.high else latest.high}), th.mutedStyle());
try col1.add(arena, try std.fmt.allocPrint(arena, " Low: ${d:.2}", .{if (quote_data) |q| q.low else latest.low}), th.mutedStyle()); try col1.add(arena, try std.fmt.allocPrint(arena, " Low: ${d:.2}", .{if (quote_data) |q| q.low else latest.low}), th.mutedStyle());
@ -3058,7 +3058,7 @@ const App = struct {
if (self.candles) |cc| { if (self.candles) |cc| {
if (cc.len > 0) { if (cc.len > 0) {
var close_buf: [24]u8 = undefined; var close_buf: [24]u8 = undefined;
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Latest close: {s}", .{fmt.fmtMoney(&close_buf, cc[cc.len - 1].close)}), .style = th.contentStyle() }); try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Latest close: {s}", .{fmt.fmtMoneyAbs(&close_buf, cc[cc.len - 1].close)}), .style = th.contentStyle() });
} }
} }
@ -3182,7 +3182,7 @@ const App = struct {
if (chains[0].underlying_price) |price| { if (chains[0].underlying_price) |price| {
var price_buf: [24]u8 = undefined; var price_buf: [24]u8 = undefined;
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Underlying: {s} {d} expiration(s) +/- {d} strikes NTM (Ctrl+1-9 to change)", .{ fmt.fmtMoney(&price_buf, price), chains.len, self.options_near_the_money }), .style = th.contentStyle() }); try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Underlying: {s} {d} expiration(s) +/- {d} strikes NTM (Ctrl+1-9 to change)", .{ fmt.fmtMoneyAbs(&price_buf, price), chains.len, self.options_near_the_money }), .style = th.contentStyle() });
} }
try lines.append(arena, .{ .text = "", .style = th.contentStyle() }); try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
@ -3233,7 +3233,8 @@ const App = struct {
const atm_price = chains[0].underlying_price orelse 0; const atm_price = chains[0].underlying_price orelse 0;
const itm = cc.strike <= atm_price; const itm = cc.strike <= atm_price;
const prefix: []const u8 = if (itm) " |" else " "; const prefix: []const u8 = if (itm) " |" else " ";
const text = try fmt.fmtContractLine(arena, prefix, cc); var contract_buf: [128]u8 = undefined;
const text = try arena.dupe(u8, fmt.fmtContractLine(&contract_buf, prefix, cc));
const style = if (is_cursor) th.selectStyle() else th.contentStyle(); const style = if (is_cursor) th.selectStyle() else th.contentStyle();
try lines.append(arena, .{ .text = text, .style = style }); try lines.append(arena, .{ .text = text, .style = style });
} }
@ -3243,7 +3244,8 @@ const App = struct {
const atm_price = chains[0].underlying_price orelse 0; const atm_price = chains[0].underlying_price orelse 0;
const itm = p.strike >= atm_price; const itm = p.strike >= atm_price;
const prefix: []const u8 = if (itm) " |" else " "; const prefix: []const u8 = if (itm) " |" else " ";
const text = try fmt.fmtContractLine(arena, prefix, p); var contract_buf: [128]u8 = undefined;
const text = try arena.dupe(u8, fmt.fmtContractLine(&contract_buf, prefix, p));
const style = if (is_cursor) th.selectStyle() else th.contentStyle(); const style = if (is_cursor) th.selectStyle() else th.contentStyle();
try lines.append(arena, .{ .text = text, .style = style }); try lines.append(arena, .{ .text = text, .style = style });
} }
@ -3351,7 +3353,7 @@ const App = struct {
@memcpy(padded_label[0..lbl_len], lbl[0..lbl_len]); @memcpy(padded_label[0..lbl_len], lbl[0..lbl_len]);
if (lbl_len < label_width) @memset(padded_label[lbl_len..], ' '); if (lbl_len < label_width) @memset(padded_label[lbl_len..], ' ');
return std.fmt.allocPrint(arena, " {s} {s} {d:>5.1}% {s}", .{ return std.fmt.allocPrint(arena, " {s} {s} {d:>5.1}% {s}", .{
padded_label, bar, pct, fmt.fmtMoney(&val_buf, item.value), padded_label, bar, pct, fmt.fmtMoneyAbs(&val_buf, item.value),
}); });
} }