add ADDITIONAL kitty-based projections chart to the top of the projections command if terminal supports
This commit is contained in:
parent
692a28aed7
commit
dd550a85d9
1 changed files with 66 additions and 5 deletions
|
|
@ -22,6 +22,10 @@ const forecast = @import("../analytics/forecast_evaluation.zig");
|
|||
const milestones = @import("../analytics/milestones.zig");
|
||||
const shiller = @import("../data/shiller.zig");
|
||||
const chart_export = @import("../chart_export.zig");
|
||||
const projection_chart = @import("../charts/projection_chart.zig");
|
||||
const term_graphics = @import("../term_graphics.zig");
|
||||
const term_query = @import("../term_query.zig");
|
||||
const theme = @import("../tui/theme.zig");
|
||||
|
||||
/// Tagged-union args for the four projection sub-modes. Mutually-
|
||||
/// exclusive flag combos (--convergence with --vs, --real with
|
||||
|
|
@ -279,6 +283,14 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
// total. Snapshot-only as-of paths ignore it.
|
||||
var live = try loadLiveData(ctx, today, color);
|
||||
defer if (live) |*l| l.deinit(allocator);
|
||||
// Inline kitty band chart when supported (or forced). There's
|
||||
// no braille fallback for projections - non-kitty terminals
|
||||
// keep the table-only output.
|
||||
const kitty_caps: ?term_query.Caps = switch (ctx.globals.chart_config.mode) {
|
||||
.braille => null,
|
||||
.kitty => ctx.graphics_caps,
|
||||
.auto => if (ctx.graphics_caps.kitty) ctx.graphics_caps else null,
|
||||
};
|
||||
try runBands(
|
||||
io,
|
||||
allocator,
|
||||
|
|
@ -295,6 +307,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
|||
},
|
||||
color,
|
||||
out,
|
||||
kitty_caps,
|
||||
);
|
||||
},
|
||||
}
|
||||
|
|
@ -541,6 +554,42 @@ pub fn anyImportedOnly(
|
|||
return now_res.source == .imported;
|
||||
}
|
||||
|
||||
/// Render the percentile-band chart (longest horizon, with the actuals
|
||||
/// overlay when present) as kitty graphics at `term_graphics.projection_cols`
|
||||
/// wide and emit it inline. Returns `error.InsufficientData` when bands
|
||||
/// aren't available so the caller can skip the chart - projections has
|
||||
/// no braille fallback. All allocations come from the arena `va`.
|
||||
fn emitBandsKitty(
|
||||
io: std.Io,
|
||||
va: std.mem.Allocator,
|
||||
ctx: *const view.ProjectionContext,
|
||||
caps: term_query.Caps,
|
||||
out: *std.Io.Writer,
|
||||
) !void {
|
||||
const horizons = ctx.config.getHorizons();
|
||||
if (horizons.len == 0) return error.InsufficientData;
|
||||
const bands_ec = ctx.data.bands[horizons.len - 1] orelse return error.InsufficientData;
|
||||
|
||||
// Translate the view-layer overlay (if any) into the chart module's
|
||||
// ActualsPoint shape - same conversion as the PNG export path. The
|
||||
// arena owns the buffer; it lives as long as `overlay_input`.
|
||||
const overlay_input = blk: {
|
||||
const ov = ctx.overlay_actuals orelse break :blk @as(?projection_chart.ActualsOverlay, null);
|
||||
const buf = va.alloc(projection_chart.ActualsPoint, ov.points.len) catch break :blk @as(?projection_chart.ActualsOverlay, null);
|
||||
for (ov.points, 0..) |p, i| buf[i] = .{ .years_from_as_of = p.years_from_as_of, .liquid = p.liquid };
|
||||
break :blk projection_chart.ActualsOverlay{ .points = buf, .today_years = ov.today_years };
|
||||
};
|
||||
|
||||
const cols = term_graphics.projection_cols;
|
||||
const rows = term_graphics.rowsForWidth(cols, caps.cell_w, caps.cell_h);
|
||||
const dims = term_graphics.pixelDims(cols, rows, caps.cell_w, caps.cell_h);
|
||||
|
||||
var rendered = try projection_chart.renderToSurface(io, va, bands_ec, dims.width, dims.height, theme.default_theme, overlay_input, true);
|
||||
defer rendered.deinit(va);
|
||||
const rgb = try rendered.extractRgb(va);
|
||||
try term_graphics.placeInline(out, va, rgb, dims.width, dims.height, cols, rows);
|
||||
}
|
||||
|
||||
pub fn runBands(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
|
|
@ -549,6 +598,7 @@ pub fn runBands(
|
|||
opts: BandsOptions,
|
||||
color: bool,
|
||||
out: *std.Io.Writer,
|
||||
kitty_caps: ?term_query.Caps,
|
||||
) !void {
|
||||
// Single arena for all view/render allocations. Same lifetime
|
||||
// regardless of live vs. as-of path.
|
||||
|
|
@ -709,6 +759,17 @@ pub fn runBands(
|
|||
}
|
||||
try out.print("========================================\n", .{});
|
||||
|
||||
// Headline percentile-band chart, inline via kitty graphics when
|
||||
// supported (or forced). No braille fallback - non-kitty terminals
|
||||
// keep the table-only view below.
|
||||
if (kitty_caps) |kc| {
|
||||
try out.print("\n", .{});
|
||||
emitBandsKitty(io, va, &ctx, kc, out) catch |err| switch (err) {
|
||||
error.InsufficientData => {}, // no bands yet; fall through to the table
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
// If auto-snapped, print a muted note so the user knows the
|
||||
// requested date wasn't an exact hit. The wording reflects the
|
||||
// resolution source - "nearest snapshot" vs "nearest imported
|
||||
|
|
@ -1902,7 +1963,7 @@ test "runBands: imported-only as_of scales today's composition and renders body"
|
|||
.today = today,
|
||||
.overlay_actuals = false,
|
||||
.live = &ld,
|
||||
}, false, &stream);
|
||||
}, false, &stream, null);
|
||||
|
||||
const out = stream.buffered();
|
||||
// Header reflects the imported source, and the caveat explains
|
||||
|
|
@ -1936,7 +1997,7 @@ test "runBands: imported-only as_of without live data returns cleanly" {
|
|||
.today = Date.fromYmd(2026, 3, 13),
|
||||
.overlay_actuals = false,
|
||||
.live = null,
|
||||
}, false, &stream);
|
||||
}, false, &stream, null);
|
||||
|
||||
// The helper printed a clear stderr message (swallowed by
|
||||
// cli.stderrPrint) and returned without body output.
|
||||
|
|
@ -2129,7 +2190,7 @@ test "run: as_of with no snapshots returns without error (stderr-only)" {
|
|||
var stream = std.Io.Writer.fixed(&buf);
|
||||
|
||||
const d = Date.fromYmd(2026, 3, 13);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream, null);
|
||||
|
||||
// No body output because the resolution failed - the stderr
|
||||
// message is swallowed by `cli.stderrPrint` and doesn't land in
|
||||
|
|
@ -2161,7 +2222,7 @@ test "run: as_of with matching snapshot produces body output" {
|
|||
|
||||
var buf: [32_768]u8 = undefined;
|
||||
var stream = std.Io.Writer.fixed(&buf);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = d, .from_snapshot = true, .today = d, .overlay_actuals = false }, false, &stream, null);
|
||||
|
||||
const out = stream.buffered();
|
||||
// Header should call out the as-of date explicitly.
|
||||
|
|
@ -2192,7 +2253,7 @@ test "run: as_of auto-snap surfaces muted 'nearest' note" {
|
|||
var stream = std.Io.Writer.fixed(&buf);
|
||||
|
||||
const requested = Date.fromYmd(2026, 3, 13);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = requested, .from_snapshot = true, .today = requested, .overlay_actuals = false }, false, &stream);
|
||||
try runBands(io, testing.allocator, &svc, pf, .{ .events_enabled = false, .as_of = requested, .from_snapshot = true, .today = requested, .overlay_actuals = false }, false, &stream, null);
|
||||
|
||||
const out = stream.buffered();
|
||||
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue