additional spacing based on crypto example
This commit is contained in:
parent
baf5e2a86f
commit
3a1500bfbb
4 changed files with 226 additions and 54 deletions
|
|
@ -5,6 +5,8 @@ const framework = @import("framework.zig");
|
|||
const fmt = cli.fmt;
|
||||
const Money = @import("../Money.zig");
|
||||
const views = @import("../views/portfolio_sections.zig");
|
||||
/// Main holdings-table column layout (widths + format strings).
|
||||
const pl = views.PositionsLayout;
|
||||
|
||||
/// Visibility of expired options / matured CDs within the Options
|
||||
/// and CDs sections.
|
||||
|
|
@ -308,12 +310,8 @@ pub fn display(
|
|||
// Column headers
|
||||
try out.print("\n", .{});
|
||||
try cli.setFg(out, color, cli.CLR_MUTED);
|
||||
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}\n", .{
|
||||
"Symbol", "Shares", "Avg Cost", "Price", "Market Value", "Gain/Loss", "Weight", "Date", "Account",
|
||||
});
|
||||
try out.print(" " ++ std.fmt.comptimePrint("{{s:->{d}}}", .{fmt.sym_col_width}) ++ " {s:->8} {s:->10} {s:->10} {s:->16} {s:->14} {s:->8} {s:->13} {s:->8}\n", .{
|
||||
"", "", "", "", "", "", "", "", "",
|
||||
});
|
||||
try out.print(pl.header, pl.header_labels);
|
||||
try out.print(pl.separator, pl.separator_fills);
|
||||
try cli.reset(out, color);
|
||||
|
||||
// Position rows with lot detail
|
||||
|
|
@ -345,19 +343,19 @@ pub fn display(
|
|||
}
|
||||
|
||||
if (a.is_manual_price) try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
try out.print(" " ++ fmt.sym_col_spec ++ " {d:>8.1} {f} ", .{
|
||||
a.display_symbol, a.shares, Money.from(a.avg_cost).padRight(10),
|
||||
try out.print(" " ++ pl.symbol_spec ++ " " ++ pl.shares_num ++ " {f} ", .{
|
||||
a.display_symbol, a.shares, Money.from(a.avg_cost).padRight(pl.price_w),
|
||||
});
|
||||
try out.print("{f}", .{Money.from(a.current_price).padRight(10)});
|
||||
try out.print(" {f} ", .{Money.from(a.market_value).padRight(16)});
|
||||
try out.print("{f}", .{Money.from(a.current_price).padRight(pl.price_w)});
|
||||
try out.print(" {f} ", .{Money.from(a.market_value).padRight(pl.value_w)});
|
||||
try cli.setGainLoss(out, color, a.unrealized_gain_loss);
|
||||
try out.print("{s}{f}", .{ sign, Money.from(gl_abs).padRight(13) });
|
||||
try out.print("{s}{f}", .{ sign, Money.from(gl_abs).padRight(pl.gainloss_w - 1) });
|
||||
if (a.is_manual_price) {
|
||||
try cli.setFg(out, color, cli.CLR_WARNING);
|
||||
} else {
|
||||
try cli.reset(out, color);
|
||||
}
|
||||
try out.print(" {d:>7.1}%", .{a.weight * 100.0});
|
||||
try out.print(" " ++ pl.weight_num, .{a.weight * 100.0});
|
||||
if (date_col_len > 0) {
|
||||
try out.print(" {s}", .{date_col[0..date_col_len]});
|
||||
}
|
||||
|
|
@ -411,19 +409,17 @@ pub fn display(
|
|||
}
|
||||
|
||||
// Totals line
|
||||
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.printFg(out, color, cli.CLR_MUTED, pl.total_sep, pl.total_sep_fills);
|
||||
{
|
||||
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} {f} ", .{
|
||||
"", "", "", "TOTAL", Money.from(summary.total_value).padRight(16),
|
||||
try out.print(pl.total, .{
|
||||
"", "", "", "TOTAL", Money.from(summary.total_value).padRight(pl.value_w),
|
||||
});
|
||||
try cli.printGainLoss(out, color, summary.unrealized_gain_loss, "{c}{f}", .{
|
||||
@as(u8, if (summary.unrealized_gain_loss >= 0) '+' else '-'),
|
||||
Money.from(gl_abs).padRight(13),
|
||||
Money.from(gl_abs).padRight(pl.gainloss_w - 1),
|
||||
});
|
||||
try out.print(" {s:>7}\n", .{"100.0%"});
|
||||
try out.print(" " ++ pl.weight_str ++ "\n", .{"100.0%"});
|
||||
}
|
||||
|
||||
if (summary.realized_gain_loss != 0) {
|
||||
|
|
@ -593,7 +589,7 @@ pub fn display(
|
|||
std.fmt.bufPrint(&price_str, "{f}", .{Money.from(close)}) catch "$?"
|
||||
else
|
||||
"--";
|
||||
try out.print(" " ++ fmt.sym_col_spec ++ " {s:>10}\n", .{ sym, ps });
|
||||
try out.print(" " ++ pl.symbol_spec ++ " " ++ pl.price_str ++ "\n", .{ sym, ps });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -616,11 +612,11 @@ pub fn printLotRow(as_of: zfin.Date, out: *std.Io.Writer, color: bool, lot: zfin
|
|||
const lot_gl_abs = if (gl >= 0) gl else -gl;
|
||||
const lot_sign: []const u8 = if (gl >= 0) "+" else "-";
|
||||
|
||||
try cli.printFg(out, color, cli.CLR_MUTED, " " ++ fmt.sym_col_spec ++ " {d:>8.1} {f} {s:>10} {f} ", .{
|
||||
status_str, lot.shares, Money.from(lot.open_price).padRight(10), "", Money.from(lot.shares * use_price).padRight(16),
|
||||
try cli.printFg(out, color, cli.CLR_MUTED, " " ++ pl.symbol_spec ++ " " ++ pl.shares_num ++ " {f} " ++ pl.price_str ++ " {f} ", .{
|
||||
status_str, lot.shares, Money.from(lot.open_price).padRight(pl.price_w), "", Money.from(lot.shares * use_price).padRight(pl.value_w),
|
||||
});
|
||||
try cli.printGainLoss(out, color, gl, "{s}{f}", .{ lot_sign, Money.from(lot_gl_abs).padRight(13) });
|
||||
try cli.printFg(out, color, cli.CLR_MUTED, " {s:>8} {f} {s} {s}\n", .{ "", lot.open_date, indicator, acct_col });
|
||||
try cli.printGainLoss(out, color, gl, "{s}{f}", .{ lot_sign, Money.from(lot_gl_abs).padRight(pl.gainloss_w - 1) });
|
||||
try cli.printFg(out, color, cli.CLR_MUTED, " " ++ pl.weight_str ++ " {f} {s} {s}\n", .{ "", lot.open_date, indicator, acct_col });
|
||||
}
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────
|
||||
|
|
@ -718,6 +714,98 @@ test "display shows header and summary" {
|
|||
try testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
||||
}
|
||||
|
||||
test "display widens columns for large crypto values" {
|
||||
// Regression guard for the portfolio-table column widening. A long
|
||||
// crypto symbol (DOGE-USD, 8 chars), a large share count, and a high
|
||||
// price must render in full without overflowing or shifting the
|
||||
// symbol / shares / price / value columns.
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
||||
var lots = [_]zfin.Lot{
|
||||
.{ .symbol = "DOGE-USD", .shares = 10000, .open_date = zfin.Date.fromYmd(2024, 1, 2), .open_price = 41500.0, .account = "Sample Account" },
|
||||
};
|
||||
var portfolio = testPortfolio(&lots);
|
||||
|
||||
var positions = [_]zfin.Position{
|
||||
.{ .symbol = "DOGE-USD", .shares = 10000, .avg_cost = 41500.0, .total_cost = 415000000.0, .open_lots = 1, .closed_lots = 0, .realized_gain_loss = 0 },
|
||||
};
|
||||
|
||||
var allocs = [_]zfin.valuation.Allocation{
|
||||
.{ .symbol = "DOGE-USD", .display_symbol = "DOGE-USD", .shares = 10000, .avg_cost = 41500.0, .current_price = 42000.0, .market_value = 420000000.0, .cost_basis = 415000000.0, .weight = 1.0, .unrealized_gain_loss = 5000000.0, .unrealized_return = 0.01205 },
|
||||
};
|
||||
const summary = testSummary(&allocs);
|
||||
|
||||
var candle_map = std.StringHashMap([]const zfin.Candle).init(testing.allocator);
|
||||
defer candle_map.deinit();
|
||||
const pf_data = testPortfolioData(summary, candle_map);
|
||||
|
||||
var watch_prices = std.StringHashMap(f64).init(testing.allocator);
|
||||
defer watch_prices.deinit();
|
||||
const watch_syms: []const []const u8 = &.{};
|
||||
|
||||
try display(testing.allocator, &w, false, "crypto.srf", &portfolio, &positions, &pf_data, watch_syms, watch_prices, zfin.Date.fromYmd(2026, 5, 8), .rollup);
|
||||
const out = w.buffered();
|
||||
|
||||
// Full 8-char symbol present (not truncated).
|
||||
try testing.expect(std.mem.indexOf(u8, out, "DOGE-USD") != null);
|
||||
// Symbol column is wide enough that DOGE-USD does not butt up against
|
||||
// the next column: it is followed by >= 2 spaces (in-column padding
|
||||
// plus the separator). At the old 7-wide column there would be only
|
||||
// the single separator space, so this catches a narrow-column regression.
|
||||
try testing.expect(std.mem.indexOf(u8, out, "DOGE-USD ") != null);
|
||||
// Large share count, high price, and large market value render in full.
|
||||
try testing.expect(std.mem.indexOf(u8, out, "10000.0") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "$42,000.00") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, out, "$420,000,000.00") != null);
|
||||
// Gain/Loss of $5M fits its column without truncation.
|
||||
try testing.expect(std.mem.indexOf(u8, out, "$5,000,000.00") != null);
|
||||
// No ANSI codes when color is disabled.
|
||||
try testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null);
|
||||
|
||||
// The single position's market value equals the portfolio total, so
|
||||
// it must appear at the same column in the data row and the TOTAL
|
||||
// row. This guards the TOTAL line against drifting out of alignment
|
||||
// when the data columns are widened.
|
||||
var data_col: ?usize = null;
|
||||
var total_col: ?usize = null;
|
||||
var lit = std.mem.splitScalar(u8, out, '\n');
|
||||
while (lit.next()) |line| {
|
||||
const at = std.mem.indexOf(u8, line, "$420,000,000.00") orelse continue;
|
||||
if (std.mem.indexOf(u8, line, "DOGE-USD") != null) {
|
||||
data_col = at;
|
||||
} else if (std.mem.indexOf(u8, line, "TOTAL") != null) {
|
||||
total_col = at;
|
||||
}
|
||||
}
|
||||
try testing.expect(data_col != null);
|
||||
try testing.expect(total_col != null);
|
||||
try testing.expectEqual(data_col.?, total_col.?);
|
||||
|
||||
// The Date column is left-justified text (like the symbol column), so
|
||||
// the "Date" header sits over the START of the date values, and the
|
||||
// Account column lines up too. Verify the header label and the data
|
||||
// value share a column.
|
||||
var hdr_date: ?usize = null;
|
||||
var row_date: ?usize = null;
|
||||
var hdr_acct: ?usize = null;
|
||||
var row_acct: ?usize = null;
|
||||
var dit = std.mem.splitScalar(u8, out, '\n');
|
||||
while (dit.next()) |line| {
|
||||
if (std.mem.indexOf(u8, line, "Symbol") != null) {
|
||||
hdr_date = std.mem.indexOf(u8, line, "Date");
|
||||
hdr_acct = std.mem.indexOf(u8, line, "Account");
|
||||
} else if (std.mem.indexOf(u8, line, "DOGE-USD") != null) {
|
||||
row_date = std.mem.indexOf(u8, line, "2024-01-02");
|
||||
row_acct = std.mem.indexOf(u8, line, "Sample Account");
|
||||
}
|
||||
}
|
||||
try testing.expect(hdr_date != null and row_date != null);
|
||||
try testing.expectEqual(hdr_date.?, row_date.?);
|
||||
try testing.expect(hdr_acct != null and row_acct != null);
|
||||
try testing.expectEqual(hdr_acct.?, row_acct.?);
|
||||
}
|
||||
|
||||
test "display with watchlist" {
|
||||
var buf: [8192]u8 = undefined;
|
||||
var w: std.Io.Writer = .fixed(&buf);
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@ const PerformanceResult = @import("analytics/performance.zig").PerformanceResult
|
|||
|
||||
// ── Layout constants ─────────────────────────────────────────
|
||||
|
||||
/// Width of the symbol column in portfolio view (CLI + TUI).
|
||||
pub const sym_col_width = 7;
|
||||
/// Comptime format spec for left-aligned symbol column, e.g. "{s:<7}".
|
||||
pub const sym_col_spec = std.fmt.comptimePrint("{{s:<{d}}}", .{sym_col_width});
|
||||
|
||||
/// Width of the account name column in cash section (CLI + TUI).
|
||||
pub const cash_acct_width = 30;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ const zfin = @import("../root.zig");
|
|||
const fmt = @import("../format.zig");
|
||||
const Money = @import("../Money.zig");
|
||||
const views = @import("../views/portfolio_sections.zig");
|
||||
/// Main holdings-table column layout (widths + per-column specs).
|
||||
const pl = views.PositionsLayout;
|
||||
const theme = @import("theme.zig");
|
||||
const tui = @import("../tui.zig");
|
||||
const framework = @import("tab_framework.zig");
|
||||
|
|
@ -13,10 +15,14 @@ const StyledLine = tui.StyledLine;
|
|||
const colLabel = tui.colLabel;
|
||||
|
||||
// Portfolio column layout (display columns).
|
||||
// Each column width includes its trailing separator space.
|
||||
// prefix(4) + sym(sw+1) + shares(8+1) + avgcost(10+1) + price(10+1) + mv(16+1) + gl(14+1) + weight(8) + date(13+1) + account
|
||||
// Row shape: prefix(4) + sym + shares + avgcost + price + mv + gl + weight + date + account.
|
||||
// Every non-prefix column width is owned by views/portfolio_sections.zig
|
||||
// (PositionsLayout: symbol_w, shares_w, price_w, value_w, gainloss_w,
|
||||
// weight_w, date_w); each column adds a 1-col separator space. The
|
||||
// row/header format strings and the col_end_* hit-test offsets below all
|
||||
// derive from those same widths, so they can't drift apart.
|
||||
const prefix_cols: usize = 4;
|
||||
const sw: usize = fmt.sym_col_width;
|
||||
const sw: usize = pl.symbol_w;
|
||||
|
||||
// ── Portfolio-specific types ──────────────────────────────────
|
||||
|
||||
|
|
@ -775,14 +781,17 @@ fn toggleExpandAtCursor(state: *State, app: *App) void {
|
|||
}
|
||||
|
||||
/// Cumulative column end positions for click-to-sort hit testing.
|
||||
pub const col_end_symbol: usize = prefix_cols + sw + 1;
|
||||
pub const col_end_shares: usize = col_end_symbol + 9;
|
||||
pub const col_end_avg_cost: usize = col_end_shares + 11;
|
||||
pub const col_end_price: usize = col_end_avg_cost + 11;
|
||||
pub const col_end_market_value: usize = col_end_price + 17;
|
||||
pub const col_end_gain_loss: usize = col_end_market_value + 15;
|
||||
pub const col_end_weight: usize = col_end_gain_loss + 9;
|
||||
pub const col_end_date: usize = col_end_weight + 14;
|
||||
/// Each offset is the previous column's end plus this column's content
|
||||
/// width (from format.zig) plus the 1-col separator space, so the
|
||||
/// hit-test grid stays locked to the row/header format strings.
|
||||
const col_end_symbol: usize = prefix_cols + sw + 1;
|
||||
const col_end_shares: usize = col_end_symbol + pl.shares_w + 1;
|
||||
const col_end_avg_cost: usize = col_end_shares + pl.price_w + 1;
|
||||
const col_end_price: usize = col_end_avg_cost + pl.price_w + 1;
|
||||
const col_end_market_value: usize = col_end_price + pl.value_w + 1;
|
||||
const col_end_gain_loss: usize = col_end_market_value + pl.gainloss_w + 1;
|
||||
const col_end_weight: usize = col_end_gain_loss + pl.weight_w + 1;
|
||||
const col_end_date: usize = col_end_weight + pl.date_w + 1;
|
||||
|
||||
// Gain/loss column start position (used for alt-style coloring)
|
||||
const gl_col_start: usize = col_end_market_value;
|
||||
|
|
@ -1661,16 +1670,16 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
var mv_hdr_buf: [24]u8 = undefined;
|
||||
var gl_hdr_buf: [24]u8 = undefined;
|
||||
var wt_hdr_buf: [16]u8 = undefined;
|
||||
const sym_hdr = colLabel(&sym_hdr_buf, "Symbol", fmt.sym_col_width, true, if (sf == .symbol) si else null);
|
||||
const shr_hdr = colLabel(&shr_hdr_buf, "Shares", 8, false, if (sf == .shares) si else null);
|
||||
const avg_hdr = colLabel(&avg_hdr_buf, "Avg Cost", 10, false, if (sf == .avg_cost) si else null);
|
||||
const prc_hdr = colLabel(&prc_hdr_buf, "Price", 10, false, if (sf == .price) si else null);
|
||||
const mv_hdr = colLabel(&mv_hdr_buf, "Market Value", 16, false, if (sf == .market_value) si else null);
|
||||
const gl_hdr = colLabel(&gl_hdr_buf, "Gain/Loss", 14, false, if (sf == .gain_loss) si else null);
|
||||
const wt_hdr = colLabel(&wt_hdr_buf, "Weight", 8, false, if (sf == .weight) si else null);
|
||||
const sym_hdr = colLabel(&sym_hdr_buf, "Symbol", pl.symbol_w, true, if (sf == .symbol) si else null);
|
||||
const shr_hdr = colLabel(&shr_hdr_buf, "Shares", pl.shares_w, false, if (sf == .shares) si else null);
|
||||
const avg_hdr = colLabel(&avg_hdr_buf, "Avg Cost", pl.price_w, false, if (sf == .avg_cost) si else null);
|
||||
const prc_hdr = colLabel(&prc_hdr_buf, "Price", pl.price_w, false, if (sf == .price) si else null);
|
||||
const mv_hdr = colLabel(&mv_hdr_buf, "Market Value", pl.value_w, false, if (sf == .market_value) si else null);
|
||||
const gl_hdr = colLabel(&gl_hdr_buf, "Gain/Loss", pl.gainloss_w, false, if (sf == .gain_loss) si else null);
|
||||
const wt_hdr = colLabel(&wt_hdr_buf, "Weight", pl.weight_w, false, if (sf == .weight) si else null);
|
||||
const acct_ind: []const u8 = if (sf == .account) si else "";
|
||||
|
||||
const hdr = try std.fmt.allocPrint(arena, " {s} {s} {s} {s} {s} {s} {s} {s:>13} {s}{s}", .{
|
||||
const hdr = try std.fmt.allocPrint(arena, " {s} {s} {s} {s} {s} {s} {s} " ++ pl.date_str ++ " {s}{s}", .{
|
||||
sym_hdr, shr_hdr, avg_hdr, prc_hdr, mv_hdr, gl_hdr, wt_hdr, "Date", acct_ind, "Account",
|
||||
});
|
||||
try lines.append(arena, .{ .text = hdr, .style = th.headerStyle() });
|
||||
|
|
@ -1751,7 +1760,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
else
|
||||
a.weight;
|
||||
|
||||
const text = try std.fmt.allocPrint(arena, "{s}{s}" ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {d:>7.1}% {s:>13} {s}", .{
|
||||
const text = try std.fmt.allocPrint(arena, "{s}{s}" ++ pl.symbol_spec ++ " " ++ pl.shares_num ++ " " ++ pl.price_str ++ " " ++ pl.price_str ++ " " ++ pl.value_str ++ " " ++ pl.gainloss_str ++ " " ++ pl.weight_num ++ " " ++ pl.date_str ++ " {s}", .{
|
||||
arrow, star, a.display_symbol, display_shares, cost_str, price_str, mv_str, pnl_str, display_weight * 100.0, date_col, acct_col,
|
||||
});
|
||||
|
||||
|
|
@ -1765,7 +1774,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
.style = base_style,
|
||||
.alt_style = gl_style,
|
||||
.alt_start = gl_col_start,
|
||||
.alt_end = gl_col_start + 14,
|
||||
.alt_end = gl_col_start + pl.gainloss_w,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1799,7 +1808,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
const indicator = fmt.capitalGainsIndicator(app.today, lot.open_date);
|
||||
const lot_date_col = try std.fmt.allocPrint(arena, "{s} {s}", .{ date_str, indicator });
|
||||
const acct_col: []const u8 = lot.account orelse "";
|
||||
const text = try std.fmt.allocPrint(arena, " " ++ fmt.sym_col_spec ++ " {d:>8.1} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13} {s}", .{
|
||||
const text = try std.fmt.allocPrint(arena, " " ++ pl.symbol_spec ++ " " ++ pl.shares_num ++ " " ++ pl.price_str ++ " " ++ pl.price_str ++ " " ++ pl.value_str ++ " " ++ pl.gainloss_str ++ " " ++ pl.weight_str ++ " " ++ pl.date_str ++ " {s}", .{
|
||||
status_str, lot.shares, lot_price_str, "", lot_mv_str, lot_gl_str, "", lot_date_col, acct_col,
|
||||
});
|
||||
const base_style = if (is_cursor) th.selectStyle() else th.mutedStyle();
|
||||
|
|
@ -1809,7 +1818,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
.style = base_style,
|
||||
.alt_style = gl_col_style,
|
||||
.alt_start = gl_col_start,
|
||||
.alt_end = gl_col_start + 14,
|
||||
.alt_end = gl_col_start + pl.gainloss_w,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
@ -1820,7 +1829,7 @@ pub fn drawContent(state: *State, app: *App, arena: std.mem.Allocator, buf: []va
|
|||
else
|
||||
"--";
|
||||
const star2: []const u8 = if (is_active_sym) "* " else " ";
|
||||
const text = try std.fmt.allocPrint(arena, " {s}" ++ fmt.sym_col_spec ++ " {s:>8} {s:>10} {s:>10} {s:>16} {s:>14} {s:>8} {s:>13}", .{
|
||||
const text = try std.fmt.allocPrint(arena, " {s}" ++ pl.symbol_spec ++ " " ++ pl.shares_str ++ " " ++ pl.price_str ++ " " ++ pl.price_str ++ " " ++ pl.value_str ++ " " ++ pl.gainloss_str ++ " " ++ pl.weight_str ++ " " ++ pl.date_str, .{
|
||||
star2, row.symbol, "--", "--", ps, "--", "--", "watch", "",
|
||||
});
|
||||
const row_style = if (is_cursor) th.selectStyle() else th.contentStyle();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! View models for portfolio sections (Options, CDs).
|
||||
//! View models for portfolio sections (Positions, Options, CDs).
|
||||
//! Produces renderer-agnostic display data consumed by both CLI and TUI.
|
||||
//! Column widths, format strings, computed values, and style decisions
|
||||
//! are defined here. Renderers are thin adapters that map StyleIntent
|
||||
|
|
@ -10,6 +10,86 @@ const Date = @import("../Date.zig");
|
|||
const fmt = @import("../format.zig");
|
||||
const Money = @import("../Money.zig");
|
||||
|
||||
// ── Positions (main holdings table) ───────────────────────────
|
||||
|
||||
/// Column layout for the main holdings table - the portfolio's primary
|
||||
/// table, shared by the CLI (commands/portfolio.zig) and the TUI
|
||||
/// (tui/portfolio_tab.zig). This is the single source of truth for the
|
||||
/// column widths, so the CLI header/separator/rows, the TUI header/rows,
|
||||
/// and the TUI click-to-sort hit-test offsets (portfolio_tab.col_end_*)
|
||||
/// all derive from the same numbers and can't drift apart.
|
||||
///
|
||||
/// Unlike the Options / CDs layouts, the rows don't collapse into one
|
||||
/// `data_row` string: the gain/loss cell is colored (so the CLI splits
|
||||
/// the row across several prints) and the shares / weight cells render a
|
||||
/// float inline in data rows but a pre-formatted string in headers and
|
||||
/// the TUI's pre-rendered cells. So the per-column specs are exposed
|
||||
/// individually alongside the composed header / separator / total
|
||||
/// strings.
|
||||
pub const PositionsLayout = struct {
|
||||
const cp = std.fmt.comptimePrint;
|
||||
|
||||
// Column content widths. The 1-col separating space between columns
|
||||
// is added at layout time, not included here.
|
||||
pub const symbol_w = 10;
|
||||
pub const shares_w = 12;
|
||||
pub const price_w = 12; // avg cost and current price
|
||||
pub const value_w = 16; // market value
|
||||
pub const gainloss_w = 14; // sign + amount
|
||||
pub const weight_w = 8; // "NNN.N%"
|
||||
pub const date_w = 13;
|
||||
pub const account_w = 8; // separator rule only; header/data are natural width
|
||||
|
||||
// Per-column format specs. `symbol_spec` and `date_str` left-justify
|
||||
// their text columns (symbol and date read left-to-right, so their
|
||||
// headers sit over the start of the data); the other specs right-
|
||||
// justify their numeric columns. `shares_num` / `weight_num` render
|
||||
// a float in place (data rows); the `*_str` specs right-justify a
|
||||
// pre-formatted string (headers and the TUI's pre-rendered cells).
|
||||
pub const symbol_spec = cp("{{s:<{d}}}", .{symbol_w});
|
||||
pub const shares_num = cp("{{d:>{d}.1}}", .{shares_w});
|
||||
pub const weight_num = cp("{{d:>{d}.1}}%", .{weight_w - 1});
|
||||
pub const shares_str = cp("{{s:>{d}}}", .{shares_w});
|
||||
pub const price_str = cp("{{s:>{d}}}", .{price_w});
|
||||
pub const value_str = cp("{{s:>{d}}}", .{value_w});
|
||||
pub const gainloss_str = cp("{{s:>{d}}}", .{gainloss_w});
|
||||
pub const weight_str = cp("{{s:>{d}}}", .{weight_w});
|
||||
pub const date_str = cp("{{s:<{d}}}", .{date_w});
|
||||
|
||||
// CLI header + dashed separator (9 columns, see header_labels).
|
||||
pub const header = " " ++ symbol_spec ++ " " ++ shares_str ++ " " ++ price_str ++
|
||||
" " ++ price_str ++ " " ++ value_str ++ " " ++ gainloss_str ++
|
||||
" " ++ weight_str ++ " " ++ date_str ++ " {s}\n";
|
||||
pub const header_labels = .{ "Symbol", "Shares", "Avg Cost", "Price", "Market Value", "Gain/Loss", "Weight", "Date", "Account" };
|
||||
pub const separator = " " ++ cp("{{s:->{d}}}", .{symbol_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{shares_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{price_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{price_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{value_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{gainloss_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{weight_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{date_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{account_w}) ++ "\n";
|
||||
pub const separator_fills = .{ "", "", "", "", "", "", "", "", "" };
|
||||
|
||||
// CLI TOTAL row: a sum rule spanning Symbol..Weight, then the totals
|
||||
// row. `total` blanks the symbol / shares / avg-cost cells, right-
|
||||
// justifies "TOTAL" in the price cell, and renders the market-value
|
||||
// cell (4 string args + the market-value `{f}`); the caller prints
|
||||
// the gain/loss and weight cells separately because gain/loss needs
|
||||
// color.
|
||||
pub const total_sep = " " ++ cp("{{s:->{d}}}", .{symbol_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{shares_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{price_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{price_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{value_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{gainloss_w}) ++
|
||||
" " ++ cp("{{s:->{d}}}", .{weight_w}) ++ "\n";
|
||||
pub const total_sep_fills = .{ "", "", "", "", "", "", "" };
|
||||
pub const total = " " ++ symbol_spec ++ " " ++ shares_str ++ " " ++ price_str ++
|
||||
" " ++ price_str ++ " {f} ";
|
||||
};
|
||||
|
||||
// ── Options ───────────────────────────────────────────────────
|
||||
|
||||
/// Column layout for the Options section.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue