opencode_provider_manager/app/
state.rs1use crate::config_core::{ConfigLayer, ConfigPaths, OpenCodeConfig, ProviderConfig};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum AppMode {
8 MergedView,
10 SplitView,
12 AddProvider,
14 EditProvider { provider_id: String },
16 ModelSelector { provider_id: String },
18 AuthStatus,
20 Import,
22}
23
24#[derive(Debug)]
26pub struct AppState {
27 pub global_config: Option<OpenCodeConfig>,
29 pub project_config: Option<OpenCodeConfig>,
31 pub custom_config: Option<OpenCodeConfig>,
33 pub merged_config: OpenCodeConfig,
35 pub paths: ConfigPaths,
37 pub mode: AppMode,
39 pub dirty: bool,
41 dirty_global: bool,
43 dirty_project: bool,
44 dirty_custom: bool,
45 pub edit_layer: ConfigLayer,
47}
48
49impl AppState {
50 pub fn new() -> crate::config_core::Result<Self> {
52 let paths = ConfigPaths::discover()?;
53 Ok(Self {
54 global_config: None,
55 project_config: None,
56 custom_config: None,
57 merged_config: OpenCodeConfig::default(),
58 paths,
59 mode: AppMode::MergedView,
60 dirty: false,
61 dirty_global: false,
62 dirty_project: false,
63 dirty_custom: false,
64 edit_layer: ConfigLayer::Project,
65 })
66 }
67
68 pub fn load_configs(&mut self) -> crate::config_core::Result<()> {
73 self.global_config = if self.paths.global.exists() {
75 Some(crate::config_core::jsonc::read_config(&self.paths.global)?)
76 } else {
77 None
78 };
79
80 self.custom_config = if let Some(ref custom_path) = self.paths.custom {
82 if custom_path.exists() {
83 Some(crate::config_core::jsonc::read_config(custom_path)?)
84 } else {
85 None
86 }
87 } else {
88 None
89 };
90
91 self.project_config = if let Some(ref project_path) = self.paths.project {
93 if project_path.exists() {
94 Some(crate::config_core::jsonc::read_config(project_path)?)
95 } else {
96 None
97 }
98 } else {
99 None
100 };
101
102 let mut configs_to_merge = Vec::new();
104 if let Some(global) = &self.global_config {
105 configs_to_merge.push(global.clone());
106 }
107 if let Some(custom) = &self.custom_config {
108 configs_to_merge.push(custom.clone());
109 }
110 if let Some(project) = &self.project_config {
111 configs_to_merge.push(project.clone());
112 }
113
114 self.merged_config = crate::config_core::merge_configs(&configs_to_merge);
115 self.dirty = false;
116 self.dirty_global = false;
117 self.dirty_project = false;
118 self.dirty_custom = false;
119
120 Ok(())
121 }
122
123 pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderConfig> {
125 self.merged_config.provider.as_ref()?.get(provider_id)
126 }
127
128 pub fn provider_ids(&self) -> Vec<String> {
130 self.merged_config
131 .provider
132 .as_ref()
133 .map(|p| p.keys().cloned().collect())
134 .unwrap_or_default()
135 }
136
137 pub fn provider_ids_for_layer(&self, layer: crate::config_core::ConfigLayer) -> Vec<String> {
139 let config = match layer {
140 crate::config_core::ConfigLayer::Global => self.global_config.as_ref(),
141 crate::config_core::ConfigLayer::Project => self.project_config.as_ref(),
142 crate::config_core::ConfigLayer::Custom => self.custom_config.as_ref(),
143 };
144 config
145 .and_then(|c| c.provider.as_ref())
146 .map(|p| p.keys().cloned().collect())
147 .unwrap_or_default()
148 }
149
150 pub fn mark_dirty(&mut self, layer: crate::config_core::ConfigLayer) {
152 match layer {
153 crate::config_core::ConfigLayer::Global => self.dirty_global = true,
154 crate::config_core::ConfigLayer::Project => self.dirty_project = true,
155 crate::config_core::ConfigLayer::Custom => self.dirty_custom = true,
156 }
157 self.dirty = true;
158 }
159
160 pub fn mark_clean(&mut self, layer: crate::config_core::ConfigLayer) {
162 match layer {
163 crate::config_core::ConfigLayer::Global => self.dirty_global = false,
164 crate::config_core::ConfigLayer::Project => self.dirty_project = false,
165 crate::config_core::ConfigLayer::Custom => self.dirty_custom = false,
166 }
167 self.dirty = self.dirty_global || self.dirty_project || self.dirty_custom;
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[test]
176 fn test_app_state_creation() {
177 let state = AppState::new();
178 assert!(state.is_ok());
179 let state = state.unwrap();
180 assert_eq!(state.mode, AppMode::MergedView);
181 assert!(!state.dirty);
182 }
183
184 #[test]
185 fn test_app_state_default_mode() {
186 let state = AppState::new().unwrap();
187 assert!(matches!(state.mode, AppMode::MergedView));
188 }
189
190 #[test]
191 fn test_provider_ids_empty() {
192 let state = AppState::new().unwrap();
193 assert!(state.provider_ids().is_empty());
194 }
195}