use anyhow::Result;
use console::style;
use crate::exec;
#[derive(Debug, Clone, PartialEq)]
pub enum Source {
Nix,
BrewCask,
BrewFormula,
}
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::Nix => write!(f, "nixpkgs"),
Source::BrewCask => write!(f, "brew cask"),
Source::BrewFormula => write!(f, "brew formula"),
}
}
}
#[derive(Debug, Clone)]
pub struct Candidate {
pub source: Source,
pub version: String,
}
pub enum Resolution {
Single(Candidate),
Conflict {
candidates: Vec<Candidate>,
recommended: Source,
reason: String,
},
NotFound,
}
pub fn resolve(pkg: &str) -> Result<Resolution> {
let mut candidates = Vec::new();
if let Some(version) = exec::nix_eval_version(pkg)? {
candidates.push(Candidate {
source: Source::Nix,
version,
});
}
if let Some(version) = exec::brew_cask_info(pkg)? {
candidates.push(Candidate {
source: Source::BrewCask,
version,
});
}
if !candidates.iter().any(|c| c.source == Source::BrewCask) {
if let Some(version) = exec::brew_formula_info(pkg)? {
candidates.push(Candidate {
source: Source::BrewFormula,
version,
});
}
}
match candidates.len() {
0 => Ok(Resolution::NotFound),
1 => {
#[allow(clippy::unwrap_used)]
let candidate = candidates.into_iter().next().unwrap();
Ok(Resolution::Single(candidate))
}
_ => {
let (recommended, reason) = recommend(&candidates);
Ok(Resolution::Conflict {
candidates,
recommended,
reason,
})
}
}
}
fn recommend(candidates: &[Candidate]) -> (Source, String) {
let has_cask = candidates.iter().any(|c| c.source == Source::BrewCask);
let has_nix = candidates.iter().any(|c| c.source == Source::Nix);
if has_cask && has_nix {
let nix_ver = candidates
.iter()
.find(|c| c.source == Source::Nix)
.map(|c| c.version.as_str())
.unwrap_or("");
let cask_ver = candidates
.iter()
.find(|c| c.source == Source::BrewCask)
.map(|c| c.version.as_str())
.unwrap_or("");
if nix_ver == cask_ver {
return (
Source::BrewCask,
"GUI app — cask provides native .app bundle with code signing".into(),
);
}
return (
Source::BrewCask,
format!(
"GUI app — cask is {} (nix has {}), with native .app bundle",
cask_ver, nix_ver
),
);
}
(
Source::Nix,
"CLI tool — nix provides declarative management".into(),
)
}
pub fn prompt_resolution(
pkg: &str,
candidates: &[Candidate],
recommended: &Source,
reason: &str,
) -> Result<Option<Source>> {
eprintln!();
eprintln!(" {} found in multiple sources:", style(pkg).bold());
eprintln!();
for c in candidates {
let marker = if c.source == *recommended {
style("*").green().bold().to_string()
} else {
" ".to_string()
};
let source_label = format!("{:<14}", c.source.to_string());
eprintln!(
" {} {} {}",
marker,
style(source_label).cyan(),
c.version
);
}
eprintln!();
eprintln!(" {} {}", style("recommended:").dim(), reason,);
eprintln!();
let items: Vec<String> = candidates
.iter()
.map(|c| {
let rec = if c.source == *recommended {
" (recommended)"
} else {
""
};
format!("{} {}{}", c.source, c.version, rec)
})
.collect();
let default_idx = candidates
.iter()
.position(|c| c.source == *recommended)
.unwrap_or(0);
let selection = dialoguer::Select::new()
.with_prompt("Install as")
.items(&items)
.default(default_idx)
.interact_opt()?;
match selection {
Some(idx) => Ok(Some(candidates[idx].source.clone())),
None => Ok(None),
}
}