update:ui qol

This commit is contained in:
kokopi-dev
2026-04-05 00:36:10 +09:00
parent 35d33a6f12
commit 85cd4c6406
2 changed files with 72 additions and 24 deletions

View File

@@ -9,8 +9,8 @@ use crate::{error::AppError, services::ServiceStore};
#[derive(Clone, Eq, PartialEq)]
enum MenuAction {
Add,
List,
Add,
Edit,
Remove,
}
@@ -19,15 +19,15 @@ pub fn show_main_menu(store: &ServiceStore) -> Result<(), AppError> {
intro("scripts-organizer")?;
let action = select("What would you like to do?")
.item(MenuAction::Add, "Add", "register a new script")
.item(MenuAction::List, "List", "view and run registered scripts")
.item(MenuAction::Add, "Add", "register a new script")
.item(MenuAction::Edit, "Edit", "update a registered script's settings")
.item(MenuAction::Remove, "Remove", "unregister a script")
.interact()?;
match action {
MenuAction::Add => add::run(store)?,
MenuAction::List => list::run(store)?,
MenuAction::Add => add::run(store)?,
MenuAction::Edit => edit::run(store)?,
MenuAction::Remove => remove::run(store)?,
}

View File

@@ -3,17 +3,25 @@ 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},
};
/// Health of a registered script's files on disk.
#[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()?;
@@ -25,33 +33,74 @@ pub fn run(store: &ServiceStore) -> Result<(), AppError> {
return Ok(());
}
// Annotate each entry with its health before building the select items
let annotated: Vec<(&crate::config::types::ScriptEntry, Health)> = registry
// ── Group by primary tag, sort within each group by name ──────────────────
let mut scripts_with_health: Vec<(&ScriptEntry, Health)> = registry
.scripts
.iter()
.map(|entry| {
let health = if is_healthy(entry) { Health::Ok } else { Health::Broken };
(entry, health)
.map(|e| {
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
(e, health)
})
.collect();
// ── Script selection ───────────────────────────────────────────────────────
let selected_name: String = {
let mut prompt = select("Which script?");
for (entry, health) in &annotated {
let label = match health {
Health::Ok => entry.name.clone(),
Health::Broken => format!("{}", entry.name),
};
prompt = prompt.item(entry.name.clone(), label, &entry.description);
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,
}
prompt.interact()?
};
let (entry, health) = annotated
.into_iter()
// ── 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 annotated list");
.expect("selected name must exist in registry");
// ── Broken entry handling ──────────────────────────────────────────────────
if health == Health::Broken {
@@ -69,11 +118,10 @@ pub fn run(store: &ServiceStore) -> Result<(), AppError> {
return Ok(());
}
// ── Healthy entry: exec shim --help, replacing this process ───────────────
// ── Healthy: exec shim --help, replacing this process ─────────────────────
outro(format!("Running {} --help...", entry.name))?;
let err = Command::new(&entry.shim_path).arg("--help").exec();
// exec() only returns on failure
Err(AppError::Io(err))
}