Compare commits

...

28 Commits

Author SHA1 Message Date
d9941b38b5 closes #11 2025-06-01 14:07:55 -06:00
7442a14a8f closes #10 2025-05-29 08:28:12 -06:00
Oliver Atkinson
165d946e0d close #3 2025-05-16 11:54:30 -06:00
Oliver Atkinson
d9150c302f code cleanup 2025-05-16 11:54:20 -06:00
Oliver Atkinson
46e4030f46 starting on #3 2025-05-16 11:25:44 -06:00
Oliver Atkinson
9c756f3550 fixes #4 2025-05-16 10:54:47 -06:00
Oliver Atkinson
035bd21466 closes #7 2025-05-16 09:43:49 -06:00
Oliver Atkinson
ba260b02bc Fix #2 2025-05-16 09:22:39 -06:00
Oliver Atkinson
141da7c17c moved todos into issues 2025-05-16 09:22:30 -06:00
Oliver Atkinson
e487bb9a69 Merge branch 'master' of https://git.oliveratkinson.net/Oliver/bible 2025-05-16 07:47:29 -06:00
2e6eb194f8 updates from sg 2025-05-16 07:19:11 -06:00
82de9a07cc my version of the bible lol 2025-04-11 17:40:46 -06:00
Oliver Atkinson
8a7804e780 formatting 2025-04-08 13:04:34 -06:00
Oliver Atkinson
acb49d7a4d Tabbing now works, impelemented preliminary error message handling 2025-04-08 12:13:19 -06:00
Oliver Atkinson
727b2f0b51 working on tab... checkpoint 2025-04-02 15:42:55 -06:00
789c593f7e grey out search results once it's visted 2025-04-02 01:16:43 -06:00
6d24b304ce add regex support 2025-04-02 01:05:50 -06:00
11eb29df4f remove unused import 2025-04-02 00:50:46 -06:00
94e9b38552 Merge pull request 'merge GUI' (#1) from gui into master
Reviewed-on: #1
2025-04-02 06:37:37 +00:00
5f7ab37935 restore cli capabilites 2025-04-02 00:35:56 -06:00
46987e11ad Added delete row button, styling improvements 2025-04-02 00:30:55 -06:00
8d8bba1ccc generally improved 2025-03-29 11:43:47 -06:00
8a10b62755 Works, scroll isn't as nice as I'd like 2025-03-27 02:33:57 -06:00
82c662f00f I'm cooking 2025-03-27 01:36:03 -06:00
e64aee2b31 checkpoint 2025-03-27 00:54:58 -06:00
3e7509a9c7 improved search and focus 2025-03-26 23:35:07 -06:00
Oliver Atkinson
2874617e4c gui is working 2025-03-26 15:06:06 -06:00
Oliver Atkinson
09acb5af50 working 2025-03-26 13:46:51 -06:00
11 changed files with 5019 additions and 308 deletions

2
.gitmodules vendored
View File

@@ -3,4 +3,4 @@
url = https://github.com/openscriptures/HebrewLexicon.git
[submodule "Holy-Bible-XML-Format"]
path = Holy-Bible-XML-Format
url = https://github.com/Beblia/Holy-Bible-XML-Format.git
url = https://github.com/Rushmore75/Holy-Bible-XML-Format.git

45
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'bible'",
"cargo": {
"args": [
"build",
"--bin=bible",
"--package=bible"
],
"filter": {
"name": "bible",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'bible'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=bible",
"--package=bible"
],
"filter": {
"name": "bible",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

4041
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
iced = "0.13.1"
inline_colorization = "0.1.6"
quick-xml = { version="0.37.2", features=["serialize"] }
regex = "1.11.1"
serde = { version = "1.0.219", features = ["derive"] }

View File

@@ -1,10 +1,13 @@
# cli bible
# Bible
## Important Links
[Tanach.us](https://tanach.us)
[XML bible in 190+ languages](https://github.com/Beblia/Holy-Bible-XML-Format)
[XML Bibles in 190+ languages](https://github.com/Beblia/Holy-Bible-XML-Format)
[HewbrewLexicon](https://github.com/openscriptures/HebrewLexicon/tree/master)
## Could be useful later links
[Tanach.us](https://tanach.us)
[AndBible's formats](https://github.com/AndBible/and-bible/wiki/Supported-3rd-party-module-formats)

View File

@@ -1,293 +0,0 @@
use inline_colorization::*;
use std::{fmt::Display, fs};
use quick_xml::de::from_str;
use serde::Deserialize;
const BOOKS_IN_ORDER: [&str; 66] = [
"Genesis",
"Exodus",
"Leviticus",
"Numbers",
"Deuteronomy",
"Joshua",
"Judges",
"Ruth",
"1 Samuel",
"2 Samuel",
"1 Kings",
"2 Kings",
"1 Chronicles",
"2 Chronicles",
"Ezra",
"Nehemiah",
"Esther",
"Job",
"Psalms",
"Proverbs",
"Ecclesiastes",
"Song of Solomon",
"Isaiah",
"Jeremiah",
"Lamentations",
"Ezekiel",
"Daniel",
"hosea",
"Joel",
"Amos",
"Obadiah",
"Jonah",
"Micah",
"Nahum",
"Habakkuk",
"Zephaniah",
"Haggai",
"Zechariah",
"Malachi",
"Matthew",
"Mark",
"Luke",
"John",
"Acts",
"Romans",
"1 Corinthians",
"2 Corinthians",
"Galations",
"Ephesians",
"Philippians",
"Colossians",
"1 Thessalonians",
"2 Thessalonians",
"1 Timothy",
"2 Timothy",
"Titus",
"Philemon",
"Hebrews",
"James",
"1 Peter",
"2 Peter",
"1 John",
"2 John",
"3 John",
"Jude",
"Revelation",
];
// This would eventaully be a list built by the user
const BIBLES: [&str; 2] = [
"/usr/local/bible/EnglishNASBBible.xml",
"/usr/local/bible/EnglishNIVBible.xml",
];
pub fn get(query: &str, loc: &str) {
// expecting query to be in format:
// Gen 1:2
// Gen 1:2-5
// Gen 1
// The book name needs to be just long enough to be unique
let mut splits = loc.split(':');
let chapter = splits.next();
let verse = splits.next();
// ###########################################
// Figure out what book they are talking about
let res = BOOKS_IN_ORDER
.iter()
.enumerate()
.filter(|(_, book)| book.to_lowercase().contains(&query.to_lowercase()))
.collect::<Vec<(usize, &&str)>>();
let (book_idx, book_name) = match res.len() {
1 => res[0],
2.. => {
eprintln!("Err: Ambigious input '{query}', could be any of:");
for (_, i) in &res {
eprintln!("\t{i}");
}
return;
}
_ => {
eprintln!("'{query}' is not a book");
return;
}
};
// Load Bibles into memory
let bibles = BIBLES
.iter()
.filter_map(|path| fs::read_to_string(path).ok())
.filter_map(|contents| from_str::<Bible>(&contents).ok())
.collect::<Vec<Bible>>();
// Select the book in each Bible
let books = bibles
.iter()
// Book are 1 indexed in the xml spec
.map(|bible| (bible, bible.get_book_by_index(book_idx + 1)))
.filter(|(_,e)| e.is_some())
.map(|(bible,book)| (bible,book.unwrap()))
.collect::<Vec<(&Bible,&Book)>>();
// Select the chapter in each Bible
let chapters = if let Some(chapter) = chapter {
if let Ok(ch_num) = chapter.parse::<usize>() {
books
.iter()
.map(|(bible,book)| (bible, book, book.get_chapter_by_index(ch_num)))
.filter(|(_,_,book)| book.is_some())
.map(|(bible,book,chapter)| (*bible,*book,chapter.unwrap()))
.collect::<Vec<(&Bible, &Book, &Chapter)>>()
} else {
return;
}
} else {
return;
};
// Get the verse in each Bible
match verse {
Some(verse) => {
let mut splits = verse.split("-");
let start = splits.next();
let end = splits.next();
match (start, end) {
// range of verses
(Some(sn), Some(en)) => {
if let (Ok(start), Ok(end)) = (sn.parse::<usize>(), en.parse::<usize>()) {
for num in start..=end {
for (bible, _book, chapter) in &chapters {
if let Some(verse) = chapter.get_verse_by_index(num) {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}:{}{style_reset}: {verse}",
bible.translation_name, chapter.number, verse.number
);
}
}
}
}
}
// only one verse
(Some(ver_num), None) => {
if let Ok(num) = ver_num.parse::<usize>() {
for (bible, _book, chapter) in chapters {
if let Some(verse) = chapter.get_verse_by_index(num) {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}:{}{style_reset}: {verse}",
bible.translation_name, chapter.number, verse.number
);
}
}
}
}
_ => {
// couldn't parse verse
return;
}
}
}
None => {
// only chapter
for (bible, _book, chapter) in chapters {
println!(
"{style_bold}[{}] {style_underline}{book_name} {}{style_reset}:",
bible.translation_name, chapter.number
);
print!("{}", chapter);
}
}
}
}
impl Bible {
fn get_book_by_index(&self, idx: usize) -> Option<&Book> {
for i in &self.testaments {
for book in &i.books {
if book.number == idx {
return Some(&book);
}
}
}
None
}
}
impl Book {
fn get_chapter_by_index(&self, idx: usize) -> Option<&Chapter> {
for chap in &self.chapters {
if chap.number == idx {
return Some(&chap);
}
}
None
}
}
impl Chapter {
fn get_verse_by_index(&self, idx: usize) -> Option<&Verse> {
for verse in &self.verses {
if verse.number == idx {
return Some(&verse);
}
}
None
}
}
#[derive(Deserialize)]
struct Bible {
#[serde(rename = "@translation")]
translation_name: String,
#[serde(rename = "testament")]
testaments: Vec<Testament>,
}
#[derive(Deserialize)]
struct Testament {
#[serde(rename = "@name")]
// yes, this would make a nice enum
_testament: String,
#[serde(rename = "book")]
books: Vec<Book>,
}
#[derive(Deserialize)]
struct Book {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "chapter")]
chapters: Vec<Chapter>,
}
#[derive(Deserialize)]
struct Chapter {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "verse")]
verses: Vec<Verse>,
}
impl Display for Chapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for v in &self.verses {
writeln!(
f,
"{style_bold}{style_underline}{}{style_reset}: {}",
v.number, v
)?;
}
Ok(())
}
}
#[derive(Deserialize)]
struct Verse {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "$text")]
text: String,
}
impl Display for Verse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}

