Skip to content

Commit

Permalink
prompts in spinner (#48)
Browse files Browse the repository at this point in the history
* fix: spinner eating the screen, also dont clear rest of screen since its not needed anymore

fix: confirm, select and multi select help text being eaten by spinner

chore: update tests

chore: example

fix: typo

* chore: renamed prompts_in_spinner.rs -> spinner-prompts.rs

* feat: custom themeable cursor for Input prompt

fix: spinner eating input prompt

* chore: fix tests

* refactor: rename unclear var

* fix: cursor rendering on success

* fix: cursor rendering after placeholder

* fix: spinner eating input success prompt

chore: update example

* chore: fix tests

* fix: cursor color not being reset when input is empty and there isnt a placeholder or suggestion

* fix: missing space

chore: remove commented code

fix: doc comment was a normal comment
  • Loading branch information
Vulpesx authored Apr 27, 2024
1 parent 81e8343 commit dfc6a56
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 36 deletions.
44 changes: 44 additions & 0 deletions examples/spinner-prompts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use demand::{Confirm, DemandOption, Input, MultiSelect, Select, Spinner, Theme};

fn main() {
let spinner = Spinner::new("im out here");
spinner
.run(|| {
Confirm::new("confirm")
.description("it says confirm")
.run()
.unwrap();
Input::new("input ")
.description("go on say something")
.suggestions(vec!["hello there"])
.validation(|s| match !s.contains('j') {
true => Ok(()),
false => Err("ew stinky 'j' not welcome here"),
})
.theme(&Theme::catppuccin())
.placeholder("Words go here")
.run()
.unwrap();
Select::new("select")
.description("hi")
.options(vec![
DemandOption::new("hi"),
DemandOption::new("hello"),
DemandOption::new("how are you"),
])
.run()
.unwrap();
MultiSelect::new("more select")
.description("hewo")
.options(vec![
DemandOption::new("hi"),
DemandOption::new("hello"),
DemandOption::new("how are you"),
])
.run()
.unwrap();
// Spinner::new("spinnerception")
// .run(|| std::thread::sleep(std::time::Duration::from_secs(1)))
})
.unwrap();
}
4 changes: 3 additions & 1 deletion src/confirm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ impl<'a> Confirm<'a> {
out.set_color(&self.theme.help_desc)?;
write!(out, " {}", desc)?;
}
writeln!(out)?;

out.reset()?;
Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
Expand Down Expand Up @@ -220,7 +221,8 @@ mod tests {
Yes! No.
←/→ toggle • y/n/enter submit"
←/→ toggle • y/n/enter submit
"
},
without_ansi(confirm.render().unwrap().as_str())
);
Expand Down
105 changes: 77 additions & 28 deletions src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl<'a> Input<'a> {
self
}

