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]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
@@ -121,9 +121,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -188,9 +188,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
@@ -425,15 +425,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.91"
|
||||
version = "0.3.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -453,9 +453,9 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -478,6 +478,17 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "notcore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notfiles"
|
||||
version = "0.1.0"
|
||||
@@ -488,12 +499,24 @@ dependencies = [
|
||||
"clap",
|
||||
"dirs",
|
||||
"globset",
|
||||
"notcore",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nothooks"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "notsecrets"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "notstrap"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -853,9 +876,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
version = "0.2.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -866,9 +889,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.114"
|
||||
version = "0.2.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -876,9 +899,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.114"
|
||||
version = "0.2.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -889,9 +912,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.114"
|
||||
version = "0.2.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -2,3 +2,26 @@
|
||||
name = "notfiles"
|
||||
version = "0.1.0"
|
||||
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 std::path::Path;
|
||||
|
||||
use crate::error::NotfilesError;
|
||||
use notcore::NotfilesError;
|
||||
|
||||
pub struct IgnoreMatcher {
|
||||
globset: GlobSet,
|
||||
@@ -17,6 +17,7 @@ impl IgnoreMatcher {
|
||||
.map_err(|e| NotfilesError::Other(format!("invalid ignore pattern '{pattern}': {e}")))?;
|
||||
builder.add(glob);
|
||||
// Also add a recursive variant so "foo" matches "a/foo" etc.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if !pattern.contains('/') && !pattern.starts_with("**/") {
|
||||
if let Ok(g) = Glob::new(&format!("**/{pattern}")) {
|
||||
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 serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{Config, Method};
|
||||
use crate::error::NotfilesError;
|
||||
use notcore::{Config, Method, NotfilesError, expand_tilde};
|
||||
use crate::package::collect_files;
|
||||
use crate::paths::expand_tilde;
|
||||
|
||||
const STATE_FILE: &str = ".notfiles-state.toml";
|
||||
|
||||
@@ -132,15 +130,13 @@ pub fn link_package(
|
||||
}
|
||||
|
||||
// Create parent directories
|
||||
if let Some(parent) = target.parent() {
|
||||
if !parent.exists() {
|
||||
if opts.dry_run {
|
||||
if opts.verbose {
|
||||
println!(" \x1b[90mwould create dir\x1b[0m {}", parent.display());
|
||||
}
|
||||
} else {
|
||||
fs::create_dir_all(parent)?;
|
||||
if let Some(parent) = target.parent().filter(|p| !p.exists()) {
|
||||
if opts.dry_run {
|
||||
if opts.verbose {
|
||||
println!(" \x1b[90mwould create dir\x1b[0m {}", parent.display());
|
||||
}
|
||||
} else {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 clap::Parser;
|
||||
use std::fs;
|
||||
|
||||
use cli::{Cli, Command};
|
||||
use config::Config;
|
||||
use linker::{LinkOptions, State};
|
||||
use package::resolve_packages;
|
||||
use notfiles::cli::{Cli, Command};
|
||||
use notfiles::linker::{LinkOptions, State};
|
||||
use notfiles::package::resolve_packages;
|
||||
use notfiles::{linker, status};
|
||||
use notcore::Config;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
@@ -66,6 +57,7 @@ fn main() -> Result<()> {
|
||||
}
|
||||
Command::Unlink { packages } => {
|
||||
let config = Config::load(&dotfiles_dir)?;
|
||||
let _ = &config; // loaded but not needed for unlink
|
||||
let mut state = State::load(&dotfiles_dir)?;
|
||||
let pkgs = if packages.is_empty() {
|
||||
state
|
||||
@@ -94,7 +86,6 @@ fn main() -> Result<()> {
|
||||
println!("\x1b[36m(dry run)\x1b[0m");
|
||||
}
|
||||
|
||||
let _ = &config; // loaded but not needed for unlink
|
||||
for pkg in &pkgs {
|
||||
if cli.verbose || cli.dry_run {
|
||||
println!("Unlinking {pkg}...");
|
||||
@@ -104,7 +95,11 @@ fn main() -> Result<()> {
|
||||
|
||||
if !cli.dry_run {
|
||||
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 } => {
|
||||
@@ -128,7 +123,7 @@ fn cmd_init(dotfiles_dir: &std::path::Path) -> Result<()> {
|
||||
println!("notfiles.toml already exists.");
|
||||
return Ok(());
|
||||
}
|
||||
fs::write(&config_path, config::starter_toml())?;
|
||||
fs::write(&config_path, notcore::config::starter_toml())?;
|
||||
println!("Created notfiles.toml");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::NotfilesError;
|
||||
use notcore::{Config, NotfilesError};
|
||||
use crate::ignore::IgnoreMatcher;
|
||||
|
||||
/// Discover available packages (subdirectories of the dotfiles dir).
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::config::{Config, Method};
|
||||
use notcore::{Config, Method, expand_tilde};
|
||||
use crate::linker::State;
|
||||
use crate::package::collect_files;
|
||||
use crate::paths::expand_tilde;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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