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( vec![ActionDescription::new(
format!("Move the downloaded Nix into `/nix`"), format!("Move the downloaded Nix into `/nix`"),
vec![format!( 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(), self.src.display(),
)], )],
)] )]

View file

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

View file

@ -52,21 +52,17 @@ impl Action for CreateNixTree {
} }
fn execute_description(&self) -> Vec<ActionDescription> { 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( vec![ActionDescription::new(
self.tracing_synopsis(), self.tracing_synopsis(),
vec![ create_directory_descriptions,
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(", ")
),
],
)] )]
} }

View file

@ -3,11 +3,22 @@ use std::io::{stdin, stdout, BufRead, Write};
use eyre::{eyre, WrapErr}; use eyre::{eyre, WrapErr};
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PromptChoice {
Yes,
No,
Explain,
}
// Do not try to get clever! // 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. // 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 // 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 stdout = stdout();
let mut term = let mut term =
term::terminfo::TerminfoTerminal::new(stdout).ok_or(eyre!("Couldn't get terminal"))?; 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\ {question}\n\
\n\ \n\
{are_you_sure} ({yes}/{no}): \ {are_you_sure} ({yes}/{no}{maybe_explain}): \
", ",
question = question.as_ref(), question = question.as_ref(),
are_you_sure = "Proceed?".bold(), are_you_sure = "Proceed?".bold(),
no = if default { "n" } else { "N" }.red(), no = if default == PromptChoice::No {
yes = if default { "Y" } else { "y" }.green(), "[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())?; 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 input = read_line()?;
let r = match &*input.to_lowercase() { let r = match &*input.to_lowercase() {
"y" | "yes" => true, "y" | "yes" => PromptChoice::Yes,
"n" | "no" => false, "n" | "no" => PromptChoice::No,
"e" | "explain" => PromptChoice::Explain,
"" => default, "" => default,
_ => false, _ => PromptChoice::No,
}; };
Ok(r) Ok(r)

View file

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

View file

@ -5,7 +5,7 @@ use std::{
}; };
use crate::{ use crate::{
cli::{ensure_root, signal_channel}, cli::{ensure_root, interaction::PromptChoice, signal_channel},
error::HasExpectedErrors, error::HasExpectedErrors,
plan::RECEIPT_LOCATION, plan::RECEIPT_LOCATION,
InstallPlan, InstallPlan,
@ -127,13 +127,22 @@ impl CommandExecute for Uninstall {
let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?; let mut plan: InstallPlan = serde_json::from_str(&install_receipt_string)?;
if !no_confirm { if !no_confirm {
if !interaction::confirm( let mut currently_explaining = explain;
plan.describe_uninstall(explain).map_err(|e| eyre!(e))?, loop {
true, match interaction::prompt(
plan.describe_uninstall(currently_explaining)
.map_err(|e| eyre!(e))?,
PromptChoice::Yes,
currently_explaining,
) )
.await? .await?
{ {
interaction::clean_exit_with_message("Okay, didn't do anything! Bye!").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\ \n\
{plan_settings}\n\ {plan_settings}\n\
\n\ \n\
The following actions will be taken{maybe_explain}:\n\ The following actions will be taken:\n\
\n\ \n\
{actions}\n\ {actions}\n\
", ",
maybe_explain = if !explain {
" (`--explain` for more context)"
} else {
""
},
planner = planner.typetag_name(), planner = planner.typetag_name(),
plan_settings = planner plan_settings = planner
.settings()? .settings()?