use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum ConfigScope {
#[default]
Global,
Project(String),
}
impl ConfigScope {
pub fn display_name(&self) -> &str {
match self {
ConfigScope::Global => "Global",
ConfigScope::Project(name) => name,
}
}
pub fn is_global(&self) -> bool {
matches!(self, ConfigScope::Global)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigBoolField {
Review,
Commit,
PullRequest,
PullRequestDraft,
Worktree,
WorktreeCleanup,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigTextField {
WorktreePathPattern,
}
pub type BoolFieldChanges = Vec<(ConfigBoolField, bool)>;
pub type TextFieldChanges = Vec<(ConfigTextField, String)>;
#[derive(Debug, Default)]
pub struct ConfigEditorActions {
pub create_project_config: Option<String>,
pub bool_changes: Vec<(ConfigBoolField, bool)>,
pub text_changes: Vec<(ConfigTextField, String)>,
pub is_global: bool,
pub project_name: Option<String>,
pub reset_to_defaults: bool,
}
pub const CONFIG_SCOPE_ROW_HEIGHT: f32 = 44.0;
pub const CONFIG_SCOPE_ROW_PADDING_H: f32 = 12.0;
pub const CONFIG_SCOPE_ROW_PADDING_V: f32 = 8.0;
#[derive(Debug, Default)]
pub struct ConfigTabState {
pub selected_scope: ConfigScope,
pub scope_projects: Vec<String>,
pub scope_has_config: HashMap<String, bool>,
pub cached_global_config: Option<crate::config::Config>,
pub global_config_error: Option<String>,
pub cached_project_config: Option<(String, crate::config::Config)>,
pub project_config_error: Option<String>,
pub last_modified: Option<Instant>,
}
impl ConfigTabState {
pub fn new() -> Self {
Self::default()
}
pub fn selected_scope(&self) -> &ConfigScope {
&self.selected_scope
}
pub fn set_selected_scope(&mut self, scope: ConfigScope) {
self.selected_scope = scope;
}
pub fn scope_projects(&self) -> &[String] {
&self.scope_projects
}
pub fn project_has_config(&self, project_name: &str) -> bool {
self.scope_has_config
.get(project_name)
.copied()
.unwrap_or(false)
}
pub fn refresh_scope_data(&mut self) {
if let Ok(projects) = crate::config::list_projects() {
self.scope_projects = projects;
self.scope_has_config.clear();
for project in &self.scope_projects {
if let Ok(config_path) = crate::config::project_config_path_for(project) {
self.scope_has_config
.insert(project.clone(), config_path.exists());
}
}
}
if self.selected_scope.is_global() && self.cached_global_config.is_none() {
self.load_global_config();
}
if let ConfigScope::Project(project_name) = &self.selected_scope {
let needs_load = match &self.cached_project_config {
Some((cached_name, _)) => cached_name != project_name,
None => self.project_has_config(project_name),
};
if needs_load {
let project_name = project_name.clone();
self.load_project_config(&project_name);
}
}
}
pub fn load_global_config(&mut self) {
match crate::config::load_global_config() {
Ok(config) => {
self.cached_global_config = Some(config);
self.global_config_error = None;
}
Err(e) => {
self.cached_global_config = None;
self.global_config_error = Some(format!("Failed to load config: {}", e));
}
}
}
pub fn cached_global_config(&self) -> Option<&crate::config::Config> {
self.cached_global_config.as_ref()
}
pub fn global_config_error(&self) -> Option<&str> {
self.global_config_error.as_deref()
}
pub fn cached_project_config(&self, project_name: &str) -> Option<&crate::config::Config> {
self.cached_project_config
.as_ref()
.filter(|(name, _)| name == project_name)
.map(|(_, config)| config)
}
pub fn project_config_error(&self) -> Option<&str> {
self.project_config_error.as_deref()
}
pub fn load_project_config(&mut self, project_name: &str) {
let config_path = match crate::config::project_config_path_for(project_name) {
Ok(path) => path,
Err(e) => {
self.cached_project_config = None;
self.project_config_error = Some(format!("Failed to get config path: {}", e));
return;
}
};
if !config_path.exists() {
self.cached_project_config = None;
self.project_config_error = None;
return;
}
match std::fs::read_to_string(&config_path) {
Ok(content) => match toml::from_str::<crate::config::Config>(&content) {
Ok(config) => {
self.cached_project_config = Some((project_name.to_string(), config));
self.project_config_error = None;
}
Err(e) => {
self.cached_project_config = None;
self.project_config_error = Some(format!("Failed to parse config: {}", e));
}
},
Err(e) => {
self.cached_project_config = None;
self.project_config_error = Some(format!("Failed to read config: {}", e));
}
}
}
pub fn create_project_config_from_global(&mut self, project_name: &str) -> Result<(), String> {
let global_config = self.cached_global_config.clone().unwrap_or_default();
if let Err(e) = crate::config::save_project_config_for(project_name, &global_config) {
return Err(format!("Failed to create project config: {}", e));
}
self.scope_has_config.insert(project_name.to_string(), true);
self.cached_project_config = Some((project_name.to_string(), global_config));
self.project_config_error = None;
self.last_modified = Some(Instant::now());
Ok(())
}
pub fn apply_bool_changes(
&mut self,
is_global: bool,
project_name: Option<&str>,
changes: &[(ConfigBoolField, bool)],
) {
if changes.is_empty() {
return;
}
let config = if is_global {
self.cached_global_config.as_mut()
} else {
match (&mut self.cached_project_config, project_name) {
(Some((cached_name, config)), Some(project)) if cached_name == project => {
Some(config)
}
_ => None,
}
};
let Some(config) = config else {
return;
};
for (field, value) in changes {
match field {
ConfigBoolField::Review => config.review = *value,
ConfigBoolField::Commit => config.commit = *value,
ConfigBoolField::PullRequest => config.pull_request = *value,
ConfigBoolField::PullRequestDraft => config.pull_request_draft = *value,
ConfigBoolField::Worktree => config.worktree = *value,
ConfigBoolField::WorktreeCleanup => config.worktree_cleanup = *value,
}
}
let save_result = if is_global {
crate::config::save_global_config(config)
} else if let Some(project) = project_name {
crate::config::save_project_config_for(project, config)
} else {
return;
};
if let Err(e) = save_result {
if is_global {
self.global_config_error = Some(format!("Failed to save config: {}", e));
} else {
self.project_config_error = Some(format!("Failed to save config: {}", e));
}
} else {
self.last_modified = Some(Instant::now());
}
}
pub fn apply_text_changes(
&mut self,
is_global: bool,
project_name: Option<&str>,
changes: &[(ConfigTextField, String)],
) {
if changes.is_empty() {
return;
}
let config = if is_global {
self.cached_global_config.as_mut()
} else {
match (&mut self.cached_project_config, project_name) {
(Some((cached_name, config)), Some(project)) if cached_name == project => {
Some(config)
}
_ => None,
}
};
let Some(config) = config else {
return;
};
for (field, value) in changes {
match field {
ConfigTextField::WorktreePathPattern => {
config.worktree_path_pattern = value.clone();
}
}
}
let save_result = if is_global {
crate::config::save_global_config(config)
} else if let Some(project) = project_name {
crate::config::save_project_config_for(project, config)
} else {
return;
};
if let Err(e) = save_result {
if is_global {
self.global_config_error = Some(format!("Failed to save config: {}", e));
} else {
self.project_config_error = Some(format!("Failed to save config: {}", e));
}
} else {
self.last_modified = Some(Instant::now());
}
}
pub fn reset_to_defaults(&mut self, is_global: bool, project_name: Option<&str>) {
let default_config = crate::config::Config::default();
if is_global {
self.cached_global_config = Some(default_config.clone());
if let Err(e) = crate::config::save_global_config(&default_config) {
self.global_config_error = Some(format!("Failed to save config: {}", e));
} else {
self.last_modified = Some(Instant::now());
}
} else if let Some(project) = project_name {
self.cached_project_config = Some((project.to_string(), default_config.clone()));
if let Err(e) = crate::config::save_project_config_for(project, &default_config) {
self.project_config_error = Some(format!("Failed to save config: {}", e));
} else {
self.last_modified = Some(Instant::now());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_scope_enum_global_default() {
let scope = ConfigScope::default();
assert!(matches!(scope, ConfigScope::Global));
}
#[test]
fn test_config_scope_enum_display_names() {
assert_eq!(ConfigScope::Global.display_name(), "Global");
assert_eq!(
ConfigScope::Project("my-project".to_string()).display_name(),
"my-project"
);
}
#[test]
fn test_config_scope_is_global() {
assert!(ConfigScope::Global.is_global());
assert!(!ConfigScope::Project("test".to_string()).is_global());
}
#[test]
fn test_config_scope_equality() {
assert_eq!(ConfigScope::Global, ConfigScope::Global);
assert_eq!(
ConfigScope::Project("a".to_string()),
ConfigScope::Project("a".to_string())
);
assert_ne!(
ConfigScope::Project("a".to_string()),
ConfigScope::Project("b".to_string())
);
assert_ne!(ConfigScope::Global, ConfigScope::Project("a".to_string()));
}
#[test]
fn test_config_scope_constants_exist() {
assert!(CONFIG_SCOPE_ROW_HEIGHT > 0.0);
assert!(CONFIG_SCOPE_ROW_PADDING_H > 0.0);
assert!(CONFIG_SCOPE_ROW_PADDING_V > 0.0);
}
#[test]
fn test_config_tab_state_default() {
let state = ConfigTabState::new();
assert!(matches!(state.selected_scope, ConfigScope::Global));
assert!(state.scope_projects.is_empty());
assert!(state.scope_has_config.is_empty());
assert!(state.cached_global_config.is_none());
assert!(state.global_config_error.is_none());
assert!(state.cached_project_config.is_none());
assert!(state.project_config_error.is_none());
assert!(state.last_modified.is_none());
}
#[test]
fn test_config_tab_state_set_selected_scope() {
let mut state = ConfigTabState::new();
state.set_selected_scope(ConfigScope::Project("test-project".to_string()));
assert!(matches!(
state.selected_scope(),
ConfigScope::Project(name) if name == "test-project"
));
}
#[test]
fn test_config_tab_state_project_has_config() {
let mut state = ConfigTabState::new();
state.scope_has_config.insert("project-a".to_string(), true);
state
.scope_has_config
.insert("project-b".to_string(), false);
assert!(state.project_has_config("project-a"));
assert!(!state.project_has_config("project-b"));
assert!(!state.project_has_config("project-c")); }
#[test]
fn test_config_bool_field_enum_variants() {
let _ = ConfigBoolField::Review;
let _ = ConfigBoolField::Commit;
let _ = ConfigBoolField::PullRequest;
let _ = ConfigBoolField::PullRequestDraft;
let _ = ConfigBoolField::Worktree;
let _ = ConfigBoolField::WorktreeCleanup;
}
#[test]
fn test_config_editor_actions_default() {
let actions = ConfigEditorActions::default();
assert!(actions.create_project_config.is_none());
assert!(actions.bool_changes.is_empty());
assert!(actions.text_changes.is_empty());
assert!(!actions.is_global);
assert!(actions.project_name.is_none());
assert!(!actions.reset_to_defaults);
}
}