summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-07-10 01:36:06 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-07-10 19:13:49 -0700
commita82f6e88847c3c655bfddeda17d3c957001380af (patch)
treeb3e5bb7375e768cd3d399952c38182d9c1461a13
parent46e019ddf3b67a8307773edd109aa29d5be1a8b3 (diff)
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
-rw-r--r--TODO.md9
-rw-r--r--src/cli.rs1
-rw-r--r--src/main.rs45
-rw-r--r--src/term.rs236
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<Config> so I can test this
pub fn get_opts() -> Result<Opts> {
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<Option<Vec<Question<Markdown>>>> {
+async fn run() -> Result<Option<Vec<Question<Markdown>>>> {
+ // 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<Option<Vec<Question<Markdown>>>> {
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<Option<Vec<Question<Markdown>>>> {
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<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,
- _ => (),
- }
+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<F>(future: F) -> Result<F::Output>
-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<Result<()>>) {
- 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<bool> {
+ 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<R, F>(skin: &mut MadSkin, f: F) -> Result<R>
-where
- F: FnOnce(&MadSkin, &mut Stderr) -> Result<R, termimad::Error>,
-{
- 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, Error>(r)
-}
+ /// 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) = 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<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));
+ 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)
}