use std::io::Write as _;
use dialoguer::theme::ColorfulTheme;
use dialoguer::{Confirm, Input, Password, Select};
use crate::cli::guidance;
use crate::config::OrgKind;
use crate::exit::ExitCode;
#[derive(Debug, thiserror::Error)]
pub enum WizardError {
#[error("cancelled")]
Cancelled,
#[error("could not read your answer")]
Io(#[from] dialoguer::Error),
}
impl WizardError {
#[must_use]
pub fn exit_code(&self) -> ExitCode {
match self {
Self::Cancelled => ExitCode::ConfirmationRequired,
Self::Io(_) => ExitCode::Failure,
}
}
}
fn theme() -> ColorfulTheme {
ColorfulTheme::default()
}
#[must_use]
pub fn is_interactive() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
}
pub fn introduce() {
let mut err = anstream::stderr();
let _ = writeln!(err, "\n{}", guidance::full());
}
pub fn account(existing: &[String]) -> Result<String, WizardError> {
let mut err = anstream::stderr();
if existing.is_empty() {
let _ = writeln!(
err,
"{}",
guidance::block(
"An account holds one token. A **profile** is an organisation seen through an account — so one account can serve several organisations."
)
);
} else {
let _ = writeln!(err, "\nAccounts you already have: {}", existing.join(", "));
}
let name: String = Input::with_theme(&theme())
.with_prompt("Account name")
.default("default".to_owned())
.interact_text()?;
Ok(name.trim().to_owned())
}
pub fn token(account: &str) -> Result<String, WizardError> {
let token = Password::with_theme(&theme())
.with_prompt(format!("Paste the OAuth token for `{account}`"))
.interact()?;
let token = token.trim().to_owned();
if token.is_empty() {
return Err(WizardError::Cancelled);
}
Ok(token)
}
pub fn organisation() -> Result<(String, Option<OrgKind>), WizardError> {
let id: String = Input::with_theme(&theme())
.with_prompt("Organisation id")
.interact_text()?;
let choice = Select::with_theme(&theme())
.with_prompt("Organisation kind")
.default(0)
.items([
"Detect it for me",
"Yandex Cloud Organization (X-Cloud-Org-Id)",
"Yandex 360 for Business (X-Org-Id)",
])
.interact()?;
let kind = match choice {
1 => Some(OrgKind::Cloud),
2 => Some(OrgKind::Yandex360),
_ => None,
};
Ok((id.trim().to_owned(), kind))
}
pub fn profile(default: &str) -> Result<String, WizardError> {
let name: String = Input::with_theme(&theme())
.with_prompt("Profile name")
.default(default.to_owned())
.interact_text()?;
Ok(name.trim().to_owned())
}
pub fn queue(available: &[String]) -> Result<Option<String>, WizardError> {
if available.is_empty() {
let typed: String = Input::with_theme(&theme())
.with_prompt("Default queue (optional)")
.allow_empty(true)
.interact_text()?;
let typed = typed.trim();
return Ok((!typed.is_empty()).then(|| typed.to_owned()));
}
let mut items: Vec<String> = vec!["(none)".to_owned()];
items.extend(available.iter().cloned());
let choice = Select::with_theme(&theme())
.with_prompt("Default queue for this profile")
.default(0)
.items(&items)
.interact()?;
Ok(if choice == 0 {
None
} else {
items.get(choice).cloned()
})
}
pub fn make_default(profile: &str, current: Option<&str>) -> Result<bool, WizardError> {
let Some(current) = current else {
return Ok(true);
};
if current == profile {
return Ok(true);
}
Ok(Confirm::with_theme(&theme())
.with_prompt(format!(
"Make `{profile}` the default profile? (currently `{current}`)"
))
.default(false)
.interact()?)
}