feat(workspace): migrate notfiles into crates/ workspace layout
Move src/ and tests/ into crates/notfiles/ as a proper lib+bin crate. Internal config/error/paths modules replaced with notcore imports. All 31 workspace tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
65
Cargo.lock
generated
65
Cargo.lock
generated
@@ -37,9 +37,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle"
|
name = "anstyle"
|
||||||
version = "1.0.13"
|
version = "1.0.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anstyle-parse"
|
name = "anstyle-parse"
|
||||||
@@ -121,9 +121,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.56"
|
version = "1.2.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -188,9 +188,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorchoice"
|
name = "colorchoice"
|
||||||
version = "1.0.4"
|
version = "1.0.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
@@ -425,15 +425,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -453,9 +453,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.14"
|
version = "0.1.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
@@ -478,6 +478,17 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notcore"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dirs",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notfiles"
|
name = "notfiles"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -488,12 +499,24 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
"globset",
|
"globset",
|
||||||
|
"notcore",
|
||||||
"serde",
|
"serde",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror",
|
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nothooks"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notsecrets"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notstrap"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -853,9 +876,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.114"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -866,9 +889,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.114"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -876,9 +899,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.114"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -889,9 +912,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.114"
|
version = "0.2.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,3 +2,26 @@
|
|||||||
name = "notfiles"
|
name = "notfiles"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
description = "A modern dotfiles manager — pure Rust alternative to GNU Stow"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "notfiles"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "notfiles"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
notcore = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
globset = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
dirs = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
assert_fs = { workspace = true }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::error::NotfilesError;
|
use notcore::NotfilesError;
|
||||||
|
|
||||||
pub struct IgnoreMatcher {
|
pub struct IgnoreMatcher {
|
||||||
globset: GlobSet,
|
globset: GlobSet,
|
||||||
@@ -17,6 +17,7 @@ impl IgnoreMatcher {
|
|||||||
.map_err(|e| NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}")))?;
|
.map_err(|e| NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}")))?;
|
||||||
builder.add(glob);
|
builder.add(glob);
|
||||||
// Also add a recursive variant so "foo" matches "a/foo" etc.
|
// Also add a recursive variant so "foo" matches "a/foo" etc.
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
if !pattern.contains('/') && !pattern.starts_with("**/") {
|
if !pattern.contains('/') && !pattern.starts_with("**/") {
|
||||||
if let Ok(g) = Glob::new(&format!("**/{pattern}")) {
|
if let Ok(g) = Glob::new(&format!("**/{pattern}")) {
|
||||||
builder.add(g);
|
builder.add(g);
|
||||||
@@ -1 +1,46 @@
|
|||||||
// placeholder
|
pub mod cli;
|
||||||
|
pub mod ignore;
|
||||||
|
pub mod linker;
|
||||||
|
pub mod package;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub use linker::{LinkOptions, State};
|
||||||
|
pub use package::resolve_packages;
|
||||||
|
|
||||||
|
pub fn link(
|
||||||
|
dotfiles_dir: &Path,
|
||||||
|
packages: &[String],
|
||||||
|
opts: &LinkOptions,
|
||||||
|
) -> Result<State> {
|
||||||
|
let config = notcore::Config::load(dotfiles_dir)?;
|
||||||
|
let mut state = State::load(dotfiles_dir)?;
|
||||||
|
let pkgs = resolve_packages(dotfiles_dir, packages)?;
|
||||||
|
for pkg in &pkgs {
|
||||||
|
linker::link_package(dotfiles_dir, &config, &mut state, pkg, opts)?;
|
||||||
|
}
|
||||||
|
state.save(dotfiles_dir)?;
|
||||||
|
Ok(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlink(dotfiles_dir: &Path, packages: &[String], opts: &LinkOptions) -> Result<()> {
|
||||||
|
let mut state = State::load(dotfiles_dir)?;
|
||||||
|
let pkgs = if packages.is_empty() {
|
||||||
|
state
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.package.clone())
|
||||||
|
.collect::<std::collections::HashSet<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
} else {
|
||||||
|
packages.to_vec()
|
||||||
|
};
|
||||||
|
for pkg in &pkgs {
|
||||||
|
linker::unlink_package(dotfiles_dir, &mut state, pkg, opts)?;
|
||||||
|
}
|
||||||
|
state.save(dotfiles_dir)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::config::{Config, Method};
|
use notcore::{Config, Method, NotfilesError, expand_tilde};
|
||||||
use crate::error::NotfilesError;
|
|
||||||
use crate::package::collect_files;
|
use crate::package::collect_files;
|
||||||
use crate::paths::expand_tilde;
|
|
||||||
|
|
||||||
const STATE_FILE: &str = ".notfiles-state.toml";
|
const STATE_FILE: &str = ".notfiles-state.toml";
|
||||||
|
|
||||||
@@ -132,8 +130,7 @@ pub fn link_package(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create parent directories
|
// Create parent directories
|
||||||
if let Some(parent) = target.parent() {
|
if let Some(parent) = target.parent().filter(|p| !p.exists()) {
|
||||||
if !parent.exists() {
|
|
||||||
if opts.dry_run {
|
if opts.dry_run {
|
||||||
if opts.verbose {
|
if opts.verbose {
|
||||||
println!(" \x1b[90mwould create dir\x1b[0m {}", parent.display());
|
println!(" \x1b[90mwould create dir\x1b[0m {}", parent.display());
|
||||||
@@ -142,7 +139,6 @@ pub fn link_package(
|
|||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create link or copy
|
// Create link or copy
|
||||||
let action_word = match method {
|
let action_word = match method {
|
||||||
@@ -1,21 +1,12 @@
|
|||||||
mod cli;
|
|
||||||
mod config;
|
|
||||||
mod error;
|
|
||||||
mod ignore;
|
|
||||||
mod linker;
|
|
||||||
mod package;
|
|
||||||
mod paths;
|
|
||||||
mod status;
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use cli::{Cli, Command};
|
use notfiles::cli::{Cli, Command};
|
||||||
use config::Config;
|
use notfiles::linker::{LinkOptions, State};
|
||||||
use linker::{LinkOptions, State};
|
use notfiles::package::resolve_packages;
|
||||||
use package::resolve_packages;
|
use notfiles::{linker, status};
|
||||||
|
use notcore::Config;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -66,6 +57,7 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
Command::Unlink { packages } => {
|
Command::Unlink { packages } => {
|
||||||
let config = Config::load(&dotfiles_dir)?;
|
let config = Config::load(&dotfiles_dir)?;
|
||||||
|
let _ = &config; // loaded but not needed for unlink
|
||||||
let mut state = State::load(&dotfiles_dir)?;
|
let mut state = State::load(&dotfiles_dir)?;
|
||||||
let pkgs = if packages.is_empty() {
|
let pkgs = if packages.is_empty() {
|
||||||
state
|
state
|
||||||
@@ -94,7 +86,6 @@ fn main() -> Result<()> {
|
|||||||
println!("\x1b[36m(dry run)\x1b[0m");
|
println!("\x1b[36m(dry run)\x1b[0m");
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = &config; // loaded but not needed for unlink
|
|
||||||
for pkg in &pkgs {
|
for pkg in &pkgs {
|
||||||
if cli.verbose || cli.dry_run {
|
if cli.verbose || cli.dry_run {
|
||||||
println!("Unlinking {pkg}...");
|
println!("Unlinking {pkg}...");
|
||||||
@@ -104,7 +95,11 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
if !cli.dry_run {
|
if !cli.dry_run {
|
||||||
state.save(&dotfiles_dir)?;
|
state.save(&dotfiles_dir)?;
|
||||||
println!("\x1b[32mUnlinked {} package{}.\x1b[0m", pkgs.len(), if pkgs.len() == 1 { "" } else { "s" });
|
println!(
|
||||||
|
"\x1b[32mUnlinked {} package{}.\x1b[0m",
|
||||||
|
pkgs.len(),
|
||||||
|
if pkgs.len() == 1 { "" } else { "s" }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Status { packages } => {
|
Command::Status { packages } => {
|
||||||
@@ -128,7 +123,7 @@ fn cmd_init(dotfiles_dir: &std::path::Path) -> Result<()> {
|
|||||||
println!("notfiles.toml already exists.");
|
println!("notfiles.toml already exists.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
fs::write(&config_path, config::starter_toml())?;
|
fs::write(&config_path, notcore::config::starter_toml())?;
|
||||||
println!("Created notfiles.toml");
|
println!("Created notfiles.toml");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config::Config;
|
use notcore::{Config, NotfilesError};
|
||||||
use crate::error::NotfilesError;
|
|
||||||
use crate::ignore::IgnoreMatcher;
|
use crate::ignore::IgnoreMatcher;
|
||||||
|
|
||||||
/// Discover available packages (subdirectories of the dotfiles dir).
|
/// Discover available packages (subdirectories of the dotfiles dir).
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config::{Config, Method};
|
use notcore::{Config, Method, expand_tilde};
|
||||||
use crate::linker::State;
|
use crate::linker::State;
|
||||||
use crate::package::collect_files;
|
use crate::package::collect_files;
|
||||||
use crate::paths::expand_tilde;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum FileStatus {
|
pub enum FileStatus {
|
||||||
163
src/config.rs
163
src/config.rs
@@ -1,163 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::error::NotfilesError;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Config {
|
|
||||||
#[serde(default)]
|
|
||||||
pub defaults: Defaults,
|
|
||||||
#[serde(default)]
|
|
||||||
pub packages: HashMap<String, PackageConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Defaults {
|
|
||||||
#[serde(default = "default_target")]
|
|
||||||
pub target: String,
|
|
||||||
#[serde(default = "default_ignore")]
|
|
||||||
pub ignore: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Defaults {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
target: default_target(),
|
|
||||||
ignore: default_ignore(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_target() -> String {
|
|
||||||
"~".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_ignore() -> Vec<String> {
|
|
||||||
vec![
|
|
||||||
".git".to_string(),
|
|
||||||
".DS_Store".to_string(),
|
|
||||||
"README.md".to_string(),
|
|
||||||
"LICENSE".to_string(),
|
|
||||||
"notfiles.toml".to_string(),
|
|
||||||
".notfiles-state.toml".to_string(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct PackageConfig {
|
|
||||||
#[serde(default)]
|
|
||||||
pub method: Option<Method>,
|
|
||||||
pub target: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub ignore: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum Method {
|
|
||||||
#[default]
|
|
||||||
Symlink,
|
|
||||||
Copy,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Method {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Method::Symlink => write!(f, "symlink"),
|
|
||||||
Method::Copy => write!(f, "copy"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn load(dotfiles_dir: &Path) -> Result<Self, NotfilesError> {
|
|
||||||
let config_path = dotfiles_dir.join("notfiles.toml");
|
|
||||||
if !config_path.exists() {
|
|
||||||
return Ok(Config::default());
|
|
||||||
}
|
|
||||||
let content = std::fs::read_to_string(&config_path)
|
|
||||||
.map_err(|e| NotfilesError::Config(format!("reading {}: {e}", config_path.display())))?;
|
|
||||||
let config: Config = toml::from_str(&content)
|
|
||||||
.map_err(|e| NotfilesError::Config(format!("parsing {}: {e}", config_path.display())))?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn method_for(&self, package: &str) -> Method {
|
|
||||||
self.packages
|
|
||||||
.get(package)
|
|
||||||
.and_then(|p| p.method)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn target_for(&self, package: &str) -> &str {
|
|
||||||
self.packages
|
|
||||||
.get(package)
|
|
||||||
.and_then(|p| p.target.as_deref())
|
|
||||||
.unwrap_or(&self.defaults.target)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ignore_patterns_for(&self, package: &str) -> Vec<&str> {
|
|
||||||
let mut patterns: Vec<&str> = self.defaults.ignore.iter().map(|s| s.as_str()).collect();
|
|
||||||
if let Some(pkg) = self.packages.get(package) {
|
|
||||||
for p in &pkg.ignore {
|
|
||||||
patterns.push(p.as_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patterns
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn starter_toml() -> &'static str {
|
|
||||||
r#"[defaults]
|
|
||||||
target = "~"
|
|
||||||
ignore = [".git", ".DS_Store", "README.md", "LICENSE", "notfiles.toml"]
|
|
||||||
|
|
||||||
# Per-package overrides (only needed for non-default behavior):
|
|
||||||
# [packages.ssh]
|
|
||||||
# method = "copy"
|
|
||||||
# ignore = ["known_hosts"]
|
|
||||||
#
|
|
||||||
# [packages.scripts]
|
|
||||||
# target = "~/bin"
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_default_config() {
|
|
||||||
let config = Config::default();
|
|
||||||
assert_eq!(config.defaults.target, "~");
|
|
||||||
assert!(config.defaults.ignore.contains(&".git".to_string()));
|
|
||||||
assert_eq!(config.method_for("anything"), Method::Symlink);
|
|
||||||
assert_eq!(config.target_for("anything"), "~");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_config() {
|
|
||||||
let toml_str = r#"
|
|
||||||
[defaults]
|
|
||||||
target = "~"
|
|
||||||
ignore = [".git"]
|
|
||||||
|
|
||||||
[packages.ssh]
|
|
||||||
method = "copy"
|
|
||||||
ignore = ["known_hosts"]
|
|
||||||
|
|
||||||
[packages.scripts]
|
|
||||||
target = "~/bin"
|
|
||||||
"#;
|
|
||||||
let config: Config = toml::from_str(toml_str).unwrap();
|
|
||||||
assert_eq!(config.method_for("ssh"), Method::Copy);
|
|
||||||
assert_eq!(config.method_for("scripts"), Method::Symlink);
|
|
||||||
assert_eq!(config.target_for("scripts"), "~/bin");
|
|
||||||
assert_eq!(config.target_for("ssh"), "~");
|
|
||||||
|
|
||||||
let ssh_ignores = config.ignore_patterns_for("ssh");
|
|
||||||
assert!(ssh_ignores.contains(&".git"));
|
|
||||||
assert!(ssh_ignores.contains(&"known_hosts"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
25
src/error.rs
25
src/error.rs
@@ -1,25 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum NotfilesError {
|
|
||||||
#[error("config file error: {0}")]
|
|
||||||
Config(String),
|
|
||||||
|
|
||||||
#[error("package not found: {name}")]
|
|
||||||
PackageNotFound { name: String },
|
|
||||||
|
|
||||||
#[error("conflict at {path}: {reason}")]
|
|
||||||
Conflict { path: PathBuf, reason: String },
|
|
||||||
|
|
||||||
#[error("path error: {0}")]
|
|
||||||
Path(String),
|
|
||||||
|
|
||||||
#[error("state file error: {0}")]
|
|
||||||
State(String),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
39
src/paths.rs
39
src/paths.rs
@@ -1,39 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::error::NotfilesError;
|
|
||||||
|
|
||||||
/// Expand a leading `~` to the user's home directory.
|
|
||||||
pub fn expand_tilde(path: &str) -> Result<PathBuf, NotfilesError> {
|
|
||||||
if path == "~" {
|
|
||||||
return dirs::home_dir().ok_or_else(|| NotfilesError::Path("cannot determine home directory".into()));
|
|
||||||
}
|
|
||||||
if let Some(rest) = path.strip_prefix("~/") {
|
|
||||||
let home = dirs::home_dir()
|
|
||||||
.ok_or_else(|| NotfilesError::Path("cannot determine home directory".into()))?;
|
|
||||||
return Ok(home.join(rest));
|
|
||||||
}
|
|
||||||
Ok(PathBuf::from(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_expand_tilde_home() {
|
|
||||||
let home = dirs::home_dir().unwrap();
|
|
||||||
assert_eq!(expand_tilde("~").unwrap(), home);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_expand_tilde_subpath() {
|
|
||||||
let home = dirs::home_dir().unwrap();
|
|
||||||
assert_eq!(expand_tilde("~/foo/bar").unwrap(), home.join("foo/bar"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_expand_tilde_absolute() {
|
|
||||||
assert_eq!(expand_tilde("/usr/bin").unwrap(), PathBuf::from("/usr/bin"));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user