diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 4 | ||||
-rw-r--r-- | src/error.rs | 6 | ||||
-rw-r--r-- | src/main.rs | 7 | ||||
-rw-r--r-- | src/stackexchange/api.rs | 2 | ||||
-rw-r--r-- | src/stackexchange/local_storage.rs | 10 | ||||
-rw-r--r-- | src/stackexchange/scraper.rs | 37 | ||||
-rw-r--r-- | src/term.rs | 17 | ||||
-rw-r--r-- | src/tui/app.rs | 4 | ||||
-rw-r--r-- | src/tui/markdown.rs | 2 | ||||
-rw-r--r-- | src/tui/views.rs | 48 | ||||
-rw-r--r-- | src/utils.rs | 15 |
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) } |