Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore cursor if dialoguer Ctrl+C-ed #2559

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions crates/common/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,49 @@ use std::path::Path;
pub fn quoted_path(path: impl AsRef<Path>) -> impl std::fmt::Display {
format!("\"{}\"", path.as_ref().display())
}

/// An operation that may be interrupted using Ctrl+C. This is usable only
/// when Ctrl+C is handled via the ctrlc crate - otherwise Ctrl+C terminates
/// the program. But in such situations, this trait helps you to convert
/// the interrupt into a 'cancel' value which you can use to gracefully
/// exit just as if the interrupt had been allowed to go through.
pub trait Interruptible {
/// The result type that captures the cancellation.
type Result;

/// Converts an interrupt error to a value representing cancellation.
fn cancel_on_interrupt(self) -> Self::Result;
}

impl<T> Interruptible for Result<Option<T>, std::io::Error> {
type Result = Self;
fn cancel_on_interrupt(self) -> Self::Result {
match self {
Ok(opt) => Ok(opt),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok(None),
Err(e) => Err(e),
}
}
}

impl Interruptible for Result<bool, std::io::Error> {
type Result = Self;
fn cancel_on_interrupt(self) -> Self::Result {
match self {
Ok(b) => Ok(b),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok(false),
Err(e) => Err(e),
}
}
}

impl Interruptible for Result<String, std::io::Error> {
type Result = Result<Option<String>, std::io::Error>;
fn cancel_on_interrupt(self) -> Self::Result {
match self {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => Ok(None),
Err(e) => Err(e),
}
}
}
19 changes: 13 additions & 6 deletions crates/templates/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
use anyhow::anyhow;
// use console::style;
use dialoguer::{Confirm, Input};
use spin_common::ui::Interruptible;

