This commit is contained in:
ZockerKatze
2025-04-30 10:50:17 +02:00
parent 60979f2d9c
commit 3609d5674b
3 changed files with 256 additions and 30 deletions

View File

@@ -1,7 +1,19 @@
use clap::Command;
use colored::*;
use colored::{Color, Colorize};
use serde::{Deserialize, Serialize};
use std::error::Error;
use std::io::{stdout, Write};
use std::sync::{Arc, Mutex};
use termion::{clear, cursor, event::Key, input::TermRead, raw::IntoRawMode, terminal_size};
use tokio::time::{sleep, Duration};
const BORDER_COLOR: Color = Color::TrueColor { r: 117, g: 142, b: 205 }; // #758ECD
const REF_COLOR: Color = Color::TrueColor { r: 113, g: 137, b: 255 }; // #7189FF
const TEXT_COLOR: Color = Color::TrueColor { r: 193, g: 206, b: 254 }; // #C1CEFE
const ATTR_COLOR: Color = Color::TrueColor { r: 160, g: 221, b: 255 }; // #A0DDFF
const NEXT_BTN_COLOR: Color = Color::TrueColor { r: 98, g: 76, b: 171 }; // #624CAB
const QUIT_BTN_COLOR: Color = Color::TrueColor { r: 160, g: 221, b: 255 }; // #A0DDFF
#[derive(Debug, Serialize, Deserialize)]
struct BibleVerse {
@@ -33,24 +45,149 @@ async fn fetch_random_verse() -> Result<BibleVerse, Box<dyn Error>> {
Ok(verse)
}
fn display_verse(verse: &BibleVerse) {
println!("{}", "".color("cyan").repeat(60));
fn word_wrap(text: &str, max_width: usize) -> Vec<String> {
let mut result = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.len() + word.len() + 1 > max_width {
result.push(current_line);
current_line = word.to_string();
} else {
if !current_line.is_empty() {
current_line.push(' ');
}
current_line.push_str(word);
}
}
if !current_line.is_empty() {
result.push(current_line);
}
result
}
fn display_verse(verse: &BibleVerse, term_width: u16, term_height: u16) {
let max_width = std::cmp::min(80, term_width as usize - 10);
let box_width = max_width + 8;
// Create a reference string like "Book chapter:verse"
let reference = format!("{} {}:{}",
verse.random_verse.book,
verse.random_verse.chapter,
verse.random_verse.verse);
verse.random_verse.book,
verse.random_verse.chapter,
verse.random_verse.verse);
println!("{}", reference.bold().color("yellow"));
println!();
println!("{}", verse.random_verse.text.trim());
println!();
println!("{} ({}) [{}]",
verse.translation.name.italic().color("green"),
verse.translation.identifier.italic(),
verse.translation.language);
println!("{}", "".color("cyan").repeat(60));
let wrapped_text = word_wrap(&verse.random_verse.text, max_width - 4);
let attribution = format!("{} ({})", verse.translation.name, verse.translation.identifier);
let box_height = wrapped_text.len() + 7;
let start_y = (term_height as usize / 2).saturating_sub(box_height / 2);
let start_x = (term_width as usize / 2).saturating_sub(box_width / 2);
print!("{}{}", clear::All, cursor::Goto(1, 1));
print!("{}", cursor::Goto(start_x as u16, start_y as u16));
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
"".repeat(box_width - 2).color(BORDER_COLOR).bold(),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16, (start_y + 1) as u16));
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
" ".repeat(box_width - 2),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16, (start_y + 2) as u16));
let centered_ref = format!("{:^width$}", reference, width = box_width - 2);
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
centered_ref.color(REF_COLOR).bold(),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16, (start_y + 3) as u16));
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
" ".repeat(box_width - 2),
"".color(BORDER_COLOR).bold()
);
for (i, line) in wrapped_text.iter().enumerate() {
print!("{}", cursor::Goto(start_x as u16, (start_y + 4 + i) as u16));
let padding = (box_width - 2 - line.len()) / 2;
let left_padding = " ".repeat(padding);
let right_padding = " ".repeat(box_width - 2 - padding - line.len());
println!(
"{}{}{}{}",
"".color(BORDER_COLOR).bold(),
left_padding,
line.color(TEXT_COLOR),
format!("{}{}", right_padding, "").color(BORDER_COLOR).bold()
);
}
print!("{}", cursor::Goto(start_x as u16, (start_y + 4 + wrapped_text.len()) as u16));
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
" ".repeat(box_width - 2),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16, (start_y + 5 + wrapped_text.len()) as u16));
let centered_attr = format!("{:^width$}", attribution, width = box_width - 2);
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
centered_attr.color(ATTR_COLOR).italic(),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16, (start_y + 6 + wrapped_text.len()) as u16));
println!(
"{}{}{}",
"".color(BORDER_COLOR).bold(),
"".repeat(box_width - 2).color(BORDER_COLOR).bold(),
"".color(BORDER_COLOR).bold()
);
print!("{}", cursor::Goto(start_x as u16 + 4, (start_y + 8 + wrapped_text.len()) as u16));
print!("{}", "[N]ext Verse".color(NEXT_BTN_COLOR).bold());
print!("{}", cursor::Goto(start_x as u16 + box_width as u16 - 15, (start_y + 8 + wrapped_text.len()) as u16));
print!("{}", "[Q]uit".color(QUIT_BTN_COLOR).bold());
print!("{}", cursor::Goto(start_x as u16 + (box_width / 2) as u16 - 15, (start_y + 10 + wrapped_text.len()) as u16));
print!("{}", "Press N for next verse or Q to quit".color(REF_COLOR));
stdout().flush().unwrap();
}
async fn display_loading_animation(loading: Arc<Mutex<bool>>, term_width: u16, term_height: u16) {
let spinner_frames = ["", "", "", "", "", "", "", "", "", ""];
let mut frame_index = 0;
let message = "Loading Bible verse";
let message_len = message.len() + 2;
while *loading.lock().unwrap() {
let spinner = spinner_frames[frame_index].color(NEXT_BTN_COLOR).bold();
let x_pos = (term_width as usize / 2).saturating_sub(message_len / 2);
let y_pos = term_height as usize / 2;
print!("{}", cursor::Goto(x_pos as u16, y_pos as u16));
print!("{} {}", spinner, message.color(REF_COLOR).bold());
stdout().flush().unwrap();
frame_index = (frame_index + 1) % spinner_frames.len();
sleep(Duration::from_millis(80)).await;
}
}
#[tokio::main]
@@ -58,20 +195,72 @@ async fn main() -> Result<(), Box<dyn Error>> {
let _matches = Command::new("Bible Verse")
.version("1.0")
.author("BibleVerse")
.about("Fetches random Bible verses")
.about("A beautiful Bible verse display")
.get_matches();
println!("Fetching a random Bible verse...");
let mut stdout = stdout().into_raw_mode()?;
write!(stdout, "{}", clear::All)?;
stdout.flush()?;
match fetch_random_verse().await {
Ok(verse) => {
display_verse(&verse);
}
Err(e) => {
eprintln!("{} {}", "Error:".bold().color("red"), e);
eprintln!("Try checking your internet connection or the API endpoint.");
let mut stdin = termion::async_stdin().keys();
let (term_width, term_height) = terminal_size()?;
let loading = Arc::new(Mutex::new(true));
let loading_clone = loading.clone();
let animation_task = tokio::spawn(async move {
display_loading_animation(loading_clone, term_width, term_height).await;
});
let mut current_verse = fetch_random_verse().await?;
*loading.lock().unwrap() = false;
animation_task.await?;
loop {
let (term_width, term_height) = terminal_size()?;
display_verse(&current_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 => {
write!(stdout, "{}{}", clear::All, cursor::Show)?;
stdout.flush()?;
return Ok(());
}
Key::Char('n') | Key::Char('N') | Key::Char(' ') => {
key_pressed = true;
let loading = Arc::new(Mutex::new(true));
let loading_clone = loading.clone();
write!(stdout, "{}", clear::All)?;
stdout.flush()?;
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().color(QUIT_BTN_COLOR), e);
return Err(e);
}
}
}
_ => {}
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
Ok(())
}
}