ai/
config.rs

1use std::io::Write;
2use std::path::PathBuf;
3use std::fs::File;
4
5use serde::{Deserialize, Serialize};
6use config::{Config, FileFormat};
7use anyhow::{Context, Result};
8use lazy_static::lazy_static;
9use console::Emoji;
10
11// Constants
12const DEFAULT_TIMEOUT: i64 = 30;
13const DEFAULT_MAX_COMMIT_LENGTH: i64 = 72;
14const DEFAULT_MAX_TOKENS: i64 = 2024;
15const DEFAULT_MODEL: &str = "gpt-4o-mini";
16const DEFAULT_API_KEY: &str = "<PLACE HOLDER FOR YOUR API KEY>";
17
18#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
19pub struct App {
20  pub openai_api_key:    Option<String>,
21  pub model:             Option<String>,
22  pub max_tokens:        Option<usize>,
23  pub max_commit_length: Option<usize>,
24  pub timeout:           Option<usize>
25}
26
27#[derive(Debug)]
28pub struct ConfigPaths {
29  pub dir:  PathBuf,
30  pub file: PathBuf
31}
32
33lazy_static! {
34  static ref PATHS: ConfigPaths = ConfigPaths::new();
35  pub static ref APP: App = App::new().expect("Failed to load config");
36}
37
38impl ConfigPaths {
39  fn new() -> Self {
40    let dir = home::home_dir()
41      .expect("Failed to determine home directory")
42      .join(".config/git-ai");
43    let file = dir.join("config.ini");
44    Self { dir, file }
45  }
46
47  fn ensure_exists(&self) -> Result<()> {
48    if !self.dir.exists() {
49      std::fs::create_dir_all(&self.dir).with_context(|| format!("Failed to create config directory at {:?}", self.dir))?;
50    }
51    if !self.file.exists() {
52      File::create(&self.file).with_context(|| format!("Failed to create config file at {:?}", self.file))?;
53    }
54    Ok(())
55  }
56}
57
58impl App {
59  pub fn new() -> Result<Self> {
60    dotenv::dotenv().ok();
61    PATHS.ensure_exists()?;
62
63    let config = Config::builder()
64      .add_source(config::Environment::with_prefix("APP").try_parsing(true))
65      .add_source(config::File::new(PATHS.file.to_string_lossy().as_ref(), FileFormat::Ini))
66      .set_default("language", "en")?
67      .set_default("timeout", DEFAULT_TIMEOUT)?
68      .set_default("max_commit_length", DEFAULT_MAX_COMMIT_LENGTH)?
69      .set_default("max_tokens", DEFAULT_MAX_TOKENS)?
70      .set_default("model", DEFAULT_MODEL)?
71      .set_default("openai_api_key", DEFAULT_API_KEY)?
72      .build()?;
73
74    config
75      .try_deserialize()
76      .context("Failed to deserialize existing config. Please run `git ai config reset` and try again")
77  }
78
79  pub fn save(&self) -> Result<()> {
80    let contents = serde_ini::to_string(&self).context(format!("Failed to serialize config: {self:?}"))?;
81    let mut file = File::create(&PATHS.file).with_context(|| format!("Failed to create config file at {:?}", PATHS.file))?;
82    file
83      .write_all(contents.as_bytes())
84      .context("Failed to write config file")
85  }
86
87  pub fn update_model(&mut self, value: String) -> Result<()> {
88    self.model = Some(value);
89    self.save_with_message("model")
90  }
91
92  pub fn update_max_tokens(&mut self, value: usize) -> Result<()> {
93    self.max_tokens = Some(value);
94    self.save_with_message("max-tokens")
95  }
96
97  pub fn update_max_commit_length(&mut self, value: usize) -> Result<()> {
98    self.max_commit_length = Some(value);
99    self.save_with_message("max-commit-length")
100  }
101
102  pub fn update_openai_api_key(&mut self, value: String) -> Result<()> {
103    self.openai_api_key = Some(value);
104    self.save_with_message("openai-api-key")
105  }
106
107  fn save_with_message(&self, option: &str) -> Result<()> {
108    println!("{} Configuration option {} updated!", Emoji("✨", ":-)"), option);
109    self.save()
110  }
111}