summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-07-01 22:35:19 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-07-01 22:35:40 -0700
commit20933e71a78975cbcaf7621107423c06f62da8e7 (patch)
treeec908f2eeebed66c85f77e3184486d989af58e84
parentdb41c97158bb20056b42f57c4c826402b2496ebb (diff)
Add a CLI spinner
-rw-r--r--TODO.md2
-rw-r--r--src/main.rs15
-rw-r--r--src/term.rs107
-rw-r--r--src/utils.rs22
4 files changed, 113 insertions, 33 deletions
diff --git a/TODO.md b/TODO.md
index 6047985..e6456cd 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,8 +1,6 @@
# TODO
### chores
-0. Add a simpler loading spinner during --lucky retrieval and between SPACE and
- TUI.
0. Use `include_str!` instead of hardcoding `colors.toml`.
1. Move to github actions ASAP, travis & appveyor are a PITA. See resources below.
2. Refactor layout handling (see TODO on `tui::views::LayoutView::relayout`)
diff --git a/src/main.rs b/src/main.rs
index 7ce78a1..2db6c99 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15,7 +15,6 @@ use tokio::task;
use error::{Error, Result};
use stackexchange::{LocalStorage, Question, Search};
-use term::mk_print_error;
use tui::markdown::Markdown;
fn main() -> Result<()> {
@@ -23,7 +22,7 @@ fn main() -> Result<()> {
let mut skin = MadSkin::default();
skin.inline_code = CompoundStyle::with_fg(Color::Cyan);
skin.code_block.compound_style = CompoundStyle::with_fg(Color::Cyan);
- let mut print_error = mk_print_error(&skin);
+ let mut print_error = term::mk_print_error(&skin);
// Tokio runtime
let mut rt = Runtime::new()?;
@@ -84,17 +83,21 @@ async fn run(skin: &mut MadSkin) -> Result<Option<Vec<Question<Markdown>>>> {
if let Some(q) = opts.query {
let mut search = Search::new(config, ls, q);
if lucky {
- let md = search.search_lucky().await?;
+ // Show top answer
+ let md = term::wrap_spinner(search.search_lucky()).await??;
skin.print_text(&md);
skin.print_text("\nPress **[SPACE]** to see more results, or any other key to exit");
+
// Kick off the rest of the search in the background
let qs = task::spawn(async move { search.search_md().await });
- if !utils::wait_for_char(' ')? {
+ if !term::wait_for_char(' ')? {
return Ok(None);
}
- return Ok(Some(qs.await.unwrap()?));
+
+ // Get the rest of the questions
+ return Ok(Some(term::wrap_spinner(qs).await?.unwrap()?));
} else {
- return Ok(Some(search.search_md().await?));
+ return Ok(Some(term::wrap_spinner(search.search_md()).await??));
}
}
Ok(None)
diff --git a/src/term.rs b/src/term.rs
index 8297a6a..9ad880b 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -1,10 +1,111 @@
-use crate::error::{Error, Result};
-use crossterm::style::Color;
+use crossterm::event::{read, Event, KeyCode, KeyEvent};
+use crossterm::style::{Color, Print};
+use crossterm::terminal::ClearType;
+use crossterm::{cursor, execute, terminal};
+use futures::Future;
use lazy_static::lazy_static;
use minimad::mad_inline;
-use std::io::Stderr;
+use std::io::{stderr, Stderr, Write};
use termimad::{mad_write_inline, MadSkin};
+use tokio::sync::{
+ oneshot,
+ oneshot::{error::TryRecvError, Sender},
+};
+use tokio::task::JoinHandle;
+use tokio::time;
+
+use crate::error::{Error, Result};
+
+const LOADING_SPINNER_DELAY: u64 = 40;
+const LOADING_SPINNER_DOTS: [&str; 56] = [
+ "⢀⠀", "⡀⠀", "⠄⠀", "⢂⠀", "⡂⠀", "⠅⠀", "⢃⠀", "⡃⠀", "⠍⠀", "⢋⠀", "⡋⠀", "⠍⠁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉",
+ "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⢈⠩", "⡀⢙", "⠄⡙", "⢂⠩", "⡂⢘", "⠅⡘", "⢃⠨", "⡃⢐", "⠍⡐", "⢋⠠",
+ "⡋⢀", "⠍⡁", "⢋⠁", "⡋⠁", "⠍⠉", "⠋⠉", "⠋⠉", "⠉⠙", "⠉⠙", "⠉⠩", "⠈⢙", "⠈⡙", "⠈⠩", "⠀⢙", "⠀⡙", "⠀⠩",
+ "⠀⢘", "⠀⡘", "⠀⠨", "⠀⢐", "⠀⡐", "⠀⠠", "⠀⢀", "⠀⡀",
+];
+
+/// Blocks and waits for the user to press any key. Returns whether or not that key is the
+/// character key `c`.
+pub fn wait_for_char(c: char) -> Result<bool> {
+ let mut pressed = false;
+ terminal::enable_raw_mode()?;
+ loop {
+ match read()? {
+ Event::Key(KeyEvent {
+ code: KeyCode::Char(ch),
+ ..
+ }) if ch == c => {
+ pressed = true;
+ break;
+ }
+ Event::Key(_) => break,
+ _ => (),
+ }
+ }
+ terminal::disable_raw_mode()?;
+ Ok(pressed)
+}
+
+/// As it sounds, takes a future and shows a CLI spinner until it's output is ready
+pub async fn wrap_spinner<F>(future: F) -> Result<F::Output>
+where
+ F: Future,
+{
+ // Start spinner
+ let (tx, spinner_handle) = spinner();
+
+ let result = future.await;
+
+ // Stop spinner
+ tx.send(()).ok();
+ spinner_handle.await??;
+
+ Ok(result)
+}
+
+/// Start a CLI spinner on the current cursor line. To stop it, call `send` on the `Sender`. To
+/// wait until it's done cleaning up it's current action (which is very important), await it's
+/// `JoinHandle`.
+pub fn spinner() -> (Sender<()>, JoinHandle<Result<()>>) {
+ let (tx, mut rx) = oneshot::channel();
+ let spinner_handle = tokio::spawn(async move {
+ let mut dots = LOADING_SPINNER_DOTS.iter().cycle();
+ terminal::enable_raw_mode()?;
+ execute!(
+ stderr(),
+ cursor::SavePosition,
+ cursor::Hide,
+ terminal::Clear(ClearType::CurrentLine),
+ )?;
+ let mut interval = time::interval(time::Duration::from_millis(LOADING_SPINNER_DELAY));
+ loop {
+ match rx.try_recv() {
+ Err(TryRecvError::Empty) => {
+ execute!(
+ stderr(),
+ cursor::MoveToColumn(0),
+ terminal::Clear(ClearType::CurrentLine),
+ Print(dots.next().unwrap())
+ )?;
+ interval.tick().await;
+ }
+ _ => break,
+ }
+ }
+ execute!(
+ stderr(),
+ terminal::Clear(ClearType::CurrentLine),
+ cursor::RestorePosition,
+ cursor::Show,
+ )?;
+ terminal::disable_raw_mode()?;
+ Ok(())
+ });
+ (tx, spinner_handle)
+}
+/// Temporarily modifies a skin with error styles (e.g. red fg) for use with the given closure.
+/// Once the closure finishes, the skin is returned to original state.
pub fn with_error_style<R, F>(skin: &mut MadSkin, f: F) -> Result<R>
where
F: FnOnce(&MadSkin, &mut Stderr) -> Result<R, termimad::Error>,
diff --git a/src/utils.rs b/src/utils.rs
index e7a8830..d1ffed8 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,6 +1,4 @@
use crate::error::{Error, PermissionType, Result};
-use crossterm::event::{read, Event, KeyCode, KeyEvent};
-use crossterm::terminal;
use std::fs::File;
use std::io::ErrorKind;
use std::path::PathBuf;
@@ -24,23 +22,3 @@ pub fn create_file(filename: &PathBuf) -> Result<File> {
}
})
}
-
-pub fn wait_for_char(c: char) -> Result<bool> {
- let mut pressed = false;
- terminal::enable_raw_mode()?;
- loop {
- match read()? {
- Event::Key(KeyEvent {
- code: KeyCode::Char(ch),
- ..
- }) if ch == c => {
- pressed = true;
- break;
- }
- Event::Key(_) => break,
- _ => (),
- }
- }
- terminal::disable_raw_mode()?;
- Ok(pressed)
-}