pub(crate) trait InteractionStrategy {
fn allow_generate_into(&self, target_dir: &Path) -> Cancellable<(), anyhow::Error>;
Expand Down Expand Up @@ -45,7 +46,7 @@ impl InteractionStrategy for Interactive {
"Directory '{}' already contains other files. Generate into it anyway?",
target_dir.display()
);
match crate::interaction::confirm(&prompt) {
match crate::interaction::confirm(&prompt).cancel_on_interrupt() {
Ok(true) => Cancellable::Ok(()),
Ok(false) => Cancellable::Cancelled,
Err(e) => Cancellable::Err(anyhow::Error::from(e)),
Expand Down Expand Up @@ -115,27 +116,33 @@ pub(crate) fn prompt_parameter(parameter: &TemplateParameter) -> Option<String>
};

match input {
Ok(text) => match parameter.validate_value(text) {
Cancellable::Ok(text) => match parameter.validate_value(text) {
Ok(text) => return Some(text),
Err(e) => {
println!("Invalid value: {}", e);
}
},
Err(e) => {
Cancellable::Cancelled => {
return None;
}
Cancellable::Err(e) => {
println!("Invalid value: {}", e);
}
}
}
}

fn ask_free_text(prompt: &str, default_value: &Option<String>) -> anyhow::Result<String> {
fn ask_free_text(
prompt: &str,
default_value: &Option<String>,
) -> Cancellable<String, std::io::Error> {
let mut input = Input::<String>::new();
input.with_prompt(prompt);
if let Some(s) = default_value {
input.default(s.to_owned());
}
let result = input.interact_text()?;
Ok(result)
let result = input.interact_text().cancel_on_interrupt();
result.into()
}

fn is_directory_empty(path: &Path) -> bool {
Expand Down
12 changes: 10 additions & 2 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{fmt::Debug, path::PathBuf};
use anyhow::Result;
use clap::Parser;
use dialoguer::{console::Emoji, Confirm, Select};
use spin_common::ui::Interruptible;
use spin_doctor::{Diagnosis, DryRunNotSupported, PatientDiagnosis};

use crate::opts::{APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE};
Expand All @@ -25,6 +26,11 @@ pub struct DoctorCommand {

impl DoctorCommand {
pub async fn run(self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let manifest_file = spin_common::paths::resolve_manifest_file_path(&self.app_source)?;

println!("{icon}The Spin Doctor is in.", icon = Emoji("📟 ", ""));
Expand Down Expand Up @@ -105,7 +111,8 @@ fn prompt_treatment(summary: String, dry_run: Option<String>) -> Result<bool> {
.with_prompt(prompt)
.items(&items)
.default(0)
.interact_opt()?;
.interact_opt()
.cancel_on_interrupt()?;

match selection {
Some(2) => {
Expand All @@ -117,7 +124,8 @@ fn prompt_treatment(summary: String, dry_run: Option<String>) -> Result<bool> {
Ok(Confirm::new()
.with_prompt("Would you like to apply this fix?")
.default(true)
.interact_opt()?
.interact_opt()
.cancel_on_interrupt()?
.unwrap_or_default())
}
Some(0) => Ok(true),
Expand Down
10 changes: 8 additions & 2 deletions src/commands/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::build_info::*;
use crate::commands::plugins::{update, Install};
use crate::opts::PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG;
use anyhow::{anyhow, Result};
use spin_common::ui::quoted_path;
use spin_common::ui::{quoted_path, Interruptible};
use spin_plugins::{
badger::BadgerChecker, error::Error as PluginError, manifest::warn_unsupported_version,
PluginStore,
Expand Down Expand Up @@ -56,6 +56,11 @@ pub async fn execute_external_subcommand(
cmd: Vec<String>,
app: clap::App<'_>,
) -> anyhow::Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let (plugin_name, args, override_compatibility_check) = parse_subcommand(cmd)?;
let plugin_store = PluginStore::try_default()?;
let plugin_version = ensure_plugin_available(
Expand Down Expand Up @@ -196,7 +201,8 @@ fn offer_install(
let choice = dialoguer::Confirm::new()
.with_prompt("Would you like to install this plugin and run it now?")
.default(false)
.interact_opt()?
.interact_opt()
.cancel_on_interrupt()?
.unwrap_or(false);
Ok(choice)
}
Expand Down
25 changes: 20 additions & 5 deletions src/commands/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use itertools::Itertools;
use path_absolutize::Absolutize;
use tokio;

use spin_common::ui::Interruptible;
use spin_templates::{RunOptions, Template, TemplateManager, TemplateVariantInfo};

use crate::opts::{APP_MANIFEST_FILE_OPT, DEFAULT_MANIFEST_FILE};
Expand Down Expand Up @@ -125,6 +126,11 @@ impl AddCommand {

impl TemplateNewCommandCore {
pub async fn run(&self, variant: TemplateVariantInfo) -> Result<()> {
// work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let template_manager = TemplateManager::try_default()
.context("Failed to construct template directory path")?;

Expand Down Expand Up @@ -160,7 +166,10 @@ impl TemplateNewCommandCore {

let name = match &name {
Some(name) => name.to_owned(),
None => prompt_name(&variant).await?,
None => match prompt_name(&variant).await? {
Some(name) => name,
None => return Ok(()),
},
};

let output_path = if self.init {
Expand Down Expand Up @@ -310,7 +319,8 @@ async fn prompt_template(
.with_prompt(prompt)
.items(&opts)
.default(0)
.interact_opt()?
.interact_opt()
.cancel_on_interrupt()?
{
Some(i) => i,
None => return Ok(None),
Expand All @@ -331,18 +341,23 @@ async fn list_or_install_templates(
}
}

async fn prompt_name(variant: &TemplateVariantInfo) -> anyhow::Result<String> {
async fn prompt_name(variant: &TemplateVariantInfo) -> anyhow::Result<Option<String>> {
let noun = variant.prompt_noun();
let mut prompt = format!("Enter a name for your new {noun}");
loop {
let result = dialoguer::Input::<String>::new()
.with_prompt(prompt)
.interact_text()?;
.interact_text()
.cancel_on_interrupt()?;
let Some(result) = result else {
return Ok(None);
};

if result.trim().is_empty() {
prompt = format!("Name is required. Try another {noun} name (or Crl+C to exit)");
continue;
} else {
return Ok(result);
return Ok(Some(result));
}
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/commands/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use semver::Version;
use spin_common::ui::Interruptible;
use spin_plugins::{
error::Error,
lookup::{fetch_plugins_repo, plugins_repo_url, PluginLookup},
Expand Down Expand Up @@ -108,6 +109,11 @@ pub struct Install {

impl Install {
pub async fn run(&self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let manifest_location = match (&self.local_manifest_src, &self.remote_manifest_src, &self.name) {
(Some(path), None, None) => ManifestLocation::Local(path.to_path_buf()),
(None, Some(url), None) => ManifestLocation::Remote(url.clone()),
Expand Down Expand Up @@ -231,6 +237,11 @@ impl Upgrade {
/// Also, by default, Spin displays the list of installed plugins that are in
/// the catalogue and prompts user to choose which ones to upgrade.
pub async fn run(self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let manager = PluginManager::try_default()?;
let manifests_dir = manager.store().installed_manifests_directory();

Expand Down Expand Up @@ -317,7 +328,11 @@ impl Upgrade {
eprintln!(
"Select plugins to upgrade. Use Space to select/deselect and Enter to confirm selection."
);
let selected_indexes = match dialoguer::MultiSelect::new().items(&names).interact_opt()? {
let selected_indexes = match dialoguer::MultiSelect::new()
.items(&names)
.interact_opt()
.cancel_on_interrupt()?
{
Some(indexes) => indexes,
None => return Ok(()),
};
Expand Down Expand Up @@ -653,7 +668,8 @@ fn prompt_confirm_install(manifest: &PluginManifest, package: &PluginPackage) ->
let install = dialoguer::Confirm::new()
.with_prompt(prompt)
.default(true)
.interact_opt()?
.interact_opt()
.cancel_on_interrupt()?
.unwrap_or(false);
if !install {
println!("Plugin '{}' will not be installed", manifest.name());
Expand Down
13 changes: 11 additions & 2 deletions src/commands/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::opts::*;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use indicatif::{ProgressBar, ProgressStyle};
use spin_common::arg_parser::parse_kv;
use spin_common::{arg_parser::parse_kv, ui::Interruptible};
use spin_oci::Client;
use std::{io::Read, path::PathBuf, time::Duration};

Expand Down Expand Up @@ -155,14 +155,23 @@ pub struct Login {

impl Login {
pub async fn run(self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let username = match self.username {
Some(u) => u,
None => {
let prompt = "Username";
loop {
let result = dialoguer::Input::<String>::new()
.with_prompt(prompt)
.interact_text()?;
.interact_text()
.cancel_on_interrupt()?;
let Some(result) = result else {
return Ok(());
};
if result.trim().is_empty() {
continue;
} else {
Expand Down
22 changes: 20 additions & 2 deletions src/commands/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use comfy_table::Table;
use path_absolutize::Absolutize;

use serde::Serialize;
use spin_common::ui::Interruptible;
use spin_templates::{
InstallOptions, InstallationResults, InstalledTemplateWarning, ListResults, ProgressReporter,
SkippedReason, Template, TemplateManager, TemplateSource,
Expand Down Expand Up @@ -117,6 +118,11 @@ pub struct Uninstall {

impl Install {
pub async fn run(self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let template_manager = TemplateManager::try_default()
.context("Failed to construct template directory path")?;
let source = match (&self.git, &self.dir) {
Expand Down Expand Up @@ -198,6 +204,11 @@ fn infer_github(raw: &str) -> String {

impl Upgrade {
pub async fn run(&self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

if self.git.is_some() {
// This is equivalent to `install --update`
let install = Install {
Expand Down Expand Up @@ -316,7 +327,8 @@ impl Upgrade {
eprintln!("Select repos to upgrade. Use Space to select/deselect and Enter to confirm selection.");
let selected_indexes = match dialoguer::MultiSelect::new()
.items(&sources)
.interact_opt()?
.interact_opt()
.cancel_on_interrupt()?
{
Some(indexes) => indexes,
None => return Ok(None),
Expand Down Expand Up @@ -478,6 +490,11 @@ pub enum ListFormat {

impl List {
pub async fn run(self) -> Result<()> {
// This may use dialoguer so we work around https://github.com/console-rs/dialoguer/issues/294
_ = ctrlc::set_handler(|| {
_ = dialoguer::console::Term::stderr().show_cursor();
});

let template_manager = TemplateManager::try_default()
.context("Failed to construct template directory path")?;

Expand Down Expand Up @@ -602,7 +619,8 @@ pub(crate) async fn prompt_install_default_templates(
let should_install = dialoguer::Confirm::new()
.with_prompt(DEFAULT_TEMPLATES_INSTALL_PROMPT)
.default(true)
.interact_opt()?;
.interact_opt()
.cancel_on_interrupt()?;
if should_install == Some(true) {
install_default_templates().await?;
Ok(Some(template_manager.list().await?.templates))
Expand Down
Loading