use std::io::IsTerminal;
use anyhow::{Result, bail};
use dialoguer::{Confirm, Select};
use crate::model::{App, Credential, Group, Server, State, Target};
pub fn interactive() -> bool {
std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
}
pub fn ensure_interactive(reason: &str) -> Result<()> {
if !interactive() {
bail!("{reason}: no terminal to prompt on");
}
Ok(())
}
pub fn confirm_destructive(prompt: String, assume_yes: bool) -> Result<bool> {
if assume_yes {
return Ok(true);
}
ensure_interactive("this needs confirming; pass --yes to skip the prompt")?;
Ok(Confirm::new().with_prompt(prompt).default(false).interact()?)
}
fn select(prompt: &str, labels: &[String]) -> Result<usize> {
Ok(Select::new().with_prompt(prompt).items(labels).default(0).interact()?)
}
pub fn app(apps: &[App], state: &State, prompt: &str) -> Result<String> {
if apps.is_empty() {
bail!("no apps in the catalog yet - run `turnout app add` first");
}
ensure_interactive("app name is required")?;
let width = apps.iter().map(|a| a.name.len()).max().unwrap_or(0);
let labels: Vec<String> = apps
.iter()
.map(|app| match state.bindings.get(&app.name) {
Some(server) => format!("{:width$} -> {server}", app.name),
None => format!("{:width$} (unbound)", app.name),
})
.collect();
Ok(apps[select(prompt, &labels)?].name.clone())
}
pub fn server(servers: &[Server], prompt: &str) -> Result<String> {
if servers.is_empty() {
bail!("no servers in the catalog yet - run `turnout server add` first");
}
ensure_interactive("server name is required")?;
let width = servers.iter().map(|s| s.name.len()).max().unwrap_or(0);
let labels: Vec<String> = servers.iter().map(|s| format!("{:width$} {}", s.name, s.url)).collect();
Ok(servers[select(prompt, &labels)?].name.clone())
}
pub fn server_for_app(servers: &[Server], app: &App, prompt: &str) -> Result<String> {
let allowed: Vec<Server> = if app.servers.is_empty() {
servers.to_vec()
} else {
servers.iter().filter(|s| app.servers.contains(&s.name)).cloned().collect()
};
if allowed.is_empty() {
bail!(
"no servers allowed for '{}' - add one with `turnout app edit {} --add-server NAME`",
app.name,
app.name
);
}
server(&allowed, prompt)
}
pub fn group(groups: &[Group], prompt: &str) -> Result<String> {
if groups.is_empty() {
bail!("no groups yet - run `turnout group add` first");
}
ensure_interactive("group name is required")?;
let width = groups.iter().map(|g| g.name.len()).max().unwrap_or(0);
let labels: Vec<String> = groups.iter().map(|g| format!("{:width$} {}", g.name, g.apps.join(", "))).collect();
Ok(groups[select(prompt, &labels)?].name.clone())
}
pub fn app_or_group(apps: &[App], groups: &[Group], state: &State, prompt: &str) -> Result<String> {
if groups.is_empty() {
return app(apps, state, prompt);
}
if apps.is_empty() {
bail!("no apps in the catalog yet - run `turnout app add` first");
}
ensure_interactive("app or group name is required")?;
let mut names: Vec<String> = Vec::with_capacity(apps.len() + groups.len());
let mut labels: Vec<String> = Vec::with_capacity(apps.len() + groups.len());
let width = apps.iter().map(|a| a.name.len()).chain(groups.iter().map(|g| g.name.len())).max().unwrap_or(0);
for group in groups {
names.push(group.name.clone());
labels.push(format!("{:width$} group: {}", group.name, group.apps.join(", ")));
}
for app in apps {
names.push(app.name.clone());
labels.push(match state.bindings.get(&app.name) {
Some(server) => format!("{:width$} -> {server}", app.name),
None => format!("{:width$} (unbound)", app.name),
});
}
Ok(names[select(prompt, &labels)?].clone())
}
pub fn credential(credentials: &[Credential], prompt: &str) -> Result<String> {
if credentials.is_empty() {
bail!("no credentials yet - run `turnout credential add` first");
}
if let [only] = credentials {
return Ok(only.name.clone());
}
ensure_interactive("credential name is required")?;
let width = credentials.iter().map(|c| c.name.len()).max().unwrap_or(0);
let labels: Vec<String> = credentials.iter().map(|c| format!("{:width$} {}@ {}", c.name, c.user, c.auth)).collect();
Ok(credentials[select(prompt, &labels)?].name.clone())
}
pub fn target(targets: &[Target], prompt: &str) -> Result<String> {
if targets.is_empty() {
bail!("no targets yet - run `turnout target add` first");
}
ensure_interactive("target name is required")?;
let width = targets.iter().map(|t| t.name.len()).max().unwrap_or(0);
let labels: Vec<String> = targets
.iter()
.map(|t| format!("{:width$} {} -> {} ({})", t.name, t.app, t.server, t.path))
.collect();
Ok(targets[select(prompt, &labels)?].name.clone())
}
pub fn path(paths: &[crate::model::Path], prompt: &str) -> Result<String> {
if paths.is_empty() {
bail!("no paths yet - run `turnout path add` first");
}
if let [only] = paths {
return Ok(only.name.clone());
}
ensure_interactive("path name is required")?;
let width = paths.iter().map(|p| p.name.len()).max().unwrap_or(0);
let labels: Vec<String> = paths.iter().map(|p| format!("{:width$} {}", p.name, p.dir)).collect();
Ok(paths[select(prompt, &labels)?].name.clone())
}