use chrono::offset::Utc;
use chrono::DateTime;
use feed_rs::parser;
use hemlib::ProcessedFeed;
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::path::Path;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(
name = "Hemingway",
about = "An economical RSS reader for your terminal."
)]
struct Cli {
#[structopt(subcommand)]
sub_cmd: Option<Cmd>,
}
#[derive(StructOpt, Debug)]
enum Cmd {
Add {
feed_url: String,
},
Top5,
}
#[derive(Debug, Serialize, Deserialize)]
struct Feed {
uri: String,
last_accessed: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ConfigObj {
feeds: Vec<Feed>,
}
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() -> ConfigObj {
let config = fs::read_to_string(&find_config()).expect("reading config failed");
serde_json::from_str(&config).unwrap()
}
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 :(");
}
fn add_feed(feed: &str) {
let mut my_feeds: ConfigObj = config_to_rust();
rust_to_config(serde_json::to_string(&my_feeds).unwrap().as_bytes());
my_feeds.feeds.push(Feed {
uri: feed.to_owned(),
last_accessed: Utc::now().to_rfc3339().to_owned(),
});
}
async fn top5<'a>() -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let config_path = find_config();
let mut processed: Vec<ProcessedFeed> = Vec::new();
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 mut bufreader = BufReader::new(configfile);
let mut contents = String::new();
bufreader.read_to_string(&mut contents)?;
contents
} else {
return Err(Box::from("Catastrophe!"));
}
}
};
let config_obj: ConfigObj = match serde_json::from_str(&config) {
Ok(config_obj) => config_obj,
Err(_) => {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
))
}
};
for i in 0..config_obj.feeds.len() {
let resp = reqwest::get(&config_obj.feeds[i].uri).await?.text().await?;
let feed = parser::parse(resp.as_bytes()).unwrap();
let procfeed = {
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut it = Vec::<String>::new();
for (j, e) in entries {
if j < 5 {
let e_title = e.title.as_ref().unwrap();
it.push(format!("{} 🔗{}", e_title.content.clone(), e.id));
}
}
ProcessedFeed {
title: title_owned,
items: it,
}
};
processed.push(procfeed);
}
rust_to_config(serde_json::to_string(&config_obj).unwrap().as_bytes());
Ok(processed)
}
async fn process_feed<'a>() -> Result<Vec<ProcessedFeed>, Box<dyn std::error::Error>> {
let config_path = find_config();
let mut processed: Vec<ProcessedFeed> = Vec::new();
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 mut bufreader = BufReader::new(configfile);
let mut contents = String::new();
bufreader.read_to_string(&mut contents)?;
contents
} else {
return Err(Box::from("Catastrophe!"));
}
}
};
let mut config_obj: ConfigObj = match serde_json::from_str(&config) {
Ok(config_obj) => config_obj,
Err(_) => {
return Err(Box::from(
"Your feeds list is empty! use `hem add` to add a feed.",
))
}
};
for i in 0..config_obj.feeds.len() {
let resp = reqwest::get(&config_obj.feeds[i].uri).await?.text().await?;
let feed = parser::parse(resp.as_bytes()).unwrap();
let last_accessed = DateTime::from(
DateTime::parse_from_rfc3339(&config_obj.feeds[i].last_accessed).unwrap(),
);
let duration = last_accessed - feed.updated.unwrap();
if duration.num_seconds() > 0 {
println!("{}: Nothing new here...", feed.title.unwrap().content);
config_obj.feeds[i].last_accessed = Utc::now().to_rfc3339().to_owned();
continue;
}
let procfeed = {
let title = feed.title.unwrap();
let title_owned = title.content.to_owned();
let entries = feed.entries.iter().enumerate();
let mut it = Vec::<String>::new();
for (j, e) in entries {
let entry_duration = last_accessed - e.updated.unwrap();
if j < 5 && entry_duration.num_seconds() < 0 {
let e_title = e.title.as_ref().unwrap();
it.push(format!("{} 🔗{}", e_title.content.clone(), e.id));
}
}
ProcessedFeed {
title: title_owned,
items: it,
}
};
processed.push(procfeed);
config_obj.feeds[i].last_accessed = Utc::now().to_rfc3339().to_owned();
}
rust_to_config(serde_json::to_string(&config_obj).unwrap().as_bytes());
Ok(processed)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Cli::from_args();
match args.sub_cmd {
None => {
let processed = process_feed().await?;
for e in processed {
println!("{}", e);
}
None
}
Some(i) => {
match &i {
Cmd::Add { feed_url } => add_feed(feed_url),
Cmd::Top5 => {
let top5_entries = top5().await?;
for e in top5_entries {
println!("{}", e);
}
}
};
Some(i)
}
};
Ok(())
}