aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/arguments.rs59
-rw-r--r--src/commands/mod.rs88
-rw-r--r--src/commands/sync.rs125
-rw-r--r--src/main.rs60
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(())
+}