From 4d1443ea05083c91cd7fe5d6649ff10a6afd19a8 Mon Sep 17 00:00:00 2001 From: Julian Popescu Date: Tue, 14 Jan 2025 18:18:07 +0100 Subject: [PATCH] refactor: add convenience methods for no-prompt --- src/commands/global_args.rs | 23 +++-- src/commands/lab.rs | 9 +- src/commands/login.rs | 7 +- src/commands/template.rs | 6 +- src/commands/upload.rs | 41 ++++----- src/dialog.rs | 174 +++++++++++++++++++++++++++++++----- src/dirs.rs | 9 +- 7 files changed, 201 insertions(+), 68 deletions(-) diff --git a/src/commands/global_args.rs b/src/commands/global_args.rs index 4b67d2c..e9c4379 100644 --- a/src/commands/global_args.rs +++ b/src/commands/global_args.rs @@ -1,5 +1,6 @@ use crate::{ colors::ColorChoiceExt, + dialog::{Confirm, FuzzySelect}, dirs::{init_venv, opt_init_venv}, error::Result, graphql_client::graphql_url, @@ -46,12 +47,12 @@ pub struct GlobalArgs { pub dep_link_mode: LinkMode, #[arg( short = 'y', - long = "yes", - help = "Skip interactive dialogs and automatically confirm all prompts", + long = "no-prompt", + help = "Skip interactive dialogs and automatically confirm", default_value_t = false, global = true )] - pub yes: bool, + pub no_prompt: bool, } impl GlobalArgs { @@ -85,7 +86,7 @@ impl GlobalArgs { self.python.as_ref(), self.color.forced(), self.dep_link_mode, - self.yes, + self.no_prompt, pb, ) .await @@ -98,9 +99,21 @@ impl GlobalArgs { self.python.as_ref(), self.color.forced(), self.dep_link_mode, - self.yes, + self.no_prompt, pb, ) .await } + + pub fn confirm(&self) -> Confirm { + Confirm::new() + .with_theme(self.color.dialoguer()) + .no_prompt(self.no_prompt) + } + + pub fn fuzzy_select(&self) -> FuzzySelect { + FuzzySelect::new() + .with_theme(self.color.dialoguer()) + .no_prompt(self.no_prompt) + } } diff --git a/src/commands/lab.rs b/src/commands/lab.rs index e539be2..6e2c463 100644 --- a/src/commands/lab.rs +++ b/src/commands/lab.rs @@ -13,7 +13,6 @@ use tokio::process::Command; use url::Url; use crate::{ - dialog::AutoConfirmDialog, dirs::{project_vscode_dir, vscode_settings_path}, error::{self, Result}, process::run_command, @@ -116,7 +115,7 @@ async fn handle_vscode_integration( async fn ask_for_install_vscode_extensions( allow_vscode_extensions: Option, pb: &ProgressBar, - auto_confirm: bool, + global_args: &GlobalArgs, ) -> Result<()> { let mut vscode_settings = UserVSCodeSettings::load().await?; @@ -151,10 +150,10 @@ async fn ask_for_install_vscode_extensions( let can_install = tokio::task::spawn_blocking({ let pb = pb.clone(); + let args = global_args.clone(); move || { pb.suspend(|| { - AutoConfirmDialog::new() - .auto_confirm(auto_confirm) + args.confirm() .with_prompt(prompt_message) .default(true) .interact() @@ -201,7 +200,7 @@ pub async fn lab(args: Lab, global_args: GlobalArgs) -> Result<()> { .can_install_extensions .is_none() { - ask_for_install_vscode_extensions(args.allow_vscode_extensions, &pb, global_args.yes) + ask_for_install_vscode_extensions(args.allow_vscode_extensions, &pb, &global_args) .await?; } handle_vscode_integration(global_args, &env, &pb).await diff --git a/src/commands/login.rs b/src/commands/login.rs index 1ff6bf7..525d462 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -1,8 +1,6 @@ use crate::{ - colors::ColorChoiceExt, commands::GlobalArgs, credentials::{get_credentials, with_locked_credentials, Credentials}, - dialog::AutoConfirmDialog, error::{self, Result}, progress_bar::default_spinner, shutdown::shutdown_signal, @@ -392,12 +390,13 @@ pub async fn check_login(global: GlobalArgs, multi_progress: &MultiProgress) -> return Ok(true); } let confirmation = multi_progress.suspend(|| { - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt( "Your aqora account is not currently connected. Would you like to connect it now?", ) .default(true) + .no_prompt_value(false) .interact() })?; if confirmation { diff --git a/src/commands/template.rs b/src/commands/template.rs index e1e7c79..01d5ffd 100644 --- a/src/commands/template.rs +++ b/src/commands/template.rs @@ -1,5 +1,4 @@ use crate::{ - colors::ColorChoiceExt, commands::{ install::{install, Install}, login::check_login, @@ -132,9 +131,10 @@ pub async fn template(args: Template, global: GlobalArgs) -> Result<()> { format!("@{} ({})", org.username.clone(), org.display_name.clone()) })); Result::Ok( - dialoguer::FuzzySelect::with_theme(global.color.dialoguer().as_ref()) + global + .fuzzy_select() .with_prompt("Would you like to submit with a team? (Press ESC to skip)") - .items(&items) + .items(items) .interact_opt() .map_err(|err| { error::system( diff --git a/src/commands/upload.rs b/src/commands/upload.rs index 0d11765..6d78d61 100644 --- a/src/commands/upload.rs +++ b/src/commands/upload.rs @@ -1,8 +1,6 @@ use crate::{ - colors::ColorChoiceExt, commands::{login::check_login, GlobalArgs}, compress::{compress, DEFAULT_ARCH_EXTENSION, DEFAULT_ARCH_MIME_TYPE}, - dialog::AutoConfirmDialog, dirs::{ project_last_run_dir, project_last_run_result, project_use_case_toml_path, pyproject_path, read_pyproject, @@ -18,7 +16,6 @@ use crate::{ upload::upload_project_version_file, }; use aqora_config::{PyProject, Version}; -use aqora_runner::python::ColorChoice; use clap::Args; use futures::prelude::*; use graphql_client::GraphQLQuery; @@ -388,8 +385,7 @@ async fn update_project_version( project_path: impl AsRef, last_version: Option<&Version>, pb: &ProgressBar, - color: ColorChoice, - auto_confirm: bool, + global_args: &GlobalArgs, ) -> Result { let mut version = project.version().unwrap(); @@ -397,8 +393,8 @@ async fn update_project_version( if last_version >= &version { let new_version = increment_version(last_version); let confirmation = pb.suspend(|| { - AutoConfirmDialog::with_theme(color.dialoguer().as_ref()) - .auto_confirm(auto_confirm) + global_args + .confirm() .with_prompt(format!( r#"Project version must be greater than {last_version}. Do you want to update the version to {new_version} now?"# @@ -493,8 +489,7 @@ pub async fn upload_use_case( &global.project, competition.version.as_ref(), &use_case_pb, - global.color, - global.yes, + &global, ) .await?; @@ -823,8 +818,8 @@ pub async fn upload_submission( } let accepts = m.suspend(|| { - let will_review = AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + let will_review = global + .confirm() .with_prompt(format!("{message} Would you like to review them now?")) .default(true) .interact() @@ -836,9 +831,10 @@ pub async fn upload_submission( if dialoguer::Editor::new().edit(&rules).is_err() { return false; } - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt("Would you like to accept?") + .no_prompt_value(true) .interact() .ok() .unwrap_or_default() @@ -879,8 +875,8 @@ pub async fn upload_submission( let evaluation_path = project_last_run_dir(&global.project); if !evaluation_path.exists() { let confirmation = m.suspend(|| { - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt( r#"No last run result found. Would you like to run the tests now?"#, @@ -912,8 +908,8 @@ Would you like to run the tests now?"#, if let Ok(last_run_result) = last_run_result.as_ref() { if last_run_result.use_case_version.as_ref() != Some(&use_case_version) { let confirmation = m.suspend(|| { - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt( r#"It seems the use case version has changed since the last test run. It is required to run the tests again. @@ -950,8 +946,8 @@ Do you want to run the tests now?"#, } if should_run_tests { let confirmation = m.suspend(|| { - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt( r#"It seems you have made some changes since since the last test run. Those changes may not be reflected in the submission unless you re-run the tests. @@ -967,8 +963,8 @@ Do you want to re-run the tests now?"#, } } else { let confirmation = m.suspend(|| { - AutoConfirmDialog::with_theme(global.color.dialoguer().as_ref()) - .auto_confirm(global.yes) + global + .confirm() .with_prompt( r#"It seems the last test run result is corrupted or missing. It is required to run the tests again. @@ -992,8 +988,7 @@ Do you want to run the tests now?"#, &global.project, submission_version.as_ref(), &use_case_pb, - global.color, - global.yes, + &global, ) .await?; diff --git a/src/dialog.rs b/src/dialog.rs index 6ae25d9..3b8c199 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1,45 +1,171 @@ -use dialoguer::{theme::Theme, Confirm}; +use std::boxed::Box; -#[derive(Clone)] -pub struct AutoConfirmDialog<'a> { - confirm: Confirm<'a>, - auto_confirm: bool, +use dialoguer::{ + theme::{SimpleTheme, Theme}, + Confirm as BaseConfirm, FuzzySelect as BaseFuzzySelect, +}; + +pub struct Confirm { + theme: Box, + no_prompt: bool, + no_prompt_value: Option, + prompt: String, + report: bool, + default: Option, + show_default: bool, + wait_for_newline: bool, +} + +impl Default for Confirm { + fn default() -> Self { + Confirm::new() + } } -impl<'a> AutoConfirmDialog<'a> { +impl Confirm { pub fn new() -> Self { Self { - confirm: Confirm::new(), - auto_confirm: false, + theme: Box::new(SimpleTheme), + no_prompt: false, + no_prompt_value: None, + prompt: "".into(), + report: true, + default: None, + show_default: true, + wait_for_newline: false, } } - pub fn with_theme(theme: &'a dyn Theme) -> Self { + pub fn with_theme(self, theme: Box) -> Self { + Self { theme, ..self } + } + + pub fn no_prompt(self, no_prompt: bool) -> Self { + Self { no_prompt, ..self } + } + + pub fn no_prompt_value(self, no_prompt_value: bool) -> Self { Self { - confirm: Confirm::with_theme(theme), - auto_confirm: false, + no_prompt_value: Some(no_prompt_value), + ..self } } - pub fn auto_confirm(mut self, yes: bool) -> Self { - self.auto_confirm = yes; - self + pub fn with_prompt>(self, prompt: S) -> Self { + Self { + prompt: prompt.into(), + ..self + } } - pub fn with_prompt>(mut self, prompt: S) -> Self { - self.confirm = self.confirm.with_prompt(prompt); - self + pub fn default(self, val: bool) -> Self { + Self { + default: Some(val), + ..self + } } - pub fn default(mut self, val: bool) -> Self { - self.confirm = self.confirm.default(val); - self - } pub fn interact(self) -> dialoguer::Result { - if self.auto_confirm { - return Ok(true); + if self.no_prompt { + if let Some(default) = self.no_prompt_value.or(self.default) { + return Ok(default); + } else { + return Err(dialoguer::Error::IO(std::io::Error::other( + "No auto confirm value set on dialog", + ))); + } + } + let mut confirm = BaseConfirm::with_theme(self.theme.as_ref()) + .report(self.report) + .with_prompt(self.prompt) + .show_default(self.show_default) + .wait_for_newline(self.wait_for_newline); + if let Some(default) = self.default { + confirm = confirm.default(default); + } + confirm.interact() + } +} + +pub struct FuzzySelect { + theme: Box, + no_prompt: bool, + default: Option, + items: Vec, + prompt: String, + report: bool, + clear: bool, + highlight_matches: bool, + enable_vim_mode: bool, + max_length: Option, + initial_text: String, +} + +impl Default for FuzzySelect { + fn default() -> Self { + FuzzySelect::new() + } +} + +impl FuzzySelect { + pub fn new() -> Self { + Self { + theme: Box::new(SimpleTheme), + no_prompt: false, + default: None, + items: vec![], + prompt: "".into(), + report: true, + clear: true, + highlight_matches: true, + enable_vim_mode: false, + max_length: None, + initial_text: "".into(), } + } - self.confirm.interact() + pub fn with_theme(self, theme: Box) -> Self { + Self { theme, ..self } + } + + pub fn no_prompt(self, no_prompt: bool) -> Self { + Self { no_prompt, ..self } + } + + pub fn with_prompt>(self, prompt: S) -> Self { + Self { + prompt: prompt.into(), + ..self + } + } + + pub fn items(self, items: impl IntoIterator) -> Self { + Self { + items: items.into_iter().map(|item| item.to_string()).collect(), + ..self + } + } + + pub fn interact_opt(self) -> dialoguer::Result> { + if self.no_prompt { + return Ok(None); + } + + let mut select = BaseFuzzySelect::with_theme(self.theme.as_ref()) + .report(self.report) + .clear(self.clear) + .with_prompt(self.prompt) + .items(&self.items) + .clear(self.clear) + .highlight_matches(self.highlight_matches) + .vim_mode(self.enable_vim_mode) + .with_initial_text(self.initial_text); + if let Some(max_length) = self.max_length { + select = select.max_length(max_length) + } + if let Some(default) = self.default { + select = select.default(default); + } + select.interact_opt() } } diff --git a/src/dirs.rs b/src/dirs.rs index fd3633a..b381469 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -1,7 +1,7 @@ use crate::{ cfg_file::read_cfg_file_key, colors::ColorChoiceExt, - dialog::AutoConfirmDialog, + dialog::Confirm, error::{self, Result}, manifest::manifest_name, process::run_command, @@ -208,8 +208,9 @@ async fn ensure_uv( } let confirmation = pb.suspend(|| { - AutoConfirmDialog::with_theme(color.dialoguer().as_ref()) - .auto_confirm(auto_confirm) + Confirm::new() + .with_theme(color.dialoguer()) + .no_prompt(auto_confirm) .with_prompt("`uv` is required. Install it now? (python3 -m pip install uv)") .default(true) .interact() @@ -290,8 +291,8 @@ pub async fn init_venv( auto_confirm: bool, pb: &ProgressBar, ) -> Result { - pb.set_message("Initializing the Python environment..."); let uv_path = ensure_uv(uv_path, pb, color, auto_confirm).await?; + pb.set_message("Initializing the Python environment..."); let venv_dir = project_venv_dir(&project_dir); if let Some(python) = python.as_ref() { if let Ok(Some(installed_python)) = get_installed_python_version(&venv_dir).await {