update:ui qol
This commit is contained in:
@@ -9,8 +9,8 @@ use crate::{error::AppError, services::ServiceStore};
|
|||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
enum MenuAction {
|
enum MenuAction {
|
||||||
Add,
|
|
||||||
List,
|
List,
|
||||||
|
Add,
|
||||||
Edit,
|
Edit,
|
||||||
Remove,
|
Remove,
|
||||||
}
|
}
|
||||||
@@ -19,15 +19,15 @@ pub fn show_main_menu(store: &ServiceStore) -> Result<(), AppError> {
|
|||||||
intro("scripts-organizer")?;
|
intro("scripts-organizer")?;
|
||||||
|
|
||||||
let action = select("What would you like to do?")
|
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::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::Edit, "Edit", "update a registered script's settings")
|
||||||
.item(MenuAction::Remove, "Remove", "unregister a script")
|
.item(MenuAction::Remove, "Remove", "unregister a script")
|
||||||
.interact()?;
|
.interact()?;
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
MenuAction::Add => add::run(store)?,
|
|
||||||
MenuAction::List => list::run(store)?,
|
MenuAction::List => list::run(store)?,
|
||||||
|
MenuAction::Add => add::run(store)?,
|
||||||
MenuAction::Edit => edit::run(store)?,
|
MenuAction::Edit => edit::run(store)?,
|
||||||
MenuAction::Remove => remove::run(store)?,
|
MenuAction::Remove => remove::run(store)?,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,25 @@ use std::{os::unix::process::CommandExt, process::Command};
|
|||||||
use cliclack::{confirm, log, note, outro, select};
|
use cliclack::{confirm, log, note, outro, select};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
config::types::ScriptEntry,
|
||||||
error::AppError,
|
error::AppError,
|
||||||
services::{registry_service::is_healthy, ServiceStore},
|
services::{registry_service::is_healthy, ServiceStore},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Health of a registered script's files on disk.
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
enum Health {
|
enum Health {
|
||||||
Ok,
|
Ok,
|
||||||
Broken,
|
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> {
|
pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
||||||
let registry = store.registry.load()?;
|
let registry = store.registry.load()?;
|
||||||
|
|
||||||
@@ -25,33 +33,74 @@ pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Annotate each entry with its health before building the select items
|
// ── Group by primary tag, sort within each group by name ──────────────────
|
||||||
let annotated: Vec<(&crate::config::types::ScriptEntry, Health)> = registry
|
let mut scripts_with_health: Vec<(&ScriptEntry, Health)> = registry
|
||||||
.scripts
|
.scripts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| {
|
.map(|e| {
|
||||||
let health = if is_healthy(entry) { Health::Ok } else { Health::Broken };
|
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
|
||||||
(entry, health)
|
(e, health)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// ── Script selection ───────────────────────────────────────────────────────
|
scripts_with_health.sort_by(|(a, _), (b, _)| {
|
||||||
let selected_name: String = {
|
let tag_a = a.tags.first().map(String::as_str).unwrap_or("untagged");
|
||||||
let mut prompt = select("Which script?");
|
let tag_b = b.tags.first().map(String::as_str).unwrap_or("untagged");
|
||||||
for (entry, health) in &annotated {
|
tag_a.cmp(tag_b).then(a.name.cmp(&b.name))
|
||||||
let label = match health {
|
});
|
||||||
Health::Ok => entry.name.clone(),
|
|
||||||
Health::Broken => format!("⚠ {}", entry.name),
|
// Collect into (group_label, entries) pairs
|
||||||
};
|
let mut groups: Vec<(String, Vec<(&ScriptEntry, Health)>)> = Vec::new();
|
||||||
prompt = prompt.item(entry.name.clone(), label, &entry.description);
|
for (entry, health) in scripts_with_health {
|
||||||
}
|
let group_key = if entry.tags.is_empty() {
|
||||||
prompt.interact()?
|
"untagged".to_string()
|
||||||
|
} else {
|
||||||
|
// Header shows all tags: [git, config]
|
||||||
|
format!("[{}]", entry.tags.join(", "))
|
||||||
};
|
};
|
||||||
|
|
||||||
let (entry, health) = annotated
|
match groups.last_mut() {
|
||||||
.into_iter()
|
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)
|
.find(|(e, _)| e.name == selected_name)
|
||||||
.expect("selected name must exist in annotated list");
|
.expect("selected name must exist in registry");
|
||||||
|
|
||||||
// ── Broken entry handling ──────────────────────────────────────────────────
|
// ── Broken entry handling ──────────────────────────────────────────────────
|
||||||
if health == Health::Broken {
|
if health == Health::Broken {
|
||||||
@@ -69,11 +118,10 @@ pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Healthy entry: exec shim --help, replacing this process ───────────────
|
// ── Healthy: exec shim --help, replacing this process ─────────────────────
|
||||||
outro(format!("Running {} --help...", entry.name))?;
|
outro(format!("Running {} --help...", entry.name))?;
|
||||||
|
|
||||||
let err = Command::new(&entry.shim_path).arg("--help").exec();
|
let err = Command::new(&entry.shim_path).arg("--help").exec();
|
||||||
|
|
||||||
// exec() only returns on failure
|
|
||||||
Err(AppError::Io(err))
|
Err(AppError::Io(err))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user