From ca6d12a367752c414764835c1687d232ace377fd Mon Sep 17 00:00:00 2001 From: nathan dawit Date: Sat, 5 Aug 2023 21:34:01 +0300 Subject: [PATCH] Add shortcut for opening urls in entry fixes #21 --- Cargo.lock | 10 +++++++ Cargo.toml | 1 + README.md | 5 ++-- src/app.rs | 62 +++++++++++++++++++++++++++++++++++++++++- src/modes.rs | 1 + src/ui.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/util.rs | 17 ++++++++++++ 7 files changed, 168 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9859c06..56461dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,6 +710,15 @@ dependencies = [ "cc", ] +[[package]] +name = "linkify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" +dependencies = [ + "memchr", +] + [[package]] name = "linux-raw-sys" version = "0.3.1" @@ -1168,6 +1177,7 @@ dependencies = [ "diligent-date-parser", "directories", "html2text", + "linkify", "num_cpus", "r2d2", "r2d2_sqlite", diff --git a/Cargo.toml b/Cargo.toml index b23857d..da29cd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ ratatui = "0.20" ureq = "2.6" webbrowser = "0.8" wsl = "0.1" +linkify = "0.10.0" [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 951a7b8..3798c1a 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ Some normal mode controls vary based on whether you are currently selecting a fe - `x` - refresh all feeds - `i` - change to insert mode - `a` - toggle between read/unread entries -- `c` - copy the selected link to the clipboard (feed or entry) -- `o` - open the selected link in your browser (feed or entry) +- `c` - copy the selected link to the clipboard (feed or entry or references) +- `o` - open the selected link in your browser (feed or entry or references) +- `u` - find referenced/cited urls within current entry ### controls - insert mode diff --git a/src/app.rs b/src/app.rs index 35d8c8c..ae2a4c5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,5 @@ use crate::modes::{Mode, ReadMode, Selected}; -use crate::util; +use crate::util::{self, get_referenced_urls_from_text, StatefulList}; use anyhow::Result; use copypasta::{ClipboardContext, ClipboardProvider}; use crossterm::event::{KeyCode, KeyModifiers}; @@ -64,6 +64,7 @@ impl App { (toggle_help, Result<()>), (toggle_read, Result<()>), (toggle_read_mode, Result<()>), + (find_references, Result<()>), (update_current_feed_and_entries, Result<()>), ]; @@ -130,6 +131,7 @@ impl App { } (KeyCode::Char('c'), _) => self.put_current_link_in_clipboard(), (KeyCode::Char('o'), _) => self.open_link_in_browser(), + (KeyCode::Char('u'), _) => self.find_references(), _ => Ok(()), } } @@ -175,6 +177,8 @@ pub struct AppImpl { pub entries: util::StatefulList, pub entry_selection_position: usize, pub current_entry_text: String, + pub current_entry_references: util::StatefulList, + pub references_selection_position: usize, pub entry_scroll_position: u16, pub entry_lines_len: usize, pub entry_lines_rendered_len: u16, @@ -229,6 +233,8 @@ impl AppImpl { entry_column_width: 0, current_entry_meta: None, current_entry_text: String::new(), + current_entry_references: Vec::new().into(), + references_selection_position: 0, current_feed: initial_current_feed, feed_subscription_input: String::new(), mode: Mode::Normal, @@ -454,6 +460,35 @@ impl AppImpl { Ok(()) } + pub fn find_references(&mut self) -> Result<()> { + match &self.selected { + Selected::Entry(meta) => { + self.selected = Selected::References(meta.clone()); + if let Some(Ok(entry)) = self.get_selected_entry() { + let empty_string = String::new(); + + let entry_html = entry + .content + .as_ref() + .or(entry.description.as_ref()) + .or(Some(&empty_string)); + + self.current_entry_references = StatefulList::with_items( + get_referenced_urls_from_text(entry_html.unwrap()), + ); + } else { + self.current_entry_references = StatefulList::with_items(vec![]); + } + } + + Selected::References(meta) => { + self.selected = Selected::Entry(meta.clone()); + } + _ => {} + } + Ok(()) + } + pub fn clear_error_flash(&mut self) { self.error_flash = vec![]; } @@ -514,6 +549,7 @@ impl AppImpl { self.update_entry_selection_position(); } } + Selected::References(_) => (), Selected::Feeds => (), Selected::None => (), } @@ -530,10 +566,12 @@ impl AppImpl { match (&self.read_mode, &self.selected) { (ReadMode::ShowRead, Selected::Feeds) | (ReadMode::ShowRead, Selected::Entries) => { self.entry_selection_position = 0; + self.references_selection_position = 0; self.read_mode = ReadMode::ShowUnread } (ReadMode::ShowUnread, Selected::Feeds) | (ReadMode::ShowUnread, Selected::Entries) => { self.entry_selection_position = 0; + self.references_selection_position = 0; self.read_mode = ReadMode::ShowRead } _ => (), @@ -562,6 +600,11 @@ impl AppImpl { .items .get(self.entry_selection_position) .and_then(|entry| entry.link.as_deref()), + Selected::References(_) => self + .current_entry_references + .items + .get(self.references_selection_position) + .map(|x| x.as_str()), Selected::Entry(e) => e.link.as_deref(), Selected::None => None, } @@ -615,6 +658,7 @@ impl AppImpl { Selected::Entries } } + Selected::References(_) => (), Selected::None => (), } @@ -634,6 +678,14 @@ impl AppImpl { self.update_current_entry_meta()?; } } + Selected::References(_) => { + if !self.current_entry_references.items.is_empty() { + self.current_entry_references.previous(); + self.references_selection_position = + self.current_entry_references.state.selected().unwrap(); + self.update_current_entry_meta()?; + } + } Selected::Entry(_) => { if let Some(n) = self.entry_scroll_position.checked_sub(1) { self.entry_scroll_position = n @@ -657,6 +709,7 @@ impl AppImpl { } Selected::Entries => self.on_enter(), Selected::Entry(_) => Ok(()), + Selected::References(_) => Ok(()), Selected::None => Ok(()), } } @@ -674,6 +727,13 @@ impl AppImpl { self.update_current_entry_meta()?; } } + Selected::References(_) => { + if !self.current_entry_references.items.is_empty() { + self.current_entry_references.next(); + self.references_selection_position = + self.current_entry_references.state.selected().unwrap(); + } + } Selected::Entry(_) => { if let Some(n) = self.entry_scroll_position.checked_add(1) { self.entry_scroll_position = n diff --git a/src/modes.rs b/src/modes.rs index dd8a511..d72976c 100644 --- a/src/modes.rs +++ b/src/modes.rs @@ -3,6 +3,7 @@ pub enum Selected { Feeds, Entries, Entry(crate::rss::EntryMeta), + References(crate::rss::EntryMeta), None, } diff --git a/src/ui.rs b/src/ui.rs index 294c8f6..6e0275b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use crate::app::AppImpl; use crate::modes::{Mode, ReadMode, Selected}; -use crate::rss::EntryMeta; +use crate::rss::{EntryMeta, Feed}; const PINK: Color = Color::Rgb(255, 150, 167); @@ -29,6 +29,9 @@ pub fn draw(f: &mut Frame, chunks: Rc<[Rect]>, app: &mut AppImpl) Selected::Entry(_entry_meta) => { draw_entry(f, chunks[1], app); } + Selected::References(_) => { + draw_references(f, chunks[1], app); + } Selected::None => draw_entries(f, chunks[1], app), } } @@ -287,6 +290,16 @@ where text.push_str("r - refresh selected feed; x - refresh all feeds\n"); text.push_str("c - copy link; o - open link in browser\n") } + Selected::References(_) => { + text.push_str("c - copy link; o - open link in browser\n"); + text.push_str("u - return to entry\n"); + } + Selected::Entry(_) => { + text.push_str("r - mark entry read/un; a - toggle view read/un\n"); + text.push_str("c - copy link; o - open link in browser\n"); + text.push_str("u - list urls within entry\n") + + } _ => { text.push_str("r - mark entry read/un; a - toggle view read/un\n"); text.push_str("c - copy link; o - open link in browser\n") @@ -394,6 +407,67 @@ where } } +fn draw_references(f: &mut Frame, area: Rect, app: &mut AppImpl) +where + B: Backend, +{ + let title = match app.current_feed.as_ref() { + Some(Feed { + title: Some(title), .. + }) => format!("References for: {:?}", title), + _ => "References".to_string(), + }; + + let references = List::new( + app.current_entry_references + .items + .iter() + .map(|reference| ListItem::new(Span::raw(reference))) + .collect::>(), + ) + .block( + Block::default().borders(Borders::ALL).title(Span::styled( + title, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + ); + + let references = match app.selected { + Selected::References(_) => references + .highlight_style(Style::default().fg(PINK).add_modifier(Modifier::BOLD)) + .highlight_symbol("> "), + _ => references, + }; + + if !&app.error_flash.is_empty() { + let chunks = Layout::default() + .constraints([Constraint::Percentage(60), Constraint::Percentage(30)].as_ref()) + .direction(Direction::Vertical) + .split(area); + { + let error_text = error_text(&app.error_flash); + + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Error - press 'q' to close", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )); + + let error_widget = Paragraph::new(error_text) + .block(block) + .wrap(Wrap { trim: false }) + .scroll((0, 0)); + + f.render_widget(error_widget, chunks[1]); + } + } else { + f.render_stateful_widget(references, area, &mut app.current_entry_references.state); + } +} + fn draw_entry(f: &mut Frame, area: Rect, app: &mut AppImpl) where B: Backend, diff --git a/src/util.rs b/src/util.rs index 35973e7..1be1147 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,6 @@ +use linkify::{LinkFinder, LinkKind}; use ratatui::widgets::ListState; +use std::sync::OnceLock; #[derive(Debug)] pub struct StatefulList { @@ -57,6 +59,21 @@ impl From> for StatefulList { } } +pub static LINK_FINDER: OnceLock = OnceLock::new(); + +pub(crate) fn get_referenced_urls_from_text(text: &str) -> Vec { + let finder = LINK_FINDER.get_or_init(|| { + let mut lf = LinkFinder::new(); + lf.kinds(&[LinkKind::Url]); + lf + }); + + finder + .links(text) + .map(|link| link.as_str().to_string()) + .collect() +} + #[cfg(target_os = "linux")] pub(crate) fn set_wsl_clipboard_contents(s: &str) -> anyhow::Result<()> { use std::{