1use serde::Deserialize;
15
16#[derive(Debug, Clone, Deserialize, serde::Serialize)]
17#[serde(default)]
18pub struct Config {
19 pub tab_size: usize,
22 pub indent_guides: bool,
24}
25
26impl Default for Config {
27 fn default() -> Self {
28 Self {
29 tab_size: 4,
30 indent_guides: true,
31 }
32 }
33}
34
35pub struct Knob {
38 pub key: &'static str,
39 pub kind: &'static str, pub desc: &'static str,
41}
42
43pub const KNOBS: &[Knob] = &[
44 Knob {
45 key: "tab_size",
46 kind: "number",
47 desc: "indent width in spaces",
48 },
49 Knob {
50 key: "indent_guides",
51 kind: "bool",
52 desc: "dim │ guide per indent level",
53 },
54];
55
56impl Config {
57 pub fn print_knobs(&self) {
60 for k in KNOBS {
61 let value = match k.key {
62 "tab_size" => self.tab_size.to_string(),
63 "indent_guides" => self.indent_guides.to_string(),
64 _ => "?".into(),
65 };
66 println!(" {:<16} {:<7} {:<8} {}", k.key, k.kind, value, k.desc);
67 }
68 }
69
70 pub fn load() -> (Self, Option<String>) {
73 let Some(path) = config_path() else {
74 return (Self::default(), None);
75 };
76 let Ok(text) = std::fs::read_to_string(&path) else {
77 return (Self::default(), None); };
79 match toml::from_str::<Config>(&text) {
80 Ok(c) => (c, None),
81 Err(e) => (
82 Self::default(),
83 Some(format!("config {}: {e} — using defaults", path.display())),
84 ),
85 }
86 }
87
88 pub fn indent(&self) -> String {
89 " ".repeat(self.tab_size)
90 }
91}
92
93fn config_path() -> Option<std::path::PathBuf> {
94 let base = std::env::var_os("XDG_CONFIG_HOME")
95 .map(std::path::PathBuf::from)
96 .or_else(|| {
97 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
98 })?;
99 Some(base.join("strop").join("config.toml"))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn defaults_when_absent() {
108 let (c, err) = Config::load();
109 let _ = err; assert!(c.tab_size >= 2);
111 }
112
113 #[test]
114 fn parses_tab_size() {
115 let c: Config = toml::from_str("tab_size = 2").unwrap();
116 assert_eq!(c.tab_size, 2);
117 assert_eq!(c.indent(), " ");
118 }
119
120 #[test]
121 fn parses_indent_guides() {
122 let c: Config = toml::from_str("indent_guides = false").unwrap();
123 assert!(!c.indent_guides);
124 let c: Config = toml::from_str("").unwrap();
126 assert!(c.indent_guides);
127 }
128
129 #[test]
130 fn knobs_table_covers_every_field() {
131 assert_eq!(KNOBS.len(), 2);
134 }
135
136 #[test]
137 fn malformed_falls_back() {
138 assert!(toml::from_str::<Config>("tab_size = \"oops\"").is_err());
139 }
140}