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}