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;
11use crate::render::table::TableWrapper;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
14pub enum ConfigLocation {
15 Global,
16 Local,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct BergConfig {
22 pub base_url: String,
24 pub protocol: String,
26 pub fancy_tables: bool,
29 pub no_color: bool,
34 pub editor: String,
36 pub max_width: i32,
47}
48
49impl BergConfig {
50 fn iter_default_field_value() -> impl Iterator<Item = (&'static str, config::Value)> {
51 let BergConfig {
52 base_url,
53 protocol,
54 fancy_tables,
55 no_color,
56 editor,
57 max_width,
58 } = Default::default();
59 [
60 ("base_url", config::Value::from(base_url)),
61 ("protocol", config::Value::from(protocol)),
62 ("fancy_tables", config::Value::from(fancy_tables)),
63 ("no_color", config::Value::from(no_color)),
64 ("editor", config::Value::from(editor)),
65 ("max_width", config::Value::from(max_width)),
66 ]
67 .into_iter()
68 }
69}
70
71impl Default for BergConfig {
72 fn default() -> Self {
73 Self {
74 base_url: String::from("codeberg.org/"),
75 protocol: String::from("https"),
76 fancy_tables: true,
77 no_color: false,
78 editor: default_editor(),
79 max_width: 80,
80 }
81 }
82}
83
84impl BergConfig {
85 pub fn new() -> anyhow::Result<Self> {
98 let config = Self::raw()?.try_deserialize::<BergConfig>()?;
99 Ok(config)
100 }
101
102 pub fn raw() -> anyhow::Result<Config> {
103 let local_config_path = current_dir().map(add_berg_config_file)?;
104 let global_config_path = berg_config_dir().map(add_berg_config_file)?;
105 let mut config_builder = Config::builder();
106
107 config_builder = config_builder.add_source(file_from_path(global_config_path.as_path()));
116 tracing::debug!("config search in: {global_config_path:?}");
117
118 let mut walk_up = local_config_path.clone();
119 let walking_up = std::iter::from_fn(move || {
120 walk_up
121 .parent()
122 .and_then(|parent| parent.parent())
123 .map(add_berg_config_file)
124 .inspect(|parent| {
125 walk_up = parent.clone();
126 })
127 });
128
129 let pwd = std::iter::once(local_config_path);
130 let local_paths = pwd.chain(walking_up).collect::<Vec<_>>();
131
132 for path in local_paths.iter().rev() {
133 tracing::debug!("config search in: {path:?}");
134 config_builder = config_builder.add_source(file_from_path(path));
135 }
136
137 config_builder = config_builder.add_source(config::Environment::with_prefix("BERG"));
138 for (field_name, default_value) in BergConfig::iter_default_field_value() {
141 config_builder = config_builder.set_default(field_name, default_value)?;
142 }
143
144 config_builder.build().map_err(anyhow::Error::from)
145 }
146}
147
148impl BergConfig {
149 pub fn url(&self) -> anyhow::Result<Url> {
150 let url = format!(
151 "{protoc}://{url}",
152 protoc = self.protocol,
153 url = self.base_url
154 );
155 Url::parse(url.as_str())
156 .context("The protocol + base url in the config don't add up to a valid url")
157 }
158
159 pub fn make_table(&self) -> TableWrapper {
160 let mut table = TableWrapper::default();
161
162 table.max_width = match self.max_width.cmp(&0) {
163 std::cmp::Ordering::Less => None,
164 std::cmp::Ordering::Equal => termsize::get().map(|size| size.cols - 2),
165 std::cmp::Ordering::Greater => Some(self.max_width as u16),
166 };
167
168 let preset = if self.fancy_tables {
169 comfy_table::presets::UTF8_FULL
170 } else {
171 comfy_table::presets::NOTHING
172 };
173
174 table.load_preset(preset);
175
176 table
177 }
178}
179
180fn file_from_path(
181 path: impl AsRef<Path>,
182) -> config::File<config::FileSourceFile, config::FileFormat> {
183 config::File::new(
184 path.as_ref().to_str().unwrap_or_default(),
185 config::FileFormat::Toml,
186 )
187 .required(false)
188}
189
190fn add_berg_config_file(dir: impl AsRef<Path>) -> PathBuf {
191 dir.as_ref().join("berg.toml")
192}
193
194fn default_editor() -> String {
195 std::env::var("EDITOR")
196 .or(std::env::var("VISUAL"))
197 .unwrap_or_else(|_| {
198 let os_native_editor = if cfg!(target_os = "windows") {
199 "notepad"
201 } else if cfg!(target_os = "macos") {
202 "textedit"
204 } else {
205 "vi"
207 };
208 String::from(os_native_editor)
209 })
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 fn make_config(path: PathBuf, config: BergConfig) -> anyhow::Result<()> {
217 let config_path = add_berg_config_file(path);
218 let toml = toml::to_string(&config)?;
219 std::fs::write(config_path, toml)?;
220 Ok(())
221 }
222
223 fn delete_config(path: PathBuf) -> anyhow::Result<()> {
224 let config_path = add_berg_config_file(path);
225 std::fs::remove_file(config_path)?;
226 Ok(())
227 }
228
229 #[test]
230 #[ignore = "doesn't work on nix in 'ci' because of no r/w permissions on the system"]
231 fn berg_config_integration_test() -> anyhow::Result<()> {
232 let local_dir = current_dir()?;
235 std::fs::create_dir_all(local_dir)?;
236 let global_dir = berg_config_dir()?;
237 std::fs::create_dir_all(global_dir)?;
238
239 let config = BergConfig {
241 base_url: String::from("local"),
242 ..Default::default()
243 };
244 make_config(current_dir()?, config)?;
245 let config = BergConfig::new();
246 assert!(config.is_ok(), "{config:?}");
247 let config = config.unwrap();
248 assert_eq!(config.base_url.as_str(), "local");
249 delete_config(current_dir()?)?;
250
251 let config = BergConfig {
253 base_url: String::from("global"),
254 ..Default::default()
255 };
256 make_config(berg_config_dir()?, config)?;
257 let config = BergConfig::new();
258 assert!(config.is_ok(), "{config:?}");
259 let config = config.unwrap();
260 assert_eq!(config.base_url.as_str(), "global", "{0:?}", config.base_url);
261 delete_config(berg_config_dir()?)?;
262
263 let config = BergConfig::new();
265 assert!(config.is_ok(), "{config:?}");
266 let config = config.unwrap();
267 assert_eq!(config.base_url.as_str(), "codeberg.org/");
268
269 {
271 let config = BergConfig {
273 base_url: String::from("local"),
274 ..Default::default()
275 };
276 make_config(current_dir()?, config)?;
277 }
278 {
279 let config = BergConfig {
281 base_url: String::from("global"),
282 ..Default::default()
283 };
284 make_config(berg_config_dir()?, config)?;
285 }
286 let config = BergConfig::new();
287 assert!(config.is_ok(), "{config:?}");
288 let config = config.unwrap();
289 assert_eq!(config.base_url.as_str(), "local");
290 delete_config(current_dir()?)?;
291 delete_config(berg_config_dir()?)?;
292
293 Ok(())
294 }
295}