1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fs;
4use std::path::PathBuf;
5use dirs;
6
7const DEFAULT_MODEL_FAST: &str = "qwen2.5:3b";
8const DEFAULT_MODEL_SMART: &str = "qwen2.5:3b";
9const DEFAULT_ENDPOINT: &str = "http://localhost:11434";
10const DEFAULT_KEEP_ALIVE: &str = "10m";
11
12#[derive(Debug, Serialize, Deserialize, Clone)]
13pub struct Config {
14 #[serde(default = "default_model_fast")]
15 pub model_fast: String,
16 #[serde(default = "default_model_smart")]
17 pub model_smart: String,
18 #[serde(default)]
19 pub model: Option<String>,
20 #[serde(default = "default_endpoint")]
21 pub endpoint: String,
22 #[serde(default = "default_keep_alive")]
23 pub keep_alive: String,
24 #[serde(default)]
25 pub aliases: HashMap<String, String>,
26}
27
28fn default_model_fast() -> String {
29 DEFAULT_MODEL_FAST.to_string()
30}
31
32fn default_model_smart() -> String {
33 DEFAULT_MODEL_SMART.to_string()
34}
35
36fn default_endpoint() -> String {
37 DEFAULT_ENDPOINT.to_string()
38}
39
40fn default_keep_alive() -> String {
41 DEFAULT_KEEP_ALIVE.to_string()
42}
43
44impl Default for Config {
45 fn default() -> Self {
46 Self {
47 model_fast: default_model_fast(),
48 model_smart: default_model_smart(),
49 model: None,
50 endpoint: default_endpoint(),
51 keep_alive: default_keep_alive(),
52 aliases: HashMap::new(),
53 }
54 }
55}
56
57const COMPLEX_KEYWORDS: &[&str] = &[
58 "rewrite", "rebase", "squash", "cherry-pick", "cherry pick",
59 "bisect", "filter", "reflog", "submodule", "subtree",
60 "worktree", "every commit", "all commits", "multiple commits",
61 "rename commit", "reword", "interactive",
62 "conflict", "resolve", "hook", "migrate",
63 "convert", "split", "reorganize", "restructure",
64 "history", "rewrite history",
65 "how many", "how much", "who are", "who has", "which branches",
66 "pending", "review", "pull request", "pr ",
67 "compare", "between", "since", "contributors", "committers",
68 "analyze", "statistics", "stats", "summary",
69 "multiple branches", "all branches", "merge all",
70];
71
72pub fn is_complex_task(task: &str) -> bool {
73 let lower = task.to_lowercase();
74 COMPLEX_KEYWORDS.iter().any(|k| lower.contains(k))
75}
76
77impl Config {
78 pub fn config_path() -> Option<PathBuf> {
79 dirs::home_dir().map(|h| h.join(".git-cli.toml"))
80 }
81
82 pub fn load() -> Self {
83 let Some(path) = Self::config_path() else {
84 return Self::default();
85 };
86
87 match fs::read_to_string(&path) {
88 Ok(contents) => toml::from_str(&contents).unwrap_or_default(),
89 Err(_) => Self::default(),
90 }
91 }
92
93 pub fn save(&self) -> Result<(), String> {
94 let path = Self::config_path().ok_or("Could not determine home directory")?;
95 let contents =
96 toml::to_string_pretty(self).map_err(|e| format!("Failed to serialize config: {e}"))?;
97 fs::write(&path, contents).map_err(|e| format!("Failed to write {}: {e}", path.display()))
98 }
99
100 pub fn apply_overrides(mut self, model: Option<String>, endpoint: Option<String>) -> Self {
101 if let Some(m) = model {
102 self.model = Some(m);
103 }
104 if let Some(e) = endpoint {
105 self.endpoint = e;
106 }
107 self
108 }
109
110 pub fn select_model(&self, task: &str) -> String {
111 if let Some(ref m) = self.model {
112 return m.clone();
113 }
114 if is_complex_task(task) {
115 self.model_smart.clone()
116 } else {
117 self.model_fast.clone()
118 }
119 }
120
121 pub fn resolve_alias(&self, input: &str) -> String {
122 self.aliases
123 .get(input)
124 .cloned()
125 .unwrap_or_else(|| input.to_string())
126 }
127}
128
129#[derive(Debug, Serialize, Deserialize, Clone)]
130pub struct PromptExample {
131 pub task: String,
132 pub commands: String,
133}
134
135#[derive(Debug, Serialize, Deserialize, Clone)]
136pub struct PromptConfig {
137 #[serde(default)]
138 pub preamble: Option<String>,
139 #[serde(default)]
140 pub examples: Vec<PromptExample>,
141}
142
143impl Default for PromptConfig {
144 fn default() -> Self {
145 Self {
146 preamble: None,
147 examples: Vec::new(),
148 }
149 }
150}
151
152impl PromptConfig {
153 pub fn config_dir() -> Option<PathBuf> {
154 dirs::home_dir().map(|h| h.join(".config").join("git-cli"))
155 }
156
157 pub fn config_path() -> Option<PathBuf> {
158 Self::config_dir().map(|d| d.join("prompt.toml"))
159 }
160
161 pub fn load() -> Self {
162 let Some(path) = Self::config_path() else {
163 return Self::default();
164 };
165
166 match fs::read_to_string(&path) {
167 Ok(contents) => toml::from_str(&contents).unwrap_or_default(),
168 Err(_) => Self::default(),
169 }
170 }
171}