use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ProjectDiscoveryError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Not a directory: {0}")]
NotDirectory(PathBuf),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
pub global_shortcut: Option<String>,
#[serde(default)]
pub roles: std::collections::HashMap<String, crate::Role>,
}
impl ProjectConfig {
pub fn from_file(path: &Path) -> Result<Self, ProjectDiscoveryError> {
let content = std::fs::read_to_string(path)?;
let config: ProjectConfig = serde_json::from_str(&content)?;
Ok(config)
}
}
pub fn discover(start_dir: Option<&Path>) -> Result<Option<PathBuf>, ProjectDiscoveryError> {
let start_dir = match start_dir {
Some(d) => d.to_path_buf(),
None => std::env::current_dir()?,
};
let mut current = Some(start_dir);
while let Some(dir) = current {
let terraphim_dir = dir.join(".terraphim");
if terraphim_dir.is_dir() {
let canonical = terraphim_dir.canonicalize()?;
return Ok(Some(canonical));
}
current = dir.parent().map(|p| p.to_path_buf());
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn temp_dir_with_structure(base: &TempDir, structure: &[&str]) -> PathBuf {
let base_path = base.path().to_path_buf();
for path in structure {
let full_path = base_path.join(path);
if path.ends_with('/') {
fs::create_dir_all(&full_path).unwrap();
} else {
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&full_path, "{}").unwrap();
}
}
base_path
}
#[test]
fn test_discover_finds_terraphim_dir() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(&temp, &["work/", "work/.terraphim/", "work/src/"]);
let result = discover(Some(&base.join("work/src"))).unwrap();
let expected = std::fs::canonicalize(base.join("work/.terraphim")).unwrap();
assert_eq!(result, Some(expected));
}
#[test]
fn test_discover_not_found() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(&temp, &["src/", "src/main.rs"]);
let result = discover(Some(&base.join("src"))).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_discover_from_current_dir() {
let original_dir = std::env::current_dir().unwrap();
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(&temp, &[".terraphim/"]);
std::env::set_current_dir(&base).unwrap();
let result = discover(None).unwrap();
let expected = std::fs::canonicalize(base.join(".terraphim")).unwrap();
assert_eq!(result, Some(expected));
std::env::set_current_dir(original_dir).unwrap();
}
#[test]
fn test_discover_upwards_search() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(
&temp,
&["project/", "project/.terraphim/", "project/src/main.rs"],
);
let result = discover(Some(&base.join("project/src"))).unwrap();
let expected = std::fs::canonicalize(base.join("project/.terraphim")).unwrap();
assert_eq!(result, Some(expected));
}
#[test]
fn test_discover_multiple_levels_up() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(
&temp,
&[
"a/",
"a/b/",
"a/b/c/",
"a/b/c/.terraphim/",
"a/b/c/src/main.rs",
],
);
let result = discover(Some(&base.join("a/b/c/src"))).unwrap();
let expected = std::fs::canonicalize(base.join("a/b/c/.terraphim")).unwrap();
assert_eq!(result, Some(expected));
}
#[test]
fn test_project_config_from_file() {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.json");
let json = r#"{"global_shortcut": "Ctrl+Shift+T", "roles": {}}"#;
fs::write(&config_path, json).unwrap();
let config = ProjectConfig::from_file(&config_path).unwrap();
assert_eq!(config.global_shortcut, Some("Ctrl+Shift+T".to_string()));
}
#[test]
fn test_project_config_from_file_empty() {
let temp = TempDir::new().unwrap();
let config_path = temp.path().join("config.json");
fs::write(&config_path, "{}").unwrap();
let config = ProjectConfig::from_file(&config_path).unwrap();
assert_eq!(config.global_shortcut, None);
assert!(config.roles.is_empty());
}
#[test]
fn test_discover_returns_none_for_missing() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(&temp, &["src/", "src/main.rs"]);
let result = discover(Some(&base.join("src"))).unwrap();
assert!(result.is_none());
}
#[test]
fn test_discover_root_finds_terraphim() {
let temp = TempDir::new().unwrap();
let base = temp_dir_with_structure(&temp, &[".terraphim/"]);
let result = discover(Some(&base)).unwrap();
let expected = std::fs::canonicalize(base.join(".terraphim")).unwrap();
assert_eq!(result, Some(expected));
}
#[test]
fn test_discover_symlink_to_real_dir() {
let temp = TempDir::new().unwrap();
let real = temp.path().join("real");
let linked = temp.path().join("linked");
fs::create_dir_all(&real.join(".terraphim")).unwrap();
fs::create_dir_all(&real.join("src")).unwrap();
std::os::unix::fs::symlink(&real, &linked).unwrap();
let canonical = std::fs::canonicalize(&real.join(".terraphim")).unwrap();
let result = discover(Some(&linked.join("src"))).unwrap();
assert_eq!(result, Some(canonical));
}
}