Skip to content

Commit

Permalink
feat: quit interactive menus with single-key shortcut escape
Browse files Browse the repository at this point in the history
  • Loading branch information
roele committed Dec 8, 2024
1 parent 1d14dec commit 3599064
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 11 deletions.
3 changes: 2 additions & 1 deletion examples/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ fn main() {
.item("Twizzlers")
.item("Milk Duds")
.filterable(true)
.run();
.run()
.expect("error running list");
}
10 changes: 7 additions & 3 deletions src/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ impl<'a> Confirm<'a> {
}

/// Displays the dialog to the user and returns their response
///
/// 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> {
let affirmative_char = self.affirmative.to_lowercase().chars().next().unwrap();
let negative_char = self.negative.to_lowercase().chars().next().unwrap();
Expand All @@ -96,24 +99,25 @@ impl<'a> Confirm<'a> {
Key::ArrowRight | Key::Char('l') => self.handle_right(),
Key::Char(c) if c == affirmative_char => {
self.selected = true;
self.term.clear_to_end_of_screen()?;
return self.handle_submit();
}
Key::Char(c) if c == negative_char => {
self.selected = false;
self.term.clear_to_end_of_screen()?;
return self.handle_submit();
}
Key::Enter => {
self.term.clear_to_end_of_screen()?;
return self.handle_submit();
}
Key::Escape => {
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"))
}
_ => {}
}
}
}

fn handle_submit(mut self) -> io::Result<bool> {
self.term.clear_to_end_of_screen()?;
self.clear()?;
let output = self.render_success()?;
self.term.write_all(output.as_bytes())?;
Expand Down
6 changes: 5 additions & 1 deletion src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ impl<'a> Dialog<'a> {
///
/// The response will be the label of the selected button.
///
/// This will block until the user selects a button or presses one of the submit keys.
/// 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> {
loop {
self.clear()?;
Expand All @@ -137,6 +138,9 @@ impl<'a> Dialog<'a> {
Key::Enter => {
return self.handle_submit();
}
Key::Escape => {
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"))
}
_ => {}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ impl<'a> Input<'a> {
}

/// Displays the input to the user and returns the response
///
/// 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> {
self.term.hide_cursor()?;
loop {
Expand Down Expand Up @@ -170,6 +173,9 @@ impl<'a> Input<'a> {
}
}
Key::Tab => self.handle_tab()?,
Key::Escape => {
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
_ => {}
}
if key != Key::Enter {
Expand Down
8 changes: 6 additions & 2 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ impl<'a> List<'a> {
}

/// Displays the input to the user and returns the response
///
/// 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> {
loop {
self.clear()?;
Expand All @@ -99,7 +102,6 @@ impl<'a> List<'a> {
self.term.flush()?;
self.height = output.lines().count() - 1;
if self.filtering {
// self.term.show_cursor()?;
match self.term.read_key()? {
Key::Enter => self.handle_stop_filtering(true)?,
Key::Escape => self.handle_stop_filtering(false)?,
Expand All @@ -115,7 +117,9 @@ impl<'a> List<'a> {
Key::ArrowLeft | Key::Char('h') => self.handle_left()?,
Key::ArrowRight | Key::Char('l') => self.handle_right()?,
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => self.handle_stop_filtering(false)?,
Key::Escape => {
return Err(io::Error::new(io::ErrorKind::Interrupted, "user cancelled"));
}
Key::Enter => {
self.clear()?;
self.term.show_cursor()?;
Expand Down
13 changes: 12 additions & 1 deletion src/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ impl<'a, T> MultiSelect<'a, T> {
}

/// Displays the selector to the user and returns their selected options
///
/// 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>> {
self.max = self.max.min(self.options.len());
self.min = self.min.min(self.max);
Expand Down Expand Up @@ -155,7 +158,15 @@ impl<'a, T> MultiSelect<'a, T> {
Key::Char('x') | Key::Char(' ') => self.handle_toggle(),
Key::Char('a') => self.handle_toggle_all(),
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => self.handle_stop_filtering(false)?,
Key::Escape => {
if self.filter.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"user cancelled",
));
}
self.handle_stop_filtering(false)?
}
Key::Enter => {
let selected = self
.options
Expand Down
13 changes: 12 additions & 1 deletion src/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ impl<'a, T> Select<'a, T> {
}

/// Displays the selector to the user and returns their selected options
///
/// 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> {
loop {
self.clear()?;
Expand All @@ -128,7 +131,15 @@ impl<'a, T> Select<'a, T> {
Key::ArrowLeft | Key::Char('h') => self.handle_left()?,
Key::ArrowRight | Key::Char('l') => self.handle_right()?,
Key::Char('/') if self.filterable => self.handle_start_filtering(),
Key::Escape => self.handle_stop_filtering(false)?,
Key::Escape => {
if self.filter.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"user cancelled",
));
}
self.handle_stop_filtering(false)?;
}
Key::Enter => {
self.clear()?;
self.term.show_cursor()?;
Expand Down
4 changes: 2 additions & 2 deletions src/spinner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl<'spinner> SpinnerActionRunner<'spinner> {
&mut self, // with just this the compiler assumes that theme might be stored in self so it wont let u mutate it after this fn call
theme: &'spinner Theme,
) -> Result<(), std::sync::mpsc::SendError<SpinnerAction>> {
let theme = unsafe { std::mem::transmute(theme) };
let theme = unsafe { std::mem::transmute::<&Theme, &Theme>(theme) };
self.sender.send(SpinnerAction::Theme(theme))
}

Expand All @@ -54,7 +54,7 @@ impl<'spinner> SpinnerActionRunner<'spinner> {
&mut self, // with just this the compiler assumes that theme might be stored in self so it wont let u mutate it after this fn call
style: &'spinner SpinnerStyle,
) -> Result<(), std::sync::mpsc::SendError<SpinnerAction>> {
let style = unsafe { std::mem::transmute(style) };
let style = unsafe { std::mem::transmute::<&SpinnerStyle, &SpinnerStyle>(style) };
self.sender.send(SpinnerAction::Style(style))
}

Expand Down

0 comments on commit 3599064

Please sign in to comment.