basalt_core/obsidian/
config.rs

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