diff --git a/.idea/material_theme_project_new.xml b/.idea/material_theme_project_new.xml index 1d05485..e3d7ccb 100644 --- a/.idea/material_theme_project_new.xml +++ b/.idea/material_theme_project_new.xml @@ -3,7 +3,9 @@ diff --git a/Cargo.lock b/Cargo.lock index 892d616..b105b2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,6 +108,7 @@ dependencies = [ "colored", "reqwest", "serde", + "serde_json", "termion", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 0c362dd..980ab24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } clap = { version = "4.4", features = ["cargo"] } colored = "3.0.0" -termion = "4.0.5" \ No newline at end of file +termion = "4.0.5" +serde_json = "1.0.140" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a89e068..716057f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,24 @@ - use clap::Command; use colored::*; use serde::{Deserialize, Serialize}; -use std::error::Error; -use std::io::{stdout, Write}; -use std::sync::{Arc, Mutex}; +use std::{ + collections::VecDeque, + error::Error, + fs::{self, File}, + io::{stdout, Read, Write}, + path::PathBuf, + sync::{Arc, Mutex}, +}; use termion::{clear, cursor, event::Key, input::TermRead, raw::IntoRawMode, terminal_size}; use tokio::time::{sleep, Duration}; -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct BibleVerse { translation: Translation, random_verse: RandomVerse, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct Translation { identifier: String, name: String, @@ -23,7 +27,7 @@ struct Translation { license: String, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct RandomVerse { book_id: String, book: String, @@ -31,6 +35,7 @@ struct RandomVerse { verse: i32, text: String, } + // Fetch a random verse from the API async fn fetch_random_verse() -> Result> { let url = "https://bible-api.com/data/web/random"; @@ -152,7 +157,13 @@ fn display_verse(verse: &BibleVerse, term_width: u16, term_height: u16) { print!("{}", cursor::Goto(start_x as u16 + 2, (start_y + 7 + wrapped_text.len()) as u16)); print!("{}", "[N]ext Verse".truecolor(98, 76, 171).bold()); - print!("{}", cursor::Goto(start_x as u16 + box_width as u16 - 14, (start_y + 7 + wrapped_text.len()) as u16)); + print!("{}", cursor::Goto(start_x as u16 + box_width as u16 / 2 - 4, (start_y + 7 + wrapped_text.len()) as u16)); + print!("{}", "[B]ack".truecolor(98, 76, 171).bold()); + + print!("{}", cursor::Goto(start_x as u16 + box_width as u16 - 20, (start_y + 7 + wrapped_text.len()) as u16)); + print!("{}", "[S]ave/[O]pen".truecolor(98, 76, 171).bold()); + + print!("{}", cursor::Goto(start_x as u16 + box_width as u16 - 8, (start_y + 7 + wrapped_text.len()) as u16)); print!("{}", "[Q]uit".truecolor(98, 76, 171).bold()); stdout().flush().unwrap(); @@ -202,60 +213,142 @@ async fn main() -> Result<(), Box> { display_loading_animation(loading_clone, term_width, term_height).await; }); + let mut verse_history: VecDeque = VecDeque::new(); let mut current_verse = fetch_random_verse().await?; + verse_history.push_back(current_verse.clone()); + let history_index = Arc::new(Mutex::new(0)); // Track current position in history *loading.lock().unwrap() = false; animation_task.await?; loop { let (term_width, term_height) = terminal_size()?; - display_verse(¤t_verse, term_width, term_height); + let current_index = *history_index.lock().unwrap(); + if let Some(verse) = verse_history.get(current_index) { + display_verse(verse, term_width, term_height); + } let mut key_pressed = false; while !key_pressed { if let Some(Ok(key)) = stdin.next() { match key { Key::Char('q') | Key::Char('Q') | Key::Esc => { - // Proper cleanup write!(stdout, "{}{}", clear::All, cursor::Show)?; stdout.flush()?; return Ok(()); - }, + } Key::Char('n') | Key::Char('N') | Key::Char(' ') => { key_pressed = true; + let mut index = history_index.lock().unwrap(); + if *index < verse_history.len() - 1 { + *index += 1; + } else { + let loading = Arc::new(Mutex::new(true)); + let loading_clone = loading.clone(); - let loading = Arc::new(Mutex::new(true)); - let loading_clone = loading.clone(); + write!(stdout, "{}", clear::All)?; + stdout.flush()?; - write!(stdout, "{}", clear::All)?; - stdout.flush()?; + let animation_task = tokio::spawn(async move { + display_loading_animation(loading_clone, term_width, term_height).await; + }); - let animation_task = tokio::spawn(async move { - display_loading_animation(loading_clone, term_width, term_height).await; - }); - - match fetch_random_verse().await { - Ok(verse) => { - *loading.lock().unwrap() = false; - animation_task.await?; - current_verse = verse; - }, - Err(e) => { - *loading.lock().unwrap() = false; - animation_task.await?; - - write!(stdout, "{}{}", clear::All, cursor::Show)?; - stdout.flush()?; - eprintln!("{} {}", "Error fetching verse:".bold().red(), e); - return Err(e); + match fetch_random_verse().await { + Ok(verse) => { + *loading.lock().unwrap() = false; + animation_task.await?; + current_verse = verse.clone(); + verse_history.push_back(current_verse.clone()); + *index += 1; + } + Err(e) => { + *loading.lock().unwrap() = false; + animation_task.await?; + write!(stdout, "{}{}", clear::All, cursor::Show)?; + stdout.flush()?; + eprintln!("{} {}", "Error fetching verse:".bold().red(), e); + return Err(e); + } } } - }, + } + Key::Char('b') | Key::Char('B') => { + let mut index = history_index.lock().unwrap(); + if *index > 0 { + *index -= 1; + key_pressed = true; + } + } + Key::Char('s') | Key::Char('S') => { + let current_index = *history_index.lock().unwrap(); + if let Some(verse_to_save) = verse_history.get(current_index).cloned() { + match save_verse_to_json("saved_verses.json", &verse_to_save).await { + Ok(_) => { + // Optionally display a success message + eprintln!("{}", "Verse saved to saved_verses.json".green().bold()); + sleep(Duration::from_secs(2)).await; + } + Err(e) => { + eprintln!("{} {}", "Error saving verse:".bold().red(), e); + sleep(Duration::from_secs(2)).await; + } + } + } + } + Key::Char('o') | Key::Char('O') => { + match open_verses_from_json("saved_verses.json").await { + Ok(loaded_verses) => { + if !loaded_verses.is_empty() { + verse_history.clear(); + verse_history.extend(loaded_verses.into_iter()); + *history_index.lock().unwrap() = verse_history.len() - 1; // Show the last loaded verse + key_pressed = true; + eprintln!("{}", "Loaded verses from saved_verses.json".green().bold()); + sleep(Duration::from_secs(2)).await; + } else { + eprintln!("{}", "No verses found in saved_verses.json".yellow().bold()); + sleep(Duration::from_secs(2)).await; + } + } + Err(e) => { + eprintln!("{} {}", "Error opening verses:".bold().red(), e); + sleep(Duration::from_secs(2)).await; + } + } + } _ => {} } } - - tokio::time::sleep(Duration::from_millis(50)).await; } + + tokio::time::sleep(Duration::from_millis(50)).await; } } + +async fn save_verse_to_json(filename: &str, verse: &BibleVerse) -> Result<(), Box> { + let path = PathBuf::from(filename); + let mut saved_verses = Vec::new(); + if path.exists() { + let file = File::open(filename)?; + let reader = std::io::BufReader::new(file); + if let Ok(verses) = serde_json::from_reader(reader) { + saved_verses = verses; + } + } + saved_verses.push(verse.clone()); + let file = File::create(filename)?; + serde_json::to_writer_pretty(file, &saved_verses)?; + Ok(()) +} + +async fn open_verses_from_json(filename: &str) -> Result, Box> { + let path = PathBuf::from(filename); + if path.exists() { + let file = File::open(filename)?; + let reader = std::io::BufReader::new(file); + let verses = serde_json::from_reader(reader)?; + Ok(verses) + } else { + Ok(Vec::new()) + } +} \ No newline at end of file