mod config;
mod overlay;
pub use config::{BackendConfig, ProfileConfig, WorkloadConfig};
pub use overlay::{templates, ProfileOverlay};
use config::validate_profile_name;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq)]
pub enum ProfileError {
NotFound(String),
InvalidName(String),
IoError(String),
ParseError(String),
DirectoryError(String),
}
impl std::fmt::Display for ProfileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(name) => write!(f, "Profile not found: {}", name),
Self::InvalidName(msg) => write!(f, "Invalid profile name: {}", msg),
Self::IoError(msg) => write!(f, "IO error: {}", msg),
Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
Self::DirectoryError(msg) => write!(f, "Directory error: {}", msg),
}
}
}
impl std::error::Error for ProfileError {}
pub type ProfileResult<T> = Result<T, ProfileError>;
#[derive(Debug, Clone)]
pub struct ProfileManager {
profile_dir: PathBuf,
cache: HashMap<String, ProfileConfig>,
default_profile: Option<String>,
}
impl ProfileManager {
pub fn new(profile_dir: PathBuf) -> Self {
Self {
profile_dir,
cache: HashMap::new(),
default_profile: None,
}
}
pub fn with_default_dir() -> Self {
let profile_dir = dirs::config_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("cbtop")
.join("profiles");
Self::new(profile_dir)
}
pub fn profile_dir(&self) -> &Path {
&self.profile_dir
}
pub fn set_default(&mut self, name: &str) {
self.default_profile = Some(name.to_string());
}
pub fn default_profile(&self) -> Option<&str> {
self.default_profile.as_deref()
}
pub fn ensure_directory(&self) -> ProfileResult<()> {
if !self.profile_dir.exists() {
std::fs::create_dir_all(&self.profile_dir)
.map_err(|e| ProfileError::DirectoryError(e.to_string()))?;
}
Ok(())
}
pub fn save_profile(&mut self, profile: &ProfileConfig) -> ProfileResult<PathBuf> {
self.ensure_directory()?;
let filename = format!("{}.toml", profile.name);
let path = self.profile_dir.join(&filename);
let toml_content = profile.to_toml()?;
std::fs::write(&path, toml_content).map_err(|e| ProfileError::IoError(e.to_string()))?;
self.cache.insert(profile.name.clone(), profile.clone());
Ok(path)
}
pub fn load_profile(&mut self, name: &str) -> ProfileResult<ProfileConfig> {
validate_profile_name(name)?;
if let Some(profile) = self.cache.get(name) {
return Ok(profile.clone());
}
let filename = format!("{}.toml", name);
let path = self.profile_dir.join(&filename);
if !path.exists() {
return Err(ProfileError::NotFound(name.to_string()));
}
let content =
std::fs::read_to_string(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
let profile = ProfileConfig::from_toml(&content)?;
self.cache.insert(name.to_string(), profile.clone());
Ok(profile)
}
pub fn load_default(&mut self) -> ProfileConfig {
if let Some(name) = &self.default_profile.clone() {
if let Ok(profile) = self.load_profile(name) {
return profile;
}
}
ProfileConfig::default()
}
pub fn list_profiles(&self) -> ProfileResult<Vec<String>> {
if !self.profile_dir.exists() {
return Ok(vec![]);
}
let entries = std::fs::read_dir(&self.profile_dir)
.map_err(|e| ProfileError::IoError(e.to_string()))?;
let mut profiles = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "toml") {
if let Some(stem) = path.file_stem() {
if let Some(name) = stem.to_str() {
profiles.push(name.to_string());
}
}
}
}
profiles.sort();
Ok(profiles)
}
pub fn delete_profile(&mut self, name: &str) -> ProfileResult<()> {
validate_profile_name(name)?;
let filename = format!("{}.toml", name);
let path = self.profile_dir.join(&filename);
if !path.exists() {
return Err(ProfileError::NotFound(name.to_string()));
}
std::fs::remove_file(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
self.cache.remove(name);
Ok(())
}
pub fn export_profile(&self, name: &str, export_path: &Path) -> ProfileResult<()> {
let profile = if let Some(cached) = self.cache.get(name) {
cached.clone()
} else {
let filename = format!("{}.toml", name);
let path = self.profile_dir.join(&filename);
if !path.exists() {
return Err(ProfileError::NotFound(name.to_string()));
}
let content =
std::fs::read_to_string(&path).map_err(|e| ProfileError::IoError(e.to_string()))?;
ProfileConfig::from_toml(&content)?
};
let toml_content = profile.to_toml()?;
std::fs::write(export_path, toml_content).map_err(|e| ProfileError::IoError(e.to_string()))
}
pub fn import_profile(&mut self, import_path: &Path) -> ProfileResult<ProfileConfig> {
if !import_path.exists() {
return Err(ProfileError::NotFound(import_path.display().to_string()));
}
let content = std::fs::read_to_string(import_path)
.map_err(|e| ProfileError::IoError(e.to_string()))?;
let profile = ProfileConfig::from_toml(&content)?;
self.save_profile(&profile)?;
Ok(profile)
}
pub fn profile_exists(&self, name: &str) -> bool {
if self.cache.contains_key(name) {
return true;
}
let filename = format!("{}.toml", name);
let path = self.profile_dir.join(&filename);
path.exists()
}
pub fn profile_count(&self) -> ProfileResult<usize> {
self.list_profiles().map(|p| p.len())
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
}
#[cfg(test)]
mod tests;