add theming (chart graphics only)
This commit is contained in:
parent
3fda9eabcf
commit
ec96b60eb4
6 changed files with 102 additions and 43 deletions
|
|
@ -17,10 +17,10 @@
|
||||||
//! The TUI's adaptive chart sizing isn't used here because the
|
//! The TUI's adaptive chart sizing isn't used here because the
|
||||||
//! export target is a file, not a cell grid.
|
//! export target is a file, not a cell grid.
|
||||||
//!
|
//!
|
||||||
//! The module deliberately reuses the TUI's `default_theme` for
|
//! Each `export*` takes the `theme.Theme` to render with - the CLI
|
||||||
//! consistent visual identity across the live TUI surface and
|
//! resolves it from `--theme <PATH>` (a `theme.srf`), defaulting to
|
||||||
//! exported images. A `--theme` override at export time is out of
|
//! the built-in theme so exports keep the TUI's visual identity unless
|
||||||
//! scope for V1 (see TODO follow-up).
|
//! the user opts into a custom or presentation-friendly palette.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const z2d = @import("z2d");
|
const z2d = @import("z2d");
|
||||||
|
|
@ -50,6 +50,7 @@ pub fn exportSymbolChart(
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
candles: []const zfin.Candle,
|
candles: []const zfin.Candle,
|
||||||
display_count: usize,
|
display_count: usize,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var cached = chart.computeIndicatorsWarmup(alloc, candles, display_count, 20) catch |err| switch (err) {
|
var cached = chart.computeIndicatorsWarmup(alloc, candles, display_count, 20) catch |err| switch (err) {
|
||||||
|
|
@ -67,7 +68,7 @@ pub fn exportSymbolChart(
|
||||||
null,
|
null,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
&cached,
|
&cached,
|
||||||
true,
|
true,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
|
|
@ -90,6 +91,7 @@ pub fn exportProjectionChart(
|
||||||
bands: []const projections.YearPercentiles,
|
bands: []const projections.YearPercentiles,
|
||||||
actuals: ?projection_chart.ActualsOverlay,
|
actuals: ?projection_chart.ActualsOverlay,
|
||||||
retirement_boundary_year: ?u16,
|
retirement_boundary_year: ?u16,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var rendered = projection_chart.renderToSurface(
|
var rendered = projection_chart.renderToSurface(
|
||||||
|
|
@ -98,7 +100,7 @@ pub fn exportProjectionChart(
|
||||||
bands,
|
bands,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
actuals,
|
actuals,
|
||||||
true,
|
true,
|
||||||
retirement_boundary_year,
|
retirement_boundary_year,
|
||||||
|
|
@ -119,6 +121,7 @@ pub fn exportTimelineChart(
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
points: []const line_chart.LinePoint,
|
points: []const line_chart.LinePoint,
|
||||||
baseline: line_chart.Baseline,
|
baseline: line_chart.Baseline,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var rendered = line_chart.renderToSurface(
|
var rendered = line_chart.renderToSurface(
|
||||||
|
|
@ -127,7 +130,7 @@ pub fn exportTimelineChart(
|
||||||
points,
|
points,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
.{ .baseline = baseline, .axis_labels = true },
|
.{ .baseline = baseline, .axis_labels = true },
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.InsufficientData => return error.InsufficientData,
|
error.InsufficientData => return error.InsufficientData,
|
||||||
|
|
@ -145,6 +148,7 @@ pub fn exportConvergenceChart(
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
points: []const forecast.ConvergencePoint,
|
points: []const forecast.ConvergencePoint,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var rendered = forecast_chart.renderConvergenceToSurface(
|
var rendered = forecast_chart.renderConvergenceToSurface(
|
||||||
|
|
@ -153,7 +157,7 @@ pub fn exportConvergenceChart(
|
||||||
points,
|
points,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
true,
|
true,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.InsufficientData => return error.InsufficientData,
|
error.InsufficientData => return error.InsufficientData,
|
||||||
|
|
@ -171,6 +175,7 @@ pub fn exportBacktestChart(
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
anchors: []const forecast.BacktestAnchor,
|
anchors: []const forecast.BacktestAnchor,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var rendered = forecast_chart.renderBacktestToSurface(
|
var rendered = forecast_chart.renderBacktestToSurface(
|
||||||
|
|
@ -179,7 +184,7 @@ pub fn exportBacktestChart(
|
||||||
anchors,
|
anchors,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
true,
|
true,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.InsufficientData => return error.InsufficientData,
|
error.InsufficientData => return error.InsufficientData,
|
||||||
|
|
@ -198,6 +203,7 @@ pub fn exportCompareChart(
|
||||||
alloc: std.mem.Allocator,
|
alloc: std.mem.Allocator,
|
||||||
then_bands: []const projections.YearPercentiles,
|
then_bands: []const projections.YearPercentiles,
|
||||||
now_bands: []const projections.YearPercentiles,
|
now_bands: []const projections.YearPercentiles,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
var rendered = compare_chart.renderToSurface(
|
var rendered = compare_chart.renderToSurface(
|
||||||
|
|
@ -207,7 +213,7 @@ pub fn exportCompareChart(
|
||||||
now_bands,
|
now_bands,
|
||||||
default_width,
|
default_width,
|
||||||
default_height,
|
default_height,
|
||||||
theme.default_theme,
|
th,
|
||||||
true,
|
true,
|
||||||
) catch |err| switch (err) {
|
) catch |err| switch (err) {
|
||||||
error.InsufficientData => return error.InsufficientData,
|
error.InsufficientData => return error.InsufficientData,
|
||||||
|
|
@ -248,7 +254,7 @@ test "exportSymbolChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_symbol.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_symbol.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportSymbolChart(io, alloc, &candles, 60, path);
|
try exportSymbolChart(io, alloc, &candles, 60, theme.default_theme, path);
|
||||||
|
|
||||||
// Verify the file exists, starts with the PNG magic, and is
|
// Verify the file exists, starts with the PNG magic, and is
|
||||||
// big enough to plausibly contain a chart (not just headers).
|
// big enough to plausibly contain a chart (not just headers).
|
||||||
|
|
@ -291,7 +297,7 @@ test "exportSymbolChart returns InsufficientData on too-few candles" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportSymbolChart(io, alloc, &candles, 60, path),
|
exportSymbolChart(io, alloc, &candles, 60, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -322,7 +328,7 @@ test "exportProjectionChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_projection.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_projection.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportProjectionChart(io, alloc, &bands, null, null, path);
|
try exportProjectionChart(io, alloc, &bands, null, null, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "test_export_projection.png", .{});
|
var file = try tmp.dir.openFile(io, "test_export_projection.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -358,7 +364,7 @@ test "exportProjectionChart returns InsufficientData with single band" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportProjectionChart(io, alloc, &bands, null, null, path),
|
exportProjectionChart(io, alloc, &bands, null, null, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -383,7 +389,7 @@ test "exportTimelineChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_timeline.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_timeline.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportTimelineChart(io, alloc, &points, .fit, path);
|
try exportTimelineChart(io, alloc, &points, .fit, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "test_export_timeline.png", .{});
|
var file = try tmp.dir.openFile(io, "test_export_timeline.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -413,7 +419,7 @@ test "exportTimelineChart returns InsufficientData with a single point" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportTimelineChart(io, alloc, &points, .fit, path),
|
exportTimelineChart(io, alloc, &points, .fit, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -436,7 +442,7 @@ test "exportConvergenceChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_convergence.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_convergence.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportConvergenceChart(io, alloc, &points, path);
|
try exportConvergenceChart(io, alloc, &points, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "test_export_convergence.png", .{});
|
var file = try tmp.dir.openFile(io, "test_export_convergence.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -468,7 +474,7 @@ test "exportConvergenceChart returns InsufficientData with a single point" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportConvergenceChart(io, alloc, &points, path),
|
exportConvergenceChart(io, alloc, &points, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,7 +497,7 @@ test "exportBacktestChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_backtest.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_backtest.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportBacktestChart(io, alloc, &anchors, path);
|
try exportBacktestChart(io, alloc, &anchors, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "test_export_backtest.png", .{});
|
var file = try tmp.dir.openFile(io, "test_export_backtest.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -523,7 +529,7 @@ test "exportBacktestChart returns InsufficientData with a single anchor" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportBacktestChart(io, alloc, &anchors, path),
|
exportBacktestChart(io, alloc, &anchors, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -548,7 +554,7 @@ test "exportCompareChart writes a non-empty PNG file" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_compare.png" });
|
const path = try std.fs.path.join(alloc, &.{ dir_path, "test_export_compare.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportCompareChart(io, alloc, &then_bands, &now_bands, path);
|
try exportCompareChart(io, alloc, &then_bands, &now_bands, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "test_export_compare.png", .{});
|
var file = try tmp.dir.openFile(io, "test_export_compare.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -579,6 +585,6 @@ test "exportCompareChart returns InsufficientData with a single-year side" {
|
||||||
|
|
||||||
try std.testing.expectError(
|
try std.testing.expectError(
|
||||||
error.InsufficientData,
|
error.InsufficientData,
|
||||||
exportCompareChart(io, alloc, &then_bands, &now_bands, path),
|
exportCompareChart(io, alloc, &then_bands, &now_bands, theme.default_theme, path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ const zfin = @import("../root.zig");
|
||||||
const validator = @import("../comptime_validator.zig");
|
const validator = @import("../comptime_validator.zig");
|
||||||
const chart = @import("../charts/chart.zig");
|
const chart = @import("../charts/chart.zig");
|
||||||
const term_query = @import("../term_query.zig");
|
const term_query = @import("../term_query.zig");
|
||||||
|
const theme = @import("../tui/theme.zig");
|
||||||
|
|
||||||
// ── Group taxonomy ────────────────────────────────────────────
|
// ── Group taxonomy ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -259,6 +260,11 @@ pub const RunCtx = struct {
|
||||||
/// once at invocation entry. Commands consult this together with
|
/// once at invocation entry. Commands consult this together with
|
||||||
/// `globals.chart_config` to choose kitty-graphics vs braille output.
|
/// `globals.chart_config` to choose kitty-graphics vs braille output.
|
||||||
graphics_caps: term_query.Caps = .{},
|
graphics_caps: term_query.Caps = .{},
|
||||||
|
/// Theme for all CLI charts - inline terminal (kitty) charts and
|
||||||
|
/// `--export-chart` PNGs - resolved from the global `--theme <PATH>`
|
||||||
|
/// flag (default: the built-in theme). The TUI loads its own theme
|
||||||
|
/// from `theme.srf` independently.
|
||||||
|
chart_theme: theme.Theme = theme.default_theme,
|
||||||
|
|
||||||
/// Resolve the portfolio pattern(s) (from `-p`/`--portfolio` or
|
/// Resolve the portfolio pattern(s) (from `-p`/`--portfolio` or
|
||||||
/// the default `portfolio*.srf` pattern) through cwd -> ZFIN_HOME.
|
/// the default `portfolio*.srf` pattern) through cwd -> ZFIN_HOME.
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,11 @@ pub const PortfolioOpts = struct {
|
||||||
/// minimum if the series itself dips negative). Only consulted when
|
/// minimum if the series itself dips negative). Only consulted when
|
||||||
/// `--export-chart` is given.
|
/// `--export-chart` is given.
|
||||||
baseline: line_chart.Baseline = .fit,
|
baseline: line_chart.Baseline = .fit,
|
||||||
|
/// Theme for the chart - both the inline kitty image and the
|
||||||
|
/// `--export-chart` PNG. Comes from the global `--theme <PATH>`
|
||||||
|
/// flag; injected by `run` from `RunCtx.chart_theme`. Defaults to
|
||||||
|
/// the built-in theme.
|
||||||
|
chart_theme: theme.Theme = theme.default_theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parse the arg list for portfolio-mode flags. Pure function - no IO.
|
/// Parse the arg list for portfolio-mode flags. Pure function - no IO.
|
||||||
|
|
@ -224,9 +229,11 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
.symbol => |sym| try runSymbol(ctx.io, svc, sym, ctx.today, ctx.color, ctx.out, fetch_opts),
|
.symbol => |sym| try runSymbol(ctx.io, svc, sym, ctx.today, ctx.color, ctx.out, fetch_opts),
|
||||||
.portfolio => |opts| {
|
.portfolio => |opts| {
|
||||||
|
var o = opts;
|
||||||
|
o.chart_theme = ctx.chart_theme;
|
||||||
const pf = ctx.resolvePortfolioPath();
|
const pf = ctx.resolvePortfolioPath();
|
||||||
defer pf.deinit(ctx.allocator);
|
defer pf.deinit(ctx.allocator);
|
||||||
try runPortfolio(ctx.io, ctx.allocator, pf.path, opts, ctx.color, ctx.out, ctx.globals.chart_config, ctx.graphics_caps);
|
try runPortfolio(ctx.io, ctx.allocator, pf.path, o, ctx.color, ctx.out, ctx.globals.chart_config, ctx.graphics_caps);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -328,7 +335,7 @@ fn runPortfolio(
|
||||||
// as a PNG (honoring --since / --until / --metric / --baseline) and
|
// as a PNG (honoring --since / --until / --metric / --baseline) and
|
||||||
// exit without printing the normal timeline output.
|
// exit without printing the normal timeline output.
|
||||||
if (opts.export_chart) |path| {
|
if (opts.export_chart) |path| {
|
||||||
exportMetricChart(io, allocator, filtered, opts.metric, opts.baseline, path) catch |err| switch (err) {
|
exportMetricChart(io, allocator, filtered, opts.metric, opts.baseline, opts.chart_theme, path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(io, "Error: need at least 2 snapshots in the selected range to render a chart.\n");
|
cli.stderrPrint(io, "Error: need at least 2 snapshots in the selected range to render a chart.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -360,7 +367,7 @@ fn runPortfolio(
|
||||||
// Resolve how to draw the inline chart: kitty graphics when the
|
// Resolve how to draw the inline chart: kitty graphics when the
|
||||||
// terminal supports it (or it's forced via `--chart <WxH>`), else
|
// terminal supports it (or it's forced via `--chart <WxH>`), else
|
||||||
// braille. `--chart braille` always forces braille.
|
// braille. `--chart braille` always forces braille.
|
||||||
const k: KittyChart = .{ .io = io, .caps = caps, .baseline = opts.baseline };
|
const k: KittyChart = .{ .io = io, .caps = caps, .baseline = opts.baseline, .theme = opts.chart_theme };
|
||||||
const chart_render: ChartRender = switch (chart_config.mode) {
|
const chart_render: ChartRender = switch (chart_config.mode) {
|
||||||
.braille => .braille,
|
.braille => .braille,
|
||||||
.kitty => .{ .kitty = k },
|
.kitty => .{ .kitty = k },
|
||||||
|
|
@ -549,13 +556,14 @@ fn exportMetricChart(
|
||||||
points: []const timeline.TimelinePoint,
|
points: []const timeline.TimelinePoint,
|
||||||
metric: timeline.Metric,
|
metric: timeline.Metric,
|
||||||
baseline: line_chart.Baseline,
|
baseline: line_chart.Baseline,
|
||||||
|
th: theme.Theme,
|
||||||
path: []const u8,
|
path: []const u8,
|
||||||
) !void {
|
) !void {
|
||||||
const series = try timeline.extractChartSeries(allocator, points, metric);
|
const series = try timeline.extractChartSeries(allocator, points, metric);
|
||||||
defer allocator.free(series);
|
defer allocator.free(series);
|
||||||
const lps = try metricLinePoints(allocator, series);
|
const lps = try metricLinePoints(allocator, series);
|
||||||
defer allocator.free(lps);
|
defer allocator.free(lps);
|
||||||
try chart_export.exportTimelineChart(io, allocator, lps, baseline, path);
|
try chart_export.exportTimelineChart(io, allocator, lps, baseline, th, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How `renderPortfolio` draws the timeline chart.
|
/// How `renderPortfolio` draws the timeline chart.
|
||||||
|
|
@ -571,6 +579,7 @@ const KittyChart = struct {
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
caps: term_query.Caps,
|
caps: term_query.Caps,
|
||||||
baseline: line_chart.Baseline,
|
baseline: line_chart.Baseline,
|
||||||
|
theme: theme.Theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Draw the focused-metric timeline. Dispatches to inline kitty graphics
|
/// Draw the focused-metric timeline. Dispatches to inline kitty graphics
|
||||||
|
|
@ -614,7 +623,7 @@ fn emitTimelineKitty(
|
||||||
const rows = term_graphics.rowsForWidth(cols, k.caps.cell_w, k.caps.cell_h);
|
const rows = term_graphics.rowsForWidth(cols, k.caps.cell_w, k.caps.cell_h);
|
||||||
const dims = term_graphics.pixelDims(cols, rows, k.caps.cell_w, k.caps.cell_h);
|
const dims = term_graphics.pixelDims(cols, rows, k.caps.cell_w, k.caps.cell_h);
|
||||||
|
|
||||||
var rendered = try line_chart.renderToSurface(k.io, allocator, lps, dims.width, dims.height, theme.default_theme, .{ .baseline = k.baseline, .axis_labels = true });
|
var rendered = try line_chart.renderToSurface(k.io, allocator, lps, dims.width, dims.height, k.theme, .{ .baseline = k.baseline, .axis_labels = true });
|
||||||
defer rendered.deinit(allocator);
|
defer rendered.deinit(allocator);
|
||||||
const rgb = try rendered.extractRgb(allocator);
|
const rgb = try rendered.extractRgb(allocator);
|
||||||
defer allocator.free(rgb);
|
defer allocator.free(rgb);
|
||||||
|
|
@ -1167,7 +1176,7 @@ test "exportMetricChart writes a PNG for a multi-point timeline" {
|
||||||
const path = try std.fs.path.join(alloc, &.{ path_buf[0..dir_len], "history_timeline.png" });
|
const path = try std.fs.path.join(alloc, &.{ path_buf[0..dir_len], "history_timeline.png" });
|
||||||
defer alloc.free(path);
|
defer alloc.free(path);
|
||||||
|
|
||||||
try exportMetricChart(io, alloc, &pts, .liquid, .fit, path);
|
try exportMetricChart(io, alloc, &pts, .liquid, .fit, theme.default_theme, path);
|
||||||
|
|
||||||
var file = try tmp.dir.openFile(io, "history_timeline.png", .{});
|
var file = try tmp.dir.openFile(io, "history_timeline.png", .{});
|
||||||
defer file.close(io);
|
defer file.close(io);
|
||||||
|
|
@ -1188,7 +1197,7 @@ test "exportMetricChart returns InsufficientData with fewer than 2 points" {
|
||||||
const pts = [_]timeline.TimelinePoint{
|
const pts = [_]timeline.TimelinePoint{
|
||||||
makeTimelinePoint(2026, 1, 1, 1_000_000, 200_000, 1_200_000),
|
makeTimelinePoint(2026, 1, 1, 1_000_000, 200_000, 1_200_000),
|
||||||
};
|
};
|
||||||
try testing.expectError(error.InsufficientData, exportMetricChart(io, alloc, &pts, .liquid, .fit, "unused.png"));
|
try testing.expectError(error.InsufficientData, exportMetricChart(io, alloc, &pts, .liquid, .fit, theme.default_theme, "unused.png"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── rebuildRollup ────────────────────────────────────────────
|
// ── rebuildRollup ────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -260,8 +260,8 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
.convergence => |args| try runConvergence(io, allocator, file_path, args.export_chart, color, out, kitty_caps),
|
.convergence => |args| try runConvergence(io, allocator, file_path, args.export_chart, color, out, kitty_caps, ctx.chart_theme),
|
||||||
.return_backtest => |args| try runReturnBacktest(io, allocator, file_path, args.real, args.export_chart, color, out, kitty_caps),
|
.return_backtest => |args| try runReturnBacktest(io, allocator, file_path, args.real, args.export_chart, color, out, kitty_caps, ctx.chart_theme),
|
||||||
.compare => |args| {
|
.compare => |args| {
|
||||||
_ = ctx.svc orelse return error.MissingDataService;
|
_ = ctx.svc orelse return error.MissingDataService;
|
||||||
// Pre-load today's live composition only when it's
|
// Pre-load today's live composition only when it's
|
||||||
|
|
@ -318,6 +318,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
.overlay_actuals = args.overlay_actuals,
|
.overlay_actuals = args.overlay_actuals,
|
||||||
.export_chart = args.export_chart,
|
.export_chart = args.export_chart,
|
||||||
.live = if (live) |*l| l else null,
|
.live = if (live) |*l| l else null,
|
||||||
|
.chart_theme = ctx.chart_theme,
|
||||||
},
|
},
|
||||||
color,
|
color,
|
||||||
out,
|
out,
|
||||||
|
|
@ -465,6 +466,10 @@ pub const BandsOptions = struct {
|
||||||
/// Snapshot-only as-of paths ignore this field. See
|
/// Snapshot-only as-of paths ignore this field. See
|
||||||
/// `LiveData` for the rationale.
|
/// `LiveData` for the rationale.
|
||||||
live: ?*const LiveData = null,
|
live: ?*const LiveData = null,
|
||||||
|
/// Theme for the `--export-chart` PNG (resolved from `--theme`).
|
||||||
|
/// Defaults to the built-in theme; the inline kitty chart always
|
||||||
|
/// uses the default.
|
||||||
|
chart_theme: theme.Theme = theme.default_theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Build a `ProjectionContext` for an already-resolved as-of date,
|
/// Build a `ProjectionContext` for an already-resolved as-of date,
|
||||||
|
|
@ -622,6 +627,7 @@ fn emitBandsKitty(
|
||||||
va: std.mem.Allocator,
|
va: std.mem.Allocator,
|
||||||
ctx: *const view.ProjectionContext,
|
ctx: *const view.ProjectionContext,
|
||||||
caps: term_query.Caps,
|
caps: term_query.Caps,
|
||||||
|
th: theme.Theme,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
) !void {
|
) !void {
|
||||||
const horizons = ctx.config.getHorizons();
|
const horizons = ctx.config.getHorizons();
|
||||||
|
|
@ -633,7 +639,7 @@ fn emitBandsKitty(
|
||||||
const oc = prepOverlayChart(va, ctx, bands_ec);
|
const oc = prepOverlayChart(va, ctx, bands_ec);
|
||||||
|
|
||||||
const d = projectionChartDims(caps);
|
const d = projectionChartDims(caps);
|
||||||
var rendered = try projection_chart.renderToSurface(io, va, oc.bands, d.width, d.height, theme.default_theme, oc.overlay, true, ctx.retirement.boundaryYear());
|
var rendered = try projection_chart.renderToSurface(io, va, oc.bands, d.width, d.height, th, oc.overlay, true, ctx.retirement.boundaryYear());
|
||||||
defer rendered.deinit(va);
|
defer rendered.deinit(va);
|
||||||
const rgb = try rendered.extractRgb(va);
|
const rgb = try rendered.extractRgb(va);
|
||||||
try term_graphics.placeInline(out, va, rgb, d.width, d.height, d.cols, d.rows);
|
try term_graphics.placeInline(out, va, rgb, d.width, d.height, d.cols, d.rows);
|
||||||
|
|
@ -765,7 +771,7 @@ pub fn runBands(
|
||||||
// overlay window) the same way the inline-kitty path does.
|
// overlay window) the same way the inline-kitty path does.
|
||||||
const oc = prepOverlayChart(va, &ctx, bands_ec);
|
const oc = prepOverlayChart(va, &ctx, bands_ec);
|
||||||
|
|
||||||
chart_export.exportProjectionChart(io, allocator, oc.bands, oc.overlay, ctx.retirement.boundaryYear(), export_path) catch |err| switch (err) {
|
chart_export.exportProjectionChart(io, allocator, oc.bands, oc.overlay, ctx.retirement.boundaryYear(), opts.chart_theme, export_path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(io, "Error: not enough projection data to render a chart.\n");
|
cli.stderrPrint(io, "Error: not enough projection data to render a chart.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -799,7 +805,7 @@ pub fn runBands(
|
||||||
// keep the table-only view below.
|
// keep the table-only view below.
|
||||||
if (kitty_caps) |kc| {
|
if (kitty_caps) |kc| {
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
emitBandsKitty(io, va, &ctx, kc, out) catch |err| switch (err) {
|
emitBandsKitty(io, va, &ctx, kc, opts.chart_theme, out) catch |err| switch (err) {
|
||||||
error.InsufficientData => {}, // no bands yet; fall through to the table
|
error.InsufficientData => {}, // no bands yet; fall through to the table
|
||||||
else => return err,
|
else => return err,
|
||||||
};
|
};
|
||||||
|
|
@ -1155,7 +1161,7 @@ pub fn runCompare(
|
||||||
cli.stderrPrint(io, "Error: projection bands unavailable for one side; cannot export comparison chart.\n");
|
cli.stderrPrint(io, "Error: projection bands unavailable for one side; cannot export comparison chart.\n");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
chart_export.exportCompareChart(io, allocator, result.then_bands.?, result.now_bands.?, export_path) catch |err| switch (err) {
|
chart_export.exportCompareChart(io, allocator, result.then_bands.?, result.now_bands.?, ctx.chart_theme, export_path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(io, "Error: not enough projection data to render a comparison chart.\n");
|
cli.stderrPrint(io, "Error: not enough projection data to render a comparison chart.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1213,7 +1219,7 @@ pub fn runCompare(
|
||||||
if (kitty_caps) |kc| {
|
if (kitty_caps) |kc| {
|
||||||
if (result.then_bands != null and result.now_bands != null) {
|
if (result.then_bands != null and result.now_bands != null) {
|
||||||
const d = projectionChartDims(kc);
|
const d = projectionChartDims(kc);
|
||||||
if (compare_chart.renderCompareChart(io, va, result.then_bands.?, result.now_bands.?, d.width, d.height, theme.default_theme)) |cres| {
|
if (compare_chart.renderCompareChart(io, va, result.then_bands.?, result.now_bands.?, d.width, d.height, ctx.chart_theme)) |cres| {
|
||||||
try term_graphics.placeInline(out, va, cres.rgb_data, d.width, d.height, d.cols, d.rows);
|
try term_graphics.placeInline(out, va, cres.rgb_data, d.width, d.height, d.cols, d.rows);
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
|
|
@ -1261,6 +1267,7 @@ pub fn runConvergence(
|
||||||
color: bool,
|
color: bool,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
kitty_caps: ?term_query.Caps,
|
kitty_caps: ?term_query.Caps,
|
||||||
|
chart_theme: theme.Theme,
|
||||||
) !void {
|
) !void {
|
||||||
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
||||||
defer arena_state.deinit();
|
defer arena_state.deinit();
|
||||||
|
|
@ -1280,7 +1287,7 @@ pub fn runConvergence(
|
||||||
// --export-chart: render the convergence chart to a PNG and exit
|
// --export-chart: render the convergence chart to a PNG and exit
|
||||||
// before any text output.
|
// before any text output.
|
||||||
if (export_chart) |export_path| {
|
if (export_chart) |export_path| {
|
||||||
chart_export.exportConvergenceChart(io, allocator, points, export_path) catch |err| switch (err) {
|
chart_export.exportConvergenceChart(io, allocator, points, chart_theme, export_path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(io, "Error: not enough convergence data to render a chart.\n");
|
cli.stderrPrint(io, "Error: not enough convergence data to render a chart.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1298,7 +1305,7 @@ pub fn runConvergence(
|
||||||
// skips the chart and still renders the table below.
|
// skips the chart and still renders the table below.
|
||||||
if (kitty_caps) |kc| {
|
if (kitty_caps) |kc| {
|
||||||
const d = projectionChartDims(kc);
|
const d = projectionChartDims(kc);
|
||||||
if (forecast_chart.renderConvergenceChart(io, va, points, d.width, d.height, theme.default_theme)) |result| {
|
if (forecast_chart.renderConvergenceChart(io, va, points, d.width, d.height, chart_theme)) |result| {
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try term_graphics.placeInline(out, va, result.rgb_data, d.width, d.height, d.cols, d.rows);
|
try term_graphics.placeInline(out, va, result.rgb_data, d.width, d.height, d.cols, d.rows);
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
|
|
@ -1332,6 +1339,7 @@ pub fn runReturnBacktest(
|
||||||
color: bool,
|
color: bool,
|
||||||
out: *std.Io.Writer,
|
out: *std.Io.Writer,
|
||||||
kitty_caps: ?term_query.Caps,
|
kitty_caps: ?term_query.Caps,
|
||||||
|
chart_theme: theme.Theme,
|
||||||
) !void {
|
) !void {
|
||||||
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
var arena_state = std.heap.ArenaAllocator.init(allocator);
|
||||||
defer arena_state.deinit();
|
defer arena_state.deinit();
|
||||||
|
|
@ -1359,7 +1367,7 @@ pub fn runReturnBacktest(
|
||||||
|
|
||||||
// --export-chart: render the back-test chart to a PNG and exit.
|
// --export-chart: render the back-test chart to a PNG and exit.
|
||||||
if (export_chart) |export_path| {
|
if (export_chart) |export_path| {
|
||||||
chart_export.exportBacktestChart(io, allocator, anchors, export_path) catch |err| switch (err) {
|
chart_export.exportBacktestChart(io, allocator, anchors, chart_theme, export_path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(io, "Error: not enough back-test data to render a chart.\n");
|
cli.stderrPrint(io, "Error: not enough back-test data to render a chart.\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -1376,7 +1384,7 @@ pub fn runReturnBacktest(
|
||||||
// runConvergence). Too few anchors skips the chart; table still renders.
|
// runConvergence). Too few anchors skips the chart; table still renders.
|
||||||
if (kitty_caps) |kc| {
|
if (kitty_caps) |kc| {
|
||||||
const d = projectionChartDims(kc);
|
const d = projectionChartDims(kc);
|
||||||
if (forecast_chart.renderBacktestChart(io, va, anchors, d.width, d.height, theme.default_theme)) |result| {
|
if (forecast_chart.renderBacktestChart(io, va, anchors, d.width, d.height, chart_theme)) |result| {
|
||||||
try out.print("\n", .{});
|
try out.print("\n", .{});
|
||||||
try term_graphics.placeInline(out, va, result.rgb_data, d.width, d.height, d.cols, d.rows);
|
try term_graphics.placeInline(out, va, result.rgb_data, d.width, d.height, d.cols, d.rows);
|
||||||
} else |err| switch (err) {
|
} else |err| switch (err) {
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
|
|
||||||
// PNG export short-circuits all text rendering.
|
// PNG export short-circuits all text rendering.
|
||||||
if (parsed.export_chart) |path| {
|
if (parsed.export_chart) |path| {
|
||||||
chart_export.exportSymbolChart(ctx.io, ctx.allocator, candles, display_count, path) catch |err| switch (err) {
|
chart_export.exportSymbolChart(ctx.io, ctx.allocator, candles, display_count, ctx.chart_theme, path) catch |err| switch (err) {
|
||||||
error.InsufficientData => {
|
error.InsufficientData => {
|
||||||
cli.stderrPrint(ctx.io, "Error: not enough candle history to render a chart (need >= 20 candles).\n");
|
cli.stderrPrint(ctx.io, "Error: not enough candle history to render a chart (need >= 20 candles).\n");
|
||||||
return;
|
return;
|
||||||
|
|
@ -214,7 +214,7 @@ pub fn run(ctx: *framework.RunCtx, parsed: ParsedArgs) !void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const k: KittyChart = .{ .io = ctx.io, .caps = ctx.graphics_caps };
|
const k: KittyChart = .{ .io = ctx.io, .caps = ctx.graphics_caps, .theme = ctx.chart_theme };
|
||||||
const chart_render: ChartRender = switch (ctx.globals.chart_config.mode) {
|
const chart_render: ChartRender = switch (ctx.globals.chart_config.mode) {
|
||||||
.braille => .braille,
|
.braille => .braille,
|
||||||
.kitty => .{ .kitty = k },
|
.kitty => .{ .kitty = k },
|
||||||
|
|
@ -282,6 +282,7 @@ const ChartRender = union(enum) {
|
||||||
const KittyChart = struct {
|
const KittyChart = struct {
|
||||||
io: std.Io,
|
io: std.Io,
|
||||||
caps: term_query.Caps,
|
caps: term_query.Caps,
|
||||||
|
theme: theme.Theme,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Braille price chart of the most recent `display_count` candles (the
|
/// Braille price chart of the most recent `display_count` candles (the
|
||||||
|
|
@ -309,7 +310,7 @@ fn emitQuoteKitty(allocator: std.mem.Allocator, out: *std.Io.Writer, candles: []
|
||||||
const cols = term_graphics.quote_cols;
|
const cols = term_graphics.quote_cols;
|
||||||
const rows = term_graphics.rowsForWidth(cols, k.caps.cell_w, k.caps.cell_h);
|
const rows = term_graphics.rowsForWidth(cols, k.caps.cell_w, k.caps.cell_h);
|
||||||
const dims = term_graphics.pixelDims(cols, rows, k.caps.cell_w, k.caps.cell_h);
|
const dims = term_graphics.pixelDims(cols, rows, k.caps.cell_w, k.caps.cell_h);
|
||||||
var rendered = try tui_chart.renderToSurface(k.io, allocator, display_data, null, dims.width, dims.height, theme.default_theme, &cached, true);
|
var rendered = try tui_chart.renderToSurface(k.io, allocator, display_data, null, dims.width, dims.height, k.theme, &cached, true);
|
||||||
defer rendered.deinit(allocator);
|
defer rendered.deinit(allocator);
|
||||||
const rgb = try rendered.extractRgb(allocator);
|
const rgb = try rendered.extractRgb(allocator);
|
||||||
defer allocator.free(rgb);
|
defer allocator.free(rgb);
|
||||||
|
|
|
||||||
29
src/main.zig
29
src/main.zig
|
|
@ -4,6 +4,7 @@ const tui = @import("tui.zig");
|
||||||
const cli = @import("commands/common.zig");
|
const cli = @import("commands/common.zig");
|
||||||
const cmd_framework = @import("commands/framework.zig");
|
const cmd_framework = @import("commands/framework.zig");
|
||||||
const chart = @import("charts/chart.zig");
|
const chart = @import("charts/chart.zig");
|
||||||
|
const theme = @import("tui/theme.zig");
|
||||||
const term_query = @import("term_query.zig");
|
const term_query = @import("term_query.zig");
|
||||||
|
|
||||||
/// Comptime registry of CLI commands. Field name is the user-facing
|
/// Comptime registry of CLI commands. Field name is the user-facing
|
||||||
|
|
@ -134,6 +135,11 @@ const interactive_help =
|
||||||
\\ (e.g. 80x24); `auto` picks Kitty graphics
|
\\ (e.g. 80x24); `auto` picks Kitty graphics
|
||||||
\\ if the terminal supports it, otherwise
|
\\ if the terminal supports it, otherwise
|
||||||
\\ braille
|
\\ braille
|
||||||
|
\\ --theme <PATH> Theme file (a `theme.srf`) applied to all
|
||||||
|
\\ charts - inline terminal charts and
|
||||||
|
\\ `--export-chart` PNGs. Default: built-in
|
||||||
|
\\ theme. Generate one with
|
||||||
|
\\ `zfin interactive --default-theme`.
|
||||||
\\ --default-keys Print default keybindings as a `keys.srf`
|
\\ --default-keys Print default keybindings as a `keys.srf`
|
||||||
\\ template and exit (no TUI launched).
|
\\ template and exit (no TUI launched).
|
||||||
\\ Pipe to `~/.config/zfin/keys.srf` to
|
\\ Pipe to `~/.config/zfin/keys.srf` to
|
||||||
|
|
@ -166,6 +172,10 @@ const Globals = struct {
|
||||||
refresh_policy: cmd_framework.RefreshPolicy = .auto,
|
refresh_policy: cmd_framework.RefreshPolicy = .auto,
|
||||||
/// Chart graphics mode from `--chart` (auto / braille / WxH).
|
/// Chart graphics mode from `--chart` (auto / braille / WxH).
|
||||||
chart_config: chart.ChartConfig = .{},
|
chart_config: chart.ChartConfig = .{},
|
||||||
|
/// Theme file from `--theme <PATH>` (a `theme.srf`), applied to all
|
||||||
|
/// charts (inline terminal charts and `--export-chart` PNGs). Null =
|
||||||
|
/// the built-in default theme.
|
||||||
|
theme_path: ?[]const u8 = null,
|
||||||
/// Index into args of the first post-global token (the subcommand).
|
/// Index into args of the first post-global token (the subcommand).
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
};
|
};
|
||||||
|
|
@ -299,6 +309,12 @@ fn parseGlobals(allocator: std.mem.Allocator, args: []const []const u8) GlobalPa
|
||||||
i += 2;
|
i += 2;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (std.mem.eql(u8, a, "--theme")) {
|
||||||
|
if (i + 1 >= args.len) return error.MissingValue;
|
||||||
|
g.theme_path = args[i + 1];
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Help flags are subcommand-like tokens, stop scanning.
|
// Help flags are subcommand-like tokens, stop scanning.
|
||||||
if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break;
|
if (std.mem.eql(u8, a, "--help") or std.mem.eql(u8, a, "-h")) break;
|
||||||
|
|
||||||
|
|
@ -309,6 +325,18 @@ fn parseGlobals(allocator: std.mem.Allocator, args: []const []const u8) GlobalPa
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the `--theme <PATH>` flag into a `theme.Theme` for all chart
|
||||||
|
/// rendering (inline terminal charts and `--export-chart` PNGs). A null
|
||||||
|
/// path uses the built-in default; a non-null path that fails to load
|
||||||
|
/// warns and falls back to the default rather than aborting the command.
|
||||||
|
fn resolveChartTheme(io: std.Io, allocator: std.mem.Allocator, path: ?[]const u8) theme.Theme {
|
||||||
|
const p = path orelse return theme.default_theme;
|
||||||
|
return theme.loadFromFile(io, allocator, p) orelse blk: {
|
||||||
|
cli.stderrPrint(io, "Note: could not load --theme file; using the default theme.\n");
|
||||||
|
break :blk theme.default_theme;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn main(init: std.process.Init) !u8 {
|
pub fn main(init: std.process.Init) !u8 {
|
||||||
return runCli(init) catch |err| switch (err) {
|
return runCli(init) catch |err| switch (err) {
|
||||||
// Downstream pipe closed (e.g., `zfin earnings AAPL | head`). Zig's
|
// Downstream pipe closed (e.g., `zfin earnings AAPL | head`). Zig's
|
||||||
|
|
@ -522,6 +550,7 @@ fn runCli(init: std.process.Init) !u8 {
|
||||||
.color = color,
|
.color = color,
|
||||||
.out = out,
|
.out = out,
|
||||||
.graphics_caps = term_query.detect(io, init.environ_map),
|
.graphics_caps = term_query.detect(io, init.environ_map),
|
||||||
|
.chart_theme = resolveChartTheme(io, allocator, globals.theme_path),
|
||||||
};
|
};
|
||||||
const dispatched_args = if (comptime Module.meta.uppercase_first_arg)
|
const dispatched_args = if (comptime Module.meta.uppercase_first_arg)
|
||||||
try cmd_framework.normalizeFirstArg(allocator, cmd_args)
|
try cmd_framework.normalizeFirstArg(allocator, cmd_args)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue