1use std::io::Write as _;
12
13use dialoguer::theme::ColorfulTheme;
14use dialoguer::{Confirm, Input, Password, Select};
15
16use crate::cli::guidance;
17use crate::config::OrgKind;
18use crate::exit::ExitCode;
19
20#[derive(Debug, thiserror::Error)]
22pub enum WizardError {
23 #[error("cancelled")]
24 Cancelled,
25 #[error("could not read your answer")]
26 Io(#[from] dialoguer::Error),
27}
28
29impl WizardError {
30 #[must_use]
31 pub fn exit_code(&self) -> ExitCode {
32 match self {
33 Self::Cancelled => ExitCode::ConfirmationRequired,
34 Self::Io(_) => ExitCode::Failure,
35 }
36 }
37}
38
39fn theme() -> ColorfulTheme {
40 ColorfulTheme::default()
41}
42
43#[must_use]
45pub fn is_interactive() -> bool {
46 use std::io::IsTerminal;
47 std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
48}
49
50pub fn introduce() {
60 let mut err = anstream::stderr();
61 let _ = writeln!(err, "\n{}", guidance::full());
62}
63
64pub fn account(existing: &[String]) -> Result<String, WizardError> {
66 let mut err = anstream::stderr();
67 if existing.is_empty() {
68 let _ = writeln!(
69 err,
70 "{}",
71 guidance::block(
72 "An account holds one token. A **profile** is an organisation seen through an account — so one account can serve several organisations."
73 )
74 );
75 } else {
76 let _ = writeln!(err, "\nAccounts you already have: {}", existing.join(", "));
77 }
78
79 let name: String = Input::with_theme(&theme())
80 .with_prompt("Account name")
81 .default("default".to_owned())
82 .interact_text()?;
83
84 Ok(name.trim().to_owned())
85}
86
87pub fn token(account: &str) -> Result<String, WizardError> {
93 let token = Password::with_theme(&theme())
94 .with_prompt(format!("Paste the OAuth token for `{account}`"))
95 .interact()?;
96
97 let token = token.trim().to_owned();
98 if token.is_empty() {
99 return Err(WizardError::Cancelled);
100 }
101 Ok(token)
102}
103
104pub fn organisation() -> Result<(String, Option<OrgKind>), WizardError> {
110 let id: String = Input::with_theme(&theme())
111 .with_prompt("Organisation id")
112 .interact_text()?;
113
114 let choice = Select::with_theme(&theme())
115 .with_prompt("Organisation kind")
116 .default(0)
117 .items([
118 "Detect it for me",
119 "Yandex Cloud Organization (X-Cloud-Org-Id)",
120 "Yandex 360 for Business (X-Org-Id)",
121 ])
122 .interact()?;
123
124 let kind = match choice {
125 1 => Some(OrgKind::Cloud),
126 2 => Some(OrgKind::Yandex360),
127 _ => None,
128 };
129
130 Ok((id.trim().to_owned(), kind))
131}
132
133pub fn profile(default: &str) -> Result<String, WizardError> {
135 let name: String = Input::with_theme(&theme())
136 .with_prompt("Profile name")
137 .default(default.to_owned())
138 .interact_text()?;
139
140 Ok(name.trim().to_owned())
141}
142
143pub fn queue(available: &[String]) -> Result<Option<String>, WizardError> {
148 if available.is_empty() {
149 let typed: String = Input::with_theme(&theme())
150 .with_prompt("Default queue (optional)")
151 .allow_empty(true)
152 .interact_text()?;
153 let typed = typed.trim();
154 return Ok((!typed.is_empty()).then(|| typed.to_owned()));
155 }
156
157 let mut items: Vec<String> = vec!["(none)".to_owned()];
158 items.extend(available.iter().cloned());
159
160 let choice = Select::with_theme(&theme())
161 .with_prompt("Default queue for this profile")
162 .default(0)
163 .items(&items)
164 .interact()?;
165
166 Ok(if choice == 0 {
167 None
168 } else {
169 items.get(choice).cloned()
170 })
171}
172
173pub fn make_default(profile: &str, current: Option<&str>) -> Result<bool, WizardError> {
175 let Some(current) = current else {
176 return Ok(true);
178 };
179 if current == profile {
180 return Ok(true);
181 }
182
183 Ok(Confirm::with_theme(&theme())
184 .with_prompt(format!(
185 "Make `{profile}` the default profile? (currently `{current}`)"
186 ))
187 .default(false)
188 .interact()?)
189}