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 sign_in_in_browser() -> Result<bool, WizardError> {
92 let choice = Select::with_theme(&theme())
93 .with_prompt("How do you want to sign in?")
94 .default(0)
95 .items([
96 "In the browser, with a short code — nothing to set up",
97 "Paste an OAuth token",
98 ])
99 .interact()?;
100 Ok(choice == 0)
101}
102
103pub fn press_enter(prompt: &str) -> Result<(), WizardError> {
108 let mut err = anstream::stderr();
109 let _ = write!(err, "{prompt}");
110 let _ = err.flush();
111
112 let mut line = String::new();
113 std::io::stdin()
114 .read_line(&mut line)
115 .map_err(|error| WizardError::Io(dialoguer::Error::IO(error)))?;
116 Ok(())
117}
118
119pub fn token(account: &str) -> Result<String, WizardError> {
125 let token = Password::with_theme(&theme())
126 .with_prompt(format!("Paste the OAuth token for `{account}`"))
127 .interact()?;
128
129 let token = token.trim().to_owned();
130 if token.is_empty() {
131 return Err(WizardError::Cancelled);
132 }
133 Ok(token)
134}
135
136pub fn organisation() -> Result<(String, Option<OrgKind>), WizardError> {
142 let id: String = Input::with_theme(&theme())
143 .with_prompt("Organisation id")
144 .interact_text()?;
145
146 let choice = Select::with_theme(&theme())
147 .with_prompt("Organisation kind")
148 .default(0)
149 .items([
150 "Detect it for me",
151 "Yandex Cloud Organization (X-Cloud-Org-Id)",
152 "Yandex 360 for Business (X-Org-Id)",
153 ])
154 .interact()?;
155
156 let kind = match choice {
157 1 => Some(OrgKind::Cloud),
158 2 => Some(OrgKind::Yandex360),
159 _ => None,
160 };
161
162 Ok((id.trim().to_owned(), kind))
163}
164
165pub fn profile(default: &str) -> Result<String, WizardError> {
167 let name: String = Input::with_theme(&theme())
168 .with_prompt("Profile name")
169 .default(default.to_owned())
170 .interact_text()?;
171
172 Ok(name.trim().to_owned())
173}
174
175pub fn queue(available: &[String]) -> Result<Option<String>, WizardError> {
180 if available.is_empty() {
181 let typed: String = Input::with_theme(&theme())
182 .with_prompt("Default queue (optional)")
183 .allow_empty(true)
184 .interact_text()?;
185 let typed = typed.trim();
186 return Ok((!typed.is_empty()).then(|| typed.to_owned()));
187 }
188
189 let mut items: Vec<String> = vec!["(none)".to_owned()];
190 items.extend(available.iter().cloned());
191
192 let choice = Select::with_theme(&theme())
193 .with_prompt("Default queue for this profile")
194 .default(0)
195 .items(&items)
196 .interact()?;
197
198 Ok(if choice == 0 {
199 None
200 } else {
201 items.get(choice).cloned()
202 })
203}
204
205pub fn description(current: Option<&str>) -> Result<Option<String>, WizardError> {
211 let typed: String = Input::with_theme(&theme())
212 .with_prompt("What is this organisation? (optional)")
213 .with_initial_text(current.unwrap_or_default())
214 .allow_empty(true)
215 .interact_text()?;
216
217 let typed = typed.trim();
218 Ok((!typed.is_empty()).then(|| typed.to_owned()))
219}
220
221pub fn make_default(profile: &str, current: Option<&str>) -> Result<bool, WizardError> {
223 let Some(current) = current else {
224 return Ok(true);
226 };
227 if current == profile {
228 return Ok(true);
229 }
230
231 Ok(Confirm::with_theme(&theme())
232 .with_prompt(format!(
233 "Make `{profile}` the default profile? (currently `{current}`)"
234 ))
235 .default(false)
236 .interact()?)
237}