use std::fs;
use std::io;
use std::process::{Command, Stdio};
use std::collections::HashMap;
use serde::{Deserialize};
use clap::{Parser, Subcommand, CommandFactory};
use chrono::prelude::*;
use chrono::NaiveDate;
use regex::Regex;
use colored::Colorize;
use inquire::{Select, ui::{RenderConfig, Color, StyleSheet}};
mod biblescrape;
use biblescrape::BibleDownloader;
mod config;
use crate::config::get_storage_path;
mod generate_liturgy;
use generate_liturgy::{LiturgyGenerator, LiturgicalSeason, search_bible, Readings, Reading, Verses, Verse, RangeEnd};
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Cli {
#[arg(short, long, help = "Date of the liturgy to search")]
date: Option<String>,
#[arg(short, long, help = "Translation of the bible to use")]
translation: Option<String>,
#[arg(short, long, help = "Print all readings")]
print_all: bool,
#[arg(short, long, help = "Outputs all JSON (Can specify a date (-d) and translation (-t))")]
json: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Install {
#[arg()]
translation: String,
},
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Some(Commands::Install {translation}) => {
let rt = match tokio::runtime::Runtime::new() {
Ok(content) => content,
Err(e) => {
eprintln!("Async error: {e}");
return;
}
};
match rt.block_on(run_bible_scrape(&translation.to_uppercase())) {
Ok(_) => {},
Err(e) => eprintln!("Error running threads! {e}"),
};
}
None => {run_lectio();}
};
}
async fn run_bible_scrape(translation: &str) -> Result<(), Box<dyn std::error::Error>>{
let mut downloader = BibleDownloader::new();
let mut books = Vec::new();
for (book, _) in &*biblescrape::BOOKS {
books.push(*book);
}
let results = downloader.download_books_concurrent(books, translation).await?;
let mut bible_string = String::new();
for (book, text) in results {
if text != "" {
bible_string.push_str(&book.to_uppercase());
bible_string.push_str(&text);
}
}
let bibles_path = get_storage_path().join("bibles/");
fs::create_dir_all(&bibles_path)?;
let file_path = bibles_path.join(format!("{}.txt", translation));
fs::write(&file_path, bible_string)?;
println!("Saved in {:#?}", bibles_path);
Ok(())
}
fn run_lectio() {
let cli = Cli::parse();
let cli_command = Cli::command();
let executable_name = cli_command.get_name();
let print_all = cli.print_all;
let json = cli.json;
let mut date_chosen = true;
let date = match cli.date {
Some(content) => content,
None => {date_chosen = false; today_date()},
};
let date = match valid_date(&date) {
Ok(content) => content,
Err(e) => {
eprintln!("{date} date error: {e}");
return;
}
};
let translation = cli.translation.unwrap_or("NABRE".to_string());
let year: i32 = date.split("-").next().unwrap().parse().unwrap();
let (liturgy, season) = match generate_liturgy(year) {
Ok(content) => {content},
Err(e) => {
eprintln!("Liturgy couldn't be generated: {e}");
return;
}
};
let season = season.get(date).unwrap();
let bible = match read_bible(&translation) {
Ok(content) => content,
Err(e) => {
eprintln!("{} {}: {e}", "Error reading".red(), format!("{}.txt", translation).yellow());
eprintln!("To install {translation}, run:");
eprintln!("{}", format!("\t{} install {translation}", executable_name).bold());
return;
}
};
if json {
let mut liturgy_json: HashMap<String, Vec<Readings>> = serde_json::from_str(&liturgy).expect("N/A");
if date_chosen {
let mut chosen_liturgy = liturgy_json.get_mut(date).unwrap();
search_bible(&bible, &mut chosen_liturgy);
println!("{:#?}", chosen_liturgy);
} else {
for (date, liturgy) in &mut liturgy_json {
search_bible(&bible, liturgy);
}
println!("{:#?}", liturgy_json);
}
} else {
let mut searched_liturgy = match parse_liturgy(&liturgy, &date) {
Ok(content) => content,
Err(e) => {
eprintln!("Error parsing liturgy: {e}");
return;
}
};
search_bible(&bible, &mut searched_liturgy);
if print_all || searched_liturgy.len() == 1 {
for readings in searched_liturgy {
print_readings(readings, &date, season);
}
} else {
if searched_liturgy.len() > 1 {
let mut reading_options = Vec::new();
for readings in &searched_liturgy {
reading_options.push(readings.title.clone());
}
let mut select_config = RenderConfig::default();
select_config.help_message = StyleSheet::default().with_fg(Color::DarkGreen);
select_config.selected_option = Some(StyleSheet::default().with_fg(Color::DarkGreen));
select_config.answer = StyleSheet::default().with_fg(Color::DarkGreen);
let readings_ans = match Select::new("Which reading?", reading_options.clone())
.with_help_message("Use ↑↓ arrows to navigate")
.with_render_config(select_config)
.prompt() {
Ok(content) => content,
Err(_) => {
println!("{}", "Selection canceled".red());
return;
}
};
let selected_index = reading_options.iter().position(|title| title == &readings_ans).unwrap();
print_readings(searched_liturgy[selected_index].clone(), &date, season);
}
}
}
}
fn generate_liturgy(year: i32) -> Result<(String, HashMap<String, LiturgicalSeason>), Box<dyn std::error::Error>> {
let generator = LiturgyGenerator::new(year)?;
let (liturgy, season) = generator.generate()?;
let liturgy_string = serde_json::to_string(&liturgy).unwrap();
Ok((liturgy_string, season))
}
fn print_readings(readings: Readings, date: &str, season: &LiturgicalSeason) {
println!("{} ({})\n", parse_season_string(&readings.title, season), date);
print_reading(readings.first, "First Reading", season);
print_reading(readings.responsal, "Responsal", season);
print_reading(readings.second, "Second Reading", season);
print_reading(readings.gospel, "Gospel", season);
}
fn valid_date(date: &String) -> Result<&String, String> {
let re = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
if !re.is_match(date) {
Err("Not in correct format: YYYY-MM-DD".to_string())
} else if !NaiveDate::parse_from_str(date, "%Y-%m-%d").is_ok() {
Err("Date does not exist".to_string())
} else {
Ok(date)
}
}
fn today_date() -> String {
let local = Local::now();
return local.format("%Y-%m-%d").to_string();
}
fn print_reading(reading_option: Option<Reading>, title: &str, season: &LiturgicalSeason) {
if let Some(reading) = reading_option {
let formatted_title = &format!("{} {}", title.bold(), format!("({})", reading.raw_reading).italic()).underline();
println!("{}", formatted_title);
for (i, verses) in reading.reading.iter().enumerate() {
if i > 1 {
println!("Optional Verse:");
}
let mut full_verse = String::new();
for verse in &verses.verses {
if let Some(verse_translation) = &verse.translation {
let formatted_verse = match format_verse(verse_translation) {
Ok(content) => content,
Err(_) => verse_translation.clone(),
};
full_verse.push_str(&format!("{} ", &formatted_verse));
} else {
print!("{} {}:{} not found", verses.book, &verse.chapter, &verse.verse);
break;
}
}
let wrapped_text = textwrap::fill(&full_verse, 70);
println!("{}\n", wrapped_text);
}
}
}
fn parse_season_string(string: &str, season: &LiturgicalSeason) -> String {
let formatted_string = match season {
LiturgicalSeason::Ordinary => string.green(),
LiturgicalSeason::Christmas => string.white(),
LiturgicalSeason::Lent => string.purple(),
LiturgicalSeason::Easter => string.yellow(),
LiturgicalSeason::Advent => string.purple(),
};
formatted_string.bold().to_string()
}
fn format_verse(input: &str) -> Result<String, Box<dyn std::error::Error>> {
let result = input;
let re = Regex::new(r"\*([^*]+)\*")?;
let result = re.replace_all(result, |caps: ®ex::Captures| {
caps[1].bold().to_string()
}).to_string();
Ok(result)
}
fn read_bible(translation:&str) -> io::Result<String> {
let bible_path = get_storage_path().join(format!("bibles/{translation}.txt"));
fs::read_to_string(bible_path)
}
fn read_liturgy(year:i32) -> Result<String, std::io::Error> {
let liturgy_path = get_storage_path().join("liturgies").join(format!("liturgy{}.json", year));
fs::read_to_string(liturgy_path)
}
fn parse_liturgy(liturgy_str:&str, date:&str) -> Result<Vec<Readings>, Box<dyn std::error::Error>> {
let liturgy: HashMap<String, Vec<Readings>> = serde_json::from_str(liturgy_str)?;
liturgy.get(date)
.cloned()
.ok_or_else(|| format!("Date '{}' not found in liturgy", date).into())
}