summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs4
-rw-r--r--src/error.rs6
-rw-r--r--src/main.rs7
-rw-r--r--src/stackexchange/api.rs2
-rw-r--r--src/stackexchange/local_storage.rs10
-rw-r--r--src/stackexchange/scraper.rs37
-rw-r--r--src/term.rs17
-rw-r--r--src/tui/app.rs4
-rw-r--r--src/tui/markdown.rs2
-rw-r--r--src/tui/views.rs48
-rw-r--r--src/utils.rs15
11 files changed, 69 insertions, 83 deletions
diff --git a/src/config.rs b/src/config.rs
index 99df1cc..d5dfe22 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -39,7 +39,7 @@ impl fmt::Display for SearchEngine {
impl Default for SearchEngine {
fn default() -> Self {
- SearchEngine::Google
+ SearchEngine::DuckDuckGo
}
}
@@ -92,7 +92,7 @@ impl Config {
/// Get project directory
pub fn project_dir() -> Result<ProjectDirs> {
- ProjectDirs::from("io", "Sam Tay", "so").ok_or(Error::ProjectDir)
+ ProjectDirs::from("io", "Sam Tay", "so").ok_or_else(|| Error::ProjectDir)
}
// TODO consider switching to .toml to be consistent with colors.toml
diff --git a/src/error.rs b/src/error.rs
index 2bcb899..ec76943 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -14,8 +14,10 @@ pub enum Error {
SerdeJson(#[from] serde_json::Error),
#[error("SerdeYaml error: {0}")]
SerdeYaml(#[from] serde_yaml::Error),
+ #[error("IO error: {0}")]
+ IO(#[from] std::io::Error),
#[error("Futures Join error : {0}")]
- Join(#[from] tokio::task::JoinError),
+ JoinError(#[from] tokio::task::JoinError),
#[error("File `{}` is malformed; try removing it", .0.display())]
MalformedFile(PathBuf),
#[error("Lacking {0:?} permissions on `{}`", .1.display())]
@@ -23,7 +25,7 @@ pub enum Error {
#[error("{0}")]
StackExchange(String),
#[error("{0}")]
- Scraping(String),
+ ScrapingError(String),
#[error("Couldn't find a suitable project directory; is your OS supported?")]
ProjectDir,
#[error("Sorry, couldn't find any answers to your question")]
diff --git a/src/main.rs b/src/main.rs
index 3a07158..8a139af 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -17,11 +17,12 @@ use tui::markdown::Markdown;
fn main() -> Result<()> {
// Tokio runtime
- Runtime::new()?
- .block_on(run())
- .map(|qs| {
+ let mut rt = Runtime::new()?;
+ rt.block_on(run())
+ .and_then(|qs| {
// Run TUI
qs.map(tui::run);
+ Ok(())
})
.or_else(|e: Error| {
// Handle errors
diff --git a/src/stackexchange/api.rs b/src/stackexchange/api.rs
index cf326f2..c9b69b4 100644
--- a/src/stackexchange/api.rs
+++ b/src/stackexchange/api.rs
@@ -153,7 +153,7 @@ impl Api {
params.insert("filter", SE_FILTER);
params.insert("page", "1");
if let Some(key) = &self.api_key {
- params.insert("key", key);
+ params.insert("key", &key);
}
params
}
diff --git a/src/stackexchange/local_storage.rs b/src/stackexchange/local_storage.rs
index 5874611..d5adeb2 100644
--- a/src/stackexchange/local_storage.rs
+++ b/src/stackexchange/local_storage.rs
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::fs;
-use std::path::Path;
+use std::path::PathBuf;
use crate::config::Config;
use crate::error::{Error, Result};
@@ -14,21 +14,21 @@ pub struct LocalStorage {
}
impl LocalStorage {
- fn fetch_local_sites(filename: &Path) -> Result<Option<Vec<Site>>> {
+ fn fetch_local_sites(filename: &PathBuf) -> Result<Option<Vec<Site>>> {
if let Some(file) = utils::open_file(filename)? {
return serde_json::from_reader(file)
- .map_err(|_| Error::MalformedFile(filename.to_path_buf()));
+ .map_err(|_| Error::MalformedFile(filename.clone()));
}
Ok(None)
}
- fn store_local_sites(filename: &Path, sites: &[Site]) -> Result<()> {
+ fn store_local_sites(filename: &PathBuf, sites: &[Site]) -> Result<()> {
let file = utils::create_file(filename)?;
serde_json::to_writer(file, sites)?;
Ok(())
}
- async fn init_sites(filename: &Path, update: bool) -> Result<Vec<Site>> {
+ async fn init_sites(filename: &PathBuf, update: bool) -> Result<Vec<Site>> {
if !update {
if let Some(sites) = Self::fetch_local_sites(filename)? {
return Ok(sites);
diff --git a/src/stackexchange/scraper.rs b/src/stackexchange/scraper.rs
index b1354fa..e67f0f6 100644
--- a/src/stackexchange/scraper.rs
+++ b/src/stackexchange/scraper.rs
@@ -51,7 +51,7 @@ impl Scraper for DuckDuckGo {
parse_with_selector(anchors, html, sites, limit).and_then(|sd| {
// DDG seems to never have empty results, so assume this is blocked
if sd.question_ids.is_empty() {
- Err(Error::Scraping(String::from(
+ Err(Error::ScrapingError(String::from(
"DuckDuckGo blocked this request",
)))
} else {
@@ -85,7 +85,7 @@ impl Scraper for Google {
sites: &HashMap<String, String>,
limit: u16,
) -> Result<ScrapedData> {
- let anchors = Selector::parse("a").unwrap();
+ let anchors = Selector::parse("div.r > a").unwrap();
parse_with_selector(anchors, html, sites, limit)
}
@@ -139,24 +139,23 @@ fn parse_with_selector(
let mut ordering: HashMap<String, usize> = HashMap::new();
let mut count = 0;
for anchor in fragment.select(&anchors) {
- if let Some(url) = anchor
+ let url = anchor
.value()
.attr("href")
- .map(|href| percent_decode_str(href).decode_utf8_lossy())
- {
- sites.iter().find_map(|(site_code, site_url)| {
- let id = question_url_to_id(site_url, &url)?;
- ordering.insert(id.to_owned(), count);
- match question_ids.entry(site_code.to_owned()) {
- Entry::Occupied(mut o) => o.get_mut().push(id),
- Entry::Vacant(o) => {
- o.insert(vec![id]);
- }
+ .ok_or_else(|| Error::ScrapingError("Anchor with no href".to_string()))
+ .map(|href| percent_decode_str(href).decode_utf8_lossy().into_owned())?;
+ sites.iter().find_map(|(site_code, site_url)| {
+ let id = question_url_to_id(site_url, &url)?;
+ ordering.insert(id.to_owned(), count);
+ match question_ids.entry(site_code.to_owned()) {
+ Entry::Occupied(mut o) => o.get_mut().push(id),
+ Entry::Vacant(o) => {
+ o.insert(vec![id]);
}
- count += 1;
- Some(())
- });
- }
+ }
+ count += 1;
+ Some(())
+ });
if count >= limit as usize {
break;
}
@@ -325,7 +324,9 @@ mod tests {
);
match DuckDuckGo.parse(html, &sites, 2) {
- Err(Error::Scraping(s)) if s == "DuckDuckGo blocked this request".to_string() => Ok(()),
+ Err(Error::ScrapingError(s)) if s == "DuckDuckGo blocked this request".to_string() => {
+ Ok(())
+ }
_ => Err(String::from("Failed to detect DuckDuckGo blocker")),
}
}
diff --git a/src/term.rs b/src/term.rs
index 44544aa..0469c4d 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -3,8 +3,8 @@ use crossterm::style::{Color, Print};
use crossterm::terminal::ClearType;
use crossterm::{cursor, execute, terminal};
use futures::Future;
-use std::io::stderr;
-use termimad::{CompoundStyle, LineStyle, MadSkin};
+use std::io::{stderr, Write};
+use termimad::{CompoundStyle, MadSkin};
use tokio::sync::{
oneshot,
oneshot::{error::TryRecvError, Receiver, Sender},
@@ -39,19 +39,16 @@ struct Spinner {
impl Term {
pub fn new() -> Self {
- let skin = MadSkin {
- inline_code: CompoundStyle::with_fg(Color::Cyan),
- code_block: LineStyle {
- compound_style: CompoundStyle::with_fg(Color::Cyan),
- ..Default::default()
- },
- ..Default::default()
- };
+ 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 }
}
/// Print text to stdout
pub fn print(&self, text: &str) {
+ println!("Terminal size: {:?}", crossterm::terminal::size());
+ println!("Calling skin.print_text with: BEGIN\n{:?}\nEND", text);
self.skin.print_text(text)
}
diff --git a/src/tui/app.rs b/src/tui/app.rs
index 7160cce..bdd4665 100644
--- a/src/tui/app.rs
+++ b/src/tui/app.rs
@@ -85,7 +85,7 @@ pub fn run(qs: Vec<Question<Markdown>>) -> Result<()> {
fn question_selected_callback(
question_map: Arc<HashMap<u32, Question<Markdown>>>,
- s: &mut Cursive,
+ mut s: &mut Cursive,
qid: u32,
) {
let q = question_map.get(&qid).unwrap();
@@ -102,7 +102,7 @@ fn question_selected_callback(
v.reset_with_all(q.answers.iter().map(|a| (preview_answer(x, a), a.id)))
})
.expect("Panic: setting answer list content failed");
- cb(s)
+ cb(&mut s)
}
fn preview_question(q: &Question<Markdown>) -> StyledString {
diff --git a/src/tui/markdown.rs b/src/tui/markdown.rs
index 9b03dde..49c36db 100644
--- a/src/tui/markdown.rs
+++ b/src/tui/markdown.rs
@@ -65,7 +65,7 @@ pub fn preview(width: usize, input: &StyledString) -> StyledString {
fn drop_color(span: StyledIndexedSpan) -> StyledIndexedSpan {
IndexedSpan {
attr: Style {
- color: Default::default(),
+ color: None,
..span.attr
},
..span
diff --git a/src/tui/views.rs b/src/tui/views.rs
index 5341630..7d1593d 100644
--- a/src/tui/views.rs
+++ b/src/tui/views.rs
@@ -1,7 +1,7 @@
use cursive::event::{Callback, Event, EventResult, Key};
use cursive::traits::{Finder, Nameable, Resizable, Scrollable};
use cursive::utils::markup::StyledString;
-use cursive::view::{CannotFocus, Margins, SizeConstraint, View, ViewWrapper};
+use cursive::view::{Margins, SizeConstraint, View, ViewWrapper};
use cursive::views::{
HideableView, LinearLayout, NamedView, PaddedView, Panel, ResizedView, ScrollView, SelectView,
TextView,
@@ -82,26 +82,19 @@ impl<T: View> ViewWrapper for ListViewT<T> {
cursive::wrap_impl!(self.view: T);
// In full screen mode we always take focus, even though currently hidden
- fn wrap_take_focus(
- &mut self,
- source: cursive::direction::Direction,
- ) -> Result<EventResult, CannotFocus> {
- self.view.take_focus(source).or_else(|_| {
- self.force_take_focus
- .then(EventResult::consumed)
- .ok_or(CannotFocus)
- })
+ fn wrap_take_focus(&mut self, source: cursive::direction::Direction) -> bool {
+ self.force_take_focus || self.view.take_focus(source)
}
// Always take arrow keys, its jarring to have them move pane focus
fn wrap_on_event(&mut self, event: Event) -> EventResult {
- let should_consume = matches!(
- event,
+ let should_consume = match event {
Event::Key(Key::Right)
- | Event::Key(Key::Left)
- | Event::Key(Key::Down)
- | Event::Key(Key::Up)
- );
+ | Event::Key(Key::Left)
+ | Event::Key(Key::Down)
+ | Event::Key(Key::Up) => true,
+ _ => false,
+ };
match self.view.on_event(event) {
EventResult::Ignored if should_consume => EventResult::Consumed(None),
@@ -211,26 +204,19 @@ pub struct MdViewT<T: View> {
impl<T: View> ViewWrapper for MdViewT<T> {
cursive::wrap_impl!(self.view: T);
- fn wrap_take_focus(
- &mut self,
- source: cursive::direction::Direction,
- ) -> Result<EventResult, CannotFocus> {
- self.view.take_focus(source).or_else(|_| {
- self.force_take_focus
- .then(EventResult::consumed)
- .ok_or(CannotFocus)
- })
+ fn wrap_take_focus(&mut self, source: cursive::direction::Direction) -> bool {
+ self.force_take_focus || self.view.take_focus(source)
}
// Always take arrow keys, its jarring to have them move pane focus
fn wrap_on_event(&mut self, event: Event) -> EventResult {
- let should_consume = matches!(
- event,
+ let should_consume = match event {
Event::Key(Key::Right)
- | Event::Key(Key::Left)
- | Event::Key(Key::Down)
- | Event::Key(Key::Up)
- );
+ | Event::Key(Key::Left)
+ | Event::Key(Key::Down)
+ | Event::Key(Key::Up) => true,
+ _ => false,
+ };
match self.view.on_event(event) {
EventResult::Ignored if should_consume => EventResult::Consumed(None),
diff --git a/src/utils.rs b/src/utils.rs
index fcac095..d1ffed8 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,23 +1,22 @@
use crate::error::{Error, PermissionType, Result};
use std::fs::File;
use std::io::ErrorKind;
-use std::path::Path;
+use std::path::PathBuf;
-pub fn open_file(filename: &Path) -> Result<Option<File>> {
+pub fn open_file(filename: &PathBuf) -> Result<Option<File>> {
File::open(filename).map(Some).or_else(|e| match e {
e if e.kind() == ErrorKind::NotFound => Ok(None),
- e if e.kind() == ErrorKind::PermissionDenied => Err(Error::Permissions(
- PermissionType::Read,
- filename.to_path_buf(),
- )),
+ e if e.kind() == ErrorKind::PermissionDenied => {
+ Err(Error::Permissions(PermissionType::Read, filename.clone()))
+ }
e => Err(Error::from(e)),
})
}
-pub fn create_file(filename: &Path) -> Result<File> {
+pub fn create_file(filename: &PathBuf) -> Result<File> {
File::create(filename).map_err(|e| {
if e.kind() == ErrorKind::PermissionDenied {
- Error::Permissions(PermissionType::Write, filename.to_path_buf())
+ Error::Permissions(PermissionType::Write, filename.clone())
} else {
Error::from(e)
}