From 23fd4e3b63272de9bb56f8835899de0705527119 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 5 Sep 2026 11:43:30 -0700 Subject: [PATCH] alignment fixes in contributions --- src/commands/contributions.zig | 124 ++++++++++++++++++++++++--------- 1 file changed, 93 insertions(+), 31 deletions(-) diff --git a/src/commands/contributions.zig b/src/commands/contributions.zig index 564125d..30bf9ce 100644 --- a/src/commands/contributions.zig +++ b/src/commands/contributions.zig @@ -3274,13 +3274,10 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co try printSection(out, "Lot edits (same position, key rewritten - not counted)", color, h_color); for (report.changes) |c| switch (c.kind) { .lot_edited => { - var buf: [256]u8 = undefined; - const msg = std.fmt.bufPrint( - &buf, - " {s:<14}{s:<30} (strict key broke, shares unchanged)\n", - .{ c.symbol, c.account }, - ) catch " (lot edit)\n"; - try cli.printFg(out, color, mut_color, "{s}", .{msg}); + try cli.setFg(out, color, mut_color); + try writeRowPrefix(out, c.symbol, c.account); + try out.writeAll(" (strict key broke, shares unchanged)\n"); + try cli.reset(out, color); }, else => {}, }; @@ -3344,10 +3341,10 @@ fn printReport(out: *std.Io.Writer, report: *const Report, label: []const u8, co total_cd_int += cd_int; const acct_label = if (acct.len == 0) "(no account)" else acct; - // 30 to match the per-change rows above. `Fidelity Emil 401(k) Roth BL` - // is 28 characters, so 28 left zero gutter and the next field butted - // straight up against the name. - try out.print(" {s:<30}", .{acct_label}); + // Same width and the same gutter guarantee as the per-change rows, minus + // the symbol column this section has no use for. + try out.writeAll(" "); + try padTo(out, acct_label, acct_w); try printSummaryCell(out, " new", t.new_money, color); try printSummaryCell(out, " drip", t.drip_confirmed, color); try printSummaryCell(out, " rollup", t.rollup, color); @@ -3432,6 +3429,66 @@ fn printNone(out: *std.Io.Writer, color: bool, muted: [3]u8) !void { try cli.printFg(out, color, muted, " (none)\n", .{}); } +/// Symbol column. 16 because `Engagement Ring` is 15 - illiquid assets carry +/// free-form names rather than tickers, so the old 14 was sized for a population +/// this report no longer only contains. +const sym_w = 16; + +/// Account column. 30 because `Fidelity Emil 401(k) Roth BL` is 28. +const acct_w = 30; + +/// Pad `s` into a `w`-wide field, always leaving at least one space behind it. +/// +/// The guarantee is the point. `{s:= w) 1 else w - s.len); +} + +/// The ` symbol account ` prefix every per-change row opens with. +/// +/// One function rather than a format-string literal repeated at fourteen call +/// sites. The literal is how the widths drifted out of step in the first place: +/// twelve rows said `{s:<14}{s:<24}`, the CD continuation said the same with +/// blank arguments, the lot-edit row said `{s: <12} {s: <24}`, and the account +/// summary said `{s:<28}` - four different answers to one question, and each new +/// long value found a different one of them. +/// +/// Callers own the single space that follows, so content lands one column past +/// `acct_w`. Keep it: every section has to agree on that column or the report +/// shears between sections instead of within a row, which is harder to spot. +fn writeRowPrefix(out: *std.Io.Writer, symbol: []const u8, account: []const u8) !void { + try out.writeAll(" "); + try padTo(out, symbol, sym_w); + try padTo(out, account, acct_w); +} + +test "padTo: pads short values to width and never welds a long one" { + var buf: [128]u8 = undefined; + + // Short: padded to the column. + var w1 = std.Io.Writer.fixed(&buf); + try padTo(&w1, "CASH", 16); + try std.testing.expectEqualStrings("CASH ", w1.buffered()); + + // Exactly one under: still a gutter, and it is the last width that aligns. + var w2 = std.Io.Writer.fixed(&buf); + try padTo(&w2, "Engagement Ring", 16); + try std.testing.expectEqualStrings("Engagement Ring ", w2.buffered()); + + // Over: alignment is gone, whitespace is not. `{s:<16}` produced no space + // here at all, which ran the value into the next column. + var w3 = std.Io.Writer.fixed(&buf); + try padTo(&w3, "AMZN 09/18/2026 280.00 C", 16); + try std.testing.expectEqualStrings("AMZN 09/18/2026 280.00 C ", w3.buffered()); +} + fn printTotalLine(out: *std.Io.Writer, label: []const u8, v: f64, color: bool, hdr: [3]u8) !void { try cli.printFg(out, color, hdr, " {s}: {f}\n", .{ label, Money.from(v) }); } @@ -3445,7 +3502,7 @@ fn printChangeLine(out: *std.Io.Writer, c: Change, color: bool, pos: [3]u8) !voi const val_str = std.fmt.bufPrint(&val_buf, "{f}", .{Money.from(c.value())}) catch "$?"; const acct = if (c.account.len == 0) "(no account)" else c.account; - try out.print(" {s:<14}{s:<30}", .{ c.symbol, acct }); + try writeRowPrefix(out, c.symbol, acct); if (c.security_type == .cash) { try cli.printFg(out, color, pos, " {s}", .{val_str}); } else { @@ -3464,15 +3521,15 @@ fn printCdLine(out: *std.Io.Writer, c: Change, implied_interest: ?f64, color: bo .cd_removed_early => "removed EARLY", else => "removed", }; - try out.print(" {s:<14}{s:<30} {s:<16} face {f} maturity {s}\n", .{ - c.symbol, - acct, + try writeRowPrefix(out, c.symbol, acct); + try out.print(" {s:<16} face {f} maturity {s}\n", .{ verb, Money.from(c.face_value), mat_str, }); if (implied_interest) |i| { - try cli.printFg(out, color, cli.CLR_POSITIVE, " {s:<14}{s:<30} implied interest: {f}\n", .{ "", "", Money.from(i) }); + try writeRowPrefix(out, "", ""); + try cli.printFg(out, color, cli.CLR_POSITIVE, " implied interest: {f}\n", .{Money.from(i)}); } } @@ -3480,7 +3537,8 @@ fn printCashDeltaLine(out: *std.Io.Writer, c: Change, report: *const Report, col const v = c.value(); const acct = if (c.account.len == 0) "(no account)" else c.account; const sign = if (v >= 0) "+" else "-"; - try out.print(" {s:<14}{s:<30} cash ", .{ c.symbol, acct }); + try writeRowPrefix(out, c.symbol, acct); + try out.writeAll(" cash "); try cli.printGainLoss(out, color, v, "{s}{f}", .{ sign, Money.from(@abs(v)) }); // Hint if a CD matured in the same account. @@ -3495,9 +3553,9 @@ fn printCashDeltaLine(out: *std.Io.Writer, c: Change, report: *const Report, col fn printPriceOnlyLine(out: *std.Io.Writer, c: Change, color: bool, muted: [3]u8) !void { const acct = if (c.account.len == 0) "(no account)" else c.account; - try cli.printFg(out, color, muted, " {s:<14}{s:<30} price {f} -> {f}\n", .{ - c.symbol, - acct, + try cli.setFg(out, color, muted); + try writeRowPrefix(out, c.symbol, acct); + try out.print(" price {f} -> {f}\n", .{ Money.from(c.old_price), Money.from(c.new_price), }); @@ -3508,17 +3566,18 @@ fn printFlaggedLine(out: *std.Io.Writer, c: Change, color: bool, warn: [3]u8) !v try cli.setFg(out, color, warn); switch (c.kind) { .flagged => { - try out.print(" {s:<14}{s:<30} {s}", .{ c.symbol, acct, c.detail orelse "edited" }); + try writeRowPrefix(out, c.symbol, acct); + try out.print(" {s}", .{c.detail orelse "edited"}); }, .lot_removed => { - try out.print(" {s:<14}{s:<30} {s} lot removed (face {f})", .{ - c.symbol, acct, @tagName(c.security_type), Money.from(c.face_value), + try writeRowPrefix(out, c.symbol, acct); + try out.print(" {s} lot removed (face {f})", .{ + @tagName(c.security_type), Money.from(c.face_value), }); }, .drip_negative => { - try out.print(" {s:<14}{s:<30} shares decreased on existing lot ({f})", .{ - c.symbol, acct, Money.from(@abs(c.value())), - }); + try writeRowPrefix(out, c.symbol, acct); + try out.print(" shares decreased on existing lot ({f})", .{Money.from(@abs(c.value()))}); }, else => {}, } @@ -3637,7 +3696,7 @@ fn printPartialTransferLine(out: *std.Io.Writer, c: Change, color: bool, pos: [3 const lot_value = c.value(); const sym = if (c.symbol.len > 0) c.symbol else "cash"; - try out.print(" {s:<14}{s:<30}", .{ sym, acct }); + try writeRowPrefix(out, sym, acct); try cli.printFg(out, color, pos, " {f}", .{Money.from(residual)}); try cli.printFg( out, @@ -3659,7 +3718,7 @@ fn printCashFundedResidualLine(out: *std.Io.Writer, c: Change, color: bool, pos: const lot_value = c.value(); const sym = if (c.symbol.len > 0) c.symbol else "cash"; - try out.print(" {s:<14}{s:<30}", .{ sym, acct }); + try writeRowPrefix(out, sym, acct); try cli.printFg(out, color, pos, " {f}", .{Money.from(residual)}); try cli.printFg( out, @@ -3724,9 +3783,11 @@ fn printCollapsedSales(out: *std.Io.Writer, report: *const Report, color: bool, // current-price proxy, so say which one the reader is looking at. const basis: []const u8 = if (closed_in_place) "at close" else "at mark"; if (lots == 1) { - try out.print(" {s:<14}{s:<30} sold {s} ({f})", .{ c.symbol, acct, basis, Money.from(proceeds) }); + try writeRowPrefix(out, c.symbol, acct); + try out.print(" sold {s} ({f})", .{ basis, Money.from(proceeds) }); } else { - try out.print(" {s:<14}{s:<30} sold {d} lots {s} ({f})", .{ c.symbol, acct, lots, basis, Money.from(proceeds) }); + try writeRowPrefix(out, c.symbol, acct); + try out.print(" sold {d} lots {s} ({f})", .{ lots, basis, Money.from(proceeds) }); } try cli.reset(out, color); try out.writeAll("\n"); @@ -3744,7 +3805,8 @@ fn printInternalPurchaseLine(out: *std.Io.Writer, c: Change, color: bool, muted: const lot_value = c.value(); try cli.setFg(out, color, muted); - try out.print(" {s:<14}{s:<30} {f} from existing cash", .{ sym, acct, Money.from(c.internal_funded) }); + try writeRowPrefix(out, sym, acct); + try out.print(" {f} from existing cash", .{Money.from(c.internal_funded)}); if (c.internal_funded + 0.005 < lot_value) { try out.print(" (of {f} lot)", .{Money.from(lot_value)}); }