Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

Python3 Bible App in Tkinter

Bible Search App KJV data.txt

"""
Bible Verse Viewer with Navigation

This module provides a graphical user interface (GUI) for navigating and viewing
verses from various books and chapters of the Bible. The application uses the
`tkinter` library for its interface and supports features such as:
- Navigating to previous and next chapters.
- Highlighting specific verses.
- Displaying a popup window to view verses from a selected chapter.

The verse data for each book and chapter is defined in the `books` dictionary.

Dependencies:
- tkinter
- re (for potential regex operations)

Usage:
Run this script directly to launch the Bible Verse Viewer application.

Author: James Fraze (guiding AI)
Version: 1.0
Date: 2023-08-27
"""
import tkinter as tk
from tkinter import ttk, messagebox
import re

books = {
    "All": [0, 0, "All Books"],
    "Ge": [1, 50, "Genesis"],
    "Ex": [2, 40, "Exodus"],
    "Le": [3, 27, "Leviticus"],
    "Nu": [4, 36, "Numbers"],
    "De": [5, 34, "Deuteronomy"],
    "Jos": [6, 24, "Joshua"],
    "Jg": [7, 21, "Judges"],
    "Ru": [8, 4, "Ruth"],
    "1Sa": [9, 31, "1 Samuel"],
    "2Sa": [10, 24, "2 Samuel"],
    "1Ki": [11, 22, "1 Kings"],
    "2Ki": [12, 25, "2 Kings"],
    "1Ch": [13, 29, "1 Chronicles"],
    "2Ch": [14, 36, "2 Chronicles"],
    "Ezr": [15, 10, "Ezra"],
    "Ne": [16, 13, "Nehemiah"],
    "Es": [17, 10, "Esther"],
    "Job": [18, 42, "Job"],
    "Ps": [19, 150, "Psalms"],
    "Pr": [20, 31, "Proverbs"],
    "Ec": [21, 12, "Ecclesiastes"],
    "Song": [22, 8, "Song of Solomon"],
    "Isa": [23, 66, "Isaiah"],
    "Jer": [24, 52, "Jeremiah"],
    "La": [25, 5, "Lamentations"],
    "Eze": [26, 48, "Ezekiel"],
    "Da": [27, 12, "Daniel"],
    "Ho": [28, 14, "Hosea"],
    "Joe": [29, 3, "Joel"],
    "Am": [30, 9, "Amos"],
    "Ob": [31, 1, "Obadiah"],
    "Jon": [32, 4, "Jonah"],
    "Mic": [33, 7, "Micah"],
    "Na": [34, 3, "Nahum"],
    "Hab": [35, 3, "Habakkuk"],
    "Zep": [36, 3, "Zephaniah"],
    "Hag": [37, 2, "Haggai"],
    "Zec": [38, 14, "Zechariah"],
    "Mal": [39, 4, "Malachi"],
    "Mt": [40, 28, "Matthew"],
    "Mr": [41, 16, "Mark"],
    "Lu": [42, 24, "Luke"],
    "Joh": [43, 21, "John"],
    "Ac": [44, 28, "Acts"],
    "Ro": [45, 16, "Romans"],
    "1Co": [46, 16, "1 Corinthians"],
    "2Co": [47, 13, "2 Corinthians"],
    "Ga": [48, 6, "Galatians"],
    "Eph": [49, 6, "Ephesians"],
    "Php": [50, 4, "Philippians"],
    "Col": [51, 4, "Colossians"],
    "1Th": [52, 5, "1 Thessalonians"],
    "2Th": [53, 3, "2 Thessalonians"],
    "1Ti": [54, 6, "1 Timothy"],
    "2Ti": [55, 4, "2 Timothy"],
    "Tit": [56, 3, "Titus"],
    "Phm": [57, 1, "Philemon"],
    "Heb": [58, 13, "Hebrews"],
    "Jas": [59, 5, "James"],
    "1Pe": [60, 5, "1 Peter"],
    "2Pe": [61, 3, "2 Peter"],
    "1Jo": [62, 5, "1 John"],
    "2Jo": [63, 1, "2 John"],
    "3Jo": [64, 1, "3 John"],
    "Jude": [65, 1, "Jude"],
    "Re": [66, 22, "Revelation"],
}

