composition_cli/context/config/
mod.rs1mod default;
2
3use dirs;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::{collections::HashSet, path::Path};
7
8#[derive(Debug, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(default = "default::use_color")]
11 pub use_color: bool,
12
13 #[serde(default = "default::respect_gitignore")]
14 pub respect_gitignore: bool,
15
16 #[serde(default = "default::ignore_dotfolders")]
17 pub ignore_dotfolders: bool,
18
19 #[serde(default = "default::ignored_directories")]
20 pub ignored_directories: Vec<String>,
21
22 #[serde(default = "default::ignore_dotfiles")]
23 pub ignore_dotfiles: bool,
24
25 #[serde(default = "default::ignored_files")]
26 pub ignored_files: Vec<String>,
27
28 #[serde(default = "default::ignore_empty_lines")]
29 pub ignore_empty_lines: bool,
30
31 #[serde(default = "default::excluded_patterns")]
32 pub excluded_patterns: Vec<String>,
33
34 #[serde(default = "default::tracked")]
35 pub tracked: Vec<Tracked>,
36
37 #[serde(skip)]
38 pub compiled_excluded_patterns: Vec<Regex>,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct Tracked {
43 pub display: String,
44 pub extensions: Vec<String>,
45 pub color: Option<String>,
46
47 #[serde(default)]
48 pub excluded_patterns: Vec<String>,
49
50 #[serde(skip)]
51 pub compiled_excluded_patterns: Vec<Regex>,
52}
53
54impl Config {
55 pub fn from_config() -> (Self, bool) {
56 let config_path = dirs::config_dir()
57 .unwrap_or_else(|| std::path::PathBuf::from("."))
58 .join("composition")
59 .join("config.toml");
60
61 match Self::load_from_path(&config_path) {
62 Ok(config) => (config, true),
63 Err(_) => (Self::default(), false),
64 }
65 }
66
67 fn load_from_path(path: impl AsRef<Path>) -> Result<Self, ConfigLoadError> {
68 let path = path.as_ref();
69 let content = std::fs::read_to_string(path).map_err(|_| ConfigLoadError::FileReadFailed)?;
70
71 let mut config: Config =
72 toml::from_str(&content).map_err(|_| ConfigLoadError::TomlParseFailed)?;
73
74 config.compiled_excluded_patterns = compile_regexes(&config.excluded_patterns)?;
76
77 let mut seen_displays = HashSet::new();
78 let color_regex = Regex::new(r"^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
79 .expect("hardcoded regex should compile");
80
81 for tracked in &mut config.tracked {
82 if !seen_displays.insert(tracked.display.clone()) {
84 return Err(ConfigLoadError::DuplicateTrackedDisplay(
85 tracked.display.clone(),
86 ));
87 }
88
89 if let Some(color) = &tracked.color {
90 if !color_regex.is_match(color) {
91 return Err(ConfigLoadError::InvalidColorValue(color.clone()));
92 }
93 }
94
95 tracked.compiled_excluded_patterns = compile_regexes(&tracked.excluded_patterns)?
97 }
98
99 Ok(config)
100 }
101}
102
103impl Default for Config {
104 fn default() -> Self {
105 let mut config = Self {
106 use_color: default::use_color(),
107 respect_gitignore: default::respect_gitignore(),
108 ignore_dotfolders: default::ignore_dotfolders(),
109 ignored_directories: default::ignored_directories(),
110 ignore_dotfiles: default::ignore_dotfiles(),
111 ignored_files: default::ignored_files(),
112 ignore_empty_lines: default::ignore_empty_lines(),
113 excluded_patterns: default::excluded_patterns(),
114 tracked: default::tracked(),
115 compiled_excluded_patterns: Vec::new(),
116 };
117
118 config.compiled_excluded_patterns =
120 compile_regexes(&config.excluded_patterns).unwrap_or_default();
121
122 for tracked in &mut config.tracked {
124 tracked.compiled_excluded_patterns =
125 compile_regexes(&tracked.excluded_patterns).unwrap_or_default();
126 }
127
128 config
129 }
130}
131
132fn compile_regexes(excluded_patterns: &Vec<String>) -> Result<Vec<Regex>, ConfigLoadError> {
133 let mut compiled_patterns = Vec::with_capacity(excluded_patterns.len());
134 for pat in excluded_patterns {
135 let regex =
136 Regex::new(pat).map_err(|_| ConfigLoadError::RegexCompileFailed(pat.clone()))?;
137 compiled_patterns.push(regex);
138 }
139
140 Ok(compiled_patterns)
141}
142
143#[derive(Debug)]
144pub enum ConfigLoadError {
145 FileReadFailed,
146 TomlParseFailed,
147 InvalidColorValue(String),
148 RegexCompileFailed(String),
149 DuplicateTrackedDisplay(String),
150}
151
152impl std::error::Error for ConfigLoadError {}
153
154impl std::fmt::Display for ConfigLoadError {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 match self {
157 ConfigLoadError::FileReadFailed => write!(f, "failed to read the config file"),
158 ConfigLoadError::TomlParseFailed => {
159 write!(f, "failed to parse the config file as TOML")
160 }
161 ConfigLoadError::InvalidColorValue(color) => {
162 write!(f, "invalid color value: '{}'", color)
163 }
164 ConfigLoadError::RegexCompileFailed(pattern) => {
165 write!(f, "failed to compile regex pattern: '{}'", pattern)
166 }
167 ConfigLoadError::DuplicateTrackedDisplay(display) => {
168 write!(f, "duplicate tracked display found: '{}'", display)
169 }
170 }
171 }
172}