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
83pub 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}
134pub async fn read_config_or_fail() -> Result<GitmojiConfig> {
139 read_config().await.map_err(|_| Error::MissingConfigFile)
140}
141
142pub async fn read_config_or_default() -> GitmojiConfig {
144 read_config().await.unwrap_or_default()
145}
146
147pub 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}