feat: load mount config from KDL file
Hardcoded mounts forced users to rebuild to change volume mappings.
Config::load() reads ~/.config/ramekin/config.kdl via serde-kdl2,
falling back to the previous defaults when the file is absent.

Assisted-by: Claude Opus 4.6 via pi
change rltvnrrzlolrzuysymyvkqkmsswvqoqp
commit 1d702d3f3235e8a6ebc3f946157a0120a95ec8ec
author Alpha Chen <alpha@kejadlen.dev>
date
parent xornozzw
diff --git a/Cargo.lock b/Cargo.lock
index 43ea787..3ebde19 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -279,6 +279,17 @@ version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
 
+[[package]]
+name = "kdl"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81a29e7b50079ff44549f68c0becb1c73d7f6de2a4ea952da77966daf3d4761e"
+dependencies = [
+ "miette",
+ "num",
+ "winnow",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.5.0"
@@ -321,6 +332,16 @@ version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
 
+[[package]]
+name = "miette"
+version = "7.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7"
+dependencies = [
+ "cfg-if",
+ "unicode-width",
+]
+
 [[package]]
 name = "miniz_oxide"
 version = "0.8.9"
@@ -339,6 +360,79 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "object"
 version = "0.37.3"
@@ -404,6 +498,7 @@ dependencies = [
  "color-eyre",
  "fs-err",
  "serde",
+ "serde-kdl2",
  "serde_yaml",
  "shellexpand",
  "tracing",
@@ -461,6 +556,17 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-kdl2"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b9cea21f8a61b2de91d2edc3f523b9be71e380b6596d739638d9c20b70139de"
+dependencies = [
+ "kdl",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "serde_core"
 version = "1.0.228"
@@ -641,6 +747,12 @@ version = "1.0.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
 
+[[package]]
+name = "unicode-width"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
 [[package]]
 name = "unsafe-libyaml"
 version = "0.2.11"
@@ -680,6 +792,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "winnow"
+version = "0.6.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "xdg"
 version = "3.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 6965314..1afb4b1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,6 +10,7 @@ clap = { version = "*", features = ["derive"] }
 color-eyre = "*"
 fs-err = "*"
 serde = { version = "*", features = ["derive"] }
+serde-kdl2 = "*"
 serde_yaml = "*"
 shellexpand = "*"
 tracing = "*"
diff --git a/src/config.rs b/src/config.rs
index cacf727..5aaad2d 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,14 +1,18 @@
 use std::path::PathBuf;
 
-#[derive(Debug, PartialEq)]
+use color_eyre::eyre::{Context, Result};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct Config {
     pub mounts: Vec<Mount>,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
 pub struct Mount {
     pub source: String,
     pub target: Option<String>,
+    #[serde(default)]
     pub read_only: bool,
 }
 
@@ -22,6 +26,33 @@ pub struct ResolvedMount {
 
 impl Default for Config {
     fn default() -> Self {
+        Self::fallback()
+    }
+}
+
+impl Config {
+    /// Load configuration from `~/.config/ramekin/config.kdl`.
+    ///
+    /// If the file doesn't exist, create it with default contents.
+    /// If it exists but can't be parsed, return an error.
+    pub fn load() -> Result<Self> {
+        let xdg = xdg::BaseDirectories::with_prefix("ramekin");
+        let config_path = xdg
+            .place_config_file("config.kdl")
+            .wrap_err("failed to determine config file path")?;
+
+        if !config_path.exists() {
+            return Ok(Self::fallback());
+        }
+
+        let content =
+            fs_err::read_to_string(&config_path).wrap_err("failed to read config file")?;
+
+        serde_kdl2::from_str(&content).wrap_err("failed to parse config file")
+    }
+
+    /// Fallback configuration with hardcoded mounts.
+    fn fallback() -> Self {
         Self {
             mounts: vec![
                 Mount {
@@ -110,11 +141,46 @@ mod tests {
     use super::*;
 
     #[test]
-    fn default_has_three_mounts() {
-        let config = Config::default();
+    fn fallback_has_three_mounts() {
+        let config = Config::fallback();
         assert_eq!(config.mounts.len(), 3);
     }
 
+    #[test]
+    fn kdl_deserialization_works() {
+        let kdl_content = r#"
+            mounts{
+            source "~/.config/git"
+            read_only #true
+            }
+            mounts{
+            source "~/.config/jj"  
+            read_only #true
+            }
+            mounts{
+            source "~/.local/share/ranger"
+            read_only #false
+            }
+            mounts{
+            source "~/Downloads"
+            target "/root/downloads"
+            }
+        "#;
+
+        let parsed: Config = serde_kdl2::from_str(kdl_content).unwrap();
+        assert_eq!(parsed.mounts.len(), 4);
+        assert_eq!(parsed.mounts[0].source, "~/.config/git");
+        assert!(parsed.mounts[0].read_only);
+        assert_eq!(parsed.mounts[3].target, Some("/root/downloads".into()));
+    }
+
+    #[test]
+    fn kdl_parse_error_on_invalid_syntax() {
+        let invalid_kdl = "invalid_syntax_here{ missing closing brace";
+        let result: Result<Config, _> = serde_kdl2::from_str(invalid_kdl);
+        assert!(result.is_err());
+    }
+
     #[test]
     fn tilde_to_root_with_subpath() {
         assert_eq!(tilde_to_root("~/.config/git"), "/root/.config/git");
diff --git a/src/main.rs b/src/main.rs
index a59daa4..8cf529e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -139,7 +139,8 @@ impl Ramekin {
             .collect();
 
         // Config mounts (user-configurable, skipped when source doesn't exist)
-        mounts.extend(config::Config::default().resolve_mounts());
+        let config = config::Config::load().wrap_err("failed to load ramekin configuration")?;
+        mounts.extend(config.resolve_mounts());
 
         Ok(Self {
             workspace,