basalt_core/obsidian/
config.rs

1use dirs::config_dir;
2
3use serde::{Deserialize, Deserializer};
4use std::path::Path;
5use std::result;
6use std::{collections::BTreeMap, fs, path::PathBuf};
7
8use crate::obsidian::{Error, Result, Vault};
9
10/// Represents the Obsidian configuration, typically loaded from an `obsidian.json` file.
11#[derive(Debug, Clone, Default, PartialEq)]
12pub struct ObsidianConfig {
13    /// A mapping of vault (folder) names to [`Vault`] definitions.
14    vaults: BTreeMap<String, Vault>,
15}
16
17impl ObsidianConfig {
18    /// Attempts to locate and load the system's `obsidian.json` file as an [`ObsidianConfig`].
19    ///
20    /// Returns an [`Error`] if the filepath doesn't exist or JSON parsing failed.
21    pub fn load() -> Result<Self> {
22        obsidian_config_dir()
23            .map(|path_buf| ObsidianConfig::load_from(&path_buf))
24            .ok_or(Error::PathNotFound("Obsidian config directory".to_string()))?
25    }
26
27    /// Attempts to load `obsidian.json` file as an [`ObsidianConfig`] from the given directory
28    /// [`Path`].
29    ///
30    /// Returns an [`Error`] if the filepath doesn't exist or JSON parsing failed.
31    ///
32    /// # Examples
33    ///
34    /// ```
35    /// use basalt_core::obsidian::ObsidianConfig;
36    /// use std::path::Path;
37    ///
38    /// _ = ObsidianConfig::load_from(Path::new("./dir-with-config-file"));
39    /// ```
40    pub fn load_from(config_path: &Path) -> Result<Self> {
41        let contents = fs::read_to_string(config_path.join("obsidian.json"))?;
42        Ok(serde_json::from_str(&contents)?)
43    }
44
45    /// Returns an iterator over the vaults in the configuration.
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use basalt_core::obsidian::{ObsidianConfig, Vault};
51    ///
52    /// let config = ObsidianConfig::from([
53    ///     ("Obsidian", Vault::default()),
54    ///     ("Work", Vault::default()),
55    /// ]);
56    ///
57    /// let vaults = config.vaults();
58    ///
59    /// assert_eq!(vaults.len(), 2);
60    /// assert_eq!(vaults.get(0), Some(&Vault::default()).as_ref());
61    /// ```
62    pub fn vaults(&self) -> Vec<&Vault> {
63        self.vaults.values().collect()
64    }
65
66    /// Finds a vault by name, returning a reference if it exists.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use basalt_core::obsidian::{ObsidianConfig, Vault};
72    ///
73    /// let config = ObsidianConfig::from([
74    ///     ("Obsidian", Vault::default()),
75    ///     ("Work", Vault::default()),
76    /// ]);
77    ///
78    /// _ = config.get_vault_by_name("Obsidian");
79    /// ```
80    pub fn get_vault_by_name(&self, name: &str) -> Option<&Vault> {
81        self.vaults.get(name)
82    }
83
84    /// Gets the currently opened vault marked by Obsidian.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use basalt_core::obsidian::{ObsidianConfig, Vault};
90    ///
91    /// let config = ObsidianConfig::from([
92    ///     (
93    ///         "Obsidian",
94    ///         Vault {
95    ///             open: true,
96    ///             ..Vault::default()
97    ///         },
98    ///     ),
99    ///     ("Work", Vault::default()),
100    /// ]);
101    ///
102    /// _ = config.get_open_vault();
103    /// ```
104    pub fn get_open_vault(&self) -> Option<&Vault> {
105        self.vaults.values().find(|vault| vault.open)
106    }
107}
108
109impl<const N: usize> From<[(&str, Vault); N]> for ObsidianConfig {
110    /// # Examples
111    ///
112    /// ```
113    /// use basalt_core::obsidian::{ObsidianConfig, Vault};
114    ///
115    /// let config_1 = ObsidianConfig::from([
116    ///   ("Obsidian", Vault::default()),
117    ///   ("My Vault", Vault::default()),
118    /// ]);
119    ///
120    /// let config_2: ObsidianConfig = [
121    ///   ("Obsidian", Vault::default()),
122    ///   ("My Vault", Vault::default()),
123    /// ].into();
124    ///
125    /// assert_eq!(config_1, config_2);
126    /// ```
127    fn from(arr: [(&str, Vault); N]) -> Self {
128        Self {
129            vaults: BTreeMap::from(arr.map(|(name, vault)| (name.to_owned(), vault))),
130        }
131    }
132}
133
134impl<const N: usize> From<[(String, Vault); N]> for ObsidianConfig {
135    /// # Examples
136    ///
137    /// ```
138    /// use basalt_core::obsidian::{ObsidianConfig, Vault};
139    ///
140    /// let config_1 = ObsidianConfig::from([
141    ///   (String::from("Obsidian"), Vault::default()),
142    ///   (String::from("My Vault"), Vault::default()),
143    /// ]);
144    ///
145    /// let config_2: ObsidianConfig = [
146    ///   (String::from("Obsidian"), Vault::default()),
147    ///   (String::from("My Vault"), Vault::default()),
148    /// ].into();
149    ///
150    /// assert_eq!(config_1, config_2);
151    /// ```
152    fn from(arr: [(String, Vault); N]) -> Self {
153        Self {
154            vaults: BTreeMap::from(arr),
155        }
156    }
157}
158
159impl<'de> Deserialize<'de> for ObsidianConfig {
160    fn deserialize<D>(deserializer: D) -> result::Result<Self, D::Error>
161    where
162        D: Deserializer<'de>,
163    {
164        #[derive(Deserialize)]
165        struct Json {
166            vaults: BTreeMap<String, Vault>,
167        }
168
169        impl From<Json> for ObsidianConfig {
170            fn from(value: Json) -> Self {
171                ObsidianConfig {
172                    vaults: value
173                        .vaults
174                        .into_values()
175                        .map(|vault| (vault.name.clone(), vault))
176                        .collect(),
177                }
178            }
179        }
180
181        let deserialized: Json = Deserialize::deserialize(deserializer)?;
182        Ok(deserialized.into())
183    }
184}
185
186/// Returns the system path to Obsidian's config folder, if any.
187///
188/// For reference:
189/// - macOS:  `/Users/username/Library/Application Support/obsidian`
190/// - Windows: `%APPDATA%\Obsidian\`
191/// - Linux:   `$XDG_CONFIG_HOME/obsidian/` or `~/.config/obsidian/`
192///
193/// More info: [https://help.obsidian.md/Files+and+folders/How+Obsidian+stores+data]
194fn obsidian_config_dir() -> Option<PathBuf> {
195    #[cfg(any(target_os = "macos", target_os = "linux"))]
196    const OBSIDIAN_CONFIG_DIR_NAME: &str = "obsidian";
197
198    #[cfg(target_os = "windows")]
199    const OBSIDIAN_CONFIG_DIR_NAME: &str = "Obsidian";
200
201    config_dir().map(|config_path| config_path.join(OBSIDIAN_CONFIG_DIR_NAME))
202}