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}
142
143const fn _default_true() -> bool {
144 true
145}
146
147#[derive(Clone, Debug, Default, Deserialize)]
149pub struct Config {
150 #[serde(default)]
153 pub views: HashMap<String, ViewConfig>,
154 #[serde(default)]
156 pub command_hints: HashMap<String, HashMap<String, Vec<String>>>,
157 #[serde(default)]
159 pub hints: Vec<String>,
160 #[serde(default = "_default_true")]
162 pub enable_hints: bool,
163}
164
165pub struct ConfigBuilder {
167 sources: Vec<config::Config>,
169}
170
171impl ConfigBuilder {
172 pub fn add_source(mut self, source: impl AsRef<Path>) -> Result<Self, ConfigBuilderError> {
177 let config = match config::Config::builder()
178 .add_source(config::File::from(source.as_ref()))
179 .build()
180 {
181 Ok(config) => config,
182 Err(error) => {
183 return Err(ConfigBuilderError::FileParse {
184 source: Box::new(error),
185 builder: self,
186 path: source.as_ref().to_owned(),
187 });
188 }
189 };
190
191 if let Err(error) = config.clone().try_deserialize::<Config>() {
192 return Err(ConfigBuilderError::ConfigDeserialize {
193 source: Box::new(error),
194 builder: self,
195 path: source.as_ref().to_owned(),
196 });
197 }
198
199 self.sources.push(config);
200 Ok(self)
201 }
202
203 pub fn build(self) -> Result<Config, ConfigError> {
206 let mut config = config::Config::builder();
207
208 for source in self.sources {
209 config = config.add_source(source);
210 }
211
212 Ok(config.build()?.try_deserialize::<Config>()?)
213 }
214}
215
216impl fmt::Debug for ConfigBuilderError {
217 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218 match self {
219 ConfigBuilderError::FileParse { source, path, .. } => f
220 .debug_struct("FileParse")
221 .field("source", source)
222 .field("path", path)
223 .finish_non_exhaustive(),
224 ConfigBuilderError::ConfigDeserialize { source, path, .. } => f
225 .debug_struct("ConfigDeserialize")
226 .field("source", source)
227 .field("path", path)
228 .finish_non_exhaustive(),
229 }
230 }
231}
232
233impl Config {
234 pub fn builder() -> Result<ConfigBuilder, ConfigError> {
236 let default_config: config::Config = config::Config::builder()
237 .add_source(config::File::from_str(CONFIG, config::FileFormat::Yaml))
238 .build()?;
239
240 Ok(ConfigBuilder {
241 sources: Vec::from([default_config]),
242 })
243 }
244
245 pub fn new() -> Result<Self, ConfigError> {
247 let default_config: config::Config = config::Config::builder()
248 .add_source(config::File::from_str(CONFIG, config::FileFormat::Yaml))
249 .build()?;
250
251 let config_dir =
252 get_config_dir().ok_or_else(|| ConfigError::ConfigDirCannotBeIdentified)?;
253 let mut builder = ConfigBuilder {
254 sources: Vec::from([default_config]),
255 };
256
257 let config_files = [
258 ("config.yaml", config::FileFormat::Yaml),
259 ("views.yaml", config::FileFormat::Yaml),
260 ];
261 let mut found_config = false;
262 for (file, _format) in &config_files {
263 if config_dir.join(file).exists() {
264 found_config = true;
265
266 builder = match builder.add_source(config_dir.join(file)) {
267 Ok(builder) => builder,
268 Err(ConfigBuilderError::FileParse { source, .. }) => {
269 return Err(ConfigError::parse(*source));
270 }
271 Err(ConfigBuilderError::ConfigDeserialize {
272 source,
273 builder,
274 path,
275 }) => {
276 error!(
277 "The file {path:?} could not be deserialized and will be ignored: {source}"
278 );
279 builder
280 }
281 }
282 }
283 }
284 if !found_config {
285 tracing::error!("No configuration file found. Application may not behave as expected");
286 }
287
288 builder.build()
289 }
290}
291
292fn get_config_dir() -> Option<PathBuf> {
293 dirs::config_dir().map(|val| val.join("osc"))
294}
295
296impl fmt::Display for Config {
297 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
298 write!(f, "")
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use std::io::Write;
306 use tempfile::Builder;
307
308 #[test]
309 fn test_parse_config() {
310 let mut config_file = Builder::new().suffix(".yaml").tempfile().unwrap();
311
312 const CONFIG_DATA: &str = r#"
313 views:
314 foo:
315 default_fields: ["a", "b", "c"]
316 bar:
317 fields:
318 - name: "b"
319 min_width: 1
320 command_hints:
321 res:
322 cmd:
323 - hint1
324 - hint2
325 hints:
326 - hint1
327 - hint2
328 enable_hints: true
329 "#;
330
331 write!(config_file, "{CONFIG_DATA}").unwrap();
332
333 let _cfg = Config::builder()
334 .unwrap()
335 .add_source(config_file.path())
336 .unwrap()
337 .build();
338 }
339}