feat: TOML config file support#202
Conversation
Adds a user-level TOML configuration file for `all-smi` so operators can persist common settings without retyping CLI flags. Implements the full precedence chain (CLI flag > env var > config file > compiled default), a new `all-smi config init|print|validate` subcommand family, and backward-compatible env var aliases. Closes #192
Implementation Review SummaryIntentPR #202 implements issue #192 — TOML config file support ( Overall AssessmentThe core loader, schema, env overlay, secure-write hardening, and integration into FindingsHIGH
MEDIUM
LOW
What's correct
Verification Checklist
RecommendationAddress the four HIGH findings before merge. The orphaned sections and missing hostfile tilde expansion are the highest-impact gaps because they change documented behavior; |
Addresses review findings on PR #202. The [general], [display], [record], and [snapshot] sections were parsed but had no runtime effect. The loader was vulnerable to DoS via unbounded reads, parsed the TOML document twice, and did not escape control characters before printing unknown keys. HIGH-R1: wire orphaned config sections * general.default_mode redispatches to the matching subcommand when no CLI subcommand is given. * snapshot.default_format / default_pretty merge into SnapshotOptions via from_args_with_settings when the CLI flag is unset. * record.output_dir / record.compress merge into RecorderOptions via from_args_with_settings. output_dir is treated as the directory for the default basename with tilde expansion applied. * display.* propagates into AppState.display_config for downstream renderer consumption (TUI renderers do not yet switch on these values; future integration work can consume the field). HIGH-R2: expand tilde on view.hostfile before canonicalize so a value like '~/.config/all-smi/hosts' resolves to the user's home instead of failing with NotFound. HIGH-R3: add AppState::with_energy_config so the PowerIntegrator's gap_interpolate window is built from the merged EnergyConfig instead of the compiled default. Callers in view::runner and api::server updated. Previously, overwriting AppState.energy_config after AppState::new() had no effect on the already-constructed integrator. HIGH-R4: apply legacy alerts env aliases first, canonical second. The energy pattern already worked this way; alerts was reversed. Added tests/config_file_integration_test.rs::canonical_alerts_env_beats_legacy to cover the precedence direction. HIGH-S1: cap config file reads at 1 MiB with a defensive double-check (metadata + Read::take) and refuse to follow symlinks. Added oversized_config_file_rejected_cleanly integration test. HIGH-S2: parse TOML once via toml::from_str -> Value, then reuse the parsed Value via try_into for the typed RawConfig. Avoids the second full-document parse that the previous implementation performed. MEDIUM-S3: escape_printable() escapes U+0000-U+001F and U+007F as \\u{XXXX} for every unknown-key print site. Prevents terminal cursor/color injection via a hostile quoted TOML key. MEDIUM-S4: render_toml escape_toml now covers the full TOML 1.0 basic string escape set (\\n, \\r, \\t, \\b, \\f, \\uXXXX for other control chars). Added a roundtrip test through parse_toml exercising every branch. MEDIUM-R5: --config is now threaded into config init (write target) and config validate (default path when positional omitted), not just config print. Without this the clap-declared "global" flag was a no-op for two of the three sub-subcommands. MEDIUM-R6: main.rs surfaces unknown-key warnings at startup so operators notice typos without running 'config print' separately. MEDIUM-R9: ApiArgs::processes becomes Option<bool> with num_args=0..=1 and default_missing_value="true" so the operator can force-disable process collection via --processes=false even when the config file sets api.processes = true. Previously bool could not express the third "unset" state. LOW-R7: consolidate expand_tilde into common::paths. The duplicate in metrics::energy_wal becomes a re-export so every settings-consuming site shares the same implementation. LOW-R8: bump direct dirs dep to version 6 to match the transitive version in the dependency graph. Deferred (follow-up): webhook URL validation, string length bounds, secure_write::create_new enforcement on every consumer, and any renderer integration that consumes AppState.display_config.
…es and legacy env var aliases README.md's Schema section previously named the nine config sections without documenting individual fields. Expand it with per-section field tables (key, type, default, canonical env var, description) for all sections: [general], [local], [view], [api], [alerts], [energy], [display], [record], and [snapshot]. Add a dedicated Legacy environment variable aliases table covering ALL_SMI_ALERT_TEMP, ALL_SMI_ENERGY_PRICE, ALL_SMI_ENERGY_NO_COST, and the other six backward-compat names introduced by issues #186 and #191.
PR Finalization ReportTestsAll tests pass at HEAD
Clippy ( DocumentationREADME.md: The existing Configuration section already covered file paths, precedence chain, helpers (
DEVELOPERS.md: The "Configuration Architecture" section (added in this PR) already documents the Settings refactor, the three-layer precedence model, key file responsibilities, and the step-by-step guide for wiring new config-driven fields. No changes needed. --help / CLI: The Lint/FormatNo changes required — Clippy and fmt were already clean. |
Summary
$/kWh, etc.) without retyping CLI flags on every invocation.all-smi config init | print | validatesubcommand family for initialising, inspecting, and sanity-checking the file.Implementation
src/common/config_file.rsowns the merged runtimeSettingsstruct and the [load] entry point. Parsing and schema definitions live inconfig_schema.rs; file-to-settings projection inconfig_apply.rs; env-var overlay inconfig_env.rs. Keeping each file under ~400 lines keeps the merge logic readable.src/common/paths.rsresolves the platform-appropriate config path (XDG on Linux,~/Library/Application Supporton macOS with XDG parity fallback,%APPDATA%on Windows) plus~/~/expansion.src/common/secure_write.rscentralises theO_NOFOLLOW+0o600pattern fromsnapshot::write_output_atomicandrecord::writer::open_securesoconfig initreuses the same hardening.src/config_cmd/implements theinit/print/validateruntime glue, a canonical commented example TOML, and both TOML and JSON renderers forconfig print.webhook_urlis redacted unless--show-secretsis passed.src/cli.rs+src/cli_config.rsadd the global--config <PATH>option and theConfigsubcommand withInit/Print/Validatevariants.ApiArgs/LocalArgs/ViewArgsnow useOption<T>for every field that the config can provide so the precedence chain merges cleanly inmain.rs.src/main.rscallsconfig_file::load(cli.config.as_deref())after parsing the CLI, then merges the resultingSettingsinto the mode entry points (run_api_mode,view::run_local_mode,view::run_view_mode,view::run_replay_mode). A malformed or explicitly-missing config file exits 2; implicit discovery silently falls back to defaults.src/api/server.rsandsrc/view/runner.rsnow accept a&Settings; their existing CLI args are resolved inmain.rsbefore they are called.ALL_SMI_ALERT_TEMP,ALL_SMI_ALERT_UTIL_LOW_MINS) and feat: energy accumulation (kWh) and cost estimation with $/kWh config #191 (ALL_SMI_ENERGY_PRICE,ALL_SMI_ENERGY_NO_COST,ALL_SMI_ENERGY_WAL_PATH,ALL_SMI_ENERGY_NO_WAL,ALL_SMI_ENERGY_GAP_SECONDS,ALL_SMI_ENERGY_CURRENCY) are preserved as legacy aliases for the new canonicalALL_SMI_<SECTION>_<KEY>names.config printwarns about them,config validateaccepts them for forward compat, andconfig validate --strictrejects them.Testing
cargo test --lib— 899 passedcargo test --bin all-smi— 1029 passedcargo test --test config_file_integration_test— 12 passed (covers precedence chain, malformed TOML, unknown keys, schema version, legacy env aliases, webhook secrets, tilde paths)cargo clippy --all-targets -- -D warnings— cleancargo fmt --all -- --check— cleanall-smi config init/print/validateround-trip, including--forceoverwrite, redacted/--show-secretswebhook, and explicit vs implicit--configdiscovery.Documentation
Closes #192