notes vault library and CLI (Obsidian-compatible)
git clone https://git.lucas.co/cce-vault.git
src/config.rs (3.6K)
1 //! Where the vault is.
2 //!
3 //! In order: an explicit path (a `--vault` flag), `$CCE_VAULT`, then the
4 //! shared cce config, `~/.config/cce/config.kdl`:
5 //!
6 //! ```kdl
7 //! vault {
8 //! path "~/Dropbox/Apps/remotely-save/Vault 1"
9 //! }
10 //! ```
11 //!
12 //! The config is read with the `kdl` crate directly rather than through
13 //! cce-ui's loader, so that this crate stays free of the toolkit and a
14 //! shell tool or test can use it without a Wayland stack.
15
16 use std::path::{Path, PathBuf};
17
18 #[derive(Debug)]
19 pub enum ConfigError {
20 /// No vault is configured anywhere.
21 Unset(PathBuf),
22 /// A vault is configured but is not a directory.
23 Missing(PathBuf),
24 Parse(PathBuf, String),
25 }
26
27 impl std::fmt::Display for ConfigError {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 ConfigError::Unset(cfg) => write!(
31 f,
32 "no vault configured: set CCE_VAULT, pass --vault, or add `vault {{ path \"…\" }}` to {}",
33 cfg.display()
34 ),
35 ConfigError::Missing(p) => write!(f, "vault is not a directory: {}", p.display()),
36 ConfigError::Parse(cfg, e) => write!(f, "{}: {e}", cfg.display()),
37 }
38 }
39 }
40
41 impl std::error::Error for ConfigError {}
42
43 pub fn config_path() -> PathBuf {
44 std::env::var_os("XDG_CONFIG_HOME")
45 .map(PathBuf::from)
46 .filter(|p| p.is_absolute())
47 .unwrap_or_else(|| home().join(".config"))
48 .join("cce")
49 .join("config.kdl")
50 }
51
52 fn home() -> PathBuf {
53 PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
54 }
55
56 fn expand(path: &str) -> PathBuf {
57 match path.strip_prefix("~/") {
58 Some(rest) => home().join(rest),
59 None if path == "~" => home(),
60 None => PathBuf::from(path),
61 }
62 }
63
64 /// The `vault` block's path from a config document, if it names one.
65 /// Accepts `vault { path "…" }` and the one-line `vault path="…"`.
66 pub fn path_from_kdl(text: &str) -> Result<Option<PathBuf>, String> {
67 let doc: kdl::KdlDocument = text.parse().map_err(|e: kdl::KdlError| e.to_string())?;
68 let Some(node) = doc.get("vault") else { return Ok(None) };
69 let from_child = node.children().and_then(|c| c.get_arg("path")).and_then(|v| v.as_string());
70 let from_prop = node.get("path").and_then(|e| e.value().as_string());
71 Ok(from_child.or(from_prop).map(expand))
72 }
73
74 pub fn vault_root(explicit: Option<&Path>) -> Result<PathBuf, ConfigError> {
75 let cfg = config_path();
76 let chosen = if let Some(p) = explicit {
77 p.to_path_buf()
78 } else if let Some(p) = std::env::var_os("CCE_VAULT").filter(|v| !v.is_empty()) {
79 expand(&p.to_string_lossy())
80 } else {
81 let text = match std::fs::read_to_string(&cfg) {
82 Ok(t) => t,
83 Err(_) => return Err(ConfigError::Unset(cfg)),
84 };
85 match path_from_kdl(&text) {
86 Ok(Some(p)) => p,
87 Ok(None) => return Err(ConfigError::Unset(cfg)),
88 Err(e) => return Err(ConfigError::Parse(cfg, e)),
89 }
90 };
91 if chosen.is_dir() {
92 Ok(chosen)
93 } else {
94 Err(ConfigError::Missing(chosen))
95 }
96 }
97
98 #[cfg(test)]
99 mod tests {
100 use super::*;
101
102 #[test]
103 fn kdl_forms() {
104 let home = home();
105 assert_eq!(
106 path_from_kdl("layout { gap 4; }\nvault {\n path \"~/Notes\"\n}\n").unwrap(),
107 Some(home.join("Notes"))
108 );
109 assert_eq!(path_from_kdl("vault path=\"/v\"").unwrap(), Some(PathBuf::from("/v")));
110 assert_eq!(path_from_kdl("other 1").unwrap(), None);
111 assert!(path_from_kdl("vault {").is_err());
112 }
113 }