Skip to content

Commit

Permalink
fix: ctrl-c doesn't restore cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
roele committed Dec 17, 2024
1 parent f78ca3f commit 34ea7cd
Show file tree
Hide file tree
Showing 18 changed files with 72 additions and 30 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ keywords = ["cli", "prompt", "console"]

[dependencies]
console = "0.15"
ctrlc = "3.4.5"
fuzzy-matcher = "0.3"
itertools = "0.13"
once_cell = "1"
Expand Down
4 changes: 2 additions & 2 deletions examples/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ fn main() {
.description("This will do a thing.")
.affirmative("Yes!")
.negative("No.");
let _ = match confirm.run() {
match confirm.run() {
Ok(confirm) => confirm,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Dialog was cancelled");
println!("{}", e);
false
} else {
panic!("Error: {}", e);
Expand Down
4 changes: 2 additions & 2 deletions examples/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ fn main() {
DialogButton::new("Cancel"),
])
.selected_button(1);
let _ = match dialog.run() {
match dialog.run() {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Dialog was cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
4 changes: 2 additions & 2 deletions examples/input-password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ fn main() {
.placeholder("Enter password")
.prompt("Password: ")
.password(true);
let _ = match input.run() {
match input.run() {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
4 changes: 2 additions & 2 deletions examples/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ fn main() {
"Zack Snyder",
])
.validation(notempty_minlen);
let _ = match input.run() {
match input.run() {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
2 changes: 1 addition & 1 deletion examples/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fn main() {
Ok(_) => {}
Err(e) => {
if e.kind() == io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
} else {
panic!("Error: {}", e);
}
Expand Down
4 changes: 2 additions & 2 deletions examples/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ fn main() {
.option(DemandOption::new("Cheese"))
.option(DemandOption::new("Vegan Cheese"))
.option(DemandOption::new("Nutella"));
let _ = match multiselect.run() {
match multiselect.run() {
Ok(toppings) => toppings,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
4 changes: 2 additions & 2 deletions examples/multiselect_huge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ fn main() {
.option(DemandOption::new("Starburst"))
.option(DemandOption::new("Twizzlers"))
.option(DemandOption::new("Milk Duds"));
let _ = match multiselect.run() {
match multiselect.run() {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
4 changes: 2 additions & 2 deletions examples/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ fn main() {
.option(DemandOption::new("EG").label("Egypt"))
.option(DemandOption::new("SA").label("Saudi Arabia"))
.option(DemandOption::new("AE").label("United Arab Emirates"));
let _ = match ms.run() {
match ms.run() {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("Input cancelled");
println!("{}", e);
return;
} else {
panic!("Error: {}", e);
Expand Down
20 changes: 17 additions & 3 deletions examples/themes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ use std::env::args;

use demand::{Confirm, DemandOption, Input, MultiSelect, Theme};

fn handle_run<T>(result: Result<T, std::io::Error>) -> T {
match result {
Ok(value) => value,
Err(e) => {
if e.kind() == std::io::ErrorKind::Interrupted {
println!("{}", e);
std::process::exit(0);
} else {
panic!("Error: {}", e);
}
}
}
}

fn main() {
let theme = match args().nth(1).unwrap_or_default().as_str() {
"base16" => Theme::base16(),
Expand All @@ -16,7 +30,7 @@ fn main() {
.description("Please enter your e-mail address.")
.placeholder("[email protected]")
.theme(&theme);
i.run().expect("error running input");
handle_run(i.run());

let ms = MultiSelect::new("Interests")
.description("Select your interests")
Expand All @@ -31,12 +45,12 @@ fn main() {
.option(DemandOption::new("Travel"))
.option(DemandOption::new("Sports"))
.theme(&theme);
ms.run().expect("error running multi select");
handle_run(ms.run());

let c = Confirm::new("Confirm privacy policy")
.description("Do you accept the privacy policy?")
.affirmative("Yes")
.negative("No")
.theme(&theme);
c.run().expect("error running confirm");
handle_run(c.run());
}
7 changes: 5 additions & 2 deletions src/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::io::Write;
use console::{Key, Term};
use termcolor::{Buffer, WriteColor};

use crate::theme;
use crate::theme::Theme;
use crate::{show_cursor_after_ctrlc, theme};

/// Select multiple options from a list
///
Expand Down Expand Up @@ -95,9 +95,11 @@ impl<'a> Confirm<'a> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> io::Result<bool> {
show_cursor_after_ctrlc(&self.term);
let affirmative_char = self.affirmative.to_lowercase().chars().next().unwrap();
let negative_char = self.negative.to_lowercase().chars().next().unwrap();
self.term.clear_line()?;
self.term.hide_cursor()?;
loop {
self.clear()?;
let output = self.render()?;
Expand All @@ -119,7 +121,7 @@ impl<'a> Confirm<'a> {
return self.handle_submit();
}
Key::Escape => {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
_ => {}
Expand All @@ -130,6 +132,7 @@ impl<'a> Confirm<'a> {
fn handle_submit(mut self) -> io::Result<bool> {
self.term.clear_to_end_of_screen()?;
self.clear()?;
self.term.show_cursor()?;
let output = self.render_success()?;
self.term.write_all(output.as_bytes())?;
Ok(self.selected)
Expand Down
7 changes: 5 additions & 2 deletions src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::io::Write;
use console::{Key, Term};
use termcolor::{Buffer, WriteColor};

use crate::theme;
use crate::theme::Theme;
use crate::{show_cursor_after_ctrlc, theme};

#[derive(Clone, Debug, Default, PartialEq)]
/// A button to select in a dialog
Expand Down Expand Up @@ -131,6 +131,8 @@ impl<'a> Dialog<'a> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> io::Result<String> {
show_cursor_after_ctrlc(&self.term);
self.term.hide_cursor()?;
loop {
self.clear()?;
let output = self.render()?;
Expand All @@ -149,7 +151,7 @@ impl<'a> Dialog<'a> {
return self.handle_submit();
}
Key::Escape => {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
_ => {}
Expand All @@ -159,6 +161,7 @@ impl<'a> Dialog<'a> {

fn handle_submit(mut self) -> io::Result<String> {
self.clear()?;
self.term.show_cursor()?;
let output = self.render_success()?;
self.term.write_all(output.as_bytes())?;
let result = if !self.buttons.is_empty() {
Expand Down
5 changes: 3 additions & 2 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use console::{measure_text_width, Key, Term};
use termcolor::{Buffer, WriteColor};

use crate::{theme, Theme};
use crate::{show_cursor_after_ctrlc, theme, Theme};

/// Single line text input
///
Expand Down Expand Up @@ -153,6 +153,7 @@ impl<'a> Input<'a> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> io::Result<String> {
show_cursor_after_ctrlc(&self.term);
self.term.hide_cursor()?;
loop {
self.clear()?;
Expand Down Expand Up @@ -184,7 +185,7 @@ impl<'a> Input<'a> {
}
Key::Tab => self.handle_tab()?,
Key::Escape => {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
_ => {}
Expand Down
10 changes: 10 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A prompt library for Rust. Based on [huh? for Go](https://github.com/charmbracelet/huh).
pub use confirm::Confirm;
use console::Term;
pub use dialog::Dialog;
pub use dialog::DialogButton;
pub use input::Input;
Expand All @@ -24,3 +25,12 @@ mod theme;

#[cfg(test)]
mod test;

/// Resets the cursor when a user presses `Ctrl+C`.
/// This is useful when the cursor is hidden and the program is interrupted.
fn show_cursor_after_ctrlc(term: &Term) {
let t = term.clone();
let _ = ctrlc::set_handler(move || {
let _ = t.show_cursor();
});
}
5 changes: 3 additions & 2 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use console::{Key, Term};
use std::io::Write;
use termcolor::{Buffer, WriteColor};

use crate::{theme, Theme};
use crate::{show_cursor_after_ctrlc, theme, Theme};

/// Display a list of options
///
Expand Down Expand Up @@ -125,6 +125,7 @@ impl<'a> List<'a> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> Result<(), io::Error> {
show_cursor_after_ctrlc(&self.term);
loop {
self.clear()?;
let output = self.render()?;
Expand All @@ -148,7 +149,7 @@ impl<'a> List<'a> {
Key::ArrowRight | Key::Char('l') => self.handle_right()?,
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
Key::Enter => {
Expand Down
5 changes: 3 additions & 2 deletions src/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use console::{Key, Term};
use termcolor::{Buffer, WriteColor};

use crate::theme::Theme;
use crate::{theme, DemandOption};
use crate::{show_cursor_after_ctrlc, theme, DemandOption};

/// Select multiple options from a list
///
Expand Down Expand Up @@ -141,6 +141,7 @@ impl<'a, T> MultiSelect<'a, T> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> io::Result<Vec<T>> {
show_cursor_after_ctrlc(&self.term);
self.max = self.max.min(self.options.len());
self.min = self.min.min(self.max);

Expand Down Expand Up @@ -170,7 +171,7 @@ impl<'a, T> MultiSelect<'a, T> {
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => {
if self.filter.is_empty() {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"user cancelled",
Expand Down
6 changes: 4 additions & 2 deletions src/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io;
use std::io::Write;

use crate::theme::Theme;
use crate::{theme, DemandOption};
use crate::{show_cursor_after_ctrlc, theme, DemandOption};
use console::{Alignment, Key, Term};
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
Expand Down Expand Up @@ -131,6 +131,8 @@ impl<'a, T> Select<'a, T> {
/// This function will block until the user submits the input. If the user cancels the input,
/// an error of type `io::ErrorKind::Interrupted` is returned.
pub fn run(mut self) -> io::Result<T> {
show_cursor_after_ctrlc(&self.term);

loop {
self.clear()?;
let output = self.render()?;
Expand Down Expand Up @@ -171,7 +173,7 @@ impl<'a, T> Select<'a, T> {
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => {
if self.filter.is_empty() {
self.clear()?;
self.term.show_cursor()?;
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"user cancelled",
Expand Down
6 changes: 6 additions & 0 deletions src/spinner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ impl<'a> Spinner<'a> {
F: FnOnce(&mut SpinnerActionRunner<'spinner>) -> T + Send + 'scope,
T: Send + 'scope,
{
let t = self.term.clone();
let _ = ctrlc::set_handler(move || {
t.show_cursor().unwrap();
std::process::exit(130);
});

std::thread::scope(|s| {
let (sender, receiver) = mpsc::channel();
let handle = s.spawn(move || {
Expand Down

0 comments on commit 34ea7cd

Please sign in to comment.