hemlib/
lib.rs

1use ansi_term::Style;
2use chrono::offset::Utc;
3use chrono::DateTime;
4use dialoguer::{theme::SimpleTheme, MultiSelect};
5use feed_rs::parser;
6use futures::stream::StreamExt;
7use itertools::Itertools;
8use reqwest::Client;
9use serde::{Deserialize, Serialize};
10use std::cell::RefCell;
11use std::fs;
12use std::fs::File;
13use std::io::Write;
14use std::path::Path;
15
16#[derive(Debug, Clone)]
17pub struct ProcessedFeed {
18    pub title: String,
19    pub items: Vec<String>,
20}
21
22impl std::fmt::Display for ProcessedFeed {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(
25            f,
26            "📖 {}\n\t{}",
27            Style::new().bold().paint(&self.title),
28            format!("{}", self.items.iter().format("\n\t"))
29        )
30    }
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone)]
34pub struct Feed {
35    pub uri: String,
36    pub last_accessed: String,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub struct ConfigObj {
41    pub feeds: Vec<Feed>,
42}
43
44pub fn find_config() -> std::path::PathBuf {
45    let homedir: std::path::PathBuf = dirs::home_dir().expect("no home dir");
46    //let path_to_config: &Path =
47    Path::new(&homedir).join(".hemrc")
48}
49
50fn config_to_rust() -> Result<ConfigObj, Box<dyn std::error::Error>> {
51    let config_path = find_config();
52    let config = match fs::read_to_string(&config_path) {
53        Ok(config) => config,
54        Err(e) => {
55            if e.kind() == std::io::ErrorKind::NotFound {
56                eprintln!("Didn't find a .hemrc, creating it now...");
57                // create the file and populate it with an empty array
58                let mut configfile = fs::OpenOptions::new()
59                    .read(true)
60                    .write(true)
61                    .create_new(true)
62                    .open(&config_path)?;
63                configfile.write_all(r#"{"feeds": []}"#.as_bytes())?;
64                let contents = String::from(r#"{"feeds": []}"#);
65                contents
66            } else {
67                return Err(Box::from("Catastrophe!"));
68            }
69        }
70    };
71    Ok(serde_json::from_str(&config).unwrap())
72}
73
74pub fn rust_to_config(content: &[u8]) {
75    let mut file = match File::create(find_config()) {
76        Err(why) => panic!("config file access failed: {}", why),
77        Ok(file) => file,
78    };
79    file.write_all(content)
80        .expect("Writing to .hemrc failed :(");
81}
82
83pub fn add_feed(feed: &str) {
84    let mut my_feeds: ConfigObj = config_to_rust().unwrap();
85    my_feeds.feeds.push(Feed {
86        uri: feed.to_owned(),
87        last_accessed: Utc::now().to_rfc3339().to_owned(),
88    });
89    rust_to_config(serde_json::to_string(&my_feeds).unwrap().as_bytes());
90}
91
92pub fn list_feeds() {
93    let config: ConfigObj = config_to_rust().unwrap();
94    // let mut uris: Vec<String> = Vec::new();
95    for f in config.feeds {
96        println!("{}", f.uri);
97    }
98}
99
100pub fn get_uris_and_update() -> Vec<Feed> {
101    let mut config = config_to_rust().unwrap();
102    let mut uris: Vec<Feed> = Vec::new();
103    let len = config.feeds.len();
104    for i in 0..len {
105        let x = config.feeds[i].to_owned();
106        uris.push(x);
107        config.feeds[i].last_accessed = Utc::now().to_rfc3339().to_owned();
108    }
109    rust_to_config(serde_json::to_string(&config).unwrap().as_bytes());
110    uris
111}
112
113pub fn remove() {
114    let mut config: ConfigObj = config_to_rust().unwrap();
115    let mut uris: Vec<String> = Vec::new();
116    let feeds_list = &config.feeds;
117    for f in feeds_list {
118        uris.push(f.uri.clone());
119    }
120    let multiselected = uris;
121    let mut selections = MultiSelect::with_theme(&SimpleTheme)
122        .with_prompt("Use arrow keys to move up or down. Press the space bar to select a feed. Press enter when you're done to remove all selected feeds")
123        .items(&multiselected[..])
124        .interact()
125        .unwrap();
126
127    // println!("{:?}", selections);
128    if selections.is_empty() {
129        println!("You did not select anything :(");
130    } else {
131        println!("Removing these feeds:");
132        selections.reverse();
133        for selection in selections {
134            println!("  {}", multiselected[selection]);
135            config.feeds.remove(selection);
136        }
137    }
138    rust_to_config(serde_json::to_string(&config).unwrap().as_bytes())
139}
140
141pub async fn read_feed_fast(num: usize) -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
142    let client = &Client::builder().build()?;
143
144    let config_obj = config_to_rust().unwrap();
145    if config_obj.feeds.len() == 0 {
146        return Err(Box::from(
147            "Your feeds list is empty! use `hem add` to add a feed.",
148        ));
149    };
150    let processed = RefCell::new(Vec::<ProcessedFeed>::new());
151    let fetches = futures::stream::iter(config_obj.feeds.into_iter().map(|feed| {
152        let y = &processed;
153        async move {
154            match client.get(&feed.uri).send().await {
155                Ok(resp) => match resp.text().await {
156                    Ok(text) => {
157                        let feed = parser::parse(text.as_bytes()).unwrap();
158                        let title = feed.title.unwrap();
159                        let title_owned = title.content.to_owned();
160
161                        let entries = feed.entries.iter().enumerate();
162                        let mut processed_items = Vec::<String>::new();
163                        for (j, e) in entries {
164                            if j < num {
165                                let e_title = e.title.as_ref().unwrap();
166                                processed_items.push(format!(
167                                    "{} \n\t  {}\n",
168                                    Style::new().italic().paint(e_title.content.clone()),
169                                    e.links[0].href
170                                ));
171                            } else {
172                                break;
173                            }
174                        }
175                        let feed_to_add = ProcessedFeed {
176                            title: title_owned,
177                            items: processed_items,
178                        };
179                        y.borrow_mut().push(feed_to_add);
180                    }
181                    Err(_) => {
182                        println!("ERROR reading {}", feed.uri);
183                    }
184                },
185                Err(_) => {
186                    println!("ERROR reading {}", feed.uri);
187                }
188            };
189        }
190    }))
191    .buffer_unordered(20)
192    .collect::<Vec<()>>();
193
194    fetches.await;
195    let x = processed.borrow();
196    Ok(x.to_vec())
197}
198
199pub async fn read_feed_fast_duration() -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
200    let client = &Client::builder().build()?;
201
202    // let config_obj = config_to_rust().unwrap();
203    // if config_obj.feeds.len() == 0 {
204    //     return Err(Box::from(
205    //         "Your feeds list is empty! use `hem add` to add a feed.",
206    //     ));
207    // };
208    let uris = get_uris_and_update();
209    if uris.len() == 0 {
210        return Err(Box::from(
211            "Your feeds list is empty! use `hem add` to add a feed.",
212        ));
213    }
214    let processed = RefCell::new(Vec::<ProcessedFeed>::new());
215    let fetches = futures::stream::iter(uris.into_iter().map(|config_feed| {
216        let y = &processed;
217        async move {
218            match client.get(&config_feed.uri).send().await {
219                Ok(resp) => match resp.text().await {
220                    Ok(text) => {
221                        let feed = parser::parse(text.as_bytes()).unwrap();
222                        let last_accessed_parsed = DateTime::from(
223                            DateTime::parse_from_rfc3339(&config_feed.last_accessed).unwrap(),
224                        );
225                        let title = feed.title.unwrap();
226                        let title_owned = title.content.to_owned();
227
228                        let entries = feed.entries.iter().enumerate();
229                        let mut processed_items = Vec::<String>::new();
230                        let mut entry_date;
231                        for (j, e) in entries {
232                            if e.updated.is_none() {
233                                entry_date = e.published.unwrap();
234                            } else {
235                                entry_date = e.updated.unwrap();
236                            }
237                            let entry_duration = last_accessed_parsed - entry_date; //e.updated.unwrap();
238                            if j < 5 && entry_duration.num_seconds() < 0 {
239                                let e_title = e.title.as_ref().unwrap();
240                                processed_items.push(format!(
241                                    "{} \n\t  {}\n",
242                                    Style::new().italic().paint(e_title.content.clone()),
243                                    e.links[0].href
244                                ));
245                            } else {
246                                break;
247                            }
248                        }
249                        if processed_items.len() == 0 {
250                            processed_items = vec![String::from("Nothing new here...")];
251                        }
252                        let feed_to_add = ProcessedFeed {
253                            title: title_owned,
254                            items: processed_items,
255                        };
256                        y.borrow_mut().push(feed_to_add);
257                    }
258                    Err(_) => {
259                        println!("ERROR reading {}", config_feed.uri);
260                    }
261                },
262                Err(_) => {
263                    println!("ERROR reading {}", config_feed.uri);
264                }
265            };
266
267            // config_feed.last_accessed = Utc::now().to_rfc3339().to_owned();
268        }
269    }))
270    .buffer_unordered(20)
271    .collect::<Vec<()>>();
272    fetches.await;
273    let x = processed.borrow();
274    // rust_to_config(serde_json::to_string(&config_obj).unwrap().as_bytes());
275    Ok(x.to_vec())
276}