View File

@@ -1,19 +1,600 @@
use std::env;
#![feature(iter_map_windows)]
use iced::keyboard::key::Named;
use iced::keyboard::{Key, Modifiers};
use iced::{event, Event, Subscription};
use iced::widget::{scrollable, text_editor};
use iced::{
Element, Length, Task, Theme, clipboard, color,
widget::{
button, column, combo_box, row,
scrollable::{Direction, Scrollbar},
text, text_input,
},
};
mod strong;
mod bible;
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::{env, fs, path::PathBuf};
use texts::*;
fn main() {
mod texts;
pub const BIBLE_DIRECTORY: &str = "./Holy-Bible-XML-Format";
fn main() -> iced::Result {
// CLI
let arg = env::args().collect::<Vec<String>>();
if let Some(query) = arg.get(1) {
if query.starts_with("H") | query.starts_with("h") {
if let Some(found) = strong::get(query) {
print!("{found}");
return;
return Ok(());
}
}
if let Some(second_query) = arg.get(2) {
bible::get(query,second_query);
}
if let Some(second_query) = arg.get(2) {
if let Ok(contents) = fs::read_to_string(&format!("{BIBLE_DIRECTORY}/EnglishNASBBible.xml")) {
if let Ok(bible) = quick_xml::de::from_str::<bible::Bible>(&contents) {
if let Ok(f) = bible::get(
query,
second_query,
&bible,
) {
println!("{}", f);
}
}
}
return Ok(());
}
}
// GUI
iced::application("Bible Study", State::update, State::view)
.subscription(State::subscription)
.theme(State::theme)
.run()
}
#[derive(Debug, Clone)]
enum Message {
CycleTheme,
BibleSearchInput(usize, String),
BibleSelected(usize, String),
BibleSearchSubmit(usize),
CopyText(usize),
Clear(usize),
WordSearchInput(usize, String),
SubmitWordSearch(usize),
QuickSearch(usize, String, usize),
AddColRight,
DeleteColumn(usize),
SetErrorMessage(usize, String),
RecievedEvent(Event),
NoteInput(usize, text_editor::Action),
Save,
}
impl Message {
fn get_col_idx(&self) -> Option<usize> {
match self {
Message::SetErrorMessage(i, _) |
Message::BibleSearchInput(i, _) |
Message::BibleSelected(i, _) |
Message::BibleSearchSubmit(i) |
Message::CopyText(i) |
Message::Clear(i) |
Message::WordSearchInput(i, _) |
Message::SubmitWordSearch(i) |
Message::QuickSearch(i, _, _) |
Message::DeleteColumn(i) |
Message::NoteInput(i, _)
=> {
Some(*i)
},
_ => None
}
}
}
struct ColState {
file: Option<String>,
bible_selected: Option<bible::Bible>,
bible_search: String,
scripture_body: Option<String>,
word_search: String,
word_search_results: Option<Vec<(String, bool)>>,
notes: text_editor::Content,
error: Option<String>,
}
impl Default for ColState {
fn default() -> Self {
Self {
file: None,
bible_selected: None,
bible_search: String::new(),
scripture_body: None,
word_search: String::new(),
word_search_results: None,
notes: text_editor::Content::new(),
error: None,
}
}
}
struct State {
files: combo_box::State<String>,
states: Vec<ColState>,
// cols: usize,
tab_index: Option<i32>,
theme_idx: usize,
themes: [Theme; 3],
}
impl Default for State {
fn default() -> Self {
let files = if let Ok(contents) = fs::read_dir(BIBLE_DIRECTORY) {
let paths = contents
.into_iter()
.filter_map(|f| f.ok())
.map(|f| (f.path(), f.file_type()))
.filter(|(_, f)| f.is_ok())
.map(|(a, b)| (a, b.unwrap()))
.filter(|(_, f)| f.is_file())
.map(|(f, _)| f)
.filter(|f| f.extension().is_some())
.filter(|f| f.extension().unwrap() == "xml")
.collect::<Vec<PathBuf>>();
paths
} else {
todo!()
};
let files = files
.iter()
.filter_map(|f| f.to_str())
.map(|f| f.to_owned())
.collect::<Vec<String>>();
Self {
files: combo_box::State::new(files),
states: vec![ColState::default()],
// cols: 1,
tab_index: None,
// THEMES
// Dark - KanagawaDragon
// Default - Nord
// Light - TokyoNightLight
theme_idx: 0,
themes: [
Theme::KanagawaDragon,
Theme::Nord,
Theme::TokyoNightLight
]
}
}
}
impl State {
fn theme(&self) -> Theme {
self.themes[self.theme_idx].clone()
}
fn subscription(&self) -> Subscription<Message> {
event::listen().map(Message::RecievedEvent)
}
fn update(&mut self, msg: Message) -> Task<Message> {
// clear error messages
if let Some(idx) = msg.get_col_idx() {
self.states[idx].error = None
}
// match normal messages
match msg {
Message::Save => {
let opt = OpenOptions::new()
// create if doesn't already exist
.create(true)
.append(false)
// clear existing file
.truncate(true)
.write(true)
.open("save.md")
;
match opt {
Ok(file) => {
let mut buffer = BufWriter::new(file);
// the toml heading at the top of md files
let _ = buffer.write_all("---\n---\n".as_bytes());
for col in &self.states {
// separate each col with hr
let _ = buffer.write_all("\n---\n".as_bytes());
if let Some(scripture) = &col.scripture_body {
// code block for scripture
let _ = buffer.write_all("```\n".as_bytes());
let _ = buffer.write_all(parse(scripture).as_bytes());
let _ = buffer.write_all("```\n".as_bytes());
}
let _ = buffer.write_all(&col.notes.text().as_bytes());
}
let _ = buffer.flush();
},
Err(err) => {
eprintln!("{err}");
}
}
},
Message::NoteInput(i, action) => {
let notes = &mut self.states[i].notes;
notes.perform(action);
},
Message::CycleTheme => {
let len = self.themes.len();
let i = &mut self.theme_idx;
if *i == len-1 {
*i = 0;
} else {
*i += 1;
}
},
Message::RecievedEvent(event) => {
if let Event::Keyboard(kbd) = event {
if let iced::keyboard::Event::KeyReleased {key, modifiers, ..} = kbd {
if let Key::Named(n) = key {
if let Named::Tab = n {
if let Some(idx) = self.tab_index {
let new_idx = if modifiers == Modifiers::SHIFT { idx-1 } else { idx+1 };
// 2 because two buttons on each col
let new_idx = new_idx.clamp(0, (self.states.len()*2) as i32 -1);
self.tab_index = Some(new_idx);
let id = format!("tabId-{}", new_idx);
return text_input::focus(id);
} else {
if self.states.len() > 0 {
self.tab_index = Some(0);
return text_input::focus("tabId-0");
}
}
}
}
}
}
}
Message::SetErrorMessage(i, m) => {
self.states[i].error = Some(m);
}
Message::DeleteColumn(idx) => {
// self.cols -= 1;
self.states.remove(idx);
}
Message::AddColRight => {
self.states.push(ColState::default());
// Add the rightmost opened Bible to the new column
if let Some(last) = self.states.get(self.states.len()-2) {
let file = &last.file;
if let Some(file) = file {
return self.update(Message::BibleSelected(self.states.len()-1, file.to_string()));
}
};
}
Message::BibleSelected(i, file) => {
match fs::read_to_string(&file) {
Ok(contents) => {
match quick_xml::de::from_str::<bible::Bible>(&contents) {
Ok(bible) => {
// State is held technically in this variable, although
// the Bible is held in memory in it's own variable.
self.states[i].bible_selected = Some(bible);
self.states[i].file = Some(file);
}
Err(err) => {
eprintln!("{}", err);
return self.update(Message::SetErrorMessage(i, err.to_string()));
},
}
},
Err(err) => {
eprintln!("{}", err);
return self.update(Message::SetErrorMessage(i, err.to_string()));
}
}
}
Message::QuickSearch(i, s, result_index) => {
self.states[i].bible_search = s;
if let Some(wsr) = &mut self.states[i].word_search_results {
// set "visited" for this link
wsr[result_index].1 = true;
}
return self.update(Message::BibleSearchSubmit(i));
}
Message::BibleSearchInput(i, query) => self.states[i].bible_search = query,
Message::WordSearchInput(i, query) => self.states[i].word_search = query,
Message::BibleSearchSubmit(i) => {
let mut splits = self.states[i].bible_search.split_whitespace();
let book = splits.next();
let location = splits.next();
if let (Some(book), Some(chap_and_ver), Some(bible)) =
(book, location, &self.states[i].bible_selected)
{
self.states[i].scripture_body = match bible::get(book, chap_and_ver, bible) {
Ok(s) => {
Some(s)
},
Err(s) => {
return self.update(Message::SetErrorMessage(i, s));
},
};
} else {
if let None = book {
return self.update(Message::SetErrorMessage(i, format!("Error selecting book {:?}", book)));
}
if let None = location {
return self.update(Message::SetErrorMessage(i, format!("Error getting chapter and/or verse {:?}", location)));
}
if let None = &self.states[i].bible_selected {
return self.update(Message::SetErrorMessage(i, format!("Error getting selected bible")));
}
}
}
Message::SubmitWordSearch(i) => {
if let Some(bible) = &self.states[i].bible_selected {
let res = bible::search_for_word(&self.states[i].word_search, bible);
self.states[i].word_search_results = Some(
res
.into_iter()
.map(|f| (f, false))
.collect()
);
}
}
Message::CopyText(i) => {
if let Some(text) = &self.states[i].scripture_body {
return clipboard::write::<Message>(parse(text));
}
}
Message::Clear(i) => {
self.states[i].scripture_body = None;
self.states[i].word_search_results = None;
self.states[i].bible_search = String::new();
self.states[i].word_search = String::new();
}
};
Task::none()
}
fn view(&self) -> Element<Message> {
// TODO make this dynamic
// max height that the scripture body can be
// max height that the search resutls can be
let max_height = 600;
// Header bar, static, doesn't scroll
column![
row![
button("Cycle Themes")
.on_press(Message::CycleTheme)
.style(button::secondary)
,
button("Save")
.on_press(Message::Save)
.style(button::secondary)
,
],
scrollable(
row![
row((0..self.states.len()).map(|col_index| {
column![
// Top of Column, text entry boxes
combo_box(
&self.files,
"Select Bible",
// Some(&"IDK where this text is".to_string()),
match &self.states[col_index].bible_selected {
Some(b) => Some(&b.translation_name),
None => None,
},
move |s| Message::BibleSelected(col_index, s)
),
text_input("Search query, ie: Gen 1:1", &self.states[col_index].bible_search)
.id(format!("tabId-{}", (col_index*2)+0))
.on_input(move |s| Message::BibleSearchInput(col_index, s))
.on_submit(Message::BibleSearchSubmit(col_index)),
text_input("Word Search", &self.states[col_index].word_search)
.id(format!("tabId-{}", (col_index*2)+1))
.on_input(move |s| Message::WordSearchInput(col_index, s))
.on_submit(Message::SubmitWordSearch(col_index)),
// Horizontal Row just above scripture
row![
button("Clear All")
.on_press_with(move || Message::Clear(col_index))
.style(button::secondary),
button("Copy Scripture")
.on_press_with(move || Message::CopyText(col_index))
.style(button::primary),
button("Delete Column")
.on_press_with(move || Message::DeleteColumn(col_index))
.style(button::danger),
text(format!("{}", self.states[col_index].error.clone().unwrap_or(String::new())))
.style(text::danger),
]
.spacing(5),
// Body
row![
// Word search results
scrollable(
column(
self.states[col_index]
.word_search_results
.clone()
.unwrap_or(Vec::new())
.into_iter()
.enumerate()
.map(|(idx,i)| {
let style = if i.1 {
button::secondary
} else {
button::primary
};
button(text(i.0.clone()))
.style(style)
.on_press_with(move || Message::QuickSearch(
col_index,
i.0.to_owned(),
idx
))
.into()
})
)
.padding([5, 5])
)
.height(max_height)
.spacing(5),
// Scripture
scrollable(
if let Some(body) = &self.states[col_index].scripture_body {
column(body.split("\n").enumerate().map(|(i, msg)| {
// Color every other verse slightly differently
let msg = parse(msg);
if i & 1 == 0 {
text(msg.to_owned()).color(color![0xFFFFFF]).into()
} else {
text(msg.to_owned()).color(color![0xAAAAAA]).into()
}
}))
} else {
column(
Vec::<String>::new()
.iter()
.map(|_| text(String::new()).into()),
)
}
)
.height(max_height)
.spacing(5)
],
// Notes
text_editor(&self.states[col_index].notes)
.placeholder("Notes...")
.on_action(move |a| Message::NoteInput(col_index, a))
.height(100)
// .id(format!("tabId-{}", (col_index*2)+2))
]
.spacing(5)
.width(800.)
.into()
})),
// Right most vertical buttons
button(text("+").center())
.height(Length::Fixed(200.))
.style(button::primary)
.on_press(Message::AddColRight)
]
// 5 pixles css-like padding
.padding([5, 5])
// space elements inside this 5 pixels apart
.spacing(5),
)
.direction(Direction::Horizontal(Scrollbar::new()))
]
.into()
}
}
// parse out terminal style codes
fn parse(input: &str) -> String {
let magic_start_indices = input
.bytes()
.into_iter()
.enumerate()
.map_windows(|[(ai, a), (_, b)]| {
// 27 91 is the start sequence for a color/style
if *a == 27 && *b == 91 {
Some(*ai)
} else {
None
}
})
.filter_map(|f| f)
.collect::<Vec<usize>>();
let mut to_remove = Vec::new();
for start in magic_start_indices {
let range = &input[start..];
// "m" is the end of sequence
if let Some(end) = range.find("m") {
let end = start + end;
let code = &input[start..=end];
match code {
"\x1B[1m" => {} //style_bold
"\x1B[4m" => {} //style_underline
"\x1B[0m" => {} //style_reset
"\x1B[30m" => {} //color_black
"\x1B[31m" => {} //color_red
"\x1B[32m" => {} //color_green
"\x1B[33m" => {} //color_yellow
"\x1B[34m" => {} //color_blue
"\x1B[35m" => {} //color_magenta
"\x1B[36m" => {} //color_cyan
"\x1B[37m" => {} //color_white
"\x1B[90m" => {} //color_bright_black
"\x1B[91m" => {} //color_bright_red
"\x1B[92m" => {} //color_bright_green
"\x1B[93m" => {} //color_bright_yellow
"\x1B[94m" => {} //color_bright_blue
"\x1B[95m" => {} //color_bright_magenta
"\x1B[96m" => {} //color_bright_cyan
"\x1B[97m" => {} //color_bright_white
"\x1B[39m" => {} //color_reset
"\x1B[40m" => {} //bg_black
"\x1B[41m" => {} //bg_red
"\x1B[42m" => {} //bg_green
"\x1B[43m" => {} //bg_yellow
"\x1B[44m" => {} //bg_blue
"\x1B[45m" => {} //bg_magenta
"\x1B[46m" => {} //bg_cyan
"\x1B[47m" => {} //bg_white
"\x1B[100m" => {} //bg_bright_black
"\x1B[101m" => {} //bg_bright_red
"\x1B[102m" => {} //bg_bright_green
"\x1B[103m" => {} //bg_bright_yellow
"\x1B[104m" => {} //bg_bright_blue
"\x1B[105m" => {} //bg_bright_magenta
"\x1B[106m" => {} //bg_bright_cyan
"\x1B[107m" => {} //bg_bright_white
"\x1B[49m" => {} //bg_reset
_ => {}
}
to_remove.push((start, end));
}
}
to_remove.reverse();
let mut modified = input.to_owned();
for (s, e) in to_remove {
let front = modified[..s].to_string();
let back = modified[e + 1..].to_string();
modified = front + &back;
}
modified
}

332
src/texts/bible.rs Normal file
View File

@@ -0,0 +1,332 @@
use inline_colorization::*;
use regex::Regex;
use std::fmt::Display;
use serde::Deserialize;
pub const BOOKS_IN_ORDER: [&str; 66] = [
"Genesis",
"Exodus",
"Leviticus",
"Numbers",
"Deuteronomy",
"Joshua",
"Judges",
"Ruth",
"1-Samuel",
"2-Samuel",
"1-Kings",
"2-Kings",
"1-Chronicles",
"2-Chronicles",
"Ezra",
"Nehemiah",
"Esther",
"Job",
"Psalms",
"Proverbs",
"Ecclesiastes",
"Song-of-Solomon",
"Isaiah",
"Jeremiah",
"Lamentations",
"Ezekiel",
"Daniel",
"hosea",
"Joel",
"Amos",
"Obadiah",
"Jonah",
"Micah",
"Nahum",
"Habakkuk",
"Zephaniah",
"Haggai",
"Zechariah",
"Malachi",
"Matthew",
"Mark",
"Luke",
"John",
"Acts",
"Romans",
"1-Corinthians",
"2-Corinthians",
"Galations",
"Ephesians",
"Philippians",
"Colossians",
"1-Thessalonians",
"2-Thessalonians",
"1-Timothy",
"2-Timothy",
"Titus",
"Philemon",
"Hebrews",
"James",
"1-Peter",
"2-Peter",
"1-John",
"2-John",
"3-John",
"Jude",
"Revelation",
];
pub fn search_for_word(word: &str, from_files: &Bible) -> Vec<String> {
let mut found = Vec::new();
let regex = match Regex::new(&word.to_lowercase()) {
Ok(re) => re,
Err(err) => {
eprintln!("{}", err);
#[cfg(debug_assertions)]
todo!("IDK what to do yet if building regex engine fails");
#[cfg(not(debug_assertions))]
return Vec::new()
},
};
for test in &from_files.testaments {
for book in &test.books {
for chapter in &book.chapters {
for verse in &chapter.verses {
if regex.is_match(&verse.text.to_lowercase()) {
found.push(format!("{} {}:{}", BOOKS_IN_ORDER[book.number-1], chapter.number, verse.number));
}
}
}
}
}
found
}
fn parse_book(book: &str) -> Result<(usize, &str), String> {
// Go thru the list of books and see which
// match the passed book str
let res = BOOKS_IN_ORDER
.iter()
.enumerate()
.filter(|(_, lbook)| lbook
.to_lowercase()
.starts_with(&book
.trim()
.replace(" ", "-")
.to_lowercase()
)
)
.collect::<Vec<(usize, &&str)>>();
let (book_idx, book_name) = match res.len() {
// there is one option for what book it is
1 => res[0],
// there are >1 options for what book it is
2.. => {
eprintln!("Err: Ambigious input '{book}', could be any of:");
for (_, i) in &res {
eprintln!("\t{i}");
}
return Err(format!("Ambigious input '{book}'"));
}
_ => {
eprintln!("'{book}' is not a book");
return Err(format!("'{book}' is not a book"));
}
};
Ok((book_idx, book_name))
}
fn parse_chapter<'a>(chapter_num: &str, book: &'a Book) -> Result<&'a Chapter, String> {
let chapter = if let Ok(idx) = chapter_num.parse::<usize>() {
if let Some(chapter) = book.get_chapter_by_index(idx) {
chapter
} else {
return Err(format!("Chapter {idx} doesn't exist in book {}", BOOKS_IN_ORDER[book.number-1]));
}
} else {
return Err(format!("Chapter number could not be parsed from {chapter_num}"));
};
Ok(chapter)
}
pub fn get(book: &str, chap_and_ver: &str, bible: &Bible) -> Result<String, String> {
// expecting query to be in format:
// Gen 1:2
// Gen 1:2-5
// Gen 1
// The book name needs to be just long enough to be unique
let mut splits = chap_and_ver.split(':');
let chapter_num = splits.next();
let verse_num = splits.next();
// ###########################################
// Figure out what book they are talking about
let (book_idx, book_name) = parse_book(book)?;
let book = match bible.get_book_by_index(book_idx+1) {
Some(s) => s,
None => return Err(format!("Selected Bible {} doesn't contain book {book_name}", bible.translation_name)),
};
// Figure out what chapter they are talking about
let chapter = if let Some(ch) = chapter_num {
parse_chapter(ch, book)?
} else {
return Err("No chapter number was passed".to_string());
};
// Get the verse in each Bible
match verse_num {
Some(verse) => {
let mut splits = verse.split("-");
let start = splits.next();
let end = splits.next();
match (start, end) {
// range of verses
(Some(sn), Some(en)) => {
if let (Ok(start), Ok(end)) = (sn.parse::<usize>(), en.parse::<usize>()) {
let mut buf = String::new();
let translation = &bible.translation_name;
buf += &format!("{style_bold}[{translation}] {style_underline}{book_name} {}:{start}-{end}{style_reset}:\n", chapter.number);
for num in start..=end {
if let Some(verse) = chapter.get_verse_by_index(num) {
buf += &format!(
"{}: {verse}\n", verse.number
);
}
}
return Ok(buf);
}
return Err(format!("Could not parse '{sn}' or '{en}' into numbers"));
}
// only one verse
(Some(ver_num), None) => {
if let Ok(num) = ver_num.parse::<usize>() {
let mut buf = String::new();
if let Some(verse) = chapter.get_verse_by_index(num) {
buf += &format!(
"{style_bold}[{}] {style_underline}{book_name} {}:{}{style_reset}:\n{verse}\n",
bible.translation_name, chapter.number, verse.number
);
}
return Ok(buf);
}
return Err(format!("Could not parse '{ver_num}' into a number"));
}
_ => {
// couldn't parse verse
return Err(format!("Couldn't parse '{:?}' or '{:?}' into verse numbers", start, end));
}
}
}
None => {
// only chapter
let mut buf = String::new();
buf += &format!(
"{style_bold}[{}] {style_underline}{book_name} {}{style_reset}:\n",
bible.translation_name, chapter.number
);
buf += &format!("{}", chapter);
return Ok(buf);
}
}
}
impl Bible {
fn get_book_by_index(&self, idx: usize) -> Option<&Book> {
for i in &self.testaments {
for book in &i.books {
if book.number == idx {
return Some(&book);
}
}
}
None
}
}
impl Book {
fn get_chapter_by_index(&self, idx: usize) -> Option<&Chapter> {
for chap in &self.chapters {
if chap.number == idx {
return Some(&chap);
}
}
None
}
}
impl Chapter {
fn get_verse_by_index(&self, idx: usize) -> Option<&Verse> {
for verse in &self.verses {
if verse.number == idx {
return Some(&verse);
}
}
None
}
}
#[derive(Deserialize)]
pub struct Bible {
#[serde(rename = "@translation")]
pub translation_name: String,
#[serde(rename = "testament")]
testaments: Vec<Testament>,
}
#[derive(Deserialize)]
struct Testament {
#[serde(rename = "@name")]
// yes, this would make a nice enum
_testament: String,
#[serde(rename = "book")]
books: Vec<Book>,
}
#[derive(Deserialize)]
struct Book {
#[serde(rename = "@number")]
/// Genesis is book 1
number: usize,
#[serde(rename = "chapter")]
chapters: Vec<Chapter>,
}
#[derive(Deserialize)]
struct Chapter {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "verse")]
verses: Vec<Verse>,
}
impl Display for Chapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for v in &self.verses {
writeln!(
f,
"{style_bold}{style_underline}{}{style_reset}: {}",
v.number, v
)?;
}
Ok(())
}
}
#[derive(Deserialize)]
struct Verse {
#[serde(rename = "@number")]
number: usize,
#[serde(rename = "$text")]
text: String,
}
impl Display for Verse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}

2
src/texts/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod bible;
pub mod strong;