1use serde::{Deserialize, Serialize};
2
3use crate::domain::ai::AiProvider;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
6#[serde(default)]
7pub struct AppConfig {
8 #[serde(alias = "name", default = "default_user_name")]
9 pub user_name: String,
10 pub theme: String,
11 pub diff_view: DiffViewMode,
12 #[serde(default = "default_ignore_parley_dir")]
13 pub ignore_parley_dir: bool,
14 #[serde(default = "default_log_level")]
15 pub log_level: String,
16 pub ai: AiConfig,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "snake_case")]
21pub enum DiffViewMode {
22 SideBySide,
23 Unified,
24}
25
26impl DiffViewMode {
27 pub fn is_side_by_side(&self) -> bool {
28 matches!(self, Self::SideBySide)
29 }
30}
31
32impl Default for DiffViewMode {
33 fn default() -> Self {
34 Self::SideBySide
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum PromptTransport {
41 Stdin,
42 Argv,
43}
44
45impl Default for PromptTransport {
46 fn default() -> Self {
47 Self::Stdin
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52#[serde(default)]
53pub struct AiProviderConfig {
54 #[serde(alias = "program")]
55 pub client: String,
56 pub model: Option<String>,
57 pub model_arg: Option<String>,
58 pub args: Vec<String>,
59 pub prompt_transport: PromptTransport,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(default)]
64pub struct AiConfig {
65 pub timeout_seconds: u64,
66 pub default_provider: AiProvider,
67 pub codex: AiProviderConfig,
68 pub claude: AiProviderConfig,
69 pub opencode: AiProviderConfig,
70}
71
72pub fn default_user_name() -> String {
73 std::env::var("PARLEY_USER_NAME")
74 .ok()
75 .or_else(|| std::env::var("USER").ok())
76 .or_else(|| std::env::var("USERNAME").ok())
77 .filter(|value| !value.trim().is_empty())
78 .unwrap_or_else(|| "User".to_string())
79}
80
81pub fn default_log_level() -> String {
82 "info".to_string()
83}
84
85pub fn default_ignore_parley_dir() -> bool {
86 true
87}
88
89impl Default for AppConfig {
90 fn default() -> Self {
91 Self {
92 user_name: default_user_name(),
93 theme: "default".to_string(),
94 diff_view: DiffViewMode::default(),
95 ignore_parley_dir: default_ignore_parley_dir(),
96 log_level: default_log_level(),
97 ai: AiConfig::default(),
98 }
99 }
100}
101
102impl Default for AiProviderConfig {
103 fn default() -> Self {
104 Self {
105 client: String::new(),
106 model: None,
107 model_arg: Some("--model".to_string()),
108 args: Vec::new(),
109 prompt_transport: PromptTransport::Stdin,
110 }
111 }
112}
113
114impl AiProviderConfig {
115 pub fn with_client(client: &str) -> Self {
116 Self {
117 client: client.to_string(),
118 model: None,
119 ..Self::default()
120 }
121 }
122}
123
124impl Default for AiConfig {
125 fn default() -> Self {
126 let mut codex = AiProviderConfig::with_client("codex");
127 codex.args = vec!["exec".to_string()];
128 codex.prompt_transport = PromptTransport::Argv;
129
130 let mut claude = AiProviderConfig::with_client("claude");
131 claude.args = vec!["-p".to_string()];
132 claude.prompt_transport = PromptTransport::Argv;
133
134 let mut opencode = AiProviderConfig::with_client("opencode");
135 opencode.args = vec!["run".to_string()];
136 opencode.model_arg = Some("-m".to_string());
137 opencode.prompt_transport = PromptTransport::Argv;
138 Self {
139 timeout_seconds: 120,
140 default_provider: AiProvider::Opencode,
141 codex,
142 claude,
143 opencode,
144 }
145 }
146}
147
148impl AiConfig {
149 pub fn provider_config(&self, provider: AiProvider) -> &AiProviderConfig {
150 match provider {
151 AiProvider::Codex => &self.codex,
152 AiProvider::Claude => &self.claude,
153 AiProvider::Opencode => &self.opencode,
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::AppConfig;
161
162 #[test]
163 fn default_config_ignores_parley_dir() {
164 let config = AppConfig::default();
165
166 assert!(config.ignore_parley_dir);
167 }
168}