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,
versions_equal: bool,
},
NotFound,
}
pub struct ResolveResult {
pub resolution: Resolution,
pub brew_checked: bool,
}
pub fn resolve(pkg: &str) -> Result<ResolveResult> {
let mut candidates = Vec::new();
let brew_checked = exec::brew_available();
let nix_attr = crate::aliases::nixpkgs_attr(pkg);
let nix_version = exec::nix_eval_version(nix_attr)?.or(if nix_attr != pkg {
exec::nix_eval_version(pkg)?
} else {
None
});
if let Some(version) = nix_version {
candidates.push(Candidate {
source: Source::Nix,
version,
});
}
if brew_checked {
let cask_version =
exec::brew_cask_info(pkg)?.or(match crate::aliases::brew_cask_name(pkg) {
Some(cask_name) if cask_name != pkg => exec::brew_cask_info(cask_name)?,
_ => None,
});
if let Some(version) = cask_version {
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,
});
}
}
}
let resolution = match candidates.len() {
0 => Resolution::NotFound,
1 => {
#[allow(clippy::unwrap_used)]
let candidate = candidates.into_iter().next().unwrap();
Resolution::Single(candidate)
}
_ => {
let (recommended, reason, versions_equal) = recommend(&candidates);
Resolution::Conflict {
candidates,
recommended,
reason,
versions_equal,
}
}
};
Ok(ResolveResult {
resolution,
brew_checked,
})
}
fn recommend(candidates: &[Candidate]) -> (Source, String, bool) {
let nix = candidates.iter().find(|c| c.source == Source::Nix);
let brew = candidates
.iter()
.find(|c| c.source == Source::BrewCask || c.source == Source::BrewFormula);
if let (Some(n), Some(b)) = (nix, brew) {
let eq = n.version == b.version;
if eq {
return (
Source::Nix,
"same version in both — nix provides declarative management".into(),
true,
);
}
let brew_label = if b.source == Source::BrewCask {
"cask"
} else {
"formula"
};
return (
b.source.clone(),
format!("brew {brew_label} is {} (nix has {})", b.version, n.version),
false,
);
}
(
Source::Nix,
"nix provides declarative management".into(),
false,
)
}
pub struct PromptResult {
pub source: Source,
pub remember_nix: bool,
}
pub fn prompt_resolution(
pkg: &str,
candidates: &[Candidate],
recommended: &Source,
reason: &str,
versions_equal: bool,
) -> Result<Option<PromptResult>> {
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 mut items: Vec<String> = candidates
.iter()
.map(|c| {
let rec = if c.source == *recommended {
" (recommended)"
} else {
""
};
format!("{} {}{}", c.source, c.version, rec)
})
.collect();
if versions_equal {
items.push("Always use nix when versions match".into());
}
let default_idx = candidates
.iter()
.position(|c| c.source == *recommended)
.unwrap_or(0);
if !std::io::IsTerminal::is_terminal(&std::io::stdin()) {
return Ok(Some(PromptResult {
source: recommended.clone(),
remember_nix: false,
}));
}
let selection = dialoguer::Select::new()
.with_prompt("Install as")
.items(&items)
.default(default_idx)
.interact_opt()?;
match selection {
Some(idx) if idx < candidates.len() => Ok(Some(PromptResult {
source: candidates[idx].source.clone(),
remember_nix: false,
})),
Some(_) => {
Ok(Some(PromptResult {
source: Source::Nix,
remember_nix: true,
}))
}
None => Ok(None),
}
}