pijul_config/
lib.rs

1use std::collections::HashMap;
2use std::io::Write;
3use std::path::PathBuf;
4
5use anyhow::bail;
6use dialoguer::theme;
7use log::debug;
8use serde_derive::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct Global {
12    pub author: Author,
13    pub unrecord_changes: Option<usize>,
14    pub reset_overwrites_changes: Option<Choice>,
15    pub colors: Option<Choice>,
16    pub pager: Option<Choice>,
17    pub template: Option<Templates>,
18    pub ignore_kinds: Option<HashMap<String, Vec<String>>>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct Author {
23    // Older versions called this 'name', but 'username' is more descriptive
24    #[serde(alias = "name", default, skip_serializing_if = "String::is_empty")]
25    pub username: String,
26    #[serde(alias = "full_name", default, skip_serializing_if = "String::is_empty")]
27    pub display_name: String,
28    #[serde(default, skip_serializing_if = "String::is_empty")]
29    pub email: String,
30    #[serde(default, skip_serializing_if = "String::is_empty")]
31    pub origin: String,
32    // This has been moved to identity::Config, but we should still be able to read the values
33    #[serde(default, skip_serializing)]
34    pub key_path: Option<PathBuf>,
35}
36
37impl Default for Author {
38    fn default() -> Self {
39        Self {
40            username: String::new(),
41            email: String::new(),
42            display_name: whoami::realname(),
43            origin: String::new(),
44            key_path: None,
45        }
46    }
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub enum Choice {
51    #[serde(rename = "auto")]
52    Auto,
53    #[serde(rename = "always")]
54    Always,
55    #[serde(rename = "never")]
56    Never,
57}
58
59impl Default for Choice {
60    fn default() -> Self {
61        Self::Auto
62    }
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66pub struct Templates {
67    pub message: Option<PathBuf>,
68    pub description: Option<PathBuf>,
69}
70
71pub const GLOBAL_CONFIG_DIR: &str = ".pijulconfig";
72const CONFIG_DIR: &str = "pijul";
73
74pub fn global_config_dir() -> Option<PathBuf> {
75    if let Ok(path) = std::env::var("PIJUL_CONFIG_DIR") {
76        let dir = std::path::PathBuf::from(path);
77        Some(dir)
78    } else if let Some(mut dir) = dirs_next::config_dir() {
79        dir.push(CONFIG_DIR);
80        Some(dir)
81    } else {
82        None
83    }
84}
85
86impl Global {
87    pub fn load() -> Result<(Global, u64), anyhow::Error> {
88        if let Some(mut dir) = global_config_dir() {
89            dir.push("config.toml");
90            let (s, meta) = std::fs::read(&dir)
91                .and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
92                .or_else(|e| {
93                    // Read from `$HOME/.config/pijul` dir
94                    if let Some(mut dir) = dirs_next::home_dir() {
95                        dir.push(".config");
96                        dir.push(CONFIG_DIR);
97                        dir.push("config.toml");
98                        std::fs::read(&dir).and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
99                    } else {
100                        Err(e.into())
101                    }
102                })
103                .or_else(|e| {
104                    // Read from `$HOME/.pijulconfig`
105                    if let Some(mut dir) = dirs_next::home_dir() {
106                        dir.push(GLOBAL_CONFIG_DIR);
107                        std::fs::read(&dir).and_then(|x| Ok((x, std::fs::metadata(&dir)?)))
108                    } else {
109                        Err(e.into())
110                    }
111                })?;
112            debug!("s = {:?}", s);
113            if let Ok(t) = toml::from_slice(&s) {
114                let ts = meta
115                    .modified()?
116                    .duration_since(std::time::SystemTime::UNIX_EPOCH)
117                    .unwrap()
118                    .as_secs();
119                Ok((t, ts))
120            } else {
121                bail!("Could not read configuration file at {:?}", dir)
122            }
123        } else {
124            bail!("Global configuration file missing")
125        }
126    }
127}
128
129#[derive(Debug, Serialize, Deserialize, Default)]
130pub struct Config {
131    pub default_remote: Option<String>,
132    #[serde(default, skip_serializing_if = "Vec::is_empty")]
133    pub extra_dependencies: Vec<String>,
134    #[serde(default, skip_serializing_if = "Vec::is_empty")]
135    pub remotes: Vec<RemoteConfig>,
136    #[serde(default)]
137    pub hooks: Hooks,
138    pub unrecord_changes: Option<usize>,
139    pub reset_overwrites_changes: Option<Choice>,
140    pub colors: Option<Choice>,
141    pub pager: Option<Choice>,
142}
143
144#[derive(Debug, Serialize, Deserialize)]
145#[serde(untagged)]
146pub enum RemoteConfig {
147    Ssh {
148        name: String,
149        ssh: String,
150    },
151    Http {
152        name: String,
153        http: String,
154        #[serde(default)]
155        headers: HashMap<String, RemoteHttpHeader>,
156    },
157}
158
159impl RemoteConfig {
160    pub fn name(&self) -> &str {
161        match self {
162            RemoteConfig::Ssh { name, .. } => name,
163            RemoteConfig::Http { name, .. } => name,
164        }
165    }
166}
167
168#[derive(Debug, Serialize, Deserialize)]
169#[serde(untagged)]
170pub enum RemoteHttpHeader {
171    String(String),
172    Shell(Shell),
173}
174
175#[derive(Debug, Serialize, Deserialize)]
176pub struct Shell {
177    pub shell: String,
178}
179
180#[derive(Debug, Serialize, Deserialize, Default)]
181pub struct Hooks {
182    #[serde(default)]
183    pub record: Vec<HookEntry>,
184}
185
186#[derive(Debug, Serialize, Deserialize)]
187pub struct HookEntry(toml::Value);
188
189#[derive(Debug, Serialize, Deserialize)]
190struct RawHook {
191    command: String,
192    args: Vec<String>,
193}
194
195pub fn shell_cmd(s: &str) -> Result<String, anyhow::Error> {
196    let out = if cfg!(target_os = "windows") {
197        std::process::Command::new("cmd")
198            .args(&["/C", s])
199            .output()
200            .expect("failed to execute process")
201    } else {
202        std::process::Command::new(std::env::var("SHELL").unwrap_or("sh".to_string()))
203            .arg("-c")
204            .arg(s)
205            .output()
206            .expect("failed to execute process")
207    };
208    Ok(String::from_utf8(out.stdout)?.trim().to_string())
209}
210
211impl HookEntry {
212    pub fn run(&self, path: PathBuf) -> Result<(), anyhow::Error> {
213        let (proc, s) = match &self.0 {
214            toml::Value::String(ref s) => {
215                if s.is_empty() {
216                    return Ok(());
217                }
218                (
219                    if cfg!(target_os = "windows") {
220                        std::process::Command::new("cmd")
221                            .current_dir(path)
222                            .args(&["/C", s])
223                            .output()
224                            .expect("failed to execute process")
225                    } else {
226                        std::process::Command::new(
227                            std::env::var("SHELL").unwrap_or("sh".to_string()),
228                        )
229                        .current_dir(path)
230                        .arg("-c")
231                        .arg(s)
232                        .output()
233                        .expect("failed to execute process")
234                    },
235                    s.clone(),
236                )
237            }
238            v => {
239                let hook = v.clone().try_into::<RawHook>()?;
240                (
241                    std::process::Command::new(&hook.command)
242                        .current_dir(path)
243                        .args(&hook.args)
244                        .output()
245                        .expect("failed to execute process"),
246                    hook.command,
247                )
248            }
249        };
250        if !proc.status.success() {
251            let mut stderr = std::io::stderr();
252            writeln!(stderr, "Hook {:?} exited with code {:?}", s, proc.status)?;
253            std::process::exit(proc.status.code().unwrap_or(1))
254        }
255        Ok(())
256    }
257}
258
259#[derive(Debug, Serialize, Deserialize)]
260struct Remote_ {
261    ssh: Option<SshRemote>,
262    local: Option<String>,
263    url: Option<String>,
264}
265
266#[derive(Debug)]
267pub enum Remote {
268    Ssh(SshRemote),
269    Local { local: String },
270    Http { url: String },
271    None,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct SshRemote {
276    pub addr: String,
277}
278
279impl<'de> serde::Deserialize<'de> for Remote {
280    fn deserialize<D>(deserializer: D) -> Result<Remote, D::Error>
281    where
282        D: serde::de::Deserializer<'de>,
283    {
284        let r = Remote_::deserialize(deserializer)?;
285        if let Some(ssh) = r.ssh {
286            Ok(Remote::Ssh(ssh))
287        } else if let Some(local) = r.local {
288            Ok(Remote::Local { local })
289        } else if let Some(url) = r.url {
290            Ok(Remote::Http { url })
291        } else {
292            Ok(Remote::None)
293        }
294    }
295}
296
297impl serde::Serialize for Remote {
298    fn serialize<D>(&self, serializer: D) -> Result<D::Ok, D::Error>
299    where
300        D: serde::ser::Serializer,
301    {
302        let r = match *self {
303            Remote::Ssh(ref ssh) => Remote_ {
304                ssh: Some(ssh.clone()),
305                local: None,
306                url: None,
307            },
308            Remote::Local { ref local } => Remote_ {
309                local: Some(local.to_string()),
310                ssh: None,
311                url: None,
312            },
313            Remote::Http { ref url } => Remote_ {
314                local: None,
315                ssh: None,
316                url: Some(url.to_string()),
317            },
318            Remote::None => Remote_ {
319                local: None,
320                ssh: None,
321                url: None,
322            },
323        };
324        r.serialize(serializer)
325    }
326}
327
328/// Choose the right dialoguer theme based on user's config
329pub fn load_theme() -> Result<Box<dyn theme::Theme>, anyhow::Error> {
330    if let Ok((config, _)) = Global::load() {
331        let color_choice = config.colors.unwrap_or_default();
332
333        match color_choice {
334            Choice::Auto | Choice::Always => Ok(Box::new(theme::ColorfulTheme::default())),
335            Choice::Never => Ok(Box::new(theme::SimpleTheme)),
336        }
337    } else {
338        Ok(Box::new(theme::ColorfulTheme::default()))
339    }
340}