codeberg_cli/types/
config.rs1use std::path::PathBuf;
2use std::{env::current_dir, path::Path};
3
4use anyhow::Context;
5use clap::ValueEnum;
6use config::Config;
7use serde::{Deserialize, Serialize};
8use url::Url;
9
10use crate::paths::{berg_config_dir, config_path};
11use crate::render::table::TableWrapper;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
14pub enum ConfigLocation {
15 Global,
16 Local,
17 Stdout,
18}
19
20impl ConfigLocation {
21 pub fn path(self) -> anyhow::Result<PathBuf> {
22 match self {
23 ConfigLocation::Global => {
24 let global_path = config_path()?;
25 let global_path_dir = global_path
26 .parent()
27 .context("config path has parent directory")?;
28 Ok(global_path_dir.to_path_buf())
29 }
30 ConfigLocation::Local => current_dir().context("Getting current directory failed!"),
31 ConfigLocation::Stdout => unreachable!(
32 "Path of Stdout doesn't make sense. Impossible codepath. Please report this as an issue!"
33 ),
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct BergConfig {
41 pub base_url: String,
43 pub protocol: String,
45 pub fancy_tables: bool,
48 pub no_color: bool,
53 pub editor: String,
55 pub max_width: i32,
66}
67
68impl BergConfig {
69 fn iter_default_field_value() -> impl Iterator<Item = (&'static str, config::Value)> {
70 let BergConfig {
71 base_url,
72 protocol,
73 fancy_tables,
74 no_color,
75 editor,
76 max_width,
77 } = Default::default();
78 [
79 ("base_url", config::Value::from(base_url)),
80 ("protocol", config::Value::from(protocol)),
81 ("fancy_tables", config::Value::from(fancy_tables)),
82 ("no_color", config::Value::from(no_color)),
83 ("editor", config::Value::from(editor)),
84 ("max_width", config::Value::from(max_width)),
85 ]
86 .into_iter()
87 }
88}
89
90impl Default for BergConfig {
91 fn default() -> Self {
92 Self {
93 base_url: String::from("codeberg.org/"),
94 protocol: String::from("https"),
95 fancy_tables: true,
96 no_color: false,
97 editor: default_editor(),
98 max_width: 80,
99 }
100 }
101}
102
103impl BergConfig {
104 pub fn new() -> anyhow::Result<Self> {
117 let config = Self::raw()?.try_deserialize::<BergConfig>()?;
118 Ok(config)
119 }
120
121 pub fn raw() -> anyhow::Result<Config> {
122 let local_config_path = current_dir().map(add_berg_config_file)?;
123 let global_config_path = berg_config_dir().map(add_berg_config_file)?;
124 let mut config_builder = Config::builder();
125
126 config_builder = config_builder.add_source(file_from_path(global_config_path.as_path()));
135 tracing::debug!("config search in: {global_config_path:?}");
136
137 let mut walk_up = local_config_path.clone();
138 let walking_up = std::iter::from_fn(move || {
139 walk_up
140 .parent()
141 .and_then(|parent| parent.parent())
142 .map(add_berg_config_file)
143 .inspect(|parent| {
144 walk_up = parent.clone();
145 })
146 });
147
148 let pwd = std::iter::once(local_config_path);
149 let local_paths = pwd.chain(walking_up).collect::<Vec<_>>();
150
151 for path in local_paths.iter().rev() {
152 tracing::debug!("config search in: {path:?}");
153 config_builder = config_builder.add_source(file_from_path(path));
154 }
155
156 config_builder = config_builder.add_source(config::Environment::with_prefix("BERG"));
157 for (field_name, default_value) in BergConfig::iter_default_field_value() {
160 config_builder = config_builder.set_default(field_name, default_value)?;
161 }
162
163 config_builder.build().map_err(anyhow::Error::from)
164 }
165}
166
167impl BergConfig {
168 pub fn url(&self) -> anyhow::Result<Url> {
169 let url = format!(
170 "{protoc}://{url}",
171 protoc = self.protocol,
172 url = self.base_url
173 );
174 Url::parse(url.as_str())
175 .context("The protocol + base url in the config don't add up to a valid url")
176 }
177
178 pub fn make_table(&self) -> TableWrapper {
179 let mut table = TableWrapper::default();
180
181 table.max_width = match self.max_width.cmp(&0) {
182 std::cmp::Ordering::Less => None,
183 std::cmp::Ordering::Equal => termsize::get().map(|size| size.cols - 2),
184 std::cmp::Ordering::Greater => Some(self.max_width as u16),
185 };
186
187 let preset = if self.fancy_tables {
188 comfy_table::presets::UTF8_FULL
189 } else {
190 comfy_table::presets::NOTHING
191 };
192
193 table.load_preset(preset);
194
195 table
196 }
197}
198
199fn file_from_path(
200 path: impl AsRef<Path>,
201) -> config::File<config::FileSourceFile, config::FileFormat> {
202 config::File::new(
203 path.as_ref().to_str().unwrap_or_default(),
204 config::FileFormat::Toml,
205 )
206 .required(false)
207}
208
209fn add_berg_config_file(dir: impl AsRef<Path>) -> PathBuf {
210 dir.as_ref().join("berg.toml")
211}
212
213fn default_editor() -> String {
214 std::env::var("EDITOR")
215 .or(std::env::var("VISUAL"))
216 .unwrap_or_else(|_| {
217 let os_native_editor = if cfg!(target_os = "windows") {
218 "notepad"
220 } else if cfg!(target_os = "macos") {
221 "textedit"
223 } else {
224 "vi"
226 };
227 String::from(os_native_editor)
228 })
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn make_config(path: PathBuf, config: BergConfig) -> anyhow::Result<()> {
236 let config_path = add_berg_config_file(path);
237 let toml = toml::to_string(&config).context("Failed to create config string!")?;
238 std::fs::write(config_path, toml).context("Failed to write config file!")?;
239 Ok(())
240 }
241
242 fn delete_config(path: PathBuf) -> anyhow::Result<()> {
243 let config_path = add_berg_config_file(path);
244 std::fs::remove_file(config_path).context("Failed to remove file")?;
245 Ok(())
246 }
247
248 #[test]
249 #[ignore = "doesn't work on nix in 'ci' because of no r/w permissions on the system"]
250 fn berg_config_integration_test() -> anyhow::Result<()> {
251 let local_dir = current_dir()?;
254 std::fs::create_dir_all(local_dir)?;
255 let global_dir = berg_config_dir()?;
256 std::fs::create_dir_all(global_dir)?;
257
258 let config = BergConfig {
260 base_url: String::from("local"),
261 ..Default::default()
262 };
263 make_config(current_dir()?, config)?;
264 let config = BergConfig::new();
265 assert!(config.is_ok(), "{config:?}");
266 let config = config.unwrap();
267 assert_eq!(config.base_url.as_str(), "local");
268 delete_config(current_dir()?)?;
269
270 let config = BergConfig {
272 base_url: String::from("global"),
273 ..Default::default()
274 };
275 make_config(berg_config_dir()?, config)?;
276 let config = BergConfig::new();
277 assert!(config.is_ok(), "{config:?}");
278 let config = config.unwrap();
279 assert_eq!(config.base_url.as_str(), "global", "{0:?}", config.base_url);
280 delete_config(berg_config_dir()?)?;
281
282 let config = BergConfig::new();
284 assert!(config.is_ok(), "{config:?}");
285 let config = config.unwrap();
286 assert_eq!(config.base_url.as_str(), "codeberg.org/");
287
288 {
290 let config = BergConfig {
292 base_url: String::from("local"),
293 ..Default::default()
294 };
295 make_config(current_dir()?, config)?;
296 }
297 {
298 let config = BergConfig {
300 base_url: String::from("global"),
301 ..Default::default()
302 };
303 make_config(berg_config_dir()?, config)?;
304 }
305 let config = BergConfig::new();
306 assert!(config.is_ok(), "{config:?}");
307 let config = config.unwrap();
308 assert_eq!(config.base_url.as_str(), "local");
309 delete_config(current_dir()?)?;
310 delete_config(berg_config_dir()?)?;
311
312 Ok(())
313 }
314}