diff options
| author | ottjk <joshott16@gmail.com> | 2023-12-30 19:40:27 -0500 |
|---|---|---|
| committer | ottjk <joshott16@gmail.com> | 2023-12-30 19:40:27 -0500 |
| commit | 6536f3aae95e266f80a5a73288e181ff10ce650b (patch) | |
| tree | 2fa309c246ab4ad860da8673a82dcb4ead924919 /src | |
| download | dfa-6536f3aae95e266f80a5a73288e181ff10ce650b.tar.gz dfa-6536f3aae95e266f80a5a73288e181ff10ce650b.zip | |
initial commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/arguments.rs | 59 | ||||
| -rw-r--r-- | src/commands/mod.rs | 88 | ||||
| -rw-r--r-- | src/commands/sync.rs | 125 | ||||
| -rw-r--r-- | src/main.rs | 60 |
4 files changed, 332 insertions, 0 deletions
diff --git a/src/arguments.rs b/src/arguments.rs new file mode 100644 index 0000000..24697d9 --- /dev/null +++ b/src/arguments.rs @@ -0,0 +1,59 @@ +use clap::{Parser,Subcommand,Args}; +use std::path::PathBuf; + +#[derive(Parser)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, + + /// Configuration file location + #[arg(short, long)] + pub config: Option<PathBuf>, +} + +#[derive(Subcommand)] +pub enum Commands { + /// Add or update config from list + Modify(ModifyArgs), + + /// Remove program from list + Remove(RemoveArgs), + + /// Print list of tracked configs + List {}, + + /// Update a config + Sync(SyncArgs), +} + +#[derive(Args)] +pub struct ModifyArgs { + /// Name of program + pub name: String, + + /// Parent directory on system config resides in + pub dest: PathBuf, + + /// File(s) in configs directory + pub file: PathBuf, +} + +#[derive(Args)] +pub struct RemoveArgs { + /// Name of program + pub name: String, +} + +#[derive(Args)] +pub struct SyncArgs { + /// Names of programs to update + pub names: Vec<String>, + + /// Sync all programs + #[arg(short, long)] + pub all: bool, + + /// Never prompt to skip + #[arg(short, long)] + pub force: bool, +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..b1c5f51 --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,88 @@ +use std::{ + fs, + io::{self,Result,Write}, + path::PathBuf, +}; + +mod sync; + +use crate::{Config,Entry}; +use crate::arguments::*; + +fn save_config(config: &Config, path: &PathBuf) -> Result<()> { + let toml = toml::to_string(config).unwrap(); + fs::write(path.join("configs.toml"), toml)?; + Ok(()) +} + +pub fn modify_command( + config: &mut Config, + args: ModifyArgs, + configs_path: &PathBuf, +) -> Result<()> { + + if config.contains_key(&args.name) { loop { + let mut input = String::new(); + + print!("Entry already exists for {}. Overwrite it? (y/n) ", &args.name); + let _ = io::stdout().flush(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read user input."); + + match input.to_lowercase().trim() { + "y" => break, + "n" => return Ok(()), + _ => { + println!("Try again :)"); + continue; + }, + }; + }} + + let entry = Entry { file: args.file, parent: args.dest }; + config.insert(args.name, entry); + save_config(&config, configs_path)?; + + Ok(()) +} + +pub fn remove_command( + config: &mut Config, + args: RemoveArgs, + configs_path: &PathBuf +) -> Result<()> { + + if !config.contains_key(&args.name) { + panic!("Such an entry does not exist."); + } + config.remove(&args.name); + save_config(&config, configs_path)?; + + Ok(()) +} + +pub fn list_command(config: &Config) { + for (key,entry) in config { + println!(r#""{}" - src: {}, dest: {}"#, key, entry.file.to_str().unwrap(), entry.parent.to_str().unwrap()); + } +} + +pub fn sync_command(config: &Config, args: SyncArgs, configs_path: &PathBuf) { + if args.all { + sync::sync_all(config, configs_path, args.force); + return + } + + for name in &args.names { + let entry = match config.get(name) { + Some(e) => e, + None => { + eprintln!(r#"Entry "{name}" not found."#); + continue; + }, + }; + + sync::sync_config(name, entry, configs_path, &args.force); + } +} diff --git a/src/commands/sync.rs b/src/commands/sync.rs new file mode 100644 index 0000000..eaf3dab --- /dev/null +++ b/src/commands/sync.rs @@ -0,0 +1,125 @@ +use std::{ + io::{self,ErrorKind,Write}, + fs, + path::PathBuf, +}; +use copy_dir::copy_dir; + +use crate::{resolve_home,Config,Entry}; + +#[derive(Debug, PartialEq, Eq)] +enum ConflictOption { + Remove, + Rename, + Skip, +} + +fn remove_ambiguous(path: &PathBuf) -> io::Result<()> { + if path.is_dir() { + fs::remove_dir_all(&path)?; + } else { + fs::remove_file(&path)?; + } + + Ok(()) +} + +fn rename_conflict(path: &PathBuf) -> io::Result<()> { + let rename_path = path.with_extension("old"); + + if rename_path.try_exists()? { + remove_ambiguous(&rename_path)?; + } + + fs::rename(&path, &rename_path)?; + println!("Old config renamed to {}.", rename_path.file_name().unwrap().to_str().unwrap()); + + Ok(()) +} + +fn remove_existing(name: &str, path: &PathBuf) -> io::Result<Option<ConflictOption>> { + loop { + let mut input = String::new(); + + print!("Remove existing config for {name}? (y/n/r[ename]) "); + let _ = io::stdout().flush(); + io::stdin() + .read_line(&mut input) + .expect("Failed to read user input."); + + let choice = match input.to_lowercase().trim() { + "y" => ConflictOption::Remove, + "n" => ConflictOption::Skip, + "r" => ConflictOption::Rename, + _ => { + println!("Try again :)"); + continue; + }, + }; + + match choice { + ConflictOption::Remove => + remove_ambiguous(path)?, + ConflictOption::Rename => + rename_conflict(path)?, + ConflictOption::Skip => + println!("Skipping {name}."), + }; + + return Ok(Some(choice)); + } +} + +fn sync_file( + name: &str, + src: &PathBuf, + dest: &PathBuf, + force: &bool +) -> io::Result<Option<ConflictOption>> { + + let copy_result = copy_dir(src, dest); + + if let Err(error) = copy_result { + if error.kind() == ErrorKind::AlreadyExists { + if *force { + remove_ambiguous(dest)?; + return Ok(Some(ConflictOption::Remove)); + } else { + return remove_existing(name, dest); + } + } + return Err(error); + } + + Ok(None) +} + +pub fn sync_config(name: &str, entry: &Entry, configs_path: &PathBuf, force: &bool) { + let src = configs_path.join(&entry.file); + let mut dest = entry.parent.join(&entry.file); + resolve_home(&mut dest); + + loop { + let sync_result = sync_file(name, &src, &dest, force); + + match sync_result { + Ok(None) => println!("Updated {}.", dest.to_str().unwrap()), + Ok(Some(choice)) => match choice { + ConflictOption::Skip => break, + _ => continue, + }, + Err(error) => { + eprintln!("Skipping sync of {src:?} to {dest:?}: {error:?}"); + break; + }, + }; + + break; + } +} + +pub fn sync_all(config: &Config, configs_path: &PathBuf, force: bool) { + for (name, entry) in config { + sync_config(&name, &entry, configs_path, &force); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..59f3a3c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,60 @@ +use std::{ + fs, + io::Result, + collections::HashMap, + path::PathBuf, +}; +use serde::{Deserialize,Serialize}; +use clap::Parser; + +mod commands; +use commands::*; + +mod arguments; +use arguments::*; + +#[derive(Deserialize, Serialize)] +pub struct Entry { + file: PathBuf, + parent: PathBuf, +} + +pub type Config = HashMap<String, Entry>; + +pub fn resolve_home(path: &mut PathBuf) { + if path.starts_with("~") { + let temp = path.strip_prefix("~").unwrap(); + *path = dirs::home_dir().expect("Could not resolve home directory.") + .join(temp); + } +} + +fn read_config(path: &PathBuf) -> Config { + let file = fs::read_to_string(path).unwrap_or(String::new()); + toml::from_str(&file).unwrap() +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + let mut configs_path = cli.config + .unwrap_or(dirs::config_dir() + .expect("Could not resolve config directory.") + .join("dotfiles")); + resolve_home(&mut configs_path); + + if !configs_path.try_exists().unwrap() { + fs::create_dir_all(&configs_path).unwrap(); + } + + let mut config = read_config(&configs_path.join("configs.toml")); + + match cli.command { + Commands::Modify(args) => modify_command(&mut config, args, &configs_path)?, + Commands::Remove(args) => remove_command(&mut config, args, &configs_path)?, + Commands::Sync(args) => sync_command(&config, args, &configs_path), + Commands::List {} => list_command(&config), + }; + + Ok(()) +} |