Skip to main content

krypt_core/config/
schema.rs

1//! Typed representation of a parsed `.krypt.toml`.
2//!
3//! All structs use `#[serde(deny_unknown_fields)]` so typos in user config
4//! produce loud errors at parse time rather than silently no-oping.
5
6use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10/// Top-level parsed config.
11///
12/// Build via [`super::parse_file`] or [`super::parse_str`].
13#[derive(Debug, Deserialize, Serialize, Default, Clone)]
14#[serde(deny_unknown_fields)]
15pub struct Config {
16    /// Metadata block.
17    #[serde(default)]
18    pub meta: Meta,
19
20    /// Other `.krypt.toml` files to merge in. Resolution is handled by the
21    /// `include` pass, not the parser.
22    #[serde(default)]
23    pub include: Vec<String>,
24
25    /// User-defined path variable overrides. Values may reference other vars
26    /// via `${NAME}`; resolution is deferred to the path resolver.
27    #[serde(default)]
28    pub paths: BTreeMap<String, String>,
29
30    /// File-deploy entries. Renamed because TOML uses `[[link]]` but Rust
31    /// idiom is `links` for the collection.
32    #[serde(default, rename = "link")]
33    pub links: Vec<Link>,
34
35    /// Templated file entries. Deployed like links, then optionally patched
36    /// by `krypt setup` based on the named prompt sections.
37    #[serde(default, rename = "template")]
38    pub templates: Vec<Template>,
39
40    /// Wizard prompt definitions, keyed by section name.
41    #[serde(default)]
42    pub prompts: BTreeMap<String, PromptSection>,
43
44    /// Package dependency groups, one per cross-distro mapping.
45    #[serde(default, rename = "deps")]
46    pub deps: Vec<DepsGroup>,
47
48    /// Lifecycle hooks (post-update plugin updates, etc.).
49    #[serde(default, rename = "hook")]
50    pub hooks: Vec<Hook>,
51
52    /// Custom subcommands surfaced as `krypt <group> <name>`.
53    #[serde(default, rename = "command")]
54    pub commands: Vec<Command>,
55}
56
57/// `[meta]` section.
58#[derive(Debug, Deserialize, Serialize, Default, Clone)]
59#[serde(deny_unknown_fields)]
60pub struct Meta {
61    /// Human-readable repo name.
62    #[serde(default)]
63    pub name: String,
64
65    /// Free-form short description.
66    #[serde(default)]
67    pub description: String,
68
69    /// Minimum `krypt` binary version this repo expects. If the installed
70    /// `krypt` is older, the loader emits a warning (or errors on
71    /// `--strict`). Format: SemVer.
72    #[serde(default)]
73    pub krypt_min: Option<String>,
74
75    /// Override the notification backend. Accepted values: `auto` (default),
76    /// `notify-send`, `osascript`, `terminal-notifier`, `powershell`, `stderr`.
77    /// Unknown values produce a warning and fall through to auto-detect.
78    #[serde(default)]
79    pub notify_backend: Option<String>,
80}
81
82/// `[[link]]` entry — a file (or glob) to deploy from the repo to a path
83/// under `$HOME`.
84#[derive(Debug, Deserialize, Serialize, Clone)]
85#[serde(deny_unknown_fields)]
86pub struct Link {
87    /// Source path within the dotfiles repo. Mutually exclusive with
88    /// `src_glob`.
89    #[serde(default)]
90    pub src: Option<String>,
91
92    /// Glob pattern matched against the repo. Expanded at deploy time.
93    /// Mutually exclusive with `src`.
94    #[serde(default)]
95    pub src_glob: Option<String>,
96
97    /// Destination path. May contain `${VAR}` placeholders.
98    pub dst: String,
99
100    /// Optional OS gate. One of `linux`, `macos`, `windows`. Multiple OSes:
101    /// duplicate the link entry. Omitted = all platforms.
102    #[serde(default)]
103    pub platform: Option<String>,
104}
105
106/// `[[template]]` entry — a file copied like a `[[link]]`, but with prompt-
107/// section names that wire it to `krypt setup`.
108#[derive(Debug, Deserialize, Serialize, Clone)]
109#[serde(deny_unknown_fields)]
110pub struct Template {
111    /// Source path within the repo.
112    pub src: String,
113
114    /// Destination path. May contain `${VAR}` placeholders.
115    pub dst: String,
116
117    /// Names of `[prompts.<name>]` sections that drive this template's
118    /// post-copy patching.
119    #[serde(default)]
120    pub prompts: Vec<String>,
121
122    /// Optional OS gate. Same semantics as [`Link::platform`].
123    #[serde(default)]
124    pub platform: Option<String>,
125}
126
127/// `[prompts.<name>]` section — an interactive wizard subsection.
128#[derive(Debug, Deserialize, Serialize, Clone)]
129#[serde(deny_unknown_fields)]
130pub struct PromptSection {
131    /// Heading shown above this group of prompts.
132    #[serde(default)]
133    pub heading: String,
134
135    /// Ordered list of fields to prompt for.
136    pub fields: Vec<PromptField>,
137
138    /// Named built-in writer that applies the collected values. e.g.
139    /// `gitconfig`, `hypr_vars`, `env`, `generic_template`.
140    pub writer: String,
141}
142
143/// A single field inside a [`PromptSection`].
144#[derive(Debug, Deserialize, Serialize, Clone)]
145#[serde(deny_unknown_fields)]
146pub struct PromptField {
147    /// Variable name the value is stored under.
148    pub key: String,
149
150    /// Question presented to the user.
151    pub prompt: String,
152
153    /// Field type: `string` (default), `bool`, `int`. Future: `password`,
154    /// `path`, `enum`.
155    #[serde(default = "default_prompt_type")]
156    pub r#type: String,
157
158    /// Literal default value.
159    #[serde(default)]
160    pub default: Option<toml::Value>,
161
162    /// Lookup expression for the default. Examples:
163    /// - `git:user.name`     — run `git config --get user.name`
164    /// - `env:USER`          — read env var
165    /// - `field:email`       — value of an earlier field
166    /// - `read_var:terminal` — read existing Hyprland `$terminal` from dest
167    #[serde(default)]
168    pub default_from: Option<String>,
169
170    /// Read the current value of a Hyprland-style variable from the
171    /// destination file when computing the default. Shorthand alternative
172    /// to `default_from = "read_var:..."`.
173    #[serde(default)]
174    pub read_var: Option<String>,
175
176    /// If true, blank answers are allowed.
177    #[serde(default)]
178    pub optional: bool,
179
180    /// Only ask this field if the named earlier field has a non-empty value.
181    #[serde(default)]
182    pub requires: Option<String>,
183}
184
185fn default_prompt_type() -> String {
186    "string".into()
187}
188
189/// `[[deps]]` entry — one cross-distro dependency group.
190#[derive(Debug, Deserialize, Serialize, Default, Clone)]
191#[serde(deny_unknown_fields)]
192pub struct DepsGroup {
193    /// Group name (e.g. `core`, `fonts`).
194    pub group: String,
195
196    /// Required platforms for this group. `["all"]` or omitted = required
197    /// everywhere; otherwise restrict to listed OSes.
198    #[serde(default)]
199    pub required_platforms: Vec<String>,
200
201    /// Packages on each manager. Empty list = unavailable on that manager.
202    #[serde(default)]
203    pub pacman: Vec<String>,
204    /// Packages on apt (Debian/Ubuntu).
205    #[serde(default)]
206    pub apt: Vec<String>,
207    /// Packages on dnf (Fedora/RHEL).
208    #[serde(default)]
209    pub dnf: Vec<String>,
210    /// Packages on brew (macOS).
211    #[serde(default)]
212    pub brew: Vec<String>,
213    /// Packages on scoop (Windows).
214    #[serde(default)]
215    pub scoop: Vec<String>,
216    /// Packages on winget (Windows).
217    #[serde(default)]
218    pub winget: Vec<String>,
219}
220
221/// `[[hook]]` entry — a lifecycle hook tied to a phase.
222#[derive(Debug, Deserialize, Serialize, Clone)]
223#[serde(deny_unknown_fields)]
224pub struct Hook {
225    /// Hook name, useful in logs.
226    pub name: String,
227
228    /// Phase: `post-update`, `post-link`, etc.
229    pub when: String,
230
231    /// Optional predicate gating execution. Same syntax as `[[command]]`
232    /// step predicates.
233    #[serde(default)]
234    pub r#if: Option<String>,
235
236    /// Command + args to execute.
237    pub run: Vec<String>,
238
239    /// Don't fail the parent command if this hook errors.
240    #[serde(default)]
241    pub ignore_failure: bool,
242}
243
244/// `[[command]]` entry — a custom user subcommand built from step primitives.
245///
246/// Exposed at the CLI as `krypt <group> <name>`.
247#[derive(Debug, Deserialize, Serialize, Clone)]
248#[serde(deny_unknown_fields)]
249pub struct Command {
250    /// Group bucket (e.g. `menu`, `battery`).
251    pub group: String,
252
253    /// Command name within the group.
254    pub name: String,
255
256    /// Help text shown by `krypt <group> --help`.
257    #[serde(default)]
258    pub description: String,
259
260    /// Optional OS gate.
261    #[serde(default)]
262    pub platform: Option<String>,
263
264    /// Ordered execution steps.
265    pub steps: Vec<Step>,
266}
267
268/// One step inside a [`Command`] or [`Hook`].
269///
270/// Step kinds are mutually exclusive but all carry optional shared fields
271/// like `capture`, `if`, `on_fail`. Validation enforces exactly one of
272/// `run` / `pipe` / `notify` per step.
273#[derive(Debug, Deserialize, Serialize, Default, Clone)]
274#[serde(deny_unknown_fields)]
275pub struct Step {
276    /// Spawn a subprocess with these args. String args may interpolate
277    /// captured vars via `{name}`.
278    #[serde(default)]
279    pub run: Option<Vec<String>>,
280
281    /// Spawn a subprocess that receives `input` on stdin.
282    #[serde(default)]
283    pub pipe: Option<Vec<String>>,
284
285    /// Send a desktop notification with this title (`notify[0]`) and body
286    /// (`notify[1]`).
287    #[serde(default)]
288    pub notify: Option<Vec<String>>,
289
290    /// Variable name to capture stdout into.
291    #[serde(default)]
292    pub capture: Option<String>,
293
294    /// Input string fed to stdin (used with `pipe`). May reference captures.
295    #[serde(default)]
296    pub input: Option<String>,
297
298    /// Predicate gating this step. e.g. `command_exists:fish`,
299    /// `platform:linux`, `env:FOO=bar`, `!file_exists:/etc/passwd`.
300    #[serde(default)]
301    pub r#if: Option<String>,
302
303    /// Failure mode: `abort` (default), `notify`, `ignore`, `prompt`.
304    #[serde(default)]
305    pub on_fail: Option<String>,
306
307    /// Boolean shortcut for `on_fail = "ignore"`.
308    #[serde(default)]
309    pub ignore_failure: bool,
310}