feat(cli): default dotfiles dir to ~/.notfiles, config to ~/.config/notfiles/notfiles.toml
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
Some checks failed
Public Repo Readiness / Check Public Repo Readiness (push) Has been cancelled
- Add `default_dotfiles_dir()` (~/.notfiles) and `default_config_path()` (~/.config/notfiles/notfiles.toml) to notcore::config - Add `Config::load_from(path)` for explicit config path; keep `Config::load(dir)` as the legacy/library variant (config inside dotfiles dir) - CLI resolves config via XDG default or --config override; library functions stay legacy so the public API is unaffected - `notfiles init` creates ~/.notfiles and ~/.config/notfiles/notfiles.toml - Add --config global flag to override the config path - Update integration tests to pass --config explicitly so they are isolated from any real ~/.config/notfiles/notfiles.toml on the test machine
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::Path;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::NotfilesError;
|
use crate::NotfilesError;
|
||||||
|
|
||||||
@@ -102,13 +102,28 @@ where
|
|||||||
toml::from_str(&content).map_err(|err| parse_error(path, err))
|
toml::from_str(&content).map_err(|err| parse_error(path, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the default config path: `~/.config/notfiles/notfiles.toml`.
|
||||||
|
pub fn default_config_path() -> PathBuf {
|
||||||
|
dirs::config_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~/.config"))
|
||||||
|
.join("notfiles")
|
||||||
|
.join("notfiles.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default dotfiles directory: `~/.notfiles`.
|
||||||
|
pub fn default_dotfiles_dir() -> PathBuf {
|
||||||
|
dirs::home_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("~"))
|
||||||
|
.join(".notfiles")
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
/// Load config from an explicit path. Returns `Config::default()` if the file does not exist.
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
pub fn load_from(config_path: &Path) -> Result<Self, NotfilesError> {
|
||||||
if !config_path.exists() {
|
if !config_path.exists() {
|
||||||
return Ok(Config::default());
|
return Ok(Config::default());
|
||||||
}
|
}
|
||||||
let content = std::fs::read_to_string(&config_path).map_err(|e| {
|
let content = std::fs::read_to_string(config_path).map_err(|e| {
|
||||||
NotfilesError::Config(format!("reading {}: {e}", config_path.display()))
|
NotfilesError::Config(format!("reading {}: {e}", config_path.display()))
|
||||||
})?;
|
})?;
|
||||||
let config: Config = toml::from_str(&content).map_err(|e| {
|
let config: Config = toml::from_str(&content).map_err(|e| {
|
||||||
@@ -117,6 +132,12 @@ impl Config {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load config from `<dotfiles_dir>/notfiles.toml` (legacy location).
|
||||||
|
/// Prefer [`Config::load_from`] with an explicit path in new call sites.
|
||||||
|
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
||||||
|
Self::load_from(&dotfiles_dir.join("notfiles.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn method_for(&self, package: &str) -> Method {
|
pub fn method_for(&self, package: &str) -> Method {
|
||||||
self.packages
|
self.packages
|
||||||
.get(package)
|
.get(package)
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ use std::path::PathBuf;
|
|||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(name = "notfiles", about = "A modern dotfiles manager")]
|
#[command(name = "notfiles", about = "A modern dotfiles manager")]
|
||||||
pub struct Cli {
|
pub struct Cli {
|
||||||
/// Path to the dotfiles directory (default: current directory)
|
/// Path to the dotfiles directory (default: ~/.notfiles)
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
pub dir: Option<PathBuf>,
|
pub dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Path to the config file (default: ~/.config/notfiles/notfiles.toml)
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
/// Show what would be done without making changes
|
/// Show what would be done without making changes
|
||||||
#[arg(long, global = true)]
|
#[arg(long, global = true)]
|
||||||
pub dry_run: bool,
|
pub dry_run: bool,
|
||||||
|
|||||||
@@ -15,10 +15,26 @@ fn main() -> Result<()> {
|
|||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
let dotfiles_dir = cli
|
let dotfiles_dir = cli
|
||||||
.dir
|
.dir
|
||||||
.unwrap_or_else(|| std::env::current_dir().expect("cannot determine current directory"));
|
.unwrap_or_else(notcore::config::default_dotfiles_dir);
|
||||||
|
|
||||||
|
// Init is the only command allowed when the dotfiles dir doesn't exist yet.
|
||||||
|
if matches!(cli.command, Command::Init) {
|
||||||
|
return cmd_init(&dotfiles_dir, cli.config.as_deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dotfiles_dir.exists() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"dotfiles directory not found: {}\nRun `notfiles init` to create it.",
|
||||||
|
dotfiles_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
let dotfiles_dir = fs::canonicalize(&dotfiles_dir)
|
let dotfiles_dir = fs::canonicalize(&dotfiles_dir)
|
||||||
.with_context(|| format!("dotfiles directory not found: {}", dotfiles_dir.display()))?;
|
.with_context(|| format!("dotfiles directory not found: {}", dotfiles_dir.display()))?;
|
||||||
|
|
||||||
|
let config_path = cli
|
||||||
|
.config
|
||||||
|
.unwrap_or_else(notcore::config::default_config_path);
|
||||||
|
|
||||||
let reporter: Box<dyn Reporter> = if cli.json {
|
let reporter: Box<dyn Reporter> = if cli.json {
|
||||||
Box::new(JsonReporter)
|
Box::new(JsonReporter)
|
||||||
} else {
|
} else {
|
||||||
@@ -37,14 +53,14 @@ fn main() -> Result<()> {
|
|||||||
detect::print_detected(&managers);
|
detect::print_detected(&managers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Init => cmd_init(&dotfiles_dir)?,
|
Command::Init => unreachable!("handled above"),
|
||||||
Command::Link {
|
Command::Link {
|
||||||
force,
|
force,
|
||||||
no_backup,
|
no_backup,
|
||||||
packages,
|
packages,
|
||||||
} => {
|
} => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -89,7 +105,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Unlink { packages } => {
|
Command::Unlink { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
let _ = &config; // loaded but not needed for unlink
|
let _ = &config; // loaded but not needed for unlink
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = if packages.is_empty() {
|
let pkgs = if packages.is_empty() {
|
||||||
@@ -145,7 +161,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Check => {
|
Command::Check => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let all = resolve_packages_filtered_with_store(&dotfiles_dir, &[], &config, fs)?;
|
let all = resolve_packages_filtered_with_store(&dotfiles_dir, &[], &config, fs)?;
|
||||||
|
|
||||||
@@ -171,7 +187,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Diff { packages } => {
|
Command::Diff { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let state = State::load(&dotfiles_dir, fs)?;
|
let state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -183,7 +199,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Adopt { package, files } => {
|
Command::Adopt { package, files } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
let mut state = State::load(&dotfiles_dir, fs)?;
|
let mut state = State::load(&dotfiles_dir, fs)?;
|
||||||
let opts = LinkOptions {
|
let opts = LinkOptions {
|
||||||
force: false,
|
force: false,
|
||||||
@@ -217,7 +233,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Status { packages } => {
|
Command::Status { packages } => {
|
||||||
let fs = &adapters::FileStoreImpl;
|
let fs = &adapters::FileStoreImpl;
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load_from(&config_path)?;
|
||||||
config.validate()?;
|
config.validate()?;
|
||||||
let state = State::load(&dotfiles_dir, fs)?;
|
let state = State::load(&dotfiles_dir, fs)?;
|
||||||
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
let pkgs = resolve_packages_filtered_with_store(&dotfiles_dir, &packages, &config, fs)?;
|
||||||
@@ -236,13 +252,29 @@ fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_init(dotfiles_dir: &std::path::Path) -> Result<()> {
|
fn cmd_init(
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
dotfiles_dir: &std::path::Path,
|
||||||
|
config_override: Option<&std::path::Path>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if !dotfiles_dir.exists() {
|
||||||
|
fs::create_dir_all(dotfiles_dir)
|
||||||
|
.with_context(|| format!("creating dotfiles dir: {}", dotfiles_dir.display()))?;
|
||||||
|
println!("Created {}", dotfiles_dir.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = config_override
|
||||||
|
.map(std::path::Path::to_path_buf)
|
||||||
|
.unwrap_or_else(notcore::config::default_config_path);
|
||||||
|
|
||||||
if config_path.exists() {
|
if config_path.exists() {
|
||||||
println!("notfiles.toml already exists.");
|
println!("{} already exists.", config_path.display());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if let Some(parent) = config_path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| format!("creating config dir: {}", parent.display()))?;
|
||||||
|
}
|
||||||
fs::write(&config_path, notcore::config::starter_toml())?;
|
fs::write(&config_path, notcore::config::starter_toml())?;
|
||||||
println!("Created notfiles.toml");
|
println!("Created {}", config_path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ fn notfiles_bin() -> std::path::PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run(dotfiles: &Path, args: &[&str]) -> (String, String, bool) {
|
fn run(dotfiles: &Path, args: &[&str]) -> (String, String, bool) {
|
||||||
|
let config = dotfiles.join("notfiles.toml");
|
||||||
let output = Command::new(notfiles_bin())
|
let output = Command::new(notfiles_bin())
|
||||||
.arg("--dir")
|
.arg("--dir")
|
||||||
.arg(dotfiles)
|
.arg(dotfiles)
|
||||||
|
.arg("--config")
|
||||||
|
.arg(&config)
|
||||||
.args(args)
|
.args(args)
|
||||||
.output()
|
.output()
|
||||||
.expect("failed to run notfiles");
|
.expect("failed to run notfiles");
|
||||||
@@ -61,7 +64,7 @@ fn test_init_creates_config() {
|
|||||||
|
|
||||||
let (stdout, _, ok) = run(&dotfiles, &["init"]);
|
let (stdout, _, ok) = run(&dotfiles, &["init"]);
|
||||||
assert!(ok);
|
assert!(ok);
|
||||||
assert!(stdout.contains("Created notfiles.toml"));
|
assert!(stdout.contains("Created"), "expected Created in: {stdout}");
|
||||||
assert!(dotfiles.join("notfiles.toml").exists());
|
assert!(dotfiles.join("notfiles.toml").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user