# python -m vulture tk.biblesearch.1.0.py
# python -m site --user-base
# C:\Users\User\AppData\Roaming\Python\Python310\Scripts\pylint tk.biblesearch.1.0.py
# C:\Users\User\AppData\Roaming\Python\Python310\Scripts\deadcode tk.biblesearch.1.0.py


class BibleSearchApp(tk.Tk):
    """
    A tkinter application to search through Bible verses.

    This app provides an interface for the user to search through Bible verses
    based on keywords, book, and chapter. It allows the user to filter verses,
    search for specific terms, and navigate to specific books and chapters.

    Attributes:
    - data (list[str]): Loaded Bible verses.
    - selected_book_var (tk.StringVar): Holds the currently selected book's name.
    - selected_chapter_var (tk.StringVar): Holds the currently selected chapter.
    - search_var (tk.StringVar): Holds the text the user wishes to search for.
    - ... (additional GUI elements)
    """

    def __init__(self):
        """
        Initialize the main application window and its components.
        """
        super().__init__()

        # Setting up the window
        title_font = ("Arial", 18)
        self.title("Bible Search")
        self.option_add("*Font", title_font)

        self.geometry("1024x600")
        self.style = ttk.Style()
        self.style.theme_use("clam")

        # Set global font for all ttk widgets
        font_tuple = ("Arial", 14)

        self.style.configure("TButton", font=font_tuple)
        self.style.configure("TLabel", font=font_tuple)
        self.style.configure("TEntry", font=font_tuple)
        self.style.configure("TCombobox", font=font_tuple)
        self.style.configure("Treeview", font=font_tuple)
        self.style.configure("Treeview.Heading", font=font_tuple)

        # Variables
        self.selected_book_var = tk.StringVar(self)
        self.selected_chapter_var = tk.StringVar(self)
        self.search_var = tk.StringVar(self)
        self.chapter_popup = None

        # Load data
        self.data = self.load_data("data.txt")

        # Search
        self.search_entry = ttk.Entry(self, textvariable=self.search_var, width=50)
        self.search_entry.grid(column=0, row=0, padx=10, pady=10, sticky="ew")

        # Dropdowns
        book_names = [book_data[2] for book_data in books.values()]
        self.book_dropdown = ttk.Combobox(
            self, textvariable=self.selected_book_var, values=book_names
        )
        self.book_dropdown.bind("<<ComboboxSelected>>", self.update_chapters)
        self.book_dropdown.grid(column=1, row=0, padx=10, pady=10, sticky="ew")

        self.chapter_dropdown = ttk.Combobox(
            self, textvariable=self.selected_chapter_var, width=5
        )
        self.chapter_dropdown.grid(column=2, row=0, padx=10, pady=10, sticky="ew")

        # Buttons
        self.search_button = ttk.Button(
            self, text="Search", command=self.perform_search
        )
        self.search_button.grid(column=3, row=0, padx=10, pady=10)
        self.go_button = ttk.Button(self, text="Go", command=self.go_to_chapter)
        self.go_button.grid(column=4, row=0, padx=10, pady=10)

        # Results tree
        self.tree = ttk.Treeview(
            self, columns=("Book", "Chapter", "VerseNum", "Verse"), show="headings"
        )

        max_book_length = max(len(book_data[2]) for book_data in books.values())
        id_width = 0
        book_width = min(150, max_book_length * 12)
        chapter_width = 50
        verse_num_width = 50
        initial_frame_width = 1024
        remaining_width = (
            initial_frame_width
            - id_width
            - book_width
            - chapter_width
            - verse_num_width
            - 20
        )

        self.tree.column("Book", width=book_width)
        self.tree.column("Chapter", width=chapter_width)
        self.tree.column("VerseNum", width=verse_num_width)
        self.tree.column("Verse", width=remaining_width)

        self.tree.heading("Book", text="Book")
        self.tree.heading("Chapter", text="Chp")
        self.tree.heading("VerseNum", text="V#")
        self.tree.heading("Verse", text="Verse")

        self.tree.grid(column=0, row=1, columnspan=5, padx=10, pady=10, sticky="nsew")

        # Create an invisible image to adjust row height in treeview
        self.transparent_image = tk.PhotoImage(width=1, height=25)

        self.grid_columnconfigure(0, weight=2)
        self.grid_rowconfigure(1, weight=1)

        # Default book and chapter to "ALL"
        self.selected_book_var.set("All Books")
        self.update_chapters(None)
        self.selected_chapter_var.set("ALL")

        # Set focus to search entry
        self.search_entry.focus_set()

        # Bind Enter key to search
        self.bind("<Return>", lambda event: self.perform_search())
        self.tree.bind("<Double-1>", self.on_tree_item_double_click)

    def on_tree_item_double_click(self, event):
        """
        Handle double click event on a tree item to show the ChapterPopup.

        Event Params:
        - event: The event that triggered the method.
        """

        # Get the selected item
        item = self.tree.selection()[0]

        # Retrieve book, chapter, and verse values
        book = self.tree.item(item, "values")[0]
        chapter = self.tree.item(item, "values")[1]
        verse_text = self.tree.item(item, "values")[2]

        # Extract the verse number from the verse_text
        verse_num = None
        match = re.match(r"^(\d+)", verse_text)
        if match:
            verse_num = int(match.group(1))

        if book and chapter:
            verses = self.get_verses_for_chapter(book, chapter)
            self.chapter_popup = ChapterPopup(
                self, book, chapter, verses, highlight_verse=verse_num
            )

    def load_data(self, filename):
        """
        Load Bible data from a file.

        Parameters:
        - filename (str): Path to the file containing Bible data.

        Returns:
        - list[str]: List of lines (verses) loaded from the file.
        """
        #with open(filename, "r", encoding="utf-8") as f:
        #    return f.readlines()
        with open(filename, "r", encoding="windows-1252") as f:
            return f.readlines()

    def update_chapters(self, event):
        """
        Update the chapters dropdown based on the selected book.

        Event Params:
        - event: The event that triggered the method.
        """
        book_full_name = self.selected_book_var.get()
        if book_full_name == "All Books":
            self.chapter_dropdown["values"] = ["ALL"]
            return
        abbrev = next(
            abbrev
            for abbrev, book_data in books.items()
            if book_data[2] == book_full_name
        )
        max_chapter = books[abbrev][1]
        self.chapter_dropdown["values"] = ["ALL"] + list(range(1, max_chapter + 1))

    def perform_search(self):
        """
        Search the Bible data based on user's input and display the results.
        """

        # Clear existing items
        for item in self.tree.get_children():
            self.tree.delete(item)

        search_str = self.search_var.get().lower()
        selected_book_full = self.selected_book_var.get()
        selected_chapter = self.selected_chapter_var.get()

        # Filter data based on selected book and chapter
        if selected_book_full != "All Books":
            abbrev = next(
                abbrev
                for abbrev, book_data in books.items()
                if book_data[2] == selected_book_full
            )
            filtered_data = [line for line in self.data if line.startswith(abbrev)]
            if selected_chapter != "ALL":
                filtered_data = [
                    line
                    for line in filtered_data
                    if line.split()[1].startswith(selected_chapter + ":")
                ]
        else:
            filtered_data = self.data

        # Using regex to search for whole word matches
        pattern = r"(?:\W|^)" + re.escape(search_str) + r"(?:\W|$)"
        for line in filtered_data:
            if re.search(pattern, line, re.IGNORECASE):  # Using re.IGNORECASE to make it case insensitive
                parts = re.match(r"(\w+) (\d+):(\d+) (.+)", line)
                if parts:
                    book_abbrev, chapter, verse_num, verse_text = parts.groups()
                    book_full = books[book_abbrev][2]
                    self.tree.insert(
                        "",
                        "end",
                        values=(book_full, chapter, verse_num, verse_text),
                        image=self.transparent_image,
                    )

    def perform_search2(self):
        """
        Search the Bible data based on user's input and display the results.
        """

        # Clear existing items
        for item in self.tree.get_children():
            self.tree.delete(item)

        search_str = self.search_var.get().lower()
        selected_book_full = self.selected_book_var.get()
        selected_chapter = self.selected_chapter_var.get()

        # Filter data based on selected book and chapter
        if selected_book_full != "All Books":
            abbrev = next(
                abbrev
                for abbrev, book_data in books.items()
                if book_data[2] == selected_book_full
            )
            filtered_data = [line for line in self.data if line.startswith(abbrev)]
            if selected_chapter != "ALL":
                filtered_data = [
                    line
                    for line in filtered_data
                    if line.split()[1].startswith(selected_chapter + ":")
                ]
        else:
            filtered_data = self.data

        for line in filtered_data:
            if search_str in line.lower():
                parts = re.match(r"(\w+) (\d+):(\d+) (.+)", line)
                if parts:
                    book_abbrev, chapter, verse_num, verse_text = parts.groups()
                    book_full = books[book_abbrev][2]
                    self.tree.insert(
                        "",
                        "end",
                        values=(book_full, chapter, verse_num, verse_text),
                        image=self.transparent_image,
                    )

    def get_verses_for_chapter(self, book_full_name, chapter):
        """
        Retrieve verses for a specified book and chapter.

        Parameters:
        - book_full_name (str): Full name of the book.
        - chapter (str): Chapter number.

        Returns:
        - list[str]: List of verses for the given book and chapter.
        """
        abbrev = next(
            abbrev
            for abbrev, book_data in books.items()
            if book_data[2] == book_full_name
        )
        chapter_prefix = f"{abbrev} {chapter}:"
        return [
            " ".join(line.split()[2:])
            for line in self.data
            if line.startswith(chapter_prefix)
        ]

    def go_to_chapter(self):
        """
        Navigate to the specified book and chapter using the ChapterPopup.
        """
        if (
            self.selected_book_var.get() == "All Books"
            or self.selected_chapter_var.get() == "ALL"
        ):
            messagebox.showinfo("Info", "Please select a specific Book and Chapter.")
            return

        verses = self.get_verses_for_chapter(
            self.selected_book_var.get(), self.selected_chapter_var.get()
        )
        self.chapter_popup = ChapterPopup(
            self, self.selected_book_var.get(), self.selected_chapter_var.get(), verses
        )


