From d59555bf92c6a8f2dd2f6b125652b86c57a160d0 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 27 Jun 2026 10:19:57 -0700 Subject: [PATCH] global theme flag/theme resolution themes all the things --- src/commands/framework.zig | 6 ++-- src/main.zig | 63 ++++++++++++++++++++++---------------- src/tui.zig | 12 ++------ 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/commands/framework.zig b/src/commands/framework.zig index dc35cf8..92cccc5 100644 --- a/src/commands/framework.zig +++ b/src/commands/framework.zig @@ -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 ` - /// flag (default: the built-in theme). The TUI loads its own theme - /// from `theme.srf` independently. + /// `--export-chart` PNGs - resolved once from `--theme ` (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 diff --git a/src/main.zig b/src/main.zig index 3c21820..f8ad78b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -135,10 +135,10 @@ const interactive_help = \\ (e.g. 80x24); `auto` picks Kitty graphics \\ if the terminal supports it, otherwise \\ braille - \\ --theme Theme file (a `theme.srf`) applied to all - \\ charts - inline terminal charts and - \\ `--export-chart` PNGs. Default: built-in - \\ theme. Generate one with + \\ --theme 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 ` (a `theme.srf`), applied to all - /// charts (inline terminal charts and `--export-chart` PNGs). Null = - /// the built-in default theme. + /// Theme file from `--theme ` (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 ` 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 ` (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 ` (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 ` 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 diff --git a/src/tui.zig b/src/tui.zig index d03aa9d..d44f19a 100644 --- a/src/tui.zig +++ b/src/tui.zig @@ -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,