Allow users to answer e at prompts (#183)

* Allow users to answer e at prompts

* Rename to currently_explaining

* Speeling

* Show options fully in prompt

* Better coloring
This commit is contained in:
Ana Hobden 2023-01-12 11:29:13 -08:00 committed by GitHub
parent 4338b78135
commit f1df7ed432
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 57 deletions

View file

@ -42,7 +42,7 @@ impl Action for MoveUnpackedNix {
vec![ActionDescription::new(
format!("Move the downloaded Nix into `/nix`"),
vec![format!(
"Nix is being downloaded to `{}` and should be in `nix`",
"Nix is being downloaded to `{}` and should be in `/nix`",
self.src.display(),
)],
)]

View file

@ -61,8 +61,8 @@ impl Action for ConfigureNixDaemonService {
self.tracing_synopsis(),
vec![
"Run `systemd-tempfiles --create --prefix=/nix/var/nix`".to_string(),
"Run `systemctl link {SERVICE_SRC}`".to_string(),
"Run `systemctl link {SOCKET_SRC}`".to_string(),
format!("Run `systemctl link {SERVICE_SRC}`"),
format!("Run `systemctl link {SOCKET_SRC}`"),
"Run `systemctl daemon-reload`".to_string(),
],
)]

View file

@ -52,21 +52,17 @@ impl Action for CreateNixTree {
}
fn execute_description(&self) -> Vec<ActionDescription> {
let Self { create_directories } = &self;
let mut create_directory_descriptions = Vec::new();
for create_directory in create_directories {
if let Some(val) = create_directory.describe_execute().iter().next() {
create_directory_descriptions.push(val.description.clone())
}
}
vec![ActionDescription::new(
self.tracing_synopsis(),
vec![
format!(
"Nix and the Nix daemon require a Nix Store, which will be stored at `/nix`"
),
format!(
"Creates: {}",
PATHS
.iter()
.map(|v| format!("`{v}`"))
.collect::<Vec<_>>()
.join(", ")
),
],
create_directory_descriptions,
)]
}

View file

@ -3,11 +3,22 @@ use std::io::{stdin, stdout, BufRead, Write};
use eyre::{eyre, WrapErr};
use owo_colors::OwoColorize;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PromptChoice {
Yes,
No,
Explain,
}
// Do not try to get clever!
//
// Mac is extremely janky if you `curl $URL | sudo sh` and the TTY may not be set up right.
// The below method was adopted from Rustup at https://github.com/rust-lang/rustup/blob/3331f34c01474bf216c99a1b1706725708833de1/src/cli/term2.rs#L37
pub(crate) async fn confirm(question: impl AsRef<str>, default: bool) -> eyre::Result<bool> {
pub(crate) async fn prompt(
question: impl AsRef<str>,
default: PromptChoice,
currently_explaining: bool,
) -> eyre::Result<PromptChoice> {
let stdout = stdout();
let mut term =
term::terminfo::TerminfoTerminal::new(stdout).ok_or(eyre!("Couldn't get terminal"))?;
@ -15,12 +26,34 @@ pub(crate) async fn confirm(question: impl AsRef<str>, default: bool) -> eyre::R
"\
{question}\n\
\n\
{are_you_sure} ({yes}/{no}): \
{are_you_sure} ({yes}/{no}{maybe_explain}): \
",
question = question.as_ref(),
are_you_sure = "Proceed?".bold(),
no = if default { "n" } else { "N" }.red(),
yes = if default { "Y" } else { "y" }.green(),
no = if default == PromptChoice::No {
"[N]o"
} else {
"[n]o"
}
.red(),
yes = if default == PromptChoice::Yes {
"[Y]es"
} else {
"[y]es"
}
.green(),
maybe_explain = if !currently_explaining {
format!(
"/{}",
if default == PromptChoice::Explain {
"[E]xplain"
} else {
"[e]xplain"
}
)
} else {
"".into()
},
);
term.write_all(with_confirm.as_bytes())?;
@ -29,10 +62,11 @@ pub(crate) async fn confirm(question: impl AsRef<str>, default: bool) -> eyre::R
let input = read_line()?;
let r = match &*input.to_lowercase() {
"y" | "yes" => true,
"n" | "no" => false,
"y" | "yes" => PromptChoice::Yes,
"n" | "no" => PromptChoice::No,
"e" | "explain" => PromptChoice::Explain,
"" => default,
_ => false,
_ => PromptChoice::No,
};
Ok(r)

View file

@ -6,7 +6,11 @@ use std::{
use crate::{
action::ActionState,
cli::{ensure_root, interaction, signal_channel, CommandExecute},
cli::{
ensure_root,
interaction::{self, PromptChoice},
signal_channel, CommandExecute,
},
error::HasExpectedErrors,
plan::RECEIPT_LOCATION,
planner::Planner,
@ -144,15 +148,23 @@ impl CommandExecute for Install {
};
if !no_confirm {
if !interaction::confirm(
install_plan
.describe_install(explain)
.map_err(|e| eyre!(e))?,
true,
)
.await?
{
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
let mut currently_explaining = explain;
loop {
match interaction::prompt(
install_plan
.describe_install(currently_explaining)
.map_err(|e| eyre!(e))?,
PromptChoice::Yes,
currently_explaining,
)
.await?
{
PromptChoice::Yes => break,
PromptChoice::Explain => currently_explaining = true,
PromptChoice::No => {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await
},
}
}
}
@ -175,16 +187,26 @@ impl CommandExecute for Install {
};
eprintln!("{}", "Installation failure, offering to revert...".red());
if !interaction::confirm(
install_plan
.describe_uninstall(explain)
.map_err(|e| eyre!(e))?,
true,
)
.await?
{
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!")
.await;
let mut currently_explaining = explain;
loop {
match interaction::prompt(
install_plan
.describe_uninstall(currently_explaining)
.map_err(|e| eyre!(e))?,
PromptChoice::Yes,
currently_explaining,
)
.await?
{
PromptChoice::Yes => break,
PromptChoice::Explain => currently_explaining = true,
PromptChoice::No => {
interaction::clean_exit_with_message(
"Okay, didn't do anything! Bye!",
)
.await
},
}
}
let rx2 = tx.subscribe();
let res = install_plan.uninstall(rx2).await;

View file

@ -5,7 +5,7 @@ use std::{
};
use crate::{
cli::{ensure_root, signal_channel},
cli::{ensure_root, interaction::PromptChoice, signal_channel},
error::HasExpectedErrors,
plan::RECEIPT_LOCATION,
InstallPlan,
@ -127,13 +127,22 @@ impl CommandExecute for Uninstall {
let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?;
if !no_confirm {
if !interaction::confirm(
plan.describe_uninstall(explain).map_err(|e| eyre!(e))?,
true,
)
.await?
{
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await;
let mut currently_explaining = explain;
loop {
match interaction::prompt(
plan.describe_uninstall(currently_explaining)
.map_err(|e| eyre!(e))?,
PromptChoice::Yes,
currently_explaining,
)
.await?
{
PromptChoice::Yes => break,
PromptChoice::Explain => currently_explaining = true,
PromptChoice::No => {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").await
},
}
}
}

View file

@ -66,15 +66,10 @@ impl InstallPlan {
\n\
{plan_settings}\n\
\n\
The following actions will be taken{maybe_explain}:\n\
The following actions will be taken:\n\
\n\
{actions}\n\
",
maybe_explain = if !explain {
" (`--explain` for more context)"
} else {
""
},
planner = planner.typetag_name(),
plan_settings = planner
.settings()?