summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Tay <sam.chong.tay@gmail.com>2020-06-16 20:55:10 -0700
committerSam Tay <sam.chong.tay@gmail.com>2020-06-16 20:55:10 -0700
commitd422c8424ae76fc85e0fdf55257e0cee7fa38271 (patch)
tree0ebfd038e294b0020ca41aeb9a43c1f6879615ba
parent76be36e46a5d404cbfb93253bfba4b73f03ee811 (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.md9
-rw-r--r--roadmap.md1
-rw-r--r--src/cli.rs20
-rw-r--r--src/config.rs2
-rw-r--r--src/error.rs4
-rw-r--r--src/main.rs16
-rw-r--r--src/stackexchange.rs28
-rw-r--r--src/utils.rs24
8 files changed, 83 insertions, 21 deletions
diff --git a/TODO.md b/TODO.md
index 33e401b..d232f52 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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.
diff --git a/roadmap.md b/roadmap.md
index edab04e..1a1bc4c 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -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
diff --git a/src/cli.rs b/src/cli.rs
index 1f67edd..4ca00d0 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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)
+}