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}