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}