1use eyre::Result;
33use serde::Deserialize;
34use std::{
35 collections::HashMap,
36 fmt,
37 path::{Path, PathBuf},
38};
39use thiserror::Error;
40use tracing::error;
41
42const CONFIG: &str = include_str!("../.config/config.yaml");
43
44#[derive(Debug, Error)]
47#[non_exhaustive]
48pub enum ConfigError {
49 #[error("failed to parse config: {}", source)]
51 Parse {
52 #[from]
54 source: config::ConfigError,
55 },
56
57 #[error("config dir cannot be identified")]
59 ConfigDirCannotBeIdentified,
60
61 #[error("failed to parse config: {}", source)]
63 Builder {
64 #[from]
66 source: ConfigBuilderError,
67 },
68}
69
70impl ConfigError {
71 pub fn parse(source: config::ConfigError) -> Self {
73 ConfigError::Parse { source }
74 }
75 pub fn builder(source: ConfigBuilderError) -> Self {
77 ConfigError::Builder { source }
78 }
79}
80
81#[derive(Error)]
83#[non_exhaustive]
84pub enum ConfigBuilderError {
85 #[error("failed to parse file {path:?}: {source}")]
87 FileParse {
88 source: Box<config::ConfigError>,
90 builder: ConfigBuilder,
92 path: PathBuf,
94 },
95 #[error("failed to deserialize config {path:?}: {source}")]
97 ConfigDeserialize {
98 source: Box<config::ConfigError>,
100 builder: ConfigBuilder,
102 path: PathBuf,
104 },
105}
106#[derive(Clone, Debug, Default, Deserialize)]
111pub struct ViewConfig {
112 #[serde(default)]
114 pub default_fields: Vec<String>,
115 #[serde(default)]
117 pub fields: Vec<FieldConfig>,
118 #[serde(default)]
120 pub wide: Option<bool>,
121}
122
123#[derive(Clone, Debug, Default, Deserialize, Eq, Ord, PartialOrd, PartialEq)]
125pub struct FieldConfig {
126 pub name: String,
128 #[serde(default)]
130 pub width: Option<usize>,
131 #[serde(default)]
133 pub min_width: Option<usize>,
134 #[serde(default)]
136 pub max_width: Option<usize>,
137 #[serde(default)]
140 pub json_pointer: Option<String>,
141 #[serde(default)]
145 pub json_pointer_fallback: Option<String>,
146}
147
148const fn _default_true() -> bool {
149 true
150}
151
152#[derive(Clone, Debug, Default, Deserialize)]
154pub struct Config {
155 #[serde(default)]
158 pub views: HashMap<String, ViewConfig>,
159 #[serde(default)]
161 pub command_hints: HashMap<String, HashMap<String, Vec<String>>>,
162 #[serde(default)]
164 pub hints: Vec<String>,
165 #[serde(default = "_default_true")]
167 pub enable_hints: bool,
168}
169
170pub struct ConfigBuilder {
172 sources: Vec<config::Config>,
174}
175
176impl ConfigBuilder {
177 pub fn add_source(mut self, source: impl AsRef<Path>) -> Result<Self, ConfigBuilderError> {
182 let config = match config::Config::builder()
183 .add_source(config::File::from(source.as_ref()))
184 .build()
185 {
186 Ok(config) => config,
187 Err(error) => {
188 return Err(ConfigBuilderError::FileParse {
189 source: Box::new(error),
190 builder: self,
191 path: source.as_ref().to_owned(),
192 });
193 }
194 };
195
196 if let Err(error) = config.clone().try_deserialize::<Config>() {
197 return Err(ConfigBuilderError::ConfigDeserialize {
198 source: Box::new(error),
199 builder: self,
200 path: source.as_ref().to_owned(),
201 });
202 }
203
204 self.sources.push(config);
205 Ok(self)
206 }
207
208 pub fn build(self) -> Result<Config, ConfigError> {
211 let mut config = config::Config::builder();
212
213 for source in self.sources {
214 config = config.add_source(source);
215 }
216
217 Ok(config.build()?.try_deserialize::<Config>()?)
218 }
219}
220
221impl fmt::Debug for ConfigBuilderError {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 match self {
224 ConfigBuilderError::FileParse { source, path, .. } => f
225 .debug_struct("FileParse")
226 .field("source", source)
227 .field("path", path)
228 .finish_non_exhaustive(),
229 ConfigBuilderError::ConfigDeserialize { source, path, .. } => f
230 .debug_struct("ConfigDeserialize")
231 .field("source", source)
232 .field("path", path)
233 .finish_non_exhaustive(),
234 }
235 }
236}
237
238impl Config {
239 pub fn builder() -> Result<ConfigBuilder, ConfigError> {
241 let default_config: config::Config = config::Config::builder()
242 .add_source(config::File::from_str(CONFIG, config::FileFormat::Yaml))
243 .build()?;
244
245 Ok(ConfigBuilder {
246 sources: Vec::from([default_config]),
247 })
248 }
249
250 pub fn new() -> Result<Self, ConfigError> {
252 let default_config: config::Config = config::Config::builder()
253 .add_source(config::File::from_str(CONFIG, config::FileFormat::Yaml))
254 .build()?;
255
256 let config_dir =
257 get_config_dir().ok_or_else(|| ConfigError::ConfigDirCannotBeIdentified)?;
258 let mut builder = ConfigBuilder {
259 sources: Vec::from([default_config]),
260 };
261
262 let config_files = [
263 ("config.yaml", config::FileFormat::Yaml),
264 ("views.yaml", config::FileFormat::Yaml),
265 ];
266 let mut found_config = false;
267 for (file, _format) in &config_files {
268 if config_dir.join(file).exists() {
269 found_config = true;
270
271 builder = match builder.add_source(config_dir.join(file)) {
272 Ok(builder) => builder,
273 Err(ConfigBuilderError::FileParse { source, .. }) => {
274 return Err(ConfigError::parse(*source));
275 }
276 Err(ConfigBuilderError::ConfigDeserialize {
277 source,
278 builder,
279 path,
280 }) => {
281 error!(
282 "The file {path:?} could not be deserialized and will be ignored: {source}"
283 );
284 builder
285 }
286 }
287 }
288 }
289 if !found_config {
290 tracing::error!("No configuration file found. Application may not behave as expected");
291 }
292
293 builder.build()
294 }
295}
296
297fn get_config_dir() -> Option<PathBuf> {
298 dirs::config_dir().map(|val| val.join("osc"))
299}
300
301impl fmt::Display for Config {
302 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
303 write!(f, "")
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use std::io::Write;
311 use tempfile::Builder;
312
313 #[test]
314 fn test_parse_config() {
315 let mut config_file = Builder::new().suffix(".yaml").tempfile().unwrap();
316
317 const CONFIG_DATA: &str = r#"
318 views:
319 foo:
320 default_fields: ["a", "b", "c"]
321 bar:
322 fields:
323 - name: "b"
324 min_width: 1
325 command_hints:
326 res:
327 cmd:
328 - hint1
329 - hint2
330 hints:
331 - hint1
332 - hint2
333 enable_hints: true
334 "#;
335
336 write!(config_file, "{CONFIG_DATA}").unwrap();
337
338 let _cfg = Config::builder()
339 .unwrap()
340 .add_source(config_file.path())
341 .unwrap()
342 .build();
343 }
344}