Skip to main content

dir_aspect/
lib.rs

1//! dir-aspect: Detect what kind of application folder a directory is.
2//!
3//! Checks for marker directories (`.git/`, `.obsidian/`, etc.) to determine
4//! which "aspects" a directory has. Useful for enabling feature-specific
5//! behavior based on the kind of project a directory represents.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use dir_aspect::{Aspect, detect_aspects};
11//!
12//! let aspects = detect_aspects(std::path::Path::new("/path/to/dir"));
13//! if aspects.contains(&Aspect::Git) {
14//!     println!("This is a git repository");
15//! }
16//! ```
17
18use std::path::Path;
19
20use serde::{Deserialize, Serialize};
21
22/// Detected aspects of a directory.
23///
24/// Aspects represent what kind of application folder a directory is,
25/// determined by the presence of marker directories or files.
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
27#[cfg_attr(feature = "specta", derive(specta::Type))]
28#[serde(rename_all = "lowercase")]
29pub enum Aspect {
30    /// Directory contains `.obsidian/` folder (Obsidian vault)
31    Obsidian,
32    /// Directory contains `.git/` folder (Git repository)
33    Git,
34    /// Default -- plain directory with no special markers
35    Generic,
36}
37
38/// Detect aspects of a directory by checking for marker directories.
39///
40/// Always includes [`Aspect::Generic`]. Adds [`Aspect::Obsidian`] if `.obsidian/`
41/// is present, and [`Aspect::Git`] if `.git/` is present.
42pub fn detect_aspects(path: &Path) -> Vec<Aspect> {
43    let mut aspects = vec![Aspect::Generic];
44
45    if path.join(".obsidian").is_dir() {
46        aspects.push(Aspect::Obsidian);
47    }
48
49    if path.join(".git").is_dir() {
50        aspects.push(Aspect::Git);
51    }
52
53    aspects
54}
55
56/// Look up the Obsidian vault ID for a directory by reading Obsidian's `obsidian.json`.
57///
58/// Returns `None` if Obsidian's config file doesn't exist, can't be parsed,
59/// or doesn't contain a vault matching the given path.
60pub fn detect_obsidian_vault_id(path: &Path) -> Option<String> {
61    let config_dir = dirs::config_dir()?;
62    let obsidian_json = config_dir.join("obsidian").join("obsidian.json");
63
64    let contents = std::fs::read_to_string(&obsidian_json).ok()?;
65    let parsed: serde_json::Value = serde_json::from_str(&contents).ok()?;
66
67    let vaults = parsed.get("vaults")?.as_object()?;
68    let canonical = path.canonicalize().ok()?;
69
70    for (id, info) in vaults {
71        if let Some(vault_path_str) = info.get("path").and_then(|v| v.as_str()) {
72            if let Ok(vault_canonical) = Path::new(vault_path_str).canonicalize() {
73                if vault_canonical == canonical {
74                    return Some(id.clone());
75                }
76            }
77        }
78    }
79
80    None
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use std::fs;
87
88    fn create_test_dir(name: &str) -> std::path::PathBuf {
89        let dir = std::env::temp_dir().join(format!("dir_aspect_test_{}", name));
90        let _ = fs::remove_dir_all(&dir);
91        fs::create_dir_all(&dir).expect("create test dir");
92        dir
93    }
94
95    fn cleanup(dir: &Path) {
96        let _ = fs::remove_dir_all(dir);
97    }
98
99    #[test]
100    fn plain_directory_returns_generic_only() {
101        let dir = create_test_dir("plain");
102        let aspects = detect_aspects(&dir);
103        assert_eq!(aspects, vec![Aspect::Generic]);
104        cleanup(&dir);
105    }
106
107    #[test]
108    fn obsidian_directory_detected() {
109        let dir = create_test_dir("obsidian");
110        fs::create_dir_all(dir.join(".obsidian")).expect("create .obsidian");
111        let aspects = detect_aspects(&dir);
112        assert_eq!(aspects, vec![Aspect::Generic, Aspect::Obsidian]);
113        cleanup(&dir);
114    }
115
116    #[test]
117    fn git_directory_detected() {
118        let dir = create_test_dir("git");
119        fs::create_dir_all(dir.join(".git")).expect("create .git");
120        let aspects = detect_aspects(&dir);
121        assert_eq!(aspects, vec![Aspect::Generic, Aspect::Git]);
122        cleanup(&dir);
123    }
124
125    #[test]
126    fn both_obsidian_and_git_detected() {
127        let dir = create_test_dir("both");
128        fs::create_dir_all(dir.join(".obsidian")).expect("create .obsidian");
129        fs::create_dir_all(dir.join(".git")).expect("create .git");
130        let aspects = detect_aspects(&dir);
131        assert_eq!(aspects, vec![Aspect::Generic, Aspect::Obsidian, Aspect::Git,]);
132        cleanup(&dir);
133    }
134}