git.lucas.co / cce-vault
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 }