gitmoji_rs/cmd/
config.rs

1use std::fmt::{self, Display};
2use std::path::{Path, PathBuf};
3
4use console::Term;
5use dialoguer::theme::ColorfulTheme;
6use dialoguer::{Confirm, Input, Select};
7use directories::ProjectDirs;
8use tokio::fs;
9use tracing::{info, warn};
10
11use crate::{git, EmojiFormat, Error, GitmojiConfig, LocalGitmojiConfig, Result, DEFAULT_URL};
12
13const CONFIG_FILE: &str = "gitmojis.toml";
14const CONFIG_LOCAL_FILE: &str = "./.gitmojis.toml";
15const GIT_CONFIG_LOCAL_FILE: &str = "gitmoji.file";
16const DIR_QUALIFIER: &str = "com.github";
17const DIR_ORGANIZATION: &str = "ilaborie";
18const DIR_APPLICATION: &str = "gitmoji-rs";
19#[derive(Debug, Clone)]
20struct FormatItem<'d> {
21    name: &'d str,
22    value: EmojiFormat,
23}
24
25impl Display for FormatItem<'_> {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "{}", self.name)
28    }
29}
30
31const FORMAT_ITEMS: &[FormatItem<'static>] = &[
32    FormatItem {
33        name: ":smile:",
34        value: EmojiFormat::UseCode,
35    },
36    FormatItem {
37        name: "😄",
38        value: EmojiFormat::UseEmoji,
39    },
40];
41
42pub fn create_config(term: &Term) -> Result<GitmojiConfig> {
43    let theme = ColorfulTheme::default();
44    let auto_add = Confirm::with_theme(&theme)
45        .with_prompt(r#"Enable automatic "git add .""#)
46        .default(false)
47        .interact_on(term)?;
48
49    let format_idx = Select::with_theme(&theme)
50        .with_prompt("Select how emojis should be used in commits")
51        .default(0)
52        .items(FORMAT_ITEMS)
53        .interact_on(term)?;
54    let format = FORMAT_ITEMS[format_idx].value;
55
56    let signed = Confirm::with_theme(&theme)
57        .with_prompt("Enable signed commits")
58        .default(false)
59        .interact_on(term)?;
60
61    let scope = Confirm::with_theme(&theme)
62        .with_prompt("Enable scope prompt")
63        .default(false)
64        .interact_on(term)?;
65
66    let update_url = Input::with_theme(&theme)
67        .with_prompt("Set gitmojis api url")
68        .default(DEFAULT_URL.to_string())
69        .validate_with(validate_url)
70        .interact_text_on(term)?
71        .parse()?;
72
73    let config = GitmojiConfig::new(auto_add, format, signed, scope, update_url);
74    Ok(config)
75}
76
77#[allow(clippy::ptr_arg)]
78fn validate_url(s: &String) -> Result<()> {
79    let _url = s.parse::<url::Url>()?;
80    Ok(())
81}
82
83/// Get the configuration file
84///
85/// # Errors
86/// Fail if we cannot create the parent directory
87pub async fn get_config_file() -> Result<PathBuf> {
88    let project_dir = ProjectDirs::from(DIR_QUALIFIER, DIR_ORGANIZATION, DIR_APPLICATION)
89        .ok_or_else(|| Error::CannotGetProjectConfigFile {
90            cause: "cannot define project dir".to_string(),
91        })?;
92
93    let config_dir = project_dir.config_dir();
94    fs::create_dir_all(config_dir)
95        .await
96        .map_err(|err| Error::CannotGetProjectConfigFile {
97            cause: err.to_string(),
98        })?;
99
100    let mut config_file = config_dir.to_path_buf();
101    config_file.push(CONFIG_FILE);
102
103    Ok(config_file)
104}
105
106async fn read_config() -> Result<GitmojiConfig> {
107    let config_file = get_config_file().await?;
108    info!("Read config file {config_file:?}");
109    let bytes = fs::read(config_file).await?;
110    let mut config = toml_edit::de::from_slice::<GitmojiConfig>(&bytes)?;
111    let local_config = read_local_config().await?;
112    config.merge(&local_config);
113
114    Ok(config)
115}
116
117async fn read_local_config() -> Result<LocalGitmojiConfig> {
118    let mut path = git::get_config_value(GIT_CONFIG_LOCAL_FILE).await?;
119    if path.is_empty() {
120        path = String::from(CONFIG_LOCAL_FILE);
121    }
122    let file = Path::new(&path);
123    let result = if file.exists() {
124        info!("Read local config file {file:?}");
125        let bytes = fs::read(file).await?;
126        toml_edit::de::from_slice(&bytes)?
127    } else {
128        warn!("Cannot read local config, file {path:?} does not exists");
129        LocalGitmojiConfig::default()
130    };
131
132    Ok(result)
133}
134/// Read the user config file
135///
136/// # Errors
137/// Fail when the config file is not found
138pub async fn read_config_or_fail() -> Result<GitmojiConfig> {
139    read_config().await.map_err(|_| Error::MissingConfigFile)
140}
141
142/// Read the user config file, if the file does not exists, return the default configuration
143pub async fn read_config_or_default() -> GitmojiConfig {
144    read_config().await.unwrap_or_default()
145}
146
147/// Write config
148///
149/// # Errors
150/// Fail when I/O trouble to get or write the file
151/// Might fail during serialization of config
152pub async fn write_config(config: &GitmojiConfig) -> Result<()> {
153    let config_file = get_config_file().await?;
154    let contents = toml_edit::ser::to_string_pretty(config)?;
155    info!("Update config file {config_file:?}");
156    fs::write(config_file, contents).await?;
157    Ok(())
158}