1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use std::io::Write;
use std::path::PathBuf;
use std::fs::File;

use serde::{Deserialize, Serialize};
use config::{Config, FileFormat};
use anyhow::{Context, Result};
use lazy_static::lazy_static;
use console::{style, Emoji};
use clap::ArgMatches;

#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
pub struct App {
  pub openai_api_key:  Option<String>,
  pub model:           String,
  pub language:        String,
  pub max_diff_tokens: usize,
  pub max_length:      usize,
  pub timeout:         usize
}

impl App {
  #[allow(dead_code)]
  pub fn duration(&self) -> std::time::Duration {
    std::time::Duration::from_secs(self.timeout as u64)
  }
}

lazy_static! {
  pub static ref CONFIG_DIR: PathBuf = home::home_dir().unwrap().join(".config/git-ai");
  #[derive(Debug)]
  pub static ref APP: App = App::new().expect("Failed to load config");
  pub static ref CONFIG_PATH: PathBuf = CONFIG_DIR.join("config.ini");
}

impl App {
  pub fn new() -> Result<Self> {
    dotenv::dotenv().ok();

    if !CONFIG_DIR.exists() {
      std::fs::create_dir_all(CONFIG_DIR.to_str().unwrap()).context("Failed to create config directory")?;
      File::create(CONFIG_PATH.to_str().unwrap()).context("Failed to create config file")?;
    } else if !CONFIG_PATH.exists() {
      File::create(CONFIG_PATH.to_str().unwrap()).context("Failed to create config file")?;
    }

    let config = Config::builder()
      .add_source(config::Environment::with_prefix("APP").try_parsing(true))
      .add_source(config::File::new(CONFIG_PATH.to_str().unwrap(), FileFormat::Ini))
      .set_default("language", "en")?
      .set_default("timeout", 30)?
      .set_default("max_length", 72)?
      .set_default("max_diff_tokens", 3500)?
      .set_default("model", "gpt-4-1106-preview")?
      .build()?;

    config.try_deserialize().context("Failed to deserialize config")
  }

  pub fn save(&self) -> Result<()> {
    let contents = serde_ini::to_string(&self).context(format!("Failed to serialize config: {:?}", self))?;
    let mut file = File::create(CONFIG_PATH.to_str().unwrap()).context("Failed to create config file")?;
    file.write_all(contents.as_bytes()).context("Failed to write config file")
  }
}

pub fn run(args: &ArgMatches) -> Result<()> {
  let mut app = App::new()?;
  match args.subcommand() {
    Some(("model", args)) => {
      app.model = args.get_one::<String>("<VALUE>").context("Failed to parse model")?.clone();
    },
    Some(("language", args)) => {
      app.language = args.get_one::<String>("<VALUE>").context("Failed to parse language")?.clone();
    },
    Some(("max-diff-tokens", args)) => {
      app.max_diff_tokens = *args.get_one("max-diff-tokens").context("Failed to parse max-diff-tokens")?;
    },
    Some(("max-length", args)) => {
      app.max_length = *args.get_one("max-length").context("Failed to parse max-length")?;
    },
    Some(("openai-api-key", args)) => {
      app.openai_api_key = args
        .get_one::<String>("<VALUE>")
        .context("Failed to parse openai-api-key")?
        .clone()
        .into();
    },
    _ => unreachable!()
  }

  if let Some(key) = args.subcommand_name() {
    println!("{} Configuration option {} updated!", Emoji("✨", ":-)"), style(key).italic());
  }

  app.save()
}