// Sets the suggestions of the input
/// Sets the suggestions of the input
pub fn suggestions(mut self, suggestions: Vec<&'static str>) -> Self {
self.suggestions = suggestions;
self
Expand Down Expand Up @@ -140,7 +140,7 @@ impl<'a> Input<'a> {

/// Displays the input to the user and returns the response
pub fn run(mut self) -> io::Result<String> {
self.term.show_cursor()?;
self.term.hide_cursor()?;
loop {
self.clear()?;
let output = self.render()?;
Expand Down Expand Up @@ -297,19 +297,7 @@ impl<'a> Input<'a> {
}
out.reset()?;

if !self.placeholder.is_empty() && self.input.is_empty() {
out.set_color(&self.theme.input_placeholder)?;
write!(out, "{}", &self.placeholder)?;
out.reset()?;
}

write!(out, "{}", &self.render_input()?)?;

if self.suggestion.is_some() {
out.set_color(&self.theme.input_placeholder)?;
write!(out, "{}", self.suggestion.as_ref().unwrap())?;
out.reset()?;
}
self.render_input(&mut out)?;

if self.err.is_some() {
out.set_color(&self.theme.error_indicator)?;
Expand All @@ -319,14 +307,75 @@ impl<'a> Input<'a> {
out.reset()?;
}

writeln!(out)?;
out.reset()?;

Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
}

fn render_input(&mut self) -> io::Result<String> {
fn render_input(&mut self, out: &mut Buffer) -> io::Result<String> {
let input = match self.password {
true => self.input.chars().map(|_| '*').collect::<String>(),
false => self.input.to_string(),
};

if !self.placeholder.is_empty() && self.input.is_empty() {
out.set_color(
&self
.theme
.real_cursor_color(Some(&self.theme.input_placeholder)),
)?;
write!(out, "{}", &self.placeholder[..1])?;
if self.placeholder.len() > 1 {
out.set_color(&self.theme.input_placeholder)?;
write!(out, "{}", &self.placeholder[1..])?;
out.reset()?;
}
return Ok(input);
}

let cursor_idx = self.get_char_idx(&input, self.cursor);
write!(out, "{}", &input[..cursor_idx])?;

if cursor_idx < input.len() {
out.set_color(&self.theme.real_cursor_color(None))?;
write!(out, "{}", &input[cursor_idx..cursor_idx + 1])?;
out.reset()?;
}
if cursor_idx + 1 < input.len() {
out.reset()?;
write!(out, "{}", &input[cursor_idx + 1..])?;
}

if let Some(suggestion) = &self.suggestion {
if !suggestion.is_empty() {
if cursor_idx >= input.len() {
out.set_color(
&self
.theme
.real_cursor_color(Some(&self.theme.input_placeholder)),
)?;
write!(out, "{}", &suggestion[..1])?;
if suggestion.len() > 1 {
out.set_color(&self.theme.input_placeholder)?;
write!(out, "{}", &suggestion[1..])?;
}
} else {
out.set_color(&self.theme.input_placeholder)?;
write!(out, "{suggestion}")?;
}
out.reset()?;
} else if cursor_idx >= input.len() {
out.set_color(&self.theme.real_cursor_color(None))?;
write!(out, " ")?;
out.reset()?;
}
} else if cursor_idx >= input.len() {
out.set_color(&self.theme.real_cursor_color(None))?;
write!(out, " ")?;
out.reset()?;
}

Ok(input)
}

Expand All @@ -335,7 +384,7 @@ impl<'a> Input<'a> {
out.set_color(&self.theme.title)?;
write!(out, " {}", self.title)?;
out.set_color(&self.theme.selected_option)?;
writeln!(out, " {}", &self.render_input()?.to_string())?;
writeln!(out, " {}", self.input)?;
out.reset()?;
Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
}
Expand Down Expand Up @@ -377,7 +426,7 @@ impl<'a> Input<'a> {
}

fn set_cursor(&mut self) -> io::Result<()> {
// if we have a placeholer, move the cursor left to beginning of the input
// if we have a placeholder, move the cursor left to beginning of the input
if !self.placeholder.is_empty() && self.input.is_empty() {
self.term
.move_cursor_left(self.placeholder.chars().count())?;
Expand Down Expand Up @@ -450,7 +499,7 @@ mod tests {
.placeholder("Placeholder");

assert_eq!(
" Title\n Description\n $ Placeholder",
" Title\n Description\n $ Placeholder\n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -460,7 +509,7 @@ mod tests {
let mut input = Input::new("Title");

assert_eq!(
" Title\n > ",
" Title\n > \n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -470,7 +519,7 @@ mod tests {
let mut input = Input::new("Title").description("Description");

assert_eq!(
" Title\n Description\n > ",
" Title\n Description\n > \n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -480,7 +529,7 @@ mod tests {
let mut input = Input::new("Title").prompt("$ ");

assert_eq!(
" Title\n $ ",
" Title\n $ \n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -490,7 +539,7 @@ mod tests {
let mut input = Input::new("Title").placeholder("Placeholder");

assert_eq!(
" Title\n > Placeholder",
" Title\n > Placeholder\n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -504,7 +553,7 @@ mod tests {
.inline(true);

assert_eq!(
" Title?Description.Prompt:Placeholder",
" Title?Description.Prompt:Placeholder\n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -518,14 +567,14 @@ mod tests {
input.input = "".to_string();
input.validate().unwrap();
assert_eq!(
" Title\n Description\n > \n\n * Name cannot be empty",
" Title\n Description\n > \n\n * Name cannot be empty\n",
without_ansi(input.render().unwrap().as_str())
);

input.input = "non empty".to_string();
input.validate().unwrap();
assert_eq!(
" Title\n Description\n > non empty",
" Title\n Description\n > non empty\n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand All @@ -540,14 +589,14 @@ mod tests {
input.input = "".to_string();
input.validate().unwrap();
assert_eq!(
" Title?Description.> \n\n * Name cannot be empty",
" Title?Description.> \n\n * Name cannot be empty\n",
without_ansi(input.render().unwrap().as_str())
);

input.input = "non empty".to_string();
input.validate().unwrap();
assert_eq!(
" Title?Description.> non empty",
" Title?Description.> non empty\n",
without_ansi(input.render().unwrap().as_str())
);
}
Expand Down
7 changes: 5 additions & 2 deletions src/multiselect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ impl<'a, T> MultiSelect<'a, T> {
write!(out, " {}", desc)?;
}
}
writeln!(out)?;

out.reset()?;
Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
Expand Down Expand Up @@ -468,7 +469,8 @@ mod tests {
[ ] Vegan Cheese
[ ] Nutella
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm"
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm
"
},
without_ansi(select.render().unwrap().as_str())
);
Expand Down Expand Up @@ -517,7 +519,8 @@ mod tests {
[•] 2
[•] 3
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm"
↑/↓/k/j up/down • x/space toggle • a toggle all • enter confirm
"
},
without_ansi(select.render().unwrap().as_str())
);
Expand Down
7 changes: 5 additions & 2 deletions src/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ impl<'a, T> Select<'a, T> {
write!(out, " {}", desc)?;
}
}
writeln!(out)?;

out.reset()?;
Ok(std::str::from_utf8(out.as_slice()).unwrap().to_string())
Expand Down Expand Up @@ -353,7 +354,8 @@ mod tests {
Canada
Mexico
↑/↓/k/j up/down • enter confirm"
↑/↓/k/j up/down • enter confirm
"
},
without_ansi(select.render().unwrap().as_str())
);
Expand Down Expand Up @@ -395,7 +397,8 @@ mod tests {
> First
2
↑/↓/k/j up/down • enter confirm"
↑/↓/k/j up/down • enter confirm
"
},
without_ansi(select.render().unwrap().as_str())
);
Expand Down
9 changes: 6 additions & 3 deletions src/spinner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl<'a> Spinner<'a> {
loop {
self.clear()?;
let output = self.render()?;
self.height = output.lines().count();
self.height = output.lines().count() - 1;
self.term.write_all(output.as_bytes())?;
sleep(self.style.fps);
if handle.is_finished() {
Expand Down Expand Up @@ -110,8 +110,11 @@ impl<'a> Spinner<'a> {
}

fn clear(&mut self) -> io::Result<()> {
self.term.clear_to_end_of_screen()?;
self.term.clear_last_lines(self.height)?;
if self.height == 0 {
self.term.clear_line()?;
} else {
self.term.clear_last_lines(self.height)?;
}
self.height = 0;
Ok(())
}
Expand Down
Loading

0 comments on commit dfc6a56

Please sign in to comment.