summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/error.rs2
-rw-r--r--src/feed.rs33
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs89
-rw-r--r--src/manager.rs4
-rw-r--r--src/status.rs41
6 files changed, 101 insertions, 70 deletions
diff --git a/src/error.rs b/src/error.rs
index 2638fc2..dd2d859 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -29,7 +29,7 @@ pub enum PullError {
29 LinkUpdate, 29 LinkUpdate,
30} 30}
31 31
32#[derive(Debug, Error)] 32#[derive(Debug, Error, Copy, Clone)]
33pub enum EntryError { 33pub enum EntryError {
34 #[error("missing title")] 34 #[error("missing title")]
35 MissingTitle, 35 MissingTitle,
diff --git a/src/feed.rs b/src/feed.rs
index c3c5408..f9a7893 100644
--- a/src/feed.rs
+++ b/src/feed.rs
@@ -5,6 +5,7 @@ use crate::{
5 status::PullStatus, 5 status::PullStatus,
6}; 6};
7 7
8use ansi_term::{Color, Style};
8use chrono::prelude::*; 9use chrono::prelude::*;
9use feed_rs::{ 10use feed_rs::{
10 model::{Entry as ChannelEntry, Feed as Channel}, 11 model::{Entry as ChannelEntry, Feed as Channel},
@@ -40,6 +41,14 @@ impl Feed {
40 self.entries.len() 41 self.entries.len()
41 } 42 }
42 43
44 pub fn last_updated(&self) -> DateTime<Utc> {
45 self.entries
46 .iter()
47 .map(|e| e.published)
48 .max()
49 .unwrap_or(DateTime::<Utc>::MIN_UTC)
50 }
51
43 pub fn unread_count(&self) -> usize { 52 pub fn unread_count(&self) -> usize {
44 self.entries.iter().filter(|e| e.unread).count() 53 self.entries.iter().filter(|e| e.unread).count()
45 } 54 }
@@ -102,7 +111,15 @@ impl Feed {
102 111
103impl fmt::Display for Feed { 112impl fmt::Display for Feed {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{}", self.title.to_lowercase()) 114 write!(
115 f,
116 "{} {} {}",
117 self.last_updated().format(crate::DATE_FMT),
118 Style::new().dimmed().paint(self.title.to_ascii_lowercase()),
119 Style::new()
120 .fg(Color::Cyan)
121 .paint(self.entries.len().to_string()),
122 )
106 } 123 }
107} 124}
108 125
@@ -148,9 +165,17 @@ impl fmt::Display for Entry {
148 write!( 165 write!(
149 f, 166 f,
150 "{} {} {}", 167 "{} {} {}",
151 self.published.format("%v"), 168 self.published.format(crate::DATE_FMT),
152 self.link, 169 Style::new().fg(Color::Cyan).paint(
153 self.title.to_lowercase(), 170 self.link
171 .as_str()
172 .trim_end_matches('/')
173 .trim_start_matches("http://")
174 .trim_start_matches("https://")
175 .trim_start_matches("http://www.")
176 .trim_start_matches("https://www.")
177 ),
178 Style::new().dimmed().paint(self.title.to_ascii_lowercase()),
154 ) 179 )
155 } 180 }
156} 181}
diff --git a/src/lib.rs b/src/lib.rs
index 867a09c..7376d44 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -4,6 +4,8 @@ pub mod feed;
4pub mod manager; 4pub mod manager;
5pub mod status; 5pub mod status;
6 6
7pub(crate) static DATE_FMT: &str = "%e %b %y";
8
7pub trait PrintResult { 9pub trait PrintResult {
8 fn print(&self); 10 fn print(&self);
9} 11}
diff --git a/src/main.rs b/src/main.rs
index 65de274..8c94843 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,47 +1,7 @@
1use chrono::{naive::Days, prelude::*};
1use clap::{Args, Parser, Subcommand, ValueEnum}; 2use clap::{Args, Parser, Subcommand, ValueEnum};
2use syn::{manager::Manager, PrintResult}; 3use syn::{manager::Manager, PrintResult};
3 4
4// #[tokio::main(flavor = "current_thread")]
5// async fn main() {
6// let mut manager = Manager::load().unwrap();
7//
8// let feeds = vec![
9// "https://peppe.rs/index.xml",
10// "https://jvns.ca/atom.xml",
11// // "https://www.youtube.com/feeds/videos.xml?channel_id=UCuTaETsuCOkJ0H_GAztWt0Q",
12// ];
13//
14// for f in feeds {
15// match manager.add_feed(f).await {
16// Ok(s) => println!("{s}"),
17// Err(e) => println!("{e}"),
18// }
19// }
20//
21// for entry in manager.list_entries() {
22// println!("{entry}");
23// }
24//
25// match manager.store() {
26// Ok(s) => println!("{s}"),
27// Err(e) => eprintln!("{e}"),
28// }
29//
30// // let mut feed = Feed::new(url);
31//
32// // feed.resolve().await.unwrap();
33//
34// // let last_read = DateTime::parse_from_rfc2822("Mon, 16 Mar 2020 18:30:00 +0000")
35// // .unwrap()
36// // .with_timezone(&Utc);
37//
38// // feed.last_read = last_read;
39//
40// // for i in feed.unread().unwrap() {
41// // println!("{}", i.title.as_ref().unwrap().content)
42// // }
43// }
44
45#[derive(Parser)] 5#[derive(Parser)]
46#[command(author, version, about, long_about = None)] 6#[command(author, version, about, long_about = None)]
47struct Cli { 7struct Cli {
@@ -51,20 +11,31 @@ struct Cli {
51 11
52#[derive(Subcommand)] 12#[derive(Subcommand)]
53enum Command { 13enum Command {
14 /// track a new feed
54 Add(AddCommand), 15 Add(AddCommand),
55 List(ListCommand), 16 /// list all entries in reverse chronological order
17 ListEntries(ListEntriesCommand),
18 /// list all feeds in reverse chronological order
19 ListFeeds,
20 /// refresh feeds
56 Pull(PullCommand), 21 Pull(PullCommand),
57} 22}
58 23
24impl Default for Command {
25 fn default() -> Self {
26 Self::ListEntries(ListEntriesCommand { cutoff: None })
27 }
28}
29
59#[derive(Args)] 30#[derive(Args)]
60struct AddCommand { 31struct AddCommand {
61 url: String, 32 url: String,
62} 33}
63 34
64#[derive(Args)] 35#[derive(Args)]
65struct ListCommand { 36struct ListEntriesCommand {
66 #[arg(value_enum)] 37 #[arg(value_name = "CUTOFF")]
67 target: ListTarget, 38 cutoff: Option<u64>,
68} 39}
69 40
70#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 41#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
@@ -81,8 +52,8 @@ struct PullCommand {
81#[tokio::main(flavor = "current_thread")] 52#[tokio::main(flavor = "current_thread")]
82async fn main() { 53async fn main() {
83 let opts = Cli::parse(); 54 let opts = Cli::parse();
84 match &opts.command { 55 match &opts.command.unwrap_or_default() {
85 Some(Command::Add(AddCommand { url })) => { 56 Command::Add(AddCommand { url }) => {
86 let mut manager = Manager::load().unwrap_or_else(|e| { 57 let mut manager = Manager::load().unwrap_or_else(|e| {
87 eprintln!("{e}"); 58 eprintln!("{e}");
88 Manager::default() 59 Manager::default()
@@ -91,17 +62,28 @@ async fn main() {
91 manager.add_feed(&url).await.print(); 62 manager.add_feed(&url).await.print();
92 manager.store().print(); 63 manager.store().print();
93 } 64 }
94 Some(Command::List(ListCommand { target })) => { 65 Command::ListFeeds => {
66 let manager = Manager::load().unwrap_or_else(|e| {
67 eprintln!("{e}");
68 Manager::default()
69 });
70 manager.list_feeds().for_each(|f| println!("{f}"));
71 }
72 Command::ListEntries(ListEntriesCommand { cutoff }) => {
95 let manager = Manager::load().unwrap_or_else(|e| { 73 let manager = Manager::load().unwrap_or_else(|e| {
96 eprintln!("{e}"); 74 eprintln!("{e}");
97 Manager::default() 75 Manager::default()
98 }); 76 });
99 match target { 77 manager
100 ListTarget::Feeds => manager.list_feeds().for_each(|f| println!("{f}")), 78 .list_entries()
101 ListTarget::Entries => manager.list_entries().for_each(|f| println!("{f}")), 79 .filter(|entry| {
102 } 80 cutoff
81 .map(|c| Utc::now() - Days::new(c) <= entry.published)
82 .unwrap_or(true)
83 })
84 .for_each(|f| println!("{f}"));
103 } 85 }
104 Some(Command::Pull(PullCommand { .. })) => { 86 Command::Pull(PullCommand { .. }) => {
105 let mut manager = Manager::load().unwrap_or_else(|e| { 87 let mut manager = Manager::load().unwrap_or_else(|e| {
106 eprintln!("{e}"); 88 eprintln!("{e}");
107 Manager::default() 89 Manager::default()
@@ -112,6 +94,5 @@ async fn main() {
112 errors.iter().for_each(PrintResult::print); 94 errors.iter().for_each(PrintResult::print);
113 manager.store().print(); 95 manager.store().print();
114 } 96 }
115 _ => {}
116 } 97 }
117} 98}
diff --git a/src/manager.rs b/src/manager.rs
index 74f449b..34e19be 100644
--- a/src/manager.rs
+++ b/src/manager.rs
@@ -46,7 +46,9 @@ impl Manager {
46 } 46 }
47 47
48 pub fn list_feeds(&self) -> impl Iterator<Item = &Feed> { 48 pub fn list_feeds(&self) -> impl Iterator<Item = &Feed> {
49 self.feeds.iter() 49 let mut ordered: Vec<&Feed> = self.feeds.iter().collect();
50 ordered.sort_by(|a, b| b.last_updated().cmp(&a.last_updated()));
51 ordered.into_iter()
50 } 52 }
51 53
52 pub fn store(&self) -> Result<StoreStatus, IOError> { 54 pub fn store(&self) -> Result<StoreStatus, IOError> {
diff --git a/src/status.rs b/src/status.rs
index 7e51160..205e2a9 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -2,13 +2,13 @@ use std::{fmt, path::PathBuf};
2 2
3use crate::error::EntryError; 3use crate::error::EntryError;
4 4
5use ansi_term::Style; 5use ansi_term::{Color, Style};
6 6
7#[derive(Debug)] 7#[derive(Debug)]
8pub struct PullStatus { 8pub struct PullStatus {
9 title: String, 9 pub title: String,
10 count: usize, 10 pub count: usize,
11 errors: Vec<EntryError>, 11 pub errors: Vec<EntryError>,
12} 12}
13 13
14impl PullStatus { 14impl PullStatus {
@@ -19,17 +19,38 @@ impl PullStatus {
19 errors, 19 errors,
20 } 20 }
21 } 21 }
22
23 pub fn is_empty(&self) -> bool {
24 self.count == 0
25 }
22} 26}
23 27
24impl fmt::Display for PullStatus { 28impl fmt::Display for PullStatus {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 write!( 30 write!(
27 f, 31 f,
28 "{:<20}: pulled {:>4} entries with {:>4} errors", 32 "{}",
29 Style::new().bold().paint(self.title.to_ascii_lowercase()), 33 Style::new().dimmed().paint(self.title.to_ascii_lowercase()),
30 Style::new().bold().paint(self.count.to_string()), 34 )?;
31 Style::new().bold().paint(self.errors.len().to_string()) 35
32 ) 36 write!(
37 f,
38 " {:>2}",
39 Style::new()
40 .fg(Color::Cyan)
41 .paint(self.count.to_string() + " new"),
42 )?;
43
44 if !self.errors.is_empty() {
45 write!(
46 f,
47 " {:>2}",
48 Style::new()
49 .fg(Color::Red)
50 .paint(self.errors.len().to_string() + " err"),
51 )?;
52 }
53 Ok(())
33 } 54 }
34} 55}
35 56
@@ -49,7 +70,7 @@ impl fmt::Display for StoreStatus {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!( 71 write!(
51 f, 72 f,
52 "cached {: >4} feeds to {}", 73 "cached {:>4} feeds to {}",
53 Style::new().bold().paint(self.count.to_string()), 74 Style::new().bold().paint(self.count.to_string()),
54 Style::new() 75 Style::new()
55 .bold() 76 .bold()