Skip to main content

parley/domain/
config.rs

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}