use parking_lot::RwLock;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SpinnerVerbsMode {
Replace,
#[default]
Append,
}
#[derive(Debug, Clone, Deserialize)]
pub struct SpinnerVerbsConfig {
#[serde(default)]
pub mode: SpinnerVerbsMode,
#[serde(default)]
pub verbs: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeSettings {
pub spinner_verbs: Option<SpinnerVerbsConfig>,
}
pub struct ClaudeSettingsCache {
cache: RwLock<HashMap<PathBuf, ClaudeSettings>>,
user_settings: RwLock<Option<ClaudeSettings>>,
}
impl ClaudeSettingsCache {
pub fn new() -> Self {
Self {
cache: RwLock::new(HashMap::new()),
user_settings: RwLock::new(None),
}
}
pub fn get_settings(&self, cwd: Option<&str>) -> Option<ClaudeSettings> {
let user_settings = self.get_user_settings();
let cwd = cwd?;
let project_path = PathBuf::from(cwd);
{
let cache = self.cache.read();
if let Some(cached) = cache.get(&project_path) {
return self.merge_settings(user_settings.as_ref(), Some(cached));
}
}
let project_settings = self.read_project_settings(&project_path);
if let Some(ref settings) = project_settings {
let mut cache = self.cache.write();
cache.insert(project_path, settings.clone());
}
self.merge_settings(user_settings.as_ref(), project_settings.as_ref())
}
fn get_user_settings(&self) -> Option<ClaudeSettings> {
{
let cached = self.user_settings.read();
if cached.is_some() {
return cached.clone();
}
}
let home = dirs::home_dir()?;
let user_settings_path = home.join(".claude").join("settings.json");
let settings = Self::read_settings_file(&user_settings_path);
let mut cached = self.user_settings.write();
*cached = settings.clone();
settings
}
fn read_project_settings(&self, project_path: &Path) -> Option<ClaudeSettings> {
let claude_dir = project_path.join(".claude");
let local_path = claude_dir.join("settings.local.json");
let shared_path = claude_dir.join("settings.json");
let local_settings = Self::read_settings_file(&local_path);
let shared_settings = Self::read_settings_file(&shared_path);
self.merge_settings(shared_settings.as_ref(), local_settings.as_ref())
}
fn read_settings_file(path: &Path) -> Option<ClaudeSettings> {
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn merge_settings(
&self,
lower: Option<&ClaudeSettings>,
higher: Option<&ClaudeSettings>,
) -> Option<ClaudeSettings> {
match (lower, higher) {
(None, None) => None,
(Some(l), None) => Some(l.clone()),
(None, Some(h)) => Some(h.clone()),
(Some(l), Some(h)) => {
Some(ClaudeSettings {
spinner_verbs: h.spinner_verbs.clone().or_else(|| l.spinner_verbs.clone()),
})
}
}
}
#[allow(dead_code)]
pub fn clear(&self) {
self.cache.write().clear();
*self.user_settings.write() = None;
}
}
impl Default for ClaudeSettingsCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_creation() {
let cache = ClaudeSettingsCache::new();
assert!(cache.cache.read().is_empty());
}
#[test]
fn test_spinner_verbs_mode_default() {
assert_eq!(SpinnerVerbsMode::default(), SpinnerVerbsMode::Append);
}
#[test]
fn test_parse_spinner_verbs() {
let json = r#"{
"spinnerVerbs": {
"mode": "replace",
"verbs": ["Thinking", "Working"]
}
}"#;
let settings: ClaudeSettings = serde_json::from_str(json).unwrap();
let verbs = settings.spinner_verbs.unwrap();
assert_eq!(verbs.mode, SpinnerVerbsMode::Replace);
assert_eq!(verbs.verbs, vec!["Thinking", "Working"]);
}
#[test]
fn test_parse_spinner_verbs_append() {
let json = r#"{
"spinnerVerbs": {
"mode": "append",
"verbs": ["CustomVerb"]
}
}"#;
let settings: ClaudeSettings = serde_json::from_str(json).unwrap();
let verbs = settings.spinner_verbs.unwrap();
assert_eq!(verbs.mode, SpinnerVerbsMode::Append);
assert_eq!(verbs.verbs, vec!["CustomVerb"]);
}
#[test]
fn test_parse_empty_settings() {
let json = r#"{}"#;
let settings: ClaudeSettings = serde_json::from_str(json).unwrap();
assert!(settings.spinner_verbs.is_none());
}
#[test]
fn test_merge_settings() {
let cache = ClaudeSettingsCache::new();
let lower = ClaudeSettings {
spinner_verbs: Some(SpinnerVerbsConfig {
mode: SpinnerVerbsMode::Append,
verbs: vec!["LowerVerb".to_string()],
}),
};
let higher = ClaudeSettings {
spinner_verbs: Some(SpinnerVerbsConfig {
mode: SpinnerVerbsMode::Replace,
verbs: vec!["HigherVerb".to_string()],
}),
};
let merged = cache.merge_settings(Some(&lower), Some(&higher)).unwrap();
let verbs = merged.spinner_verbs.unwrap();
assert_eq!(verbs.mode, SpinnerVerbsMode::Replace);
assert_eq!(verbs.verbs, vec!["HigherVerb"]);
}
#[test]
fn test_merge_settings_lower_only() {
let cache = ClaudeSettingsCache::new();
let lower = ClaudeSettings {
spinner_verbs: Some(SpinnerVerbsConfig {
mode: SpinnerVerbsMode::Append,
verbs: vec!["LowerVerb".to_string()],
}),
};
let merged = cache.merge_settings(Some(&lower), None).unwrap();
let verbs = merged.spinner_verbs.unwrap();
assert_eq!(verbs.verbs, vec!["LowerVerb"]);
}
#[test]
fn test_get_settings_without_cwd() {
let cache = ClaudeSettingsCache::new();
let _result = cache.get_settings(None);
}
}