use std::io::{BufRead, IsTerminal, Write};
use std::path::Path;
use anyhow::{Result, bail};
use crate::config::UserConfig;
use crate::email::DEFAULT_FROM;
pub struct Answers {
pub merge_always: bool,
pub launch_pull: bool,
pub notifications: Option<Notifications>,
}
impl Answers {
pub fn send_test(&self) -> bool {
self.notifications
.as_ref()
.is_some_and(|notifications| notifications.send_test)
}
}
pub struct Notifications {
pub to: String,
pub from: String,
pub send_test: bool,
}
pub fn has_terminal() -> bool {
std::io::stdin().is_terminal() && std::io::stderr().is_terminal()
}
pub fn offer(path: &Path) -> Result<bool> {
yes_or_no(
&format!(
"No User config at {}. Set your defaults now?",
path.display()
),
true,
)
}
pub fn ask(
current: &UserConfig,
suggested_address: impl FnOnce() -> Option<String>,
) -> Result<Answers> {
let merge_always = yes_or_no("Every Run a Merge run?", current.merge_always)?;
let launch_pull = yes_or_no(
"Every Run first fast-forwards your checkout of the Base branch?",
current.launch_pull,
)?;
if !yes_or_no(
"Run notifications, an email as each Run ends?",
current.email.always,
)? {
return Ok(Answers {
merge_always,
launch_pull,
notifications: None,
});
}
let suggested = current.email.to.clone().or_else(suggested_address);
let to = loop {
let to = answer("Send Run notifications to", suggested.as_deref())?;
if to.contains('@') {
break to;
}
};
let from = answer(
"Send them from",
Some(current.email.from.as_deref().unwrap_or(DEFAULT_FROM)),
)?;
let has_key = std::env::var("RESEND_API_KEY").is_ok_and(|key| !key.is_empty());
let send_test = if has_key {
yes_or_no("Send a test email now?", false)?
} else {
say(
"RESEND_API_KEY is unset or empty, so no email can go yet. Add this line to your shell \
profile, with your Resend API key:\n\n export RESEND_API_KEY=re_...\n",
);
false
};
Ok(Answers {
merge_always,
launch_pull,
notifications: Some(Notifications {
to,
from,
send_test,
}),
})
}
fn yes_or_no(question: &str, default: bool) -> Result<bool> {
let choices = if default { "[Y/n]" } else { "[y/N]" };
loop {
match read(&format!("{question} {choices} "))?
.to_ascii_lowercase()
.as_str()
{
"" => return Ok(default),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => {}
}
}
}
fn answer(question: &str, default: Option<&str>) -> Result<String> {
let prompt = match default {
Some(default) => format!("{question} [{default}]: "),
None => format!("{question}: "),
};
loop {
match (read(&prompt)?, default) {
(answer, _) if !answer.is_empty() => return Ok(answer),
(_, Some(default)) => return Ok(default.to_string()),
(_, None) => {}
}
}
}
fn read(prompt: &str) -> Result<String> {
let mut stderr = std::io::stderr();
let _ = write!(stderr, "{prompt}");
let _ = stderr.flush();
let mut line = String::new();
if std::io::stdin().lock().read_line(&mut line)? == 0 {
let _ = writeln!(stderr);
bail!("Setup ended before its last answer; nothing written");
}
Ok(line.trim().to_string())
}
fn say(text: &str) {
let _ = writeln!(std::io::stderr(), "{text}");
}