Skip to content

Commit

Permalink
feat: DemandOption no longer requires item to impl Display, Select an…
Browse files Browse the repository at this point in the history
…d MultiSelect trait bounds updated to reflect that

fix: Select used item.to_string() instead of label from DemandOption

fix: DemandOption::new took impl Display, this didnt allow types that impl ToString directly

feat: Spinner::run uses a scoped thread now, allowing non static capture without the user needing unsafe

feat: Spinner::run now takes an FnOnce which allows for more flexibility with closures, run also returns the value that the closure returns
  • Loading branch information
Vulpesx committed Apr 23, 2024
1 parent aab50ef commit 3260a0c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 26 deletions.
53 changes: 51 additions & 2 deletions src/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use crate::{theme, DemandOption};
/// .option(DemandOption::new("Nutella"));
/// let toppings = multiselect.run().expect("error running multi select");
/// ```
pub struct MultiSelect<'a, T: Display> {
pub struct MultiSelect<'a, T> {
/// The title of the selector
pub title: String,
/// The colors/style of the selector
Expand All @@ -55,7 +55,7 @@ pub struct MultiSelect<'a, T: Display> {
capacity: usize,
}

impl<'a, T: Display> MultiSelect<'a, T> {
impl<'a, T> MultiSelect<'a, T> {
/// Create a new multi select with the given title
pub fn new<S: Into<String>>(title: S) -> Self {
let mut ms = MultiSelect {
Expand Down Expand Up @@ -474,4 +474,53 @@ mod tests {
without_ansi(select.render().unwrap().as_str())
);
}

#[test]
fn non_display() {
struct Thing {
num: u32,
thing: Option<()>,
}
let things = [
Thing {
num: 1,
thing: Some(()),
},
Thing {
num: 2,
thing: None,
},
Thing {
num: 3,
thing: None,
},
];
let select = MultiSelect::new("things")
.description("pick a thing")
.options(
things
.iter()
.enumerate()
.map(|(i, t)| {
if i == 0 {
DemandOption::with_label("First", t)
} else {
DemandOption::new(t.num).item(t).selected(true)
}
})
.collect(),
);
assert_eq!(
indoc! {
" things
pick a thing
>[ ] First
[•] 2
[•] 3
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm"
},
without_ansi(select.render().unwrap().as_str())
);
}
}
26 changes: 23 additions & 3 deletions src/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::sync::atomic::AtomicUsize;

/// An individual option in a select or multi-select.
#[derive(Debug, Clone)]
pub struct DemandOption<T: Display> {
pub struct DemandOption<T> {
/// Unique ID for this option.
pub(crate) id: usize,
/// The item this option represents.
Expand All @@ -14,8 +14,8 @@ pub struct DemandOption<T: Display> {
pub selected: bool,
}

impl<T: Display> DemandOption<T> {
/// Create a new option with the given key.
impl<T: ToString> DemandOption<T> {
/// Create a new option with the item as the label
pub fn new(item: T) -> Self {
static ID: AtomicUsize = AtomicUsize::new(0);
Self {
Expand All @@ -25,7 +25,27 @@ impl<T: Display> DemandOption<T> {
selected: false,
}
}
}

impl<T> DemandOption<T> {
/// Create a new option with a label and item
pub fn with_label<S: Into<String>>(label: S, item: T) -> Self {
static ID: AtomicUsize = AtomicUsize::new(0);
Self {
id: ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
label: label.into(),
item,
selected: false,
}
}
pub fn item<I>(self, item: I) -> DemandOption<I> {
DemandOption {
id: self.id,
item,
label: self.label,
selected: self.selected,
}
}
/// Set the display label for this option.
pub fn label(mut self, name: &str) -> Self {
self.label = name.to_string();
Expand Down
48 changes: 45 additions & 3 deletions src/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::{theme, DemandOption};
/// .option(DemandOption::new("Nutella"));
/// let topping = select.run().expect("error running multi select");
/// ```
pub struct Select<'a, T: Display> {
pub struct Select<'a, T> {
/// The title of the selector
pub title: String,
/// The colors/style of the selector
Expand All @@ -48,7 +48,7 @@ pub struct Select<'a, T: Display> {
capacity: usize,
}

impl<'a, T: Display> Select<'a, T> {
impl<'a, T> Select<'a, T> {
/// Create a new select with the given title
pub fn new<S: Into<String>>(title: S) -> Self {
let mut s = Select {
Expand Down Expand Up @@ -136,7 +136,7 @@ impl<'a, T: Display> Select<'a, T> {
self.term.show_cursor()?;
let id = self.visible_options().get(self.cursor).unwrap().id;
let selected = self.options.iter().find(|o| o.id == id).unwrap();
let output = self.render_success(&selected.item.to_string())?;
let output = self.render_success(&selected.label)?;
let selected = self.options.into_iter().find(|o| o.id == id).unwrap();
self.term.write_all(output.as_bytes())?;
return Ok(selected.item);
Expand Down Expand Up @@ -359,4 +359,46 @@ mod tests {
without_ansi(select.render().unwrap().as_str())
);
}

#[test]
fn non_display() {
struct Thing {
num: u32,
thing: Option<()>,
}
let things = [
Thing {
num: 1,
thing: Some(()),
},
Thing {
num: 2,
thing: None,
},
];
let select = Select::new("things").description("pick a thing").options(
things
.iter()
.enumerate()
.map(|(i, t)| {
if i == 0 {
DemandOption::with_label("First", t).selected(true)
} else {
DemandOption::new(t.num).item(t)
}
})
.collect(),
);
assert_eq!(
indoc! {
" things
pick a thing
> First
2
↑/↓/k/j up/down • enter confirm"
},
without_ansi(select.render().unwrap().as_str())
);
}
}
59 changes: 41 additions & 18 deletions src/spinner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,28 +64,30 @@ impl<'a> Spinner<'a> {
}

/// Displays the dialog to the user and returns their response
pub fn run<F>(mut self, func: F) -> io::Result<()>
pub fn run<'scope, F, T>(mut self, func: F) -> io::Result<T>
where
F: Fn() + Send + 'static,
F: FnOnce() -> T + Send + 'scope,
T: Send + 'scope,
{
let handle = std::thread::spawn(move || {
func();
});

self.term.hide_cursor()?;
loop {
self.clear()?;
let output = self.render()?;
self.height = output.lines().count() - 1;
self.term.write_all(output.as_bytes())?;
sleep(self.style.fps);
if handle.is_finished() {
std::thread::scope(|s| {
let handle = s.spawn(func);
self.term.hide_cursor()?;
loop {
self.clear()?;
self.term.show_cursor()?;
break;
let output = self.render()?;
self.height = output.lines().count();
self.term.write_all(output.as_bytes())?;
sleep(self.style.fps);
if handle.is_finished() {
self.clear()?;
self.term.show_cursor()?;
break;
}
}
}
Ok(())
handle.join().map_err(|e| {
io::Error::new(io::ErrorKind::Other, format!("thread panicked: {e:?}"))
})
})
}

/// Render the spinner and return the output
Expand Down Expand Up @@ -217,4 +219,25 @@ mod test {
}
}
}

#[test]
fn scope_test() {
let spinner = Spinner::new("Scoped");
let mut a = [1, 2, 3];
let mut i = 0;
let out = spinner
.run(|| {
for n in &mut a {
if i == 1 {
*n = 5;
}
i += 1;
std::thread::sleep(Duration::from_millis(*n));
}
i * 5
})
.unwrap();
assert_eq!(a, [1, 5, 3]);
assert_eq!(out, 15);
}
}

0 comments on commit 3260a0c

Please sign in to comment.