use crate::config_core::{ConfigLayer, ConfigPaths, OpenCodeConfig, ProviderConfig};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppMode {
MergedView,
SplitView,
AddProvider,
EditProvider { provider_id: String },
ModelSelector { provider_id: String },
AuthStatus,
Import,
}
#[derive(Debug)]
pub struct AppState {
pub global_config: Option<OpenCodeConfig>,
pub project_config: Option<OpenCodeConfig>,
pub custom_config: Option<OpenCodeConfig>,
pub merged_config: OpenCodeConfig,
pub paths: ConfigPaths,
pub mode: AppMode,
pub dirty: bool,
dirty_global: bool,
dirty_project: bool,
dirty_custom: bool,
pub edit_layer: ConfigLayer,
}
impl AppState {
pub fn new() -> crate::config_core::Result<Self> {
let paths = ConfigPaths::discover()?;
Ok(Self {
global_config: None,
project_config: None,
custom_config: None,
merged_config: OpenCodeConfig::default(),
paths,
mode: AppMode::MergedView,
dirty: false,
dirty_global: false,
dirty_project: false,
dirty_custom: false,
edit_layer: ConfigLayer::Project,
})
}
pub fn load_configs(&mut self) -> crate::config_core::Result<()> {
self.global_config = if self.paths.global.exists() {
Some(crate::config_core::jsonc::read_config(&self.paths.global)?)
} else {
None
};
self.custom_config = if let Some(ref custom_path) = self.paths.custom {
if custom_path.exists() {
Some(crate::config_core::jsonc::read_config(custom_path)?)
} else {
None
}
} else {
None
};
self.project_config = if let Some(ref project_path) = self.paths.project {
if project_path.exists() {
Some(crate::config_core::jsonc::read_config(project_path)?)
} else {
None
}
} else {
None
};
let mut configs_to_merge = Vec::new();
if let Some(global) = &self.global_config {
configs_to_merge.push(global.clone());
}
if let Some(custom) = &self.custom_config {
configs_to_merge.push(custom.clone());
}
if let Some(project) = &self.project_config {
configs_to_merge.push(project.clone());
}
self.merged_config = crate::config_core::merge_configs(&configs_to_merge);
self.dirty = false;
self.dirty_global = false;
self.dirty_project = false;
self.dirty_custom = false;
Ok(())
}
pub fn get_provider(&self, provider_id: &str) -> Option<&ProviderConfig> {
self.merged_config.provider.as_ref()?.get(provider_id)
}
pub fn provider_ids(&self) -> Vec<String> {
self.merged_config
.provider
.as_ref()
.map(|p| p.keys().cloned().collect())
.unwrap_or_default()
}
pub fn mark_dirty(&mut self, layer: crate::config_core::ConfigLayer) {
match layer {
crate::config_core::ConfigLayer::Global => self.dirty_global = true,
crate::config_core::ConfigLayer::Project => self.dirty_project = true,
crate::config_core::ConfigLayer::Custom => self.dirty_custom = true,
}
self.dirty = true;
}
pub fn mark_clean(&mut self, layer: crate::config_core::ConfigLayer) {
match layer {
crate::config_core::ConfigLayer::Global => self.dirty_global = false,
crate::config_core::ConfigLayer::Project => self.dirty_project = false,
crate::config_core::ConfigLayer::Custom => self.dirty_custom = false,
}
self.dirty = self.dirty_global || self.dirty_project || self.dirty_custom;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_app_state_creation() {
let state = AppState::new();
assert!(state.is_ok());
let state = state.unwrap();
assert_eq!(state.mode, AppMode::MergedView);
assert!(!state.dirty);
}
#[test]
fn test_app_state_default_mode() {
let state = AppState::new().unwrap();
assert!(matches!(state.mode, AppMode::MergedView));
}
#[test]
fn test_provider_ids_empty() {
let state = AppState::new().unwrap();
assert!(state.provider_ids().is_empty());
}
}