refactor(cli): replace promptly with dialoguer (#3669)

This commit is contained in:
Paolo Barbolini
2025-02-27 21:17:09 +01:00
committed by GitHub
parent 9bd3d61929
commit 277dd36c78
4 changed files with 92 additions and 183 deletions

View File

@@ -1,9 +1,11 @@
use crate::migrate;
use crate::opt::ConnectOpts;
use console::style;
use promptly::{prompt, ReadlineError};
use console::{style, Term};
use dialoguer::Confirm;
use sqlx::any::Any;
use sqlx::migrate::MigrateDatabase;
use std::{io, mem};
use tokio::task;
pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> {
// NOTE: only retry the idempotent action.
@@ -24,7 +26,7 @@ pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> {
}
pub async fn drop(connect_opts: &ConnectOpts, confirm: bool, force: bool) -> anyhow::Result<()> {
if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?) {
if confirm && !ask_to_continue_drop(connect_opts.required_db_url()?.to_owned()).await {
return Ok(());
}
@@ -58,27 +60,46 @@ pub async fn setup(migration_source: &str, connect_opts: &ConnectOpts) -> anyhow
migrate::run(migration_source, connect_opts, false, false, None).await
}
fn ask_to_continue_drop(db_url: &str) -> bool {
loop {
let r: Result<String, ReadlineError> =
prompt(format!("Drop database at {}? (y/n)", style(db_url).cyan()));
match r {
Ok(response) => {
if response == "n" || response == "N" {
return false;
} else if response == "y" || response == "Y" {
return true;
} else {
println!(
"Response not recognized: {}\nPlease type 'y' or 'n' and press enter.",
response
);
}
}
Err(e) => {
println!("{e}");
return false;
async fn ask_to_continue_drop(db_url: String) -> bool {
// If the setup operation is cancelled while we are waiting for the user to decide whether
// or not to drop the database, this will restore the terminal's cursor to its normal state.
struct RestoreCursorGuard {
disarmed: bool,
}
impl Drop for RestoreCursorGuard {
fn drop(&mut self) {
if !self.disarmed {
Term::stderr().show_cursor().unwrap()
}
}
}
let mut guard = RestoreCursorGuard { disarmed: false };
let decision_result = task::spawn_blocking(move || {
Confirm::new()
.with_prompt(format!("Drop database at {}?", style(&db_url).cyan()))
.wait_for_newline(true)
.default(false)
.show_default(true)
.interact()
})
.await
.expect("Confirm thread panicked");
match decision_result {
Ok(decision) => {
guard.disarmed = true;
decision
}
Err(dialoguer::Error::IO(err)) if err.kind() == io::ErrorKind::Interrupted => {
// Sometimes CTRL + C causes this error to be returned
mem::drop(guard);
false
}
Err(err) => {
mem::drop(guard);
panic!("Confirm dialog failed with {err}")
}
}
}

View File

@@ -5,6 +5,7 @@ use anyhow::Result;
use futures::{Future, TryFutureExt};
use sqlx::{AnyConnection, Connection};
use tokio::{select, signal};
use crate::opt::{Command, ConnectOpts, DatabaseCommand, MigrateCommand};
@@ -21,6 +22,26 @@ mod prepare;
pub use crate::opt::Opt;
pub async fn run(opt: Opt) -> Result<()> {
// This `select!` is here so that when the process receives a `SIGINT` (CTRL + C),
// the futures currently running on this task get dropped before the program exits.
// This is currently necessary for the consumers of the `dialoguer` crate to restore
// the user's terminal if the process is interrupted while a dialog is being displayed.
let ctrlc_fut = signal::ctrl_c();
let do_run_fut = do_run(opt);
select! {
biased;
_ = ctrlc_fut => {
Ok(())
},
do_run_outcome = do_run_fut => {
do_run_outcome
}
}
}
async fn do_run(opt: Opt) -> Result<()> {
match opt.command {
Command::Migrate(migrate) => match migrate.command {
MigrateCommand::Add {