use std::{fs, io};
use mingling::{
Grouped, RenderResult, Routable, ShellContext, Suggest, SuggestItem,
macros::{arg, chain, command, completion, metadata, pack, pack_err, renderer, routeify},
metadata::Description,
picker::{EntryPicker, PickerArg},
};
use crate::{
Next, eprintln_cargo,
pkg_mgr::{
ErrorNoDataDirectory, ErrorPackageNameRequired, ErrorPackageSpecInvalid, ResPackagesDir,
},
println_cargo,
};
pub static ARG_SPEC: PickerArg<String> = arg![String];
pack_err!(ErrorNoMatchingVersion = String);
pack!(StatePkgEnable = (String, String));
#[derive(Debug, Default, Grouped)]
pub struct ResultPkgEnable {
pub name: String,
pub version: String,
}
#[metadata(EntryPkgEnable)]
pub fn desc_pkg_enable() -> Description {
"Enable the specified package".into()
}
#[command(node = "pkg-enable", routeify)]
pub fn package_enable(args: EntryPkgEnable, packages_dir: &ResPackagesDir) -> Next {
let spec = args
.pick_or_route(&ARG_SPEC, || ErrorPackageNameRequired::default().to_chain())
.to_result()?;
let packages_dir = &packages_dir.path;
if packages_dir.as_os_str().is_empty() {
return ErrorNoDataDirectory::default().to_chain();
}
if spec.contains('/') || spec.contains('\\') || spec.contains("..") {
return ErrorPackageSpecInvalid::new(spec).to_chain();
}
let (name, version_part) = match spec.split_once('@') {
Some((name, version)) => (name.to_string(), Some(version.to_string())),
None => (spec.clone(), None),
};
let mut candidates: Vec<(String, semver::Version)> = Vec::new();
if let Ok(entries) = fs::read_dir(packages_dir) {
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
let Some(dir_name) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
let Some((dir_pkg, dir_version)) = dir_name.split_once('@') else {
continue;
};
if dir_pkg != name {
continue;
}
if let Some(part) = &version_part
&& !version_matches(dir_version, part)
{
continue;
}
if let Ok(version) = semver::Version::parse(dir_version) {
candidates.push((dir_name, version));
}
}
}
let Some((_, version)) = candidates.into_iter().max_by(|a, b| a.1.cmp(&b.1)) else {
return ErrorNoMatchingVersion::new(spec).to_chain();
};
StatePkgEnable::new((name, version.to_string())).to_chain()
}
#[chain(routeify)]
pub fn handle_state_pkg_enable(p: StatePkgEnable, packages_dir: &ResPackagesDir) -> Next {
let (name, version) = p.inner;
let packages_dir = &packages_dir.path;
if packages_dir.as_os_str().is_empty() {
return ErrorNoDataDirectory::default().to_chain();
}
let file = packages_dir.join(&name);
fs::write(&file, &version).map_err(|e| {
io::Error::new(e.kind(), format!("failed to write {}: {e}", file.display()))
})?;
ResultPkgEnable { name, version }.to_chain()
}
#[renderer]
pub fn render_result_pkg_enable(result: ResultPkgEnable) -> RenderResult {
let mut r = RenderResult::new();
println_cargo!(r, "Enabled: {}@{}", result.name, result.version);
r
}
#[renderer]
pub fn render_error_no_matching_version(err: ErrorNoMatchingVersion) -> RenderResult {
let mut r = RenderResult::new();
eprintln_cargo!(r, "no matching version for: {}", err.info);
r
}
#[completion(EntryPkgEnable)]
pub fn complete_pkg_enable(ctx: &ShellContext, packages_dir: &ResPackagesDir) -> Suggest {
if ctx.previous_word != "pkg-enable" {
return Suggest::FileCompletion;
}
let mut suggest = Suggest::new();
if let Ok(entries) = fs::read_dir(&packages_dir.path) {
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|t| t.is_dir()) {
continue;
}
if let Some(name) = entry.file_name().to_str() {
suggest.insert(SuggestItem::Simple(name.to_string()));
}
}
}
suggest
}
fn version_matches(version: &str, partial: &str) -> bool {
let version = version.split('-').next().unwrap_or(version);
let version_parts: Vec<&str> = version.split('.').collect();
let partial_parts: Vec<&str> = partial.split('.').collect();
if partial_parts.len() > version_parts.len() {
return false;
}
partial_parts
.iter()
.zip(version_parts.iter())
.all(|(p, v)| p == v)
}