use ansi_term::Style;
use chrono::offset::Utc;
use chrono::DateTime;
use dialoguer::{theme::SimpleTheme, MultiSelect};
use feed_rs::parser;
use futures::stream::StreamExt;
use itertools::Itertools;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ProcessedFeed {
pub title: String,
pub items: Vec<String>,
}
impl std::fmt::Display for ProcessedFeed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"📖 {}\n\t{}",
Style::new().bold().paint(&self.title),
format!("{}", self.items.iter().format("\n\t"))
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Feed {
pub uri: String,
pub last_accessed: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigObj {
pub feeds: Vec<Feed>,
}
pub fn find_config() -> std::path::PathBuf {
let homedir: std::path::PathBuf = dirs::home_dir().expect("no home dir");
Path::new(&homedir).join(".hemrc")
}
fn config_to_rust() -> Result<ConfigObj, Box<dyn std::error::Error>> {
let config_path = find_config();
let config = match fs::read_to_string(&config_path) {
Ok(config) => config,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
eprintln!("Didn't find a .hemrc, creating it now...");
let mut configfile = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&config_path)?;
configfile.write_all(r#"{"feeds": []}"#.as_bytes())?;
let contents = String::from(r#"{"feeds": []}"#);
contents
} else {
return Err(Box::from("Catastrophe!"));
}
}
};
Ok(serde_json::from_str(&config).unwrap())
}
pub fn rust_to_config(content: &[u8]) {
let mut file = match File::create(find_config()) {
Err(why) => panic!("config file access failed: {}", why),
Ok(file) => file,
};
file.write_all(content)
.expect("Writing to .hemrc failed :(");
}
pub fn add_feed(feed: &str) {
let mut my_feeds: ConfigObj = config_to_rust().unwrap();
my_feeds.feeds.push(Feed {
uri: feed.to_owned(),
last_accessed: Utc::now().to_rfc3339().to_owned(),
});
rust_to_config(serde_json::to_string(&my_feeds).unwrap().as_bytes());
}
pub fn list_feeds() {
let config: ConfigObj = config_to_rust().unwrap();
for f in config.feeds {
println!("{}", f.uri);
}
}
pub fn get_uris_and_update() -> Vec<Feed> {
let mut config = config_to_rust().unwrap();
let mut uris: Vec<Feed> = Vec::new();
let len = config.feeds.len();
for i in 0..len {
let x = config.feeds[i].to_owned();
uris.push(x);
config.feeds[i].last_accessed = Utc::now().to_rfc3339().to_owned();
}
rust_to_config(serde_json::to_string(&config).unwrap().as_bytes());
uris
}
pub fn remove() {
let mut config: ConfigObj = config_to_rust().unwrap();
let mut uris: Vec<String> = Vec::new();
let feeds_list = &config.feeds;
for f in feeds_list {
uris.push(f.uri.clone());
}
let multiselected = uris;
let mut selections = MultiSelect::with_theme(&SimpleTheme)
.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")
.items(&multiselected[..])
.interact()
.unwrap();
if selections.is_empty() {
println!("You did not select anything :(");
} else {
println!("Removing these feeds:");
selections.reverse();
for selection in selections {
println!(" {}", multiselected[selection]);
config.feeds.remove(selection);
}
}
rust_to_config(serde_json::to_string(&config).unwrap().as_bytes())
}
pub async fn read_feed_fast(num: usize) -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let client = &Client::builder().build()?;
let config_obj = config_to_rust().unwrap();
if config_obj.feeds.len() == 0 {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
));
};
let processed = RefCell::new(Vec::<ProcessedFeed>::new());
let fetches = futures::stream::iter(config_obj.feeds.into_iter().map(|feed| {
let y = &processed;
async move {
match client.get(&feed.uri).send().await {
Ok(resp) => match resp.text().await {
Ok(text) => {
let feed = parser::parse(text.as_bytes()).unwrap();
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut processed_items = Vec::<String>::new();
for (j, e) in entries {
if j < num {
let e_title = e.title.as_ref().unwrap();
processed_items.push(format!(
"{} \n\t {}\n",
Style::new().italic().paint(e_title.content.clone()),
e.links[0].href
));
} else {
break;
}
}
let feed_to_add = ProcessedFeed {
title: title_owned,
items: processed_items,
};
y.borrow_mut().push(feed_to_add);
}
Err(_) => {
println!("ERROR reading {}", feed.uri);
}
},
Err(_) => {
println!("ERROR reading {}", feed.uri);
}
};
}
}))
.buffer_unordered(20)
.collect::<Vec<()>>();
fetches.await;
let x = processed.borrow();
Ok(x.to_vec())
}
pub async fn read_feed_fast_duration() -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let client = &Client::builder().build()?;
let uris = get_uris_and_update();
if uris.len() == 0 {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
));
}
let processed = RefCell::new(Vec::<ProcessedFeed>::new());
let fetches = futures::stream::iter(uris.into_iter().map(|config_feed| {
let y = &processed;
async move {
match client.get(&config_feed.uri).send().await {
Ok(resp) => match resp.text().await {
Ok(text) => {
let feed = parser::parse(text.as_bytes()).unwrap();
let last_accessed_parsed = DateTime::from(
DateTime::parse_from_rfc3339(&config_feed.last_accessed).unwrap(),
);
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut processed_items = Vec::<String>::new();
let mut entry_date;
for (j, e) in entries {
if e.updated.is_none() {
entry_date = e.published.unwrap();
} else {
entry_date = e.updated.unwrap();
}
let entry_duration = last_accessed_parsed - entry_date; if j < 5 && entry_duration.num_seconds() < 0 {
let e_title = e.title.as_ref().unwrap();
processed_items.push(format!(
"{} \n\t {}\n",
Style::new().italic().paint(e_title.content.clone()),
e.links[0].href
));
} else {
break;
}
}
if processed_items.len() == 0 {
processed_items = vec![String::from("Nothing new here...")];
}
let feed_to_add = ProcessedFeed {
title: title_owned,
items: processed_items,
};
y.borrow_mut().push(feed_to_add);
}
Err(_) => {
println!("ERROR reading {}", config_feed.uri);
}
},
Err(_) => {
println!("ERROR reading {}", config_feed.uri);
}
};
}
}))
.buffer_unordered(20)
.collect::<Vec<()>>();
fetches.await;
let x = processed.borrow();
Ok(x.to_vec())
}