use std::path::{Path, PathBuf};
use super::daemon_registry::{DaemonRecord, StopOutcome, StopResult};
pub const STOP_ALL: &str = "__stop_all__";
pub const CANCEL: &str = "__cancel__";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PickItem {
pub value: String,
pub label: String,
pub hint: Option<String>,
}
pub fn format_uptime(ms: i64) -> String {
let seconds = ms.max(0) / 1000;
if seconds < 60 {
return format!("{seconds}s");
}
let minutes = seconds / 60;
if minutes < 60 {
return format!("{minutes}m");
}
let hours = minutes / 60;
format!("{hours}h {}m", minutes % 60)
}
pub fn build_pick_items(
daemons: &[DaemonRecord],
cwd_root: Option<&Path>,
now_ms: i64,
) -> Vec<PickItem> {
let cwd = cwd_root.map(resolve_lossy);
let mut ordered = daemons.to_vec();
ordered.sort_by(|a, b| {
if let Some(cwd) = &cwd {
let a_current = resolve_lossy(Path::new(&a.root)) == *cwd;
let b_current = resolve_lossy(Path::new(&b.root)) == *cwd;
if a_current != b_current {
return b_current.cmp(&a_current);
}
}
b.started_at.cmp(&a.started_at)
});
let mut items = Vec::new();
for daemon in ordered {
let current = cwd
.as_ref()
.is_some_and(|cwd| resolve_lossy(Path::new(&daemon.root)) == *cwd);
items.push(PickItem {
value: daemon.root.clone(),
label: if current {
format!("{} (current project)", daemon.root)
} else {
daemon.root.clone()
},
hint: Some(format!(
"pid {} - up {} - Running",
daemon.pid,
format_uptime(now_ms - daemon.started_at)
)),
});
}
if items.len() > 1 {
items.push(PickItem {
value: STOP_ALL.to_string(),
label: "Stop all".to_string(),
hint: Some(String::new()),
});
}
items.push(PickItem {
value: CANCEL.to_string(),
label: "Cancel".to_string(),
hint: Some(String::new()),
});
items
}
pub trait PickerDeps {
fn list(&self) -> Vec<DaemonRecord>;
fn stop(&mut self, root: &str) -> StopResult;
fn stop_all(&mut self) -> Vec<StopResult>;
fn cwd_root(&self) -> Option<PathBuf>;
fn now(&self) -> i64;
fn select(&mut self, items: &[PickItem], initial_value: &str) -> Option<String>;
fn note(&mut self, msg: &str);
fn done(&mut self, msg: &str);
}
pub fn run_daemon_picker<D: PickerDeps>(deps: &mut D) {
loop {
let daemons = deps.list();
if daemons.is_empty() {
deps.done("All daemons stopped.");
return;
}
let cwd_root = deps.cwd_root();
let items = build_pick_items(&daemons, cwd_root.as_deref(), deps.now());
let initial = items
.first()
.map(|item| item.value.as_str())
.unwrap_or(CANCEL);
let choice = deps.select(&items, initial);
let Some(choice) = choice else {
deps.done("Cancelled.");
return;
};
if choice == CANCEL {
deps.done("Cancelled.");
return;
}
if choice == STOP_ALL {
let results = deps.stop_all();
let n = results
.iter()
.filter(|result| matches!(result.outcome, StopOutcome::Term | StopOutcome::Kill))
.count();
deps.note(&format!(
"Stopped {n} daemon{}.",
if n == 1 { "" } else { "s" }
));
deps.done("Done.");
return;
}
let result = deps.stop(&choice);
let forced = if result.outcome == StopOutcome::Kill {
", forced"
} else {
""
};
deps.note(&format!(
"Stopped daemon (pid {}{forced}) - {choice}",
result
.pid
.map(|pid| pid.to_string())
.unwrap_or_else(|| "?".to_string())
));
}
}
fn resolve_lossy(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}