diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..8d5a1b2 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c169383..6c3260c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bpaf" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863c0b21775e45ebf9bbb3a6d0cd9b3421c88a036e825359e3d4015561f3e23c" + [[package]] name = "bumpalo" version = "3.12.0" @@ -89,6 +95,7 @@ name = "dotfiles_installer" version = "1.0.0" dependencies = [ "attohttpc", + "bpaf", "dialoguer", "indicatif", "serde", diff --git a/Cargo.toml b/Cargo.toml index efbd6d4..88c0739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +toml = "0.7.2" +bpaf = "0.7.8" + dialoguer = "0.10.3" indicatif = "0.17.3" -toml = "0.7.2" [dependencies.serde] version = "1.0.152" diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..bd981f0 --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,68 @@ +use bpaf::{command, construct, Parser}; +use crate::common::config::ModToml; +use crate::common::{operations, repository}; +use crate::common::operations::NamedModule; + +#[derive(Debug, Clone)] +pub enum CliCommand { + Install(Vec), + Collect(Vec), + Upload(String), + List(), + Pull(), + Diff() +} + +pub fn get_parser() -> impl Parser { + let modules = bpaf::positional::("Modules").many(); + let install = + command("install", construct!(CliCommand::Install(modules)).to_options()) + .help("Install modules"); + + let modules = bpaf::positional::("Modules").many(); + let collect = + command("collect", construct!(CliCommand::Collect(modules)).to_options()) + .help("Collect modules"); + + let commit_msg = bpaf::positional::("Commit message"); + let upload = + command("upload", construct!(CliCommand::Upload(commit_msg)).to_options()) + .help("Upload files"); + + let list = command("list", construct!(CliCommand::List()).to_options()) + .help("List modules"); + + let pull = command("pull", construct!(CliCommand::Pull()).to_options()) + .help("Downloads update"); + + let diff = command("diff", construct!(CliCommand::Diff()).to_options()) + .help("Show diff of current changes"); + + construct!([ + install, + collect, + upload, + list, + pull, + diff + ]) +} + +pub fn run(config: &ModToml, command: CliCommand) { + + let map_mod = |name: String| -> NamedModule { + match config.modules.get(&name) { + None => { println!("Module {name} doesn't exist"); std::process::exit(1); } + Some(v) => NamedModule(name, v.clone()) + } + }; + + match command { + CliCommand::Install(mods) => operations::install(&mods.into_iter().map(map_mod).collect::>()), + CliCommand::Collect(mods) => operations::collect(&mods.into_iter().map(map_mod).collect::>()), + CliCommand::Upload(msg) => operations::upload(|| msg), + CliCommand::List() => config.modules.iter().for_each(|m| println!("Module {}", m.0)), + CliCommand::Pull() => repository::pull_repo(), + CliCommand::Diff() => repository::diff() + } +} diff --git a/src/config.rs b/src/common/config.rs similarity index 100% rename from src/config.rs rename to src/common/config.rs diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..c5b314b --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,5 @@ +pub mod repository; +pub mod config; +pub mod operations; +pub mod utils; +pub mod update; diff --git a/src/operations.rs b/src/common/operations.rs similarity index 70% rename from src/operations.rs rename to src/common/operations.rs index e206a85..eaabb1e 100644 --- a/src/operations.rs +++ b/src/common/operations.rs @@ -1,8 +1,16 @@ -use crate::config::{ModToml, Module}; -use crate::prompt::multi_select; +use crate::common::{repository, utils, config::{ModToml, Module}}; #[derive(Debug)] -struct NamedModule(String, Module); +pub struct NamedModule(pub String, pub Module); + +impl From<&ModToml> for Vec { + fn from(config: &ModToml) -> Self { + config.modules.iter() + .map(|v| NamedModule(v.0.clone(), v.1.clone())) + .collect() + } +} + impl ToString for NamedModule { fn to_string(&self) -> String { self.0.clone() @@ -28,12 +36,12 @@ fn install_mod(m: &NamedModule) { let pb = indicatif::ProgressBar::new(0); pb.set_style(indicatif::ProgressStyle::with_template("{spinner} [{wide_bar}] {pos:>6}/{len:6}").unwrap().progress_chars("#>-")); - if let Err(err) = crate::utils::remove_dir(dst, &m.1.ignore, &pb) { + if let Err(err) = utils::remove_dir(dst, &m.1.ignore, &pb) { println!("Failed to delete directory:\n{}", err); continue 'content_iter; } - if let Err(err) = crate::utils::copy_dir(&src, dst, &[], &pb) { + if let Err(err) = utils::copy_dir(&src, dst, &[], &pb) { println!("Failed to copy directory:\n{}", err); } } @@ -53,7 +61,7 @@ fn collect_mod(m: &NamedModule) { let pb = indicatif::ProgressBar::new(0); pb.set_style(indicatif::ProgressStyle::with_template("{spinner} [{wide_bar}] {pos:>6}/{len:6}").unwrap().progress_chars("#>-")); - if let Err(err) = crate::utils::remove_dir(&dst, &[], &pb) { + if let Err(err) = utils::remove_dir(&dst, &[], &pb) { println!("Failed to delete directory:\n{}", err); continue 'content_iter; } @@ -63,7 +71,7 @@ fn collect_mod(m: &NamedModule) { continue 'content_iter; } - if let Err(err) = crate::utils::copy_dir(src, &dst, &m.1.ignore, &pb) { + if let Err(err) = utils::copy_dir(src, &dst, &m.1.ignore, &pb) { println!("Failed to copy source content:\n{}", err); continue 'content_iter; } @@ -71,14 +79,9 @@ fn collect_mod(m: &NamedModule) { } } -pub fn install(config: &ModToml) { - let to_install = multi_select( - Some("Which modules do you want to install?"), - config.modules.iter().map(|v| NamedModule(v.0.clone(), v.1.clone())).collect() - ); - - for m in to_install { - install_mod(&m); +pub fn install(mods: &[NamedModule]) { + for m in mods { + install_mod(m); if let Some(on_install) = &m.1.on_install { match std::process::Command::new("sh").arg("-c").arg(on_install).status() { @@ -89,26 +92,21 @@ pub fn install(config: &ModToml) { } } -pub fn collect(config: &ModToml) { - let to_collect = multi_select( - Some("Which modules do you want to collect?"), - config.modules.iter().map(|v| NamedModule(v.0.clone(), v.1.clone())).collect() - ); - - for m in to_collect { - collect_mod(&m); +pub fn collect(mods: &[NamedModule]) { + for m in mods { + collect_mod(m); } } -pub fn upload() { - if !crate::repository::is_clean() { - let msg = crate::prompt::input("Commit message"); - if !crate::repository::commit_changes(&msg) { +pub fn upload String>(get_commit_msg: F) { + if !repository::is_clean() { + let msg = get_commit_msg(); + if !repository::commit_changes(&msg) { println!("Failed to commit"); return; } } - if !crate::repository::push_repo() { + if !repository::push_repo() { println!("Failed to push to origin"); } } diff --git a/src/repository.rs b/src/common/repository.rs similarity index 100% rename from src/repository.rs rename to src/common/repository.rs diff --git a/src/update.rs b/src/common/update.rs similarity index 94% rename from src/update.rs rename to src/common/update.rs index 779ab77..2b617d2 100644 --- a/src/update.rs +++ b/src/common/update.rs @@ -29,7 +29,7 @@ pub fn check_for_updates(version: u64, name: &str) { fs::rename(&temp, &exe).unwrap(); - std::process::Command::new(exe).exec(); + std::process::Command::new(exe).args(std::env::args()).exec(); std::process::exit(1); } println!("No new version"); diff --git a/src/utils.rs b/src/common/utils.rs similarity index 100% rename from src/utils.rs rename to src/common/utils.rs diff --git a/src/main.rs b/src/main.rs index 3da4ce8..f169380 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,34 +1,33 @@ -mod prompt; -mod repository; -mod config; -mod operations; -mod utils; -mod update; +mod common; +mod tui; +mod cli; + +use bpaf::{construct, Parser}; +use crate::common::{config, repository, update}; #[derive(Debug, Clone)] -enum MainMenu { - Install, - Collect, - Pull, - Diff, - Upload, - Quit -} - -impl ToString for MainMenu { - fn to_string(&self) -> String { - match self { - MainMenu::Install => "Install files", - MainMenu::Collect => "Collect files", - MainMenu::Pull => "Pull from git", - MainMenu::Diff => "View git diff", - MainMenu::Upload => "(Commit) and push to git", - MainMenu::Quit => "Exit" - }.to_string() - } +enum Commands { + Tui(), + Cli(cli::CliCommand) } fn main() { + let tui_cmd = + bpaf::command("tui", construct!(Commands::Tui()).to_options()) + .help("Terminal ui for interactive use"); + + let cli_parser = cli::get_parser(); + + let cli_cmd = + bpaf::command("cli", construct!(Commands::Cli(cli_parser)).to_options()) + .help("Command line for script usage"); + + let opt = construct!([tui_cmd, cli_cmd]) + .fallback(Commands::Tui()) + .to_options() + .descr("Dotfiles multitool") + .run(); + if let (Some(version), Some(name)) = (option_env!("BUILD_ID"), option_env!("JOB_BASE_NAME")) { println!("Starting installer version {name}-{version}"); update::check_for_updates(version.parse().unwrap(), name); @@ -55,25 +54,8 @@ fn main() { } }; - 'main_loop: loop { - let res = prompt::select( - Some("What do you want to do?"), - vec![ - MainMenu::Install, - MainMenu::Collect, - MainMenu::Pull, - MainMenu::Diff, - MainMenu::Upload, - MainMenu::Quit - ] - ); - match res { - MainMenu::Install => operations::install(&mods), - MainMenu::Collect => operations::collect(&mods), - MainMenu::Pull => repository::pull_repo(), - MainMenu::Diff => repository::diff(), - MainMenu::Upload => operations::upload(), - MainMenu::Quit => break 'main_loop - } + match opt { + Commands::Tui() => tui::run(&mods), + Commands::Cli(command) => cli::run(&mods, command) } } diff --git a/src/prompt.rs b/src/prompt.rs deleted file mode 100644 index 60f4e32..0000000 --- a/src/prompt.rs +++ /dev/null @@ -1,38 +0,0 @@ -use dialoguer::{Input, MultiSelect, Select}; - -pub fn select(title: Option<&str>, mut options: Vec) -> F - where F: ToString -{ - let mut sel = Select::new(); - sel.items(&options); - sel.default(0); - if let Some(title) = title { - sel.with_prompt(title); - } - let sel = sel.interact().expect("User didn't enter anything"); - - options.remove(sel) -} - -pub fn multi_select(title: Option<&str>, options: Vec) -> Vec - where F: ToString -{ - let mut sel = MultiSelect::new(); - sel.items(&options); - if let Some(title) = title { - sel.with_prompt(title); - } - let sel = sel.interact().expect("User didn't enter anything"); - - options.into_iter() - .enumerate() - .filter_map(|(i, v)| sel.contains(&i).then_some(v)) - .collect() -} - -pub fn input(title: &str) -> String { - Input::new() - .with_prompt(title) - .interact_text() - .expect("User didn't enter anything") -} diff --git a/src/tui/mod.rs b/src/tui/mod.rs new file mode 100644 index 0000000..10124ea --- /dev/null +++ b/src/tui/mod.rs @@ -0,0 +1,95 @@ +use dialoguer::{Input, MultiSelect, Select}; +use crate::common::{operations, repository}; +use crate::common::config::ModToml; + +#[derive(Debug, Clone)] +enum MainMenu { + Install, + Collect, + Pull, + Diff, + Upload, + Quit +} + +impl ToString for MainMenu { + fn to_string(&self) -> String { + match self { + MainMenu::Install => "Install files", + MainMenu::Collect => "Collect files", + MainMenu::Pull => "Pull from git", + MainMenu::Diff => "View git diff", + MainMenu::Upload => "(Commit) and push to git", + MainMenu::Quit => "Exit" + }.to_string() + } +} + +pub fn select(title: Option<&str>, mut options: Vec) -> F + where F: ToString +{ + let mut sel = Select::new(); + sel.items(&options); + sel.default(0); + if let Some(title) = title { + sel.with_prompt(title); + } + let sel = sel.interact().expect("User didn't enter anything"); + + options.remove(sel) +} + +pub fn multi_select(title: Option<&str>, options: Vec) -> Vec + where F: ToString +{ + let mut sel = MultiSelect::new(); + sel.items(&options); + if let Some(title) = title { + sel.with_prompt(title); + } + let sel = sel.interact().expect("User didn't enter anything"); + + options.into_iter() + .enumerate() + .filter_map(|(i, v)| sel.contains(&i).then_some(v)) + .collect() +} + +pub fn input(title: &str) -> String { + Input::new() + .with_prompt(title) + .interact_text() + .expect("User didn't enter anything") +} + +pub fn run(config: &ModToml) { + 'main_loop: loop { + let res = select( + Some("What do you want to do?"), + vec![ + MainMenu::Install, + MainMenu::Collect, + MainMenu::Pull, + MainMenu::Diff, + MainMenu::Upload, + MainMenu::Quit + ] + ); + match res { + MainMenu::Install => + operations::install(&multi_select( + Some("Which modules do you want to install?"), + config.into() + )), + MainMenu::Collect => + operations::collect(&multi_select( + Some("Which modules do you want to collect?"), + config.into() + )), + MainMenu::Pull => repository::pull_repo(), + MainMenu::Diff => repository::diff(), + MainMenu::Upload => operations::upload(|| input("Commit message")), + MainMenu::Quit => break 'main_loop + } + } +} \ No newline at end of file