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 milestones = @import("../analytics/milestones.zig");
|
||||||
const shiller = @import("../data/shiller.zig");
|
const shiller = @import("../data/shiller.zig");
|
||||||
const chart_export = @import("../chart_export.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-
|
/// Tagged-union args for the four projection sub-modes. Mutually-
|
||||||
/// exclusive flag combos (--convergence with --vs, --real with
|
/// 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.
|
// total. Snapshot-only as-of paths ignore it.
|
||||||
var live = try loadLiveData(ctx, today, color);
|
var live = try loadLiveData(ctx, today, color);
|
||||||
defer if (live) |*l| l.deinit(allocator);
|
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(
|
try runBands(
|
||||||
io,
|
io,
|
||||||
allocator,
|
allocator,
|
||||||
|
|
@ -295,6 +307,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
},
|
},
|
||||||
color,
|
color,
|
||||||
out,
|
out,
|
||||||
|
kitty_caps,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -541,6 +554,42 @@ pub fn anyImportedOnly(
|
||||||
return now_res.source == .imported;
|
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(
|
pub fn runBands(
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
|
|
@ -549,6 +598,7 @@ pub fn runBands(
|
||||||
opts: BandsOptions,
|
opts: BandsOptions,
|
||||||
color: bool,
|
color: bool,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
|
kitty_caps: ?term_query.Caps,
|
||||||
) !void {
|
) !void {
|
||||||
// Single arena for all view/render allocations. Same lifetime
|
// Single arena for all view/render allocations. Same lifetime
|
||||||
// regardless of live vs. as-of path.
|
// regardless of live vs. as-of path.
|
||||||
|
|
@ -709,6 +759,17 @@ pub fn runBands(
|
||||||
}
|
}
|
||||||
try out.print("========================================\n", .{});
|
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
|
// If auto-snapped, print a muted note so the user knows the
|
||||||
// requested date wasn't an exact hit. The wording reflects the
|
// requested date wasn't an exact hit. The wording reflects the
|
||||||
// resolution source - "nearest snapshot" vs "nearest imported
|
// 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,
|
.today = today,
|
||||||
.overlay_actuals = false,
|
.overlay_actuals = false,
|
||||||
.live = &ld,
|
.live = &ld,
|
||||||
}, false, &stream);
|
}, false, &stream, null);
|
||||||
|
|
||||||
const out = stream.buffered();
|
const out = stream.buffered();
|
||||||
// Header reflects the imported source, and the caveat explains
|
// 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),
|
.today = Date.fromYmd(2026, 3, 13),
|
||||||
.overlay_actuals = false,
|
.overlay_actuals = false,
|
||||||
.live = null,
|
.live = null,
|
||||||
}, false, &stream);
|
}, false, &stream, null);
|
||||||
|
|
||||||
// The helper printed a clear stderr message (swallowed by
|
// The helper printed a clear stderr message (swallowed by
|
||||||
// cli.stderrPrint) and returned without body output.
|
// 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);
|
var stream = std.Io.Writer.fixed(&buf);
|
||||||
|
|
||||||
const d = Date.fromYmd(2026, 3, 13);
|
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
|
// No body output because the resolution failed - the stderr
|
||||||
// message is swallowed by `cli.stderrPrint` and doesn't land in
|
// 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 buf: [32_768]u8 = undefined;
|
||||||
var stream = std.Io.Writer.fixed(&buf);
|
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();
|
const out = stream.buffered();
|
||||||
// Header should call out the as-of date explicitly.
|
// 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);
|
var stream = std.Io.Writer.fixed(&buf);
|
||||||
|
|
||||||
const requested = Date.fromYmd(2026, 3, 13);
|
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();
|
const out = stream.buffered();
|
||||||
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
try testing.expect(std.mem.indexOf(u8, out, "as of 2026-03-12") != null);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue