1use serde::Deserialize;
18
19#[derive(Debug, Clone, Deserialize, serde::Serialize)]
20#[serde(default)]
21pub struct Config {
22 pub tab_size: usize,
26 pub indent_guides: bool,
28 pub indent_style: IndentStyle,
30 pub indent_detect: bool,
34 pub auto_format: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, serde::Serialize)]
41#[serde(rename_all = "lowercase")]
42pub enum IndentStyle {
43 Spaces,
44 Tabs,
45}
46
47impl Default for Config {
48 fn default() -> Self {
49 Self {
50 tab_size: 4,
51 indent_guides: true,
52 indent_style: IndentStyle::Spaces,
53 indent_detect: true,
54 auto_format: true,
55 }
56 }
57}
58
59pub struct Knob {
62 pub key: &'static str,
63 pub kind: &'static str, pub desc: &'static str,
65}
66
67pub const KNOBS: &[Knob] = &[
68 Knob {
69 key: "tab_size",
70 kind: "number",
71 desc: "indent width in spaces",
72 },
73 Knob {
74 key: "indent_guides",
75 kind: "bool",
76 desc: "dim │ guide per indent level",
77 },
78 Knob {
79 key: "indent_style",
80 kind: "string",
81 desc: "auto-indent unit: spaces or tabs",
82 },
83 Knob {
84 key: "indent_detect",
85 kind: "bool",
86 desc: "infer each document's indent from its content",
87 },
88 Knob {
89 key: "auto_format",
90 kind: "bool",
91 desc: "format through the language server before :w",
92 },
93];
94
95impl Config {
96 pub fn print_knobs(&self) {
99 for k in KNOBS {
100 let value = match k.key {
101 "tab_size" => self.tab_size.to_string(),
102 "indent_guides" => self.indent_guides.to_string(),
103 "indent_style" => format!("{:?}", self.indent_style).to_lowercase(),
104 "indent_detect" => self.indent_detect.to_string(),
105 "auto_format" => self.auto_format.to_string(),
106 _ => "?".into(),
107 };
108 println!(" {:<16} {:<7} {:<8} {}", k.key, k.kind, value, k.desc);
109 }
110 }
111
112 pub fn load() -> (Self, Option<String>) {
115 let Some(path) = config_path() else {
116 return (Self::default(), None);
117 };
118 let Ok(text) = std::fs::read_to_string(&path) else {
119 return (Self::default(), None); };
121 match toml::from_str::<Config>(&text) {
122 Ok(c) => (c, None),
123 Err(e) => (
124 Self::default(),
125 Some(format!("config {}: {e} — using defaults", path.display())),
126 ),
127 }
128 }
129
130 pub fn indent(&self) -> String {
131 match self.indent_style {
132 IndentStyle::Spaces => " ".repeat(self.tab_size),
133 IndentStyle::Tabs => "\t".into(),
134 }
135 }
136}
137
138fn config_path() -> Option<std::path::PathBuf> {
139 let base = std::env::var_os("XDG_CONFIG_HOME")
140 .map(std::path::PathBuf::from)
141 .or_else(|| {
142 std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))
143 })?;
144 Some(base.join("strop").join("config.toml"))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn defaults_when_absent() {
153 let (c, err) = Config::load();
154 let _ = err; assert!(c.tab_size >= 2);
156 }
157
158 #[test]
159 fn parses_tab_size() {
160 let c: Config = toml::from_str("tab_size = 2").unwrap();
161 assert_eq!(c.tab_size, 2);
162 assert_eq!(c.indent(), " ");
163 }
164
165 #[test]
166 fn parses_indent_guides() {
167 let c: Config = toml::from_str("indent_guides = false").unwrap();
168 assert!(!c.indent_guides);
169 let c: Config = toml::from_str("").unwrap();
171 assert!(c.indent_guides);
172 }
173
174 #[test]
175 fn knobs_name_real_fields_and_cover_all_of_them() {
176 for knob in KNOBS {
179 let snippet = match knob.key {
180 "indent_style" => "indent_style = \"spaces\"".to_string(),
181 _ => match knob.kind {
182 "number" => format!("{k} = 2", k = knob.key),
183 "bool" => format!("{k} = true", k = knob.key),
184 _ => format!("{k} = \"x\"", k = knob.key),
185 },
186 };
187 assert!(
188 toml::from_str::<Config>(&snippet).is_ok(),
189 "knob {:?} names no config field",
190 knob.key
191 );
192 }
193 assert_eq!(
194 KNOBS.len(),
195 5,
196 "tab_size, indent_guides, indent_style, indent_detect, auto_format"
197 );
198 }
199
200 #[test]
201 fn malformed_falls_back() {
202 assert!(toml::from_str::<Config>("tab_size = \"oops\"").is_err());
203 }
204}