diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/error.rs | 2 | ||||
-rw-r--r-- | src/feed.rs | 33 | ||||
-rw-r--r-- | src/lib.rs | 2 | ||||
-rw-r--r-- | src/main.rs | 89 | ||||
-rw-r--r-- | src/manager.rs | 4 | ||||
-rw-r--r-- | src/status.rs | 41 |
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)] |
33 | pub enum EntryError { | 33 | pub 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 | ||
8 | use ansi_term::{Color, Style}; | ||
8 | use chrono::prelude::*; | 9 | use chrono::prelude::*; |
9 | use feed_rs::{ | 10 | use 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 | ||
103 | impl fmt::Display for Feed { | 112 | impl 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 | } |
@@ -4,6 +4,8 @@ pub mod feed; | |||
4 | pub mod manager; | 4 | pub mod manager; |
5 | pub mod status; | 5 | pub mod status; |
6 | 6 | ||
7 | pub(crate) static DATE_FMT: &str = "%e %b %y"; | ||
8 | |||
7 | pub trait PrintResult { | 9 | pub 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 @@ | |||
1 | use chrono::{naive::Days, prelude::*}; | ||
1 | use clap::{Args, Parser, Subcommand, ValueEnum}; | 2 | use clap::{Args, Parser, Subcommand, ValueEnum}; |
2 | use syn::{manager::Manager, PrintResult}; | 3 | use 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)] |
47 | struct Cli { | 7 | struct Cli { |
@@ -51,20 +11,31 @@ struct Cli { | |||
51 | 11 | ||
52 | #[derive(Subcommand)] | 12 | #[derive(Subcommand)] |
53 | enum Command { | 13 | enum 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 | ||
24 | impl Default for Command { | ||
25 | fn default() -> Self { | ||
26 | Self::ListEntries(ListEntriesCommand { cutoff: None }) | ||
27 | } | ||
28 | } | ||
29 | |||
59 | #[derive(Args)] | 30 | #[derive(Args)] |
60 | struct AddCommand { | 31 | struct AddCommand { |
61 | url: String, | 32 | url: String, |
62 | } | 33 | } |
63 | 34 | ||
64 | #[derive(Args)] | 35 | #[derive(Args)] |
65 | struct ListCommand { | 36 | struct 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")] |
82 | async fn main() { | 53 | async 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 | ||
3 | use crate::error::EntryError; | 3 | use crate::error::EntryError; |
4 | 4 | ||
5 | use ansi_term::Style; | 5 | use ansi_term::{Color, Style}; |
6 | 6 | ||
7 | #[derive(Debug)] | 7 | #[derive(Debug)] |
8 | pub struct PullStatus { | 8 | pub 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 | ||
14 | impl PullStatus { | 14 | impl 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 | ||
24 | impl fmt::Display for PullStatus { | 28 | impl 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() |