128 lines
4.4 KiB
Rust
128 lines
4.4 KiB
Rust
use std::{os::unix::process::CommandExt, process::Command};
|
|
|
|
use cliclack::{confirm, log, note, outro, select};
|
|
|
|
use crate::{
|
|
config::types::ScriptEntry,
|
|
error::AppError,
|
|
services::{registry_service::is_healthy, ServiceStore},
|
|
};
|
|
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
enum Health {
|
|
Ok,
|
|
Broken,
|
|
}
|
|
|
|
/// Select value — either a real script name or a group header sentinel.
|
|
/// Headers are re-prompted if accidentally selected.
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
enum Item {
|
|
Script(String),
|
|
Header(String),
|
|
}
|
|
|
|
pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
|
let registry = store.registry.load()?;
|
|
|
|
if registry.scripts.is_empty() {
|
|
note(
|
|
"No scripts registered",
|
|
"Use 'Add' to register your first script.",
|
|
)?;
|
|
return Ok(());
|
|
}
|
|
|
|
// ── Group by primary tag, sort within each group by name ──────────────────
|
|
let mut scripts_with_health: Vec<(&ScriptEntry, Health)> = registry
|
|
.scripts
|
|
.iter()
|
|
.map(|e| {
|
|
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
|
|
(e, health)
|
|
})
|
|
.collect();
|
|
|
|
scripts_with_health.sort_by(|(a, _), (b, _)| {
|
|
let tag_a = a.tags.first().map(String::as_str).unwrap_or("untagged");
|
|
let tag_b = b.tags.first().map(String::as_str).unwrap_or("untagged");
|
|
tag_a.cmp(tag_b).then(a.name.cmp(&b.name))
|
|
});
|
|
|
|
// Collect into (group_label, entries) pairs
|
|
let mut groups: Vec<(String, Vec<(&ScriptEntry, Health)>)> = Vec::new();
|
|
for (entry, health) in scripts_with_health {
|
|
let group_key = if entry.tags.is_empty() {
|
|
"untagged".to_string()
|
|
} else {
|
|
// Header shows all tags: [git, config]
|
|
format!("[{}]", entry.tags.join(", "))
|
|
};
|
|
|
|
match groups.last_mut() {
|
|
Some((key, entries)) if key == &group_key => entries.push((entry, health)),
|
|
_ => groups.push((group_key, vec![(entry, health)])),
|
|
}
|
|
}
|
|
|
|
// ── Build select — headers + indented script names ─────────────────────────
|
|
let selected_name = loop {
|
|
let mut prompt = select("Which script?");
|
|
|
|
for (group_label, entries) in &groups {
|
|
prompt = prompt.item(Item::Header(group_label.clone()), group_label, "");
|
|
|
|
for (entry, health) in entries {
|
|
let name = match health {
|
|
Health::Ok => format!(" * {}", entry.name),
|
|
Health::Broken => format!(" * ⚠ {}", entry.name),
|
|
};
|
|
prompt = prompt.item(
|
|
Item::Script(entry.name.clone()),
|
|
name,
|
|
&entry.description,
|
|
);
|
|
}
|
|
}
|
|
|
|
match prompt.interact()? {
|
|
Item::Script(name) => break name,
|
|
Item::Header(_) => continue,
|
|
}
|
|
};
|
|
|
|
// ── Look up the selected entry ─────────────────────────────────────────────
|
|
let (entry, health) = registry
|
|
.scripts
|
|
.iter()
|
|
.map(|e| {
|
|
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
|
|
(e, health)
|
|
})
|
|
.find(|(e, _)| e.name == selected_name)
|
|
.expect("selected name must exist in registry");
|
|
|
|
// ── Broken entry handling ──────────────────────────────────────────────────
|
|
if health == Health::Broken {
|
|
log::warning(format!(
|
|
"'{}' is broken — shim or symlink is missing.\n Source: {}",
|
|
entry.name,
|
|
entry.source_path.display(),
|
|
))?;
|
|
|
|
let remove = confirm("Remove this broken entry from the registry?").interact()?;
|
|
if remove {
|
|
store.registry.remove(&entry.name)?;
|
|
log::success(format!("'{}' removed from registry.", entry.name))?;
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
// ── Healthy: exec shim --help, replacing this process ─────────────────────
|
|
outro(format!("Running {} --help...", entry.name))?;
|
|
|
|
let err = Command::new(&entry.shim_path).arg("--help").exec();
|
|
|
|
Err(AppError::Io(err))
|
|
}
|