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