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
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,