class ChapterPopup(tk.Toplevel):
    """
    Initialize a popup window to display the verses of a given book and chapter.

    Parameters:
    - parent (tk.Widget): The parent widget that this popup belongs to.
    - book (str): The name of the book.
    - chapter (str): The chapter number of the book.
    - verses (list[str]): List of verses for the given book and chapter.
    - highlight_verse (int, optional): The verse number to highlight. Defaults to None.

    Attributes:
    - parent (tk.Widget): The parent widget that this popup belongs to.
    - book (str): The name of the book.
    - chapter (str): The chapter number of the book.
    - highlight_verse (int): The verse number to highlight. 
      If not provided, no verse is highlighted.
    """

    def __init__(self, parent, book, chapter, verses, highlight_verse=None):
        super().__init__(parent)

        self.parent = parent
        self.book = book
        self.chapter = chapter
        self.highlight_verse = highlight_verse

        self.title(f"{self.book} {self.chapter}")
        self.geometry("800x600")
        self.style = ttk.Style()
        self.style.theme_use("clam")

        self.prev_button = ttk.Button(
            self, text="Previous", command=self.previous_chapter
        )
        self.prev_button.grid(row=0, column=0, padx=5, pady=5)

        self.next_button = ttk.Button(self, text="Next", command=self.next_chapter)
        self.next_button.grid(row=0, column=2, padx=5, pady=5)

        self.header_label = tk.Label(
            self, text=f"{self.book} {self.chapter}", font=("Arial", 24)
        )
        self.header_label.grid(row=0, column=1, padx=5, pady=5)

        self.text_widget = tk.Text(
            self,
            wrap=tk.WORD,
            font=("Arial", 14),
            borderwidth=0,
            highlightthickness=0,
            bg=self.cget("bg"),
        )
        self.text_widget.grid(
            row=1, column=0, columnspan=3, padx=20, pady=5, sticky="nsew"
        )

        # Adding a vertical scrollbar
        self.scrollbar = ttk.Scrollbar(self, command=self.text_widget.yview)
        self.scrollbar.grid(row=1, column=4, sticky="ns")
        self.text_widget.configure(yscrollcommand=self.scrollbar.set)

        # Configure the tags for bold, italic and highlight, etc
        self.text_widget.tag_configure("bold", font=("Arial", 14, "bold"))
        self.text_widget.tag_configure("highlight", background="yellow")

        # Populate the text widget with the verses from the list passed
        self.populate_verses(verses)

        self.grid_rowconfigure(1, weight=1)
        self.grid_columnconfigure(1, weight=1)

    def populate_verses(self, verses):
        """
        Clear and repopulate the text widget with the provided verses.

        Parameters:
        - verses (list[str]): A list of strings where each string represents a verse.

        This method will clear the text widget and then insert each verse from
        the provided list. The inserted verses are formatted with their
        corresponding verse number in bold. If a verse number matches the
        `highlight_verse` attribute, that verse is highlighted with a yellow background.
        """
        self.text_widget.delete(1.0, tk.END)
        for idx, verse in enumerate(verses, start=1):
            reference = f"{idx}.  "

            # Highlight the specific verse if specified
            if self.highlight_verse and idx == self.highlight_verse:
                self.text_widget.insert(tk.END, reference, ("bold", "highlight"))
                self.text_widget.insert(tk.END, f" {verse}\n\n", "highlight")
            else:
                self.text_widget.insert(tk.END, reference, "bold")
                self.text_widget.insert(tk.END, f" {verse}\n\n")

    def next_chapter(self):
        """
        Navigate to the next chapter.

        If there's a subsequent chapter, it retrieves the verses for that chapter
        using the `get_verses_for_chapter` method of the parent widget and updates
        the displayed chapter accordingly. If there's no subsequent chapter (i.e.,
        it's the last chapter of the book), the "Next" button is disabled.
        """
        self.highlight_verse = None

        # Convert the chapter to an integer, increment it, then convert back to a string
        self.chapter = str(int(self.chapter) + 1)
        new_verses = self.parent.get_verses_for_chapter(
            self.book, self.chapter
        )
        if new_verses:
            self.header_label.config(text=f"{self.book} {self.chapter}")
            self.populate_verses(new_verses)
        else:
            # (disable the "Next" button)
            self.next_button.config(state=tk.DISABLED)

    def previous_chapter(self):
        """
        Navigate to the previous chapter.

        If there's a preceding chapter, it retrieves the verses for that chapter
        using the `get_verses_for_chapter` method of the parent widget and updates
        the displayed chapter accordingly. If there's no preceding chapter (i.e.,
        it's the first chapter of the book), the "Previous" button is disabled.
        """
        self.highlight_verse = None

        # Convert the chapter to an integer, decrement (if required, convert back
        if int(self.chapter) > 1:
            self.chapter = str(int(self.chapter) - 1)
            new_verses = self.parent.get_verses_for_chapter(self.book, self.chapter)
            self.header_label.config(text=f"{self.book} {self.chapter}")
            self.populate_verses(new_verses)
        else:
            # (disable the "Previous" button)
            self.prev_button.config(state=tk.DISABLED)


if __name__ == "__main__":
    app = BibleSearchApp()
    app.mainloop()
Last updated on 27 Aug 2023
Published on 27 Aug 2023