diff options
author | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-16 20:55:10 -0700 |
---|---|---|
committer | Sam Tay <sam.chong.tay@gmail.com> | 2020-06-16 20:55:10 -0700 |
commit | d422c8424ae76fc85e0fdf55257e0cee7fa38271 (patch) | |
tree | 0ebfd038e294b0020ca41aeb9a43c1f6879615ba | |
parent | 76be36e46a5d404cbfb93253bfba4b73f03ee811 (diff) |
Add --lucky flag
Punting on async for now because loading up the cursive app is already super fast. Might be noticeably necessary after multi-site and external search engines are added
-rw-r--r-- | TODO.md | 9 | ||||
-rw-r--r-- | roadmap.md | 1 | ||||
-rw-r--r-- | src/cli.rs | 20 | ||||
-rw-r--r-- | src/config.rs | 2 | ||||
-rw-r--r-- | src/error.rs | 4 | ||||
-rw-r--r-- | src/main.rs | 16 | ||||
-rw-r--r-- | src/stackexchange.rs | 28 | ||||
-rw-r--r-- | src/utils.rs | 24 |
8 files changed, 83 insertions, 21 deletions
@@ -6,13 +6,6 @@ Going with cursive because it is way more flexible than tui-rs. benefit of incorporating termimad features will not be felt. But, this is changing [soon](https://meta.stackexchange.com/q/348746). -### v0.2.1 -1. Add `lucky: bool` to config, but -2. add --lucky and --no-lucky conflicting flags to cli -3. If --lucky, async get 1 result while getting limit results -4. Display with [space] to see more, any other key to exit. -1. maybe <query> is optional, and leaving blank starts up TUI? - ### v0.2.2 1. Site can be multiple 2. do tokio async on SE api @@ -32,6 +25,8 @@ etc. is prime for parallelization. 2. Also, we could `par_iter` the initial q&a data to SpannedStrings from the start, so that it's not done on the fly... +3. The rest of the questions should really start being fetched while waiting for + the user to press [Enter]... maybe start with just simple threads? ### Endless future improvements for the TUI 1. Init with smaller layout depending on initial screen size. @@ -24,6 +24,7 @@ [ ] Add duckduckgo scraper ### at some point +[ ] ask SE forums if I should bundle my api-key? [ ] allow new queries from TUI, e.g. hit `/` for a prompt [ ] or `/` searches current q/a [ ] clean up error.rs and term.rs ; only keep whats actually ergonomic @@ -57,14 +57,18 @@ pub fn get_opts() -> Result<Opts> { .takes_value(true) .default_value(limit) .validator(|s| s.parse::<u32>().map_err(|e| e.to_string()).map(|_| ())) - .help("Question limit per site query") - .hidden(true), // TODO unhide once more than just --lucky + .help("Question limit per site query"), ) .arg( Arg::with_name("lucky") .long("lucky") - .help("Print the top-voted answer of the most relevant question") - .hidden(true), // TODO unhide + .help("Print the top-voted answer of the most relevant question"), + ) + .arg( + Arg::with_name("no-lucky") + .long("no-lucky") + .help("Disable lucky") + .conflicts_with("lucky"), ) .arg( Arg::with_name("query") @@ -73,7 +77,11 @@ pub fn get_opts() -> Result<Opts> { .required_unless_one(&["list-sites", "update-sites", "set-api-key"]), ) .get_matches(); - + let lucky = match (matches.is_present("lucky"), matches.is_present("no-lucky")) { + (true, _) => true, + (_, true) => false, + _ => config.lucky, + }; Ok(Opts { list_sites: matches.is_present("list-sites"), update_sites: matches.is_present("update-sites"), @@ -89,8 +97,10 @@ pub fn get_opts() -> Result<Opts> { .value_of("set-api-key") .map(String::from) .or(config.api_key), + lucky, }, }) } // TODO how can I test this App given https://users.rust-lang.org/t/help-with-idiomatic-rust-and-ownership-semantics/43880 +// Maybe pass get_opts a closure that produces the Config... diff --git a/src/config.rs b/src/config.rs index 0ef5f2f..af82885 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,6 +11,7 @@ use crate::utils; pub struct Config { pub api_key: Option<String>, pub limit: u16, + pub lucky: bool, pub site: String, } @@ -20,6 +21,7 @@ impl Default for Config { Config { api_key: None, limit: 20, + lucky: true, site: String::from("stackoverflow"), } } diff --git a/src/error.rs b/src/error.rs index 35847c3..86fb55f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,8 +26,8 @@ pub enum Error { ProjectDir, #[error("Empty sites file in cache")] EmptySites, - //#[error("Sorry, couldn't find any answers for your query")] - //NoResults, + #[error("Sorry, couldn't find any answers for your query")] + NoResults, } #[derive(Debug)] diff --git a/src/main.rs b/src/main.rs index e099d21..06cacd8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ fn main() { let opts = cli::get_opts()?; let config = opts.config; let site = &config.site; + let lucky = config.lucky; let mut ls = LocalStorage::new()?; if let Some(key) = opts.set_api_key { @@ -65,10 +66,19 @@ fn main() { if let Some(q) = opts.query { let se = StackExchange::new(config); - //TODO async + // TODO get the rest of the results in the background + if lucky { + // TODO this needs preprocessing; all the more reason to do it at SE level + let md = se.search_lucky(&q)?; + skin.print_text(&md); + skin.print_text( + "\nPress **[SPACE]** to see more results, or any other key to exit", + ); + if !utils::wait_for_char(' ')? { + return Ok(()); + } + } let qs = se.search(&q)?; - //TODO do the print_text below for --lucky with option to continue - //skin.print_text(&md); tui::run(qs)?; } Ok(()) diff --git a/src/stackexchange.rs b/src/stackexchange.rs index 941aad6..e9bc6d9 100644 --- a/src/stackexchange.rs +++ b/src/stackexchange.rs @@ -81,11 +81,32 @@ impl StackExchange { StackExchange { client, config } } + // TODO also return a future with the rest of the questions + /// Search query at stack exchange and get the top answer body + pub fn search_lucky(&self, q: &str) -> Result<String> { + let ans = self + .search_advanced(q, 1)? + .into_iter() + .next() + .ok_or(Error::NoResults)? + .answers + .into_iter() + .next() + .ok_or_else(|| { + Error::StackExchange(String::from("Received question with no answers")) + })?; + Ok(ans.body) + } + + /// Search query at stack exchange and get a list of relevant questions + pub fn search(&self, q: &str) -> Result<Vec<Question>> { + self.search_advanced(q, self.config.limit) + } /// Search against the search/advanced endpoint with a given query. /// Only fetches questions that have at least one answer. /// TODO async /// TODO parallel requests over multiple sites - pub fn search(&self, q: &str) -> Result<Vec<Question>> { + fn search_advanced(&self, q: &str, limit: u16) -> Result<Vec<Question>> { let resp_body = self .client .get(stackexchange_url("search/advanced")) @@ -93,7 +114,7 @@ impl StackExchange { .query(&self.get_default_opts()) .query(&[ ("q", q), - ("pagesize", &self.config.limit.to_string()), + ("pagesize", &limit.to_string()), ("page", "1"), ("answers", "1"), ("order", "desc"), @@ -141,7 +162,7 @@ impl LocalStorage { }) } - // TODO make this async, inform user if we are downloading + // TODO inform user if we are downloading pub fn sites(&mut self) -> Result<&Vec<Site>> { // Stop once Option ~ Some or Result ~ Err if self.sites.is_none() && !self.fetch_local_sites()? { @@ -177,7 +198,6 @@ impl LocalStorage { } // TODO decide whether or not I should give LocalStorage an api key.. - // TODO cool loading animation? fn fetch_remote_sites(&mut self) -> Result<()> { let resp_body = Client::new() .get(stackexchange_url("sites")) diff --git a/src/utils.rs b/src/utils.rs index d1ffed8..58319ea 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,6 @@ 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; @@ -22,3 +24,25 @@ pub fn create_file(filename: &PathBuf) -> Result<File> { } }) } + +pub fn wait_for_char(c: char) -> Result<bool> { + terminal::enable_raw_mode()?; + let mut pressed = false; + 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) +} |