From a82f6e88847c3c655bfddeda17d3c957001380af Mon Sep 17 00:00:00 2001 From: Sam Tay Date: Fri, 10 Jul 2020 01:36:06 -0700 Subject: Refactor term module Move all of the mutable skin management within a Term struct. Also, just like the config refactor, move functions in term module up into Term implementation as associated functions --- TODO.md | 9 +-- src/cli.rs | 1 + src/main.rs | 45 +++++------- src/term.rs | 236 ++++++++++++++++++++++++++++++------------------------------ 4 files changed, 141 insertions(+), 150 deletions(-) diff --git a/TODO.md b/TODO.md index 4e3280d..3ddd9f9 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,14 @@ # TODO ### chores -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`) -3. Refactor `term` module to export a sruct `Term` with a stateful madskin; - this would clean up `main`. +1. Add test for clap app +2. Add significant cursive TUI test +3. Refactor layout handling (see TODO on `tui::views::LayoutView::relayout`) 4. Move to `directories 3.0`; optionally migrate existing macos configs? Not many people using this anyway... 5. Add github action to bump homebrew formula on tag push +6. Move to github actions ASAP, travis & appveyor are a PITA. See resources below. + ### bugs 1. Shift+TAB should move focus backwards diff --git a/src/cli.rs b/src/cli.rs index c8e4b41..af13f77 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,6 +13,7 @@ pub struct Opts { pub config: Config, } +// TODO accept FnOnce() -> Result so I can test this pub fn get_opts() -> Result { let config = Config::new()?; let limit = &config.limit.to_string(); diff --git a/src/main.rs b/src/main.rs index 48a73a4..8a139af 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,28 +6,19 @@ mod term; mod tui; mod utils; -use crossterm::style::Color; -use lazy_static::lazy_static; -use minimad::mad_inline; -use termimad::{CompoundStyle, MadSkin}; use tokio::runtime::Runtime; use tokio::task; use config::Config; use error::{Error, Result}; use stackexchange::{LocalStorage, Question, Search}; +use term::Term; use tui::markdown::Markdown; fn main() -> Result<()> { - // Markdown styles (outside of TUI) - 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 = term::mk_print_error(&skin); - // Tokio runtime let mut rt = Runtime::new()?; - rt.block_on(run(&mut skin)) + rt.block_on(run()) .and_then(|qs| { // Run TUI qs.map(tui::run); @@ -35,18 +26,22 @@ fn main() -> Result<()> { }) .or_else(|e: Error| { // Handle errors - print_error(&e.to_string()) + term::print_error(&e.to_string()) }) } /// Runs the CLI and, if the user wishes to enter the TUI, returns /// question/answer data -async fn run(skin: &mut MadSkin) -> Result>>> { +async fn run() -> Result>>> { + // Get CLI opts let opts = cli::get_opts()?; let config = opts.config; let sites = &config.sites; let lucky = config.lucky; + // Term tools and markdown styles (outside of TUI) + let mut term = Term::new(); + let ls = LocalStorage::new(opts.update_sites).await?; if let Some(key) = opts.set_api_key { @@ -66,21 +61,19 @@ async fn run(skin: &mut MadSkin) -> Result>>> { md.push_str(&format!("|{}|{}\n", s.api_site_parameter, s.site_url)); } md.push_str("|-\n"); - termimad::print_text(&md); + term.print(&md); return Ok(None); } if let Some(site) = ls.find_invalid_site(sites).await { - print_error!(skin, "$0 is not a valid StackExchange site.\n\n", site)?; - // TODO should only use inline for single lines; use termimad::text stuff - print_notice!( - skin, + term.print_error(&format!("{} is not a valid StackExchange site.\n\n", site))?; + term.print_notice( "If you think this is incorrect, try running\n\ ```\n\ so --update-sites\n\ ```\n\ - to update the cached site listing. You can also run `so --list-sites` \ - to list all available sites.", + to update the cached site listing. \ + You can also run `so --list-sites` to list all available sites.", )?; return Ok(None); } @@ -89,20 +82,20 @@ async fn run(skin: &mut MadSkin) -> Result>>> { let mut search = Search::new(config, ls, q); if lucky { // 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"); + let md = Term::wrap_spinner(search.search_lucky()).await??; + term.print(&md); + term.print("\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 !term::wait_for_char(' ')? { + if !Term::wait_for_char(' ')? { return Ok(None); } // Get the rest of the questions - return Ok(Some(term::wrap_spinner(qs).await?.unwrap()?)); + return Ok(Some(Term::wrap_spinner(qs).await?.unwrap()?)); } else { - return Ok(Some(term::wrap_spinner(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 9ce9e53..038c92b 100644 --- a/src/term.rs +++ b/src/term.rs @@ -3,10 +3,8 @@ 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, Stderr, Write}; -use termimad::{mad_write_inline, MadSkin}; +use std::io::{stderr, Write}; +use termimad::{CompoundStyle, MadSkin}; use tokio::sync::{ oneshot, oneshot::{error::TryRecvError, Sender}, @@ -14,7 +12,7 @@ use tokio::sync::{ use tokio::task::JoinHandle; use tokio::time; -use crate::error::{Error, Result}; +use crate::error::Result; const LOADING_SPINNER_DELAY: u64 = 40; const LOADING_SPINNER_DOTS: [&str; 56] = [ @@ -24,135 +22,133 @@ 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 { - 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, - _ => (), - } +pub struct Term { + skin: MadSkin, +} + +impl Default for Term { + fn default() -> Self { + Term::new() } - 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(future: F) -> Result -where - F: Future, -{ - // Start spinner - let (tx, spinner_handle) = spinner(); +impl Term { + pub fn new() -> Self { + let mut skin = MadSkin::default(); + skin.inline_code = CompoundStyle::with_fg(Color::Cyan); + skin.code_block.compound_style = CompoundStyle::with_fg(Color::Cyan); + Term { skin } + } - let result = future.await; + /// Print text to stdout + pub fn print(&self, text: &str) { + self.skin.print_text(text) + } - // Stop spinner - tx.send(()).ok(); - spinner_handle.await??; + /// Print text with error styling to stderr + /// Needs mut to temporarily modify styling (e.g. red fg) + pub fn print_error(&mut self, text: &str) -> Result<()> { + self.print_with_style(Color::Red, "✖ ", text) + } - Ok(result) -} + /// Print text with notice styling to stderr + /// Needs mut to temporarily modify styling (e.g. yellow fg) + pub fn print_notice(&mut self, text: &str) -> Result<()> { + self.print_with_style(Color::Yellow, "➜ ", text) + } -/// 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>) { - let (tx, mut rx) = oneshot::channel(); - let spinner_handle = tokio::spawn(async move { - let mut dots = LOADING_SPINNER_DOTS.iter().cycle(); + fn print_with_style(&mut self, fg: Color, prefix: &str, text: &str) -> Result<()> { + let mut styled_text = String::from(prefix); + styled_text.push_str(text); + // Set fg + self.skin.paragraph.set_fg(fg); + self.skin + .write_text_on(&mut std::io::stderr(), &styled_text)?; + // Unset fg + self.skin + .paragraph + .compound_style + .object_style + .foreground_color = None; + Ok(()) + } + + /// 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 { + let mut pressed = false; 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)); - while let Err(TryRecvError::Empty) = rx.try_recv() { - execute!( - stderr(), - cursor::MoveToColumn(0), - terminal::Clear(ClearType::CurrentLine), - Print(dots.next().unwrap()) - )?; - interval.tick().await; + loop { + match read()? { + Event::Key(KeyEvent { + code: KeyCode::Char(ch), + .. + }) if ch == c => { + pressed = true; + break; + } + Event::Key(_) => break, + _ => (), + } } - execute!( - stderr(), - terminal::Clear(ClearType::CurrentLine), - cursor::RestorePosition, - cursor::Show, - )?; terminal::disable_raw_mode()?; - Ok(()) - }); - (tx, spinner_handle) -} + Ok(pressed) + } -/// 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(skin: &mut MadSkin, f: F) -> Result -where - F: FnOnce(&MadSkin, &mut Stderr) -> Result, -{ - let err = &mut std::io::stderr(); - let p = skin.paragraph.clone(); - skin.paragraph.set_fg(Color::Red); - mad_write_inline!(err, skin, "✖ ")?; - let r: R = f(&skin, err)?; - skin.paragraph = p; - Ok::(r) -} + /// As it sounds, takes a future and shows a CLI spinner until it's output is ready + pub async fn wrap_spinner(future: F) -> Result + where + F: Future, + { + // Start spinner + let (tx, spinner_handle) = Self::spinner(); + + let result = future.await; -/// This makes code much more convenient, but would require each style to own -/// its own skin clone. Not sure if it is worth it. -pub fn mk_print_error(skin: &MadSkin) -> impl FnMut(&str) -> Result<()> + 'static { - let mut skin = skin.clone(); - move |text: &str| { - with_error_style(&mut skin, |err_skin, stderr| { - err_skin.write_text_on(stderr, text) - }) + // Stop spinner + tx.send(()).ok(); + spinner_handle.await??; + + Ok(result) } -} -#[macro_export] -macro_rules! print_error { - ($skin: expr, $md: literal $(, $value: expr )* $(,)? ) => {{ - use lazy_static::lazy_static; - use minimad::mad_inline; - use crate::error::Error; - let err = &mut std::io::stderr(); - let p = $skin.paragraph.clone(); - $skin.paragraph.set_fg(crossterm::style::Color::Red); - termimad::mad_write_inline!(err, $skin, "✖ ").map_err(Error::from)?; - $skin.write_composite(err, mad_inline!($md $(, $value)*)).map_err(Error::from)?; - $skin.paragraph = p; - Ok::<(), Error>(()) - }}; + /// 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`. + fn spinner() -> (Sender<()>, JoinHandle>) { + 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)); + while let Err(TryRecvError::Empty) = rx.try_recv() { + execute!( + stderr(), + cursor::MoveToColumn(0), + terminal::Clear(ClearType::CurrentLine), + Print(dots.next().unwrap()) + )?; + interval.tick().await; + } + execute!( + stderr(), + terminal::Clear(ClearType::CurrentLine), + cursor::RestorePosition, + cursor::Show, + )?; + terminal::disable_raw_mode()?; + Ok(()) + }); + (tx, spinner_handle) + } } -#[macro_export] -macro_rules! print_notice { - ($skin: expr, $md: literal $(, $value: expr )* $(,)? ) => {{ - use lazy_static::lazy_static; - use minimad::mad_inline; - use crate::error::Error; - let err = &mut std::io::stderr(); - let p = $skin.paragraph.clone(); - $skin.paragraph.set_fg(crossterm::style::Color::Yellow); - termimad::mad_write_inline!(err, $skin, "➜ ").map_err(Error::from)?; - $skin.write_composite(err, mad_inline!($md $(, $value)*)).map_err(Error::from)?; - $skin.paragraph = p; - Ok::<(), Error>(()) - }}; +pub fn print_error(text: &str) -> Result<()> { + Term::new().print_error(text) } -- cgit v1.2.3