use clap::{Parser, Subcommand};
use directories::ProjectDirs;
use regex::Regex;
mod actions;
#[allow(dead_code)]
mod config;
#[derive(Parser)]
#[clap(name = "Papa")]
#[clap(author = "AnAcutalEmerald <emerald_actual@proton.me>")]
#[clap(version = env!("CARGO_PKG_VERSION"))]
#[clap(about = "Command line mod manager for Northstar")]
#[clap(after_help = "Welcome back. Cockpit cooling reactivated.")]
struct Cli {
#[clap(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Install {
#[clap(value_name = "MOD")]
#[clap(help = "Mod name(s) in Author.ModName@version format")]
mod_names: Vec<String>,
},
Remove {
#[clap(value_name = "MOD")]
#[clap(help = "Mod name(s) to remove in Author.ModName format")]
mod_names: Vec<String>,
},
List {},
Clear {
#[clap(
help = "Force removal of all files in the cahce directory, not just downloaded packages"
)]
#[clap(long, short)]
full: bool,
},
}
const BASE_URL: &str = "https://northstar.thunderstore.io/package/download";
#[tokio::main]
async fn main() -> Result<(), String> {
let cli = Cli::parse();
let dirs = ProjectDirs::from("me", "greenboi", "papa").unwrap();
utils::ensure_dirs(&dirs);
let config = config::load_config(dirs.config_dir()).unwrap();
match cli.command {
Commands::List {} => {
let mods = utils::list_dir(&config.mod_dir().join("mods/"))?;
if !mods.is_empty() {
println!("Installed mods:\n");
mods.into_iter().for_each(|f| println!("\t{}", f));
} else {
println!("No mods currently installed");
}
}
Commands::Install { mod_names } => {
let mut valid = vec![];
for name in mod_names {
let re = Regex::new(r"(.+)\.(.+)@(v?\d.\d.\d)").unwrap();
if !re.is_match(&name) {
println!("Mod name should be in 'Author.ModName@1.2.3' format");
continue;
}
let url = actions::parse_mod_name(&name).unwrap();
let path = dirs.cache_dir().join(format!("{}.zip", name));
if let Some(f) = utils::check_cache(&path) {
println!("Using cached version of {}", name);
valid.push(f);
continue;
}
match actions::download_file(format!("{}{}", BASE_URL, url), path).await {
Ok(f) => valid.push(f),
Err(e) => eprintln!("{}", e),
}
}
valid.iter().for_each(|f| {
let pkg = actions::install_mod(f, &config).unwrap();
println!("Installed {}", pkg);
});
}
Commands::Remove { mod_names } => {
let re = Regex::new(r"(.+)\.(.+)").unwrap();
let valid = mod_names
.iter()
.filter(|f| re.is_match(f))
.collect::<Vec<&String>>();
actions::uninstall(valid, &config)?;
}
Commands::Clear { full } => {
if full {
println!("Clearing cache files...");
} else {
println!("Clearing cached packages...");
}
utils::remove_dir(dirs.cache_dir(), full)?;
}
}
Ok(())
}
mod utils {
use directories::ProjectDirs;
use std::fs::{self, File, OpenOptions};
use std::path::Path;
pub fn check_cache(path: &Path) -> Option<File> {
let opt = OpenOptions::new().read(true).open(path);
if let Ok(f) = opt {
Some(f)
} else {
None
}
}
pub fn ensure_dirs(dirs: &ProjectDirs) {
fs::create_dir_all(dirs.cache_dir()).unwrap();
fs::create_dir_all(dirs.config_dir()).unwrap();
}
pub fn remove_dir(dir: &Path, force: bool) -> Result<(), String> {
for entry in
fs::read_dir(dir).map_err(|_| format!("unable to read directory {}", dir.display()))?
{
let path = entry
.map_err(|_| "Error reading directory entry".to_string())?
.path();
println!("Removing {}", path.display());
if path.is_dir() {
remove_dir(&path, force)?;
fs::remove_dir(&path)
.map_err(|_| format!("Unable to remove directory {}", path.display()))?;
} else if path.ends_with(".zip") {
fs::remove_file(&path)
.map_err(|_| format!("Unable to remove file {}", path.display()))?;
} else if force {
fs::remove_file(&path)
.map_err(|_| format!("Unable to remove file {}", path.display()))?;
}
}
fs::remove_dir(&dir)
.map_err(|_| format!("Unable to remove directory {}", dir.display()))?;
println!("Removing {}", dir.display());
Ok(())
}
pub fn list_dir(dir: &Path) -> Result<Vec<String>, String> {
Ok(fs::read_dir(dir)
.map_err(|_| format!("unable to read directory {}", dir.display()))
.map_err(|_| format!("Unable to read directory {}", dir.display()))?
.filter(|f| f.is_ok())
.map(|f| f.unwrap())
.map(|f| f.file_name().to_string_lossy().into_owned())
.collect())
}
}