use anyhow::{Result, anyhow};
use colored::Colorize;
use dialoguer::Select;
use dialoguer::theme::ColorfulTheme;
pub use zoi_core::dependency::Dependency;
use zoi_core::{types, utils};
include!(concat!(env!("OUT_DIR"), "/generated_managers.rs"));
pub fn parse_dependency_string(dep_str: &str) -> Result<Dependency<'_>> {
zoi_core::dependency::parse_dependency_string(dep_str, |m| {
m == "zoi" || m == "native" || MANAGERS.contains_key(m)
})
}
type ZoiUninstaller = dyn Fn(&str) -> Result<()>;
pub fn uninstall_dependency(
dep_str: &str,
zoi_uninstaller: &ZoiUninstaller
) -> Result<()> {
let dep = parse_dependency_string(dep_str)?;
println!(
"-> Attempting to uninstall dependency: {} via {}",
dep.package.cyan(),
dep.manager.yellow()
);
if dep.manager == "zoi" {
return zoi_uninstaller(dep.package);
}
if let Some(pm_commands) = MANAGERS.get(dep.manager) {
let mut uninstall_cmd =
pm_commands.uninstall.replace("{package}", dep.package);
if pm_commands.sudo_uninstall && !utils::is_admin() {
if let Some(escalator) = utils::get_privilege_escalator() {
uninstall_cmd = format!("{escalator} {uninstall_cmd}");
} else {
eprintln!(
"{}: root privileges are required for '{}' but neither \
'sudo' nor 'doas' was found. Attempting to run without \
escalation...",
"Warning".yellow(),
dep.manager
);
}
}
println!("Running uninstall command: {}", uninstall_cmd.italic());
utils::run_shell_command(&uninstall_cmd)
} else {
Err(anyhow!(
"Unknown or unsupported package manager for uninstall: {}",
dep.manager
))
}
}
pub fn collect_dependencies_for_group(
group: &types::DependencyGroup,
sub_package_name: Option<&str>,
dep_type: Option<&str>,
yes: bool,
all_optional: bool
) -> Result<(Vec<String>, Vec<String>, Vec<String>)> {
let mut deps = Vec::new();
let mut chosen_options = Vec::new();
let mut chosen_optionals = Vec::new();
match group {
types::DependencyGroup::Simple(d) => {
deps.extend(d.clone());
}
types::DependencyGroup::Complex(g) => {
deps.extend(g.required.clone());
let options = prompt_for_options(&g.options, yes)?;
chosen_options.extend(options.clone());
deps.extend(options);
let optionals =
prompt_for_optionals(&g.optional, dep_type, yes, all_optional)?;
chosen_optionals.extend(optionals.clone());
deps.extend(optionals);
if let Some(sub_name) = sub_package_name
&& let Some(sub_deps_map) = &g.sub_packages
&& let Some(sub_dep_group) = sub_deps_map.get(sub_name)
{
let (sub_d, sub_co, sub_coo) = collect_dependencies_for_group(
sub_dep_group,
None,
dep_type,
yes,
all_optional
)?;
deps.extend(sub_d);
chosen_options.extend(sub_co);
chosen_optionals.extend(sub_coo);
}
}
}
Ok((deps, chosen_options, chosen_optionals))
}
pub fn prompt_for_options(
option_groups: &[types::DependencyOptionGroup],
yes: bool
) -> Result<Vec<String>> {
let mut chosen = Vec::new();
if option_groups.is_empty() {
return Ok(chosen);
}
for group in option_groups {
println!(
"{} There are {} options available for {}:",
"::".bold().blue(),
group.depends.len(),
group.desc.italic()
);
let parsed_deps: Vec<_> = group
.depends
.iter()
.map(|d| parse_dependency_string(d))
.collect::<Result<_>>()?;
if yes {
if group.all {
println!(
"--yes provided, selecting all options for '{}'",
group.name
);
chosen.extend(group.depends.clone());
} else {
println!(
"--yes provided, selecting first option for '{}'",
group.name
);
if let Some(dep) = group.depends.first() {
chosen.push(dep.clone());
}
}
continue;
}
if group.all {
let items: Vec<_> = parsed_deps
.iter()
.map(|d| {
format!(
"{}:{} - {}",
d.manager,
d.package,
d.description.unwrap_or("No description")
)
})
.collect();
let selections =
dialoguer::MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt(
"Choose which to install (space to select, enter to \
confirm)"
)
.items(&items)
.interact()?;
for i in selections {
if let Some(dep) = group.depends.get(i) {
chosen.push(dep.clone());
}
}
} else {
let items: Vec<_> = parsed_deps
.iter()
.map(|d| {
format!(
"{}:{} - {}",
d.manager,
d.package,
d.description.unwrap_or("No description")
)
})
.collect();
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Choose one to install")
.items(&items)
.default(0)
.interact()?;
if let Some(dep) = group.depends.get(selection) {
chosen.push(dep.clone());
}
}
}
Ok(chosen)
}
pub fn prompt_for_optionals(
deps: &[String],
dep_type: Option<&str>,
yes: bool,
all_optional: bool
) -> Result<Vec<String>> {
if deps.is_empty() {
return Ok(Vec::new());
}
let type_str = dep_type.map(|s| format!("{s} ")).unwrap_or_default();
if all_optional {
println!(
"{} Installing all optional {}dependencies...",
"::".bold().blue(),
type_str
);
return Ok(deps.to_vec());
}
if yes {
println!(
"{} Skipping optional {}dependencies (--yes provided without \
--all-optional).",
"::".bold().yellow(),
type_str
);
return Ok(Vec::new());
}
let items: Vec<_> = deps
.iter()
.map(|d| {
parse_dependency_string(d).map(|dep| {
format!(
"{}:{} - {}",
dep.manager,
dep.package,
dep.description.unwrap_or("No description")
)
})
})
.collect::<Result<_>>()?;
let selections =
dialoguer::MultiSelect::with_theme(&ColorfulTheme::default())
.with_prompt(format!(
"Select optional {type_str}dependencies to install"
))
.items(&items)
.defaults(&vec![false; deps.len()])
.interact()?;
let mut chosen = Vec::new();
for i in selections {
if let Some(dep) = deps.get(i) {
chosen.push(dep.clone());
}
}
Ok(chosen)
}