use std::{
fs,
process,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use anyhow::{bail, Context as _, Result};
use inquire::{Select, Text};
use itertools::Itertools;
use tabled::{
settings::{object::Rows, Color, Padding, Style},
Table,
Tabled,
};
use tracing::debug;
use url::Url;
use walkdir::WalkDir;
use yansi::Paint;
use crate::{
commands,
config::{Config, EditableConfig, LockedConfig, Package, Source},
context::Context,
manager::{restore_package, restore_packages, sync_package, sync_packages},
provider::Github,
util::{remove_file_if_exists, rm_rf, Emojify, Shorten as _},
};
pub fn init(ctx: &Context, from: Option<Url>) -> Result<()> {
if ctx.config_file.exists() {
bail!("config file already exists: {}", ctx.config_file.display());
}
remove_file_if_exists(&ctx.lock_file)
.with_context(|| format!("failed to remove lock file {}", &ctx.lock_file.display(),))?;
match from {
Some(url) => {
let body = ureq::get(url.as_str()).call()?.into_string()?;
debug!("fetched config file: {}", body);
toml::from_str::<Config>(&body)?;
fs::write(&ctx.config_file, body)?;
}
None => {
Config::load(ctx)?;
}
}
ctx.log_header("Initialized", ctx.config_file.shorten()?);
sync(ctx, false)
}
pub fn list(ctx: &Context) -> Result<(), anyhow::Error> {
let lcfg = LockedConfig::load(ctx)?;
ctx.log_header_v("Loaded", ctx.lock_file.shorten()?);
#[derive(Debug, Tabled)]
#[tabled(rename_all = "UPPERCASE")]
struct Item {
pkg: String,
version: String,
description: String,
}
let items = lcfg
.pkgs
.into_values()
.sorted_by(|a, b| a.name.cmp(&b.name))
.map(|lpkg| Item {
pkg: lpkg.name,
version: lpkg.version,
description: lpkg.desc.map(|s| s.emojify()).unwrap_or_default(),
});
let mut table = Table::new(items);
table
.with(Style::empty())
.modify(Rows::first(), Color::BOLD)
.with(Padding::new(0, 4, 0, 0));
println!("{table}");
Ok(())
}
pub fn add(ctx: &Context, mut pkg: Package) -> Result<()> {
let mut ecfg = EditableConfig::load(ctx)?;
ctx.log_header_v("Loaded", ctx.config_file.shorten()?);
if ecfg.contains(&pkg.name) {
bail!("package {} already exists", pkg.name.blue());
}
let lpkg = sync_package(ctx, &pkg, None, false)?;
pkg.desc = lpkg.desc.clone();
ecfg.upsert(&pkg)?;
let mut lcfg = LockedConfig::load(ctx)?;
lcfg.upsert(lpkg);
ecfg.save()?;
lcfg.save()?;
ctx.log_header_v("Locked", ctx.lock_file.shorten()?);
Ok(())
}
pub fn sync(ctx: &Context, update: bool) -> Result<(), anyhow::Error> {
let cfg = Config::load(ctx)?;
ctx.log_header_v("Loaded", ctx.config_file.shorten()?);
let mut lcfg = LockedConfig::load(ctx)?;
ctx.log_header_v("Loaded", ctx.lock_file.shorten()?);
sync_packages(ctx, &cfg, &mut lcfg, update)?;
lcfg.save()?;
ctx.log_header_v("Locked", ctx.lock_file.shorten()?);
Ok(())
}
pub fn restore(ctx: &Context, package: Option<String>) -> Result<(), anyhow::Error> {
let lcfg = LockedConfig::load(ctx)?;
ctx.log_header_v("Loaded", ctx.lock_file.shorten()?);
match package {
Some(pkg) => {
let lpkg = lcfg
.pkgs
.get(&pkg)
.with_context(|| format!("package {} not found", pkg))?;
restore_package(ctx, lpkg)?;
}
None => restore_packages(lcfg)?,
}
Ok(())
}
pub fn update(ctx: &Context, package: Option<String>) -> Result<(), anyhow::Error> {
match package {
Some(package) => {
let cfg = Config::load(ctx)?;
ctx.log_header_v("Loaded", ctx.config_file.shorten()?);
let pkg = cfg
.pkgs
.values()
.find(|pkg| pkg.name == package)
.cloned()
.with_context(|| format!("package {} not found", package))?;
let mut lcfg = LockedConfig::load(ctx)?;
let old_lpkg = lcfg.pkgs.get(&package);
let new_lpkg = sync_package(ctx, &pkg, old_lpkg, true)?;
lcfg.upsert(new_lpkg);
lcfg.save()?;
ctx.log_header_v("Locked", ctx.lock_file.shorten()?);
Ok(())
}
None => sync(ctx, true),
}
}
pub fn cleanup(ctx: &Context, clear_cache: bool) -> Result<()> {
let lcfg = LockedConfig::load(ctx)?;
for entry in WalkDir::new(&ctx.data_dir).max_depth(2) {
let entry = entry?;
match entry.depth() {
1 => match entry.file_name().to_str() {
Some(name) if lcfg.pkgs.contains_key(name) => {
continue;
}
_ => {
rm_rf(entry.path())?;
ctx.log_status("Removed", entry.path().shorten()?);
}
},
2 => {
let mut parts = entry
.path()
.components()
.map(|c| c.as_os_str().to_str())
.rev();
match (parts.next(), parts.next()) {
(Some(Some(version)), Some(Some(name))) => match lcfg.pkgs.get(name) {
Some(lpkg) if lpkg.version == version => {
continue;
}
_ => {
rm_rf(entry.path())?;
ctx.log_status("Removed", entry.path().shorten()?);
}
},
_ => {
rm_rf(entry.path())?;
ctx.log_status("Removed", entry.path().shorten()?);
}
}
}
_ => continue,
}
}
if clear_cache {
for entry in fs::read_dir(&ctx.cache_dir)? {
let entry = entry?;
rm_rf(&entry.path())?;
ctx.log_status("Removed", entry.path().shorten()?);
}
}
Ok(())
}
pub fn search(query: String, top: u8, ctx: &Context) -> Result<(), anyhow::Error> {
let gh = Github::new(ctx.clone())?;
let repos = gh.search_repo(&query, top)?;
let stars_width = Arc::new(AtomicUsize::new(0));
let fullname_width = Arc::new(AtomicUsize::new(0));
let items: Vec<_> = repos
.into_iter()
.flat_map(|repo| {
Some(RepoItem {
name: repo.name,
desc: repo.description.unwrap_or_default().emojify(),
stars: repo
.stargazers_count
.map(|x| format!("★ {x}"))
.unwrap_or_default(),
stars_width: stars_width.clone(),
fullname: repo.full_name?,
fullname_width: fullname_width.clone(),
})
})
.inspect(|item| {
stars_width.fetch_max(item.stars.len(), Ordering::Relaxed);
fullname_width.fetch_max(item.fullname.len(), Ordering::Relaxed);
})
.collect();
let page_size = items.len().min(25);
let answer = Select::new("Select a package ", items)
.with_page_size(page_size)
.prompt();
use inquire::InquireError::*;
let answer = match answer {
Err(OperationCanceled | OperationInterrupted) => process::exit(1),
answer => answer?,
};
let name = Text::new("Choose package name?")
.with_initial_value(&answer.name)
.prompt()?;
let bins = Text::new("Choose binary name(s)?")
.with_initial_value(&answer.name)
.prompt()?;
let bins: Vec<_> = bins.split(' ').map(str::trim).map(str::to_owned).collect();
let bins = if bins.is_empty() {
vec![name.clone()]
} else {
bins
};
let pkg = Package {
name,
bins,
source: Source::Github { repo: answer.fullname },
version: None,
desc: match answer.desc.is_empty() {
false => Some(answer.desc.emojify()),
true => None,
},
enabled: true.into(),
};
debug!("selected: {:?}", pkg);
commands::add(ctx, pkg)?;
Ok(())
}
struct RepoItem {
name: String,
fullname: String,
desc: String,
stars: String,
stars_width: Arc<AtomicUsize>,
fullname_width: Arc<AtomicUsize>,
}
impl std::fmt::Display for RepoItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self { stars, fullname, desc, .. } = self;
let stars_width = self.stars_width.load(Ordering::Relaxed);
let fullname_width = self.fullname_width.load(Ordering::Relaxed);
f.write_fmt(format_args!(
"{stars:stars_width$} {fullname:fullname_width$} {desc}",
))
}
}