global theme flag/theme resolution themes all the things
All checks were successful
Generic zig build / build (push) Successful in 4m45s
Generic zig build / publish-macos (push) Successful in 11s
Generic zig build / deploy (push) Successful in 17s

This commit is contained in:
Emil Lerch 2026-06-27 10:19:57 -07:00
parent ef7f8c2bd1
commit d59555bf92
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 42 additions and 39 deletions

View file

@ -261,9 +261,9 @@ pub const RunCtx = struct {
/// `globals.chart_config` to choose kitty-graphics vs braille output.
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.
/// `--export-chart` PNGs - resolved once from `--theme <PATH>` (or
/// `~/.config/zfin/theme.srf`, else the built-in default). The same
/// resolved theme also drives the CLI text palette and the TUI.
chart_theme: theme.Theme = theme.default_theme,
/// Resolve the portfolio pattern(s) (from `-p`/`--portfolio` or

View file

@ -135,10 +135,10 @@ const interactive_help =
\\ (e.g. 80x24); `auto` picks Kitty graphics
\\ if the terminal supports it, otherwise
\\ 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
\\ --theme <PATH> Theme file (a `theme.srf`) that skins the
\\ whole app: CLI text, charts, and the TUI.
\\ Falls back to ~/.config/zfin/theme.srf,
\\ then the built-in theme. Generate one with
\\ `zfin interactive --default-theme`.
\\ --default-keys Print default keybindings as a `keys.srf`
\\ template and exit (no TUI launched).
@ -172,9 +172,9 @@ const Globals = struct {
refresh_policy: cmd_framework.RefreshPolicy = .auto,
/// Chart graphics mode from `--chart` (auto / braille / WxH).
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 file from `--theme <PATH>` (a `theme.srf`) that skins the
/// whole app - CLI text palette, charts, and the TUI. Null falls
/// back to ~/.config/zfin/theme.srf, then the built-in default.
theme_path: ?[]const u8 = null,
/// Index into args of the first post-global token (the subcommand).
cursor: usize,
@ -325,16 +325,28 @@ fn parseGlobals(allocator: std.mem.Allocator, args: []const []const u8) GlobalPa
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;
};
/// Resolve the app-wide theme once. Precedence: an explicit
/// `--theme <PATH>` (warns + falls back to default if it can't load),
/// then `$HOME/.config/zfin/theme.srf` when present, then the built-in
/// default. The single resolved value skins everything identically -
/// CLI text palette, CLI charts, and the TUI.
fn resolveTheme(
io: std.Io,
allocator: std.mem.Allocator,
environ_map: *const std.process.Environ.Map,
theme_path: ?[]const u8,
) theme.Theme {
if (theme_path) |p| {
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;
};
}
const home = environ_map.get("HOME") orelse return theme.default_theme;
const cfg_path = std.fs.path.join(allocator, &.{ home, ".config", "zfin", "theme.srf" }) catch
return theme.default_theme;
defer allocator.free(cfg_path);
return theme.loadFromFile(io, allocator, cfg_path) orelse theme.default_theme;
}
pub fn main(init: std.process.Init) !u8 {
@ -458,6 +470,13 @@ fn runCli(init: std.process.Init) !u8 {
const color = @import("format.zig").shouldUseColor(io, init.environ_map, globals.no_color);
// Resolve the theme once for the entire invocation. This one value
// skins the CLI text palette (via cli.applyTheme), CLI charts (via
// RunCtx.chart_theme), and the TUI (passed into tui.run) - so
// `--theme <PATH>` (or ~/.config/zfin/theme.srf) repaints everything.
const resolved_theme = resolveTheme(io, gpa_alloc, init.environ_map, globals.theme_path);
cli.applyTheme(resolved_theme);
const command = args[globals.cursor];
const cmd_args: []const []const u8 = @ptrCast(args[globals.cursor + 1 ..]);
@ -482,7 +501,7 @@ fn runCli(init: std.process.Init) !u8 {
// loader resolve + union-merge the same way the CLI does.
// This is the load-bearing fix for "CLI and TUI report
// different totals" - there's exactly one code path now.
tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, cmd_args, today) catch |err| switch (err) {
tui.run(io, gpa_alloc, tui_config, globals.portfolio_patterns, globals.watchlist_path, resolved_theme, cmd_args, today) catch |err| switch (err) {
// tui.run already printed an actionable stderr message
// for invalid CLI args; surface as exit 1 without a
// panic / stack trace.
@ -514,14 +533,6 @@ fn runCli(init: std.process.Init) !u8 {
var svc = zfin.DataService.init(io, allocator, config);
defer svc.deinit();
// Resolve the theme once for the whole invocation. It drives BOTH
// the CLI text palette (via cli.applyTheme - gain/loss, headers,
// muted labels, warnings, accents) and chart rendering (via
// RunCtx.chart_theme), so `--theme <PATH>` repaints the entire CLI,
// not just the charts. The TUI loads its own theme separately.
const resolved_theme = resolveChartTheme(io, allocator, globals.theme_path);
cli.applyTheme(resolved_theme);
// Framework dispatch
//
// Comptime walk over `command_modules`. Each registered command

View file

@ -2451,6 +2451,7 @@ pub fn run(
config: zfin.Config,
portfolio_patterns: []const []const u8,
global_watchlist_path: ?[]const u8,
app_theme: theme.Theme,
args: []const []const u8,
today: zfin.Date,
) !void {
@ -2516,15 +2517,6 @@ pub fn run(
try stderr_writer.interface.flush();
}
const loaded_theme = blk: {
const home_opt = if (config.environ_map) |em| em.get("HOME") else null;
const home = home_opt orelse break :blk theme.default_theme;
const theme_path = std.fs.path.join(allocator, &.{ home, ".config", "zfin", "theme.srf" }) catch
break :blk theme.default_theme;
defer allocator.free(theme_path);
break :blk theme.loadFromFile(io, allocator, theme_path) orelse theme.default_theme;
};
var svc = try allocator.create(zfin.DataService);
defer allocator.destroy(svc);
svc.* = zfin.DataService.init(io, allocator, config);
@ -2546,7 +2538,7 @@ pub fn run(
.config = config,
.svc = svc,
.keymap = keymap,
.theme = loaded_theme,
.theme = app_theme,
.symbol = symbol,
.has_explicit_symbol = has_explicit_symbol,
.chart_config = chart_config,