Arithmetic is where numbers fly like pigeons in and out of your head, Carl Sandburg
This Python script, bible_gui.py, serves as a comprehensive desktop prayer tool, built with the power of PySide6 (Qt for Python). It is designed to centralize your daily devotional workflow into a single, elegantly styled window featuring four distinct tabs.
Verse Tab: This is your daily bread. It fetches a Bible verse of the day, prioritizing offline sources first (your local reading plan daily_plan.txt combined with the diatheke CLI tool and freebible library) to ensure you always have a verse even without an internet connection. If offline lookups fail, it gracefully falls back to online APIs like YouVersion and OurManna.
Videos Tab: A dedicated player for curated YouTube prayer videos. To circumvent the common “Refused to display…” error (Error 152) that plagues desktop apps trying to embed YouTube directly, this tab implements a clever local HTTP server strategy. It spins up a tiny server on localhost to serve the embed page, satisfying YouTube’s strict X-Frame-Options requirements and allowing seamless playback within the interface.
Online Tab: This tab extends your prayer resources by embedding external websites directly into the application. It loads a curated list of online prayer sites (such as Sacred Space, Pray As You Go, and Universalis) from a configuration file and displays them using a built-in browser widget. It intelligently handles site restrictions; if a site refuses to be embedded due to security headers (X-Frame-Options), the app detects the failure and provides a prominent button to open the content in your default system browser instead.
Diary Tab: A Markdown-based prayer journal. It allows you to write your daily prayers, gratitude, intentions, and reflections. It includes a live preview mode (rendering Markdown to HTML) and manages your entries as individual files sorted by date, creating a searchable history of your spiritual journey.
The application is built with user experience in mind: it auto-creates configuration files if they are missing, handles missing dependencies (like bible_verse.py) with functional fallbacks, and uses a consistent, dark-themed color palette.

# ══════════════════════════════════════════════════════════════════════════════
# TAB 5 — MY PRAYERS (SQLite-backed personal prayer library)
# ══════════════════════════════════════════════════════════════════════════════
class MyPrayersTab(QWidget):
"""
A personal prayer library viewer backed by an SQLite database.
This tab serves as the primary interface for browsing and reading
saved prayers. It is designed as a two‑panel layout:
**Left Panel** (fixed width 200 px)
- A ``QListWidget`` displays a list of prayers, each entry showing
the prayer’s date (or “undated”) followed by its title.
- The prayer for *today* (if present) is highlighted with a gold,
bold font and automatically selected at startup.
- A “+ New Prayer” button will eventually open
a full‑featured prayer editor in a separate tab.
- A “↺ Refresh” button reloads the list from the database,
re‑selecting today’s prayer if available.
Right panel (stretchable)
- **Header Card**: displays the prayer’s title, date, description,
and an optional cover image. If a cover image exists in the
database (``myprayers.image`` as BLOB), it is shown as a
``QLabel``; otherwise the label is hidden.
- **Horizontal Rule**: a thin visual separator between the header
and the page content.
- **Page Viewer**: the prayer’s content is split into one or more
*pages*, each rendered as HTML inside a ``QWebEngineView``.
This allows rich formatting, embedded images, and consistent
styling.
- **Page Navigation Footer**: “◀ Prev” and “Next ▶” buttons
flank a counter label (e.g. “2 / 5”). The user can step through
pages at their own pace, reminiscent of the *Sacred Space*
website.
Navigation
──────────
Selecting a different prayer in the list reloads the right panel
and resets the page viewer to page 1.
Prev / Next step through the pages of the current prayer at the
user's own pace, just like Sacred Space (sacredspace.com).
Images
──────
Both myprayers.image and pages.image are stored as raw bytes (BLOB).
We convert them to base64 data-URIs so they can be embedded directly
in the HTML passed to QWebEngineView — no temp files needed.
If prayer_db.py cannot be imported a banner explains this and offers
a setup command.
"""
# ── HTML template for one prayer page ────────────────────────────────
# {content} — HTML or plain text from pages.content
# {img_tag} — IMG element if the page has an image, else ""
# Colours reuse the app-level palette constants (BG, TEXT, ACCENT …)
HTML Template. The class defines _PAGE_TEMPLATE, a Python format string that is instantiated with the application’s colour palette constants (BG, TEXT, ACCENT, GOLD, MUTED, BORDER) to produce a self‑contained HTML page. The template includes:
<head> section with a <eta charset="utf-8"> tag and inline CSS that styles headings, paragraphs, blockquotes images, and an “empty” state., {fg}``, etc. are replaced with CSS colour values derived from the app’s theme constants.{img_tag} – either an <img src="data:..."> element (if the current page has an image) or an empty string.{content} – the page’s HTML (or plain text) content.The rendered page is capped at a maximum width of 680 px and centred, providing a comfortable reading experience.
_PAGE_TEMPLATE = (
"<!DOCTYPE html><html><head><meta charset="utf-8">"
"<style>"
" body{{"
" background:" + "{bg};" +
" color:" + "{fg};" +
" font:16px/1.8 \'Noto Sans\',sans-serif;"
" max-width:680px;margin:32px auto;padding:0 20px;"
" }}"
" h2{{color:" + "{accent}" + ";font-size:22px;margin-bottom:8px;}}"
" h3{{color:" + "{gold}" + ";font-size:17px;}}"
" p{{margin-bottom:1.1em;}}"
" blockquote{{"
" border-left:3px solid " + "{accent}" + ";"
" padding-left:14px;color:" + "{muted}" + ";margin-left:0;"
" }}"
" img{{max-width:100%;border-radius:8px;margin:12px 0;}}"
" .empty{{color:" + "{muted}" + ";font-style:italic;text-align:center;"
" padding:60px 0;}}"
"</style></head>"
"<body>"
"{img_tag}"
"{content}"
"</body></html>"
)
def __init__(self):
"""
Construct the tab, build the UI, initialise the database connection, and load the prayer list.
"""
super().__init__()
# ─────────────────────────────────────────────────────────────────────
# DATA FLOW OVERVIEW
#
# The GUI depends on the external module `prayer_db.py`, which defines
# the PrayerDB class. A module‑level flag `_PDB_OK` indicates whether
# the import succeeded. If the module is missing, the UI still loads,
# but a banner explains the problem and shows setup instructions.
# ─────────────────────────────────────────────────────────────────────
if _PDB_OK:
# Create the database interface. PRAYER_DB is a filesystem path
# defined in PATHS (e.g., CFG / "bible/prayers.db").
self._db = PrayerDB(PRAYER_DB)
else:
self._db = None
# Internal state containers
self._prayers: list[dict] = [] # List of prayers (metadata only)
self._current_prayer: dict | None = None
self._page_idx: int = 0 # 0‑based index into the prayer's pages
# ─────────────────────────────────────────────────────────────────────
# ROOT LAYOUT
# Two‑column layout:
# • Left: prayer list + actions
# • Right: header card + page viewer
# ─────────────────────────────────────────────────────────────────────
root = QHBoxLayout(self)
root.setContentsMargins(14, 14, 14, 14)
root.setSpacing(14)
# =====================================================================
# LEFT PANEL — PRAYER LIST
# =====================================================================
left = QVBoxLayout()
left.setSpacing(8)
# Section label
cap = QLabel("Prayers")
cap.setStyleSheet(f"color:{ACCENT};font-weight:bold;font-size:12px;")
left.addWidget(cap)
# List widget showing prayer titles
self.prayer_list = QListWidget()
self.prayer_list.setFixedWidth(200)
# When the user clicks a prayer:
# _load_prayer(row_index) is called.
#
# That method:
# • fetches the full prayer record from the DB
# • loads metadata (title, date, description, cover image)
# • loads all pages (content + optional image BLOB)
# • resets the page viewer to page 1
self.prayer_list.itemClicked.connect(
lambda _: self._load_prayer(self.prayer_list.currentRow())
)
left.addWidget(self.prayer_list, 1)
# Future feature: open a prayer editor
new_btn = QPushButton("+ New Prayer")
new_btn.clicked.connect(self._on_new_prayer)
left.addWidget(new_btn)
refresh_btn = QPushButton("↺ Refresh")
# Refresh button reloads the list of prayers from the DB.
# _refresh_list() calls PrayerDB.list_prayers(), returning up to
# 200 prayers ordered by date descending.
refresh_btn.clicked.connect(self._refresh_list)
left.addWidget(refresh_btn)
root.addLayout(left)
# =====================================================================
# RIGHT PANEL — HEADER + PAGE VIEWER
# =====================================================================
right = QVBoxLayout()
right.setSpacing(10)
# ── Title (large, gold) ──────────────────────────────────────────────
self.title_lbl = QLabel()
self.title_lbl.setStyleSheet(
f"color:{GOLD};font-size:21px;font-weight:bold;"
)
self.title_lbl.setWordWrap(True)
right.addWidget(self.title_lbl)
# ── Metadata row: date + description ─────────────────────────────────
meta_row = QHBoxLayout()
self.date_lbl = QLabel()
self.date_lbl.setStyleSheet(f"color:{MUTED};font-size:12px;")
self.desc_lbl = QLabel()
self.desc_lbl.setStyleSheet(f"color:{TEXT};font-size:13px;")
self.desc_lbl.setWordWrap(True)
meta_row.addWidget(self.date_lbl)
meta_row.addSpacing(12)
meta_row.addWidget(self.desc_lbl, 1)
right.addLayout(meta_row)
# ── Optional cover image ─────────────────────────────────────────────
# This QLabel is shown only if the prayer has a cover image BLOB.
self.cover_img = QLabel()
self.cover_img.setAlignment(Qt.AlignLeft)
self.cover_img.setMaximumHeight(180)
self.cover_img.setScaledContents(False)
self.cover_img.hide()
right.addWidget(self.cover_img)
# Divider line
hr = QFrame()
hr.setFrameShape(QFrame.HLine)
hr.setStyleSheet(f"background:{BORDER};max-height:1px;")
right.addWidget(hr)
# ── Page viewer (HTML) ───────────────────────────────────────────────
# Displays the prayer pages. Each page is rendered as HTML, with
# optional embedded base64 images.
self.page_view = QWebEngineView()
self.page_view.setHtml(self._empty_html("Select a prayer from the list."))
right.addWidget(self.page_view, 1)
# ─────────────────────────────────────────────────────────────────────
# PAGE NAVIGATION FOOTER
# Prev / Next buttons update the page index and call _render_page().
# ─────────────────────────────────────────────────────────────────────
nav = QHBoxLayout()
nav.setSpacing(10)
# Prev/Next buttons increment or decrement the current page
# index and call ``_render_page()`` to update the ``QWebEngineView``.
self.prev_btn = QPushButton("◀ Prev")
self.prev_btn.setFixedWidth(110)
self.prev_btn.setEnabled(False)
self.prev_btn.clicked.connect(self._prev_page)
self.page_counter = QLabel("— / —")
self.page_counter.setAlignment(Qt.AlignCenter)
self.page_counter.setStyleSheet(f"color:{MUTED};font-size:13px;min-width:80px;")
self.next_btn = QPushButton("Next ▶")
self.next_btn.setFixedWidth(110)
self.next_btn.setEnabled(False)
self.next_btn.clicked.connect(self._next_page)
nav.addStretch()
nav.addWidget(self.prev_btn)
nav.addWidget(self.page_counter)
nav.addWidget(self.next_btn)
nav.addStretch()
right.addLayout(nav)
root.addLayout(right, 1)
# =====================================================================
# INITIAL LOAD
# =====================================================================
if not _PDB_OK:
# If the database module is missing, show a helpful message.
self.title_lbl.setText("prayer_db.py not found")
self.desc_lbl.setText(
"Copy prayer_db.py into the same directory as bible_gui.py."
)
self.page_view.setHtml(self._empty_html(
"prayer_db.py not found.<br><br>"
"Make sure prayer_db.py is in the same directory<br>"
"as bible_gui.py and restart the app."
))
else:
# Load the list of prayers immediately.
self._refresh_list()
# ── List management ───────────────────────────────────────────────────
def _refresh_list(self) -> None:
"""
Reload the list of prayers from the database and repopulate the
left-hand QListWidget.
Behaviour summary:
------------------
• Queries the database for up to 200 prayers, ordered by date (newest first).
• Rebuilds the QListWidget with human‑readable labels: "Date Title"
• Highlights today's prayer (if any) in gold + bold font.
• Automatically selects:
- today's prayer, if present
- otherwise the first prayer in the list
- otherwise shows a "No prayers yet" message
"""
# If the database module failed to load earlier, do nothing.
if self._db is None:
return
# Clear the existing list before repopulating it.
self.prayer_list.clear()
# Retrieve prayer metadata only (no page content).
# Returns a list of dicts with keys: id, title, date, description, image?
self._prayers = self._db.list_prayers(limit=200, order_by="date DESC")
# Determine today's date in ISO format (YYYY‑MM‑DD).
today = __import__("datetime").date.today().isoformat()
today_row = -1 # This will store the index of today's prayer if found.
# Build the list widget items.
for i, p in enumerate(self._prayers):
date_str = p["date"] or "undated"
label = f"{date_str} {p['title']}"
item = QListWidgetItem(label)
# Highlight today's prayer visually.
if p["date"] == today:
item.setForeground(QColor(GOLD))
font = item.font(); font.setBold(True); item.setFont(font)
today_row = i
self.prayer_list.addItem(item)
# Decide which row to select:
# • If today's prayer exists → select it (today_row).
# • Else if list is non‑empty → select the first prayer (0).
# • Else → show "No prayers yet" (-1).
select_row = today_row if today_row >= 0 else (0 if self._prayers else -1)
if select_row >= 0:
# Trigger selection + load the prayer into the right panel.
self.prayer_list.setCurrentRow(select_row)
self._load_prayer(select_row)
else:
# No prayers in the database.
self.title_lbl.setText("No prayers yet")
self.desc_lbl.setText("Press + New Prayer to add your first prayer.")
# ── Prayer loading ────────────────────────────────────────────────────
def _load_prayer(self, list_idx: int) -> None:
"""
Load the full prayer record from the database and display page 1.
This method is the entry point for selecting a prayer from the
left‑hand list. It translates a list index into a database ID,
retrieves the complete prayer (including its pages), and updates
the entire right panel.
:param list_idx: Zero‑based index into the internal ``_prayers`` list.
This list is a cached subset of prayers obtained from self._prayers =
self._db.list_prayers(limit=200, order_by="date DESC")
Each item is a ``dict`` containing at least the key ``"id_myprayer"``.
"""
# 1. **Bounds check**. If *list_idx* is outside the range of ``self._prayers``,
# the method returns immediately without making any changes.
if not (0 <= list_idx < len(self._prayers)):
return
# 2. **Database lookup** – the prayer’s primary key is extracted from the list entry as
#``id_myprayer`` and passed to ``self._db.get_prayer_by_id(pid)``.
pid = self._prayers[list_idx]["id_myprayer"]
prayer = self._db.get_prayer_by_id(pid)
# This database method is expected to return a dictionary representing the full prayer
# record, including:
# - ``"id_myprayer"`` (int)
# - ``"title"``, ``"description"``, ``"date"`` (str or ``None``)
# - ``"image"`` (bytes or ``None``) – the cover image BLOB
# - ``"pages"`` – a list of page dictionaries, each with keys
# ``"page_number"``, ``"content"``, and ``"image"`` (optional
# BLOB). The list is ordered by ``page_number``.
# If the database returns ``None`` (e.g., the prayer was deleted after the list was loaded),
# the method returns silently, leaving the previous display untouched.
if prayer is None:
return
# 3. **State update** – the returned dictionary is stored in ``self._current_prayer``,
# and ``_page_idx`` is reset to ``0`` so that the first page is always shown when a new prayer is
# selected.
self._current_prayer = prayer
self._page_idx = 0
# **UI refresh** – two helper methods are called in succession:
# - ``_update_header(prayer)`` – populates the title, date, description, and optional cover image.
# - ``_update_page()`` – renders the first page (or an empty message if the prayer has no pages)
# and updates the navigation footer.
self._update_header(prayer)
self._update_page()
def _update_header(self, prayer: dict) -> None:
"""
Populate the header card with the prayer’s metadata and cover image.
This method fills the static labels in the right panel’s header area
and decides whether to show or hide the cover image widget.
:param prayer: A dictionary representing a full prayer record as returned by ``PrayerDB.get_prayer_by_id()``.
It must contain at least the keys ``"title"``, ``"date"``, ``"description"``, and optionally ``"image"``.
"""
# 1. **Text fields** – ``title_lbl``, ``date_lbl``, and ``desc_lbl`` are updated with the values from ``prayer``,
# using ``.get()`` with a default empty string to safely handle missing keys.
# The labels are allowed to be empty; no special formatting is applied here.
self.title_lbl.setText(prayer.get("title", ""))
self.date_lbl.setText(prayer.get("date", ""))
self.desc_lbl.setText(prayer.get("description", ""))
# 2. **Cover image** – the "image" key is expected to hold bytes representing a valid image
# if a cover image exists, or be absent (None).
img_bytes = prayer.get("image")
# If the prayer dictionary contains a cover image (stored as bytes),
# ``_show_cover_image(img_bytes)`` is called to decode, scale, and display the image inside
# ``self.cover_img``.
if img_bytes:
self._show_cover_image(img_bytes)
else:
# If the image is missing, ``self.cover_img.hide()`` is called
# to collapse the widget, freeing vertical space.
self.cover_img.hide()
def _show_cover_image(self, data: bytes) -> None:
"""
Display the prayer's cover image in the header area.
The database stores the image as raw bytes (BLOB). We decode those
bytes directly into a QPixmap using QPixmap.loadFromData(), which
avoids writing any temporary files to disk.
:param data: Raw bytes of an image file (e.g. the content of a
PNG or JPEG file). This is exactly the value stored in the
``image`` column of the ``myprayers`` table.
"""
from PySide6.QtGui import QPixmap
pix = QPixmap()
# Attempt to load the raw bytes into a QPixmap using QPixmap.loadFromData()
if pix.loadFromData(data):
# If the image loads successfully:
# 1. Scale it to a maximum height of 180 px (preserving aspect ratio)
# We cap the height at 180 px to keep the header visually balanced.
# 2. Apply smooth scaling for better visual quality
scaled = pix.scaledToHeight(
min(180, pix.height()),
Qt.SmoothTransformation,
)
# 3. **Display** – the scaled pixmap is set on ``self.cover_img``
# via ``setPixmap()``, and the label is made visible with
# ``show()`` (it may have been hidden previously).
self.cover_img.setPixmap(scaled)
self.cover_img.show()
else:
# If loading fails, hide the image widget QLabel so the UI does not show a broken/empty image box
self.cover_img.hide()
# ── Page display ──────────────────────────────────────────────────────
def _update_page(self) -> None:
"""
This is the core method for the page viewer. It assembles the HTML
for the page indicated by ``self._page_idx``, loads it into the
``QWebEngineView``, and updates the navigation footer (counter
label and Prev/Next button states).
"""
# 1. **Guard clause**. If ``self._current_prayer`` is ``None``, the
# method returns immediately, leaving the viewer in its current state.
if self._current_prayer is None:
return
# 2. **Retrieve pages** – the list of page dictionaries is extracted
# from the current prayer record via ``get("pages", [])``.
pages = self._current_prayer.get("pages", [])
n = len(pages)
# 3. **Empty prayer** (n == 0):
if n == 0:
# A) The web engine is set to display an empty‑state message
# generated by ``self._empty_html("This prayer has no pages yet.")``.
self.page_view.setHtml(self._empty_html("This prayer has no pages yet."))
# B) The page counter is set to ``"— / —"``.
self.page_counter.setText("— / —")
# C) Both navigation buttons are disabled.
self.prev_btn.setEnabled(False)
self.next_btn.setEnabled(False)
# The method then returns.
return
# 4. **Index clamping** – ``self._page_idx`` is adjusted to a valid
# range using ``max(0, min(self._page_idx, n - 1))``.
# This guarantees that even if an external operation modifies the
# pages list (e.g. a prayer is edited and saved elsewhere),
# the current page index never points beyond the list bounds.
self._page_idx = max(0, min(self._page_idx, n - 1))
# 5. **Page data extraction** – the dictionary for the current page
# is obtained: ``page = pages[self._page_idx]``.
page = pages[self._page_idx]
# - page.get("content"): the textual/HTML content. If the key is missing
# or the value is ``None``/empty, a fallback ``<p>(empty page)</p>``
# is used to avoid a completely blank page.
content = page.get("content") or "<p>(empty page)</p>"
# - ``img_bytes``: optional image BLOB (bytes). If present,
# ``self._img_tag(img_bytes)`` is called to convert the bytes
# into an ``<img src="data:...">`` tag (base64 data‑URI). If
# absent or ``None``, ``img_tag`` is an empty string
img_bytes = page.get("image")
img_tag = self._img_tag(img_bytes) if img_bytes else ""
# 6. **HTML assembly** – ``self._build_page_html(content, img_tag)`` is invoked.
# This method is expected to:
# - Take the raw ``content`` and the optional ``img_tag``.
# - Inject them into the class‑level ``_PAGE_TEMPLATE`` along
# with the app’s colour constants (``BG``, ``TEXT``, ``ACCENT``,
# ``GOLD``, ``MUTED``).
# - Return a complete HTML document (with ``<html>``, ``<head>``
# containing inline CSS, and ``<body>``) as a string.
html = self._build_page_html(content, img_tag)
# 7. **Display** – the assembled HTML string is passed to
# ``self.page_view.setHtml(html)``. ``QWebEngineView`` renders
# it immediately, executing any JavaScript and loading embedded
# images (the data‑URIs).
self.page_view.setHtml(html)
# 8. **Navigation footer update**:
# - ``page_counter`` label text is set to ``"index_page / total_pages"`` style,
# using 1‑based numbering for user friendliness.
self.page_counter.setText(f"{self._page_idx + 1} / {n}")
# - ``prev_btn`` is enabled only if ``_page_idx > 0``.
self.prev_btn.setEnabled(self._page_idx > 0)
# - ``next_btn`` is enabled only if ``_page_idx < n - 1``.
self.next_btn.setEnabled(self._page_idx < n - 1)
def _prev_page(self) -> None:
"""
Navigate to the previous page of the current prayer.
This method is connected to the “◀ Prev” button. It decrements the
internal page index by one and refreshes the page viewer, provided
the prayer is not already at the first page..
"""
# 1. The method first checks ``self._page_idx > 0``. If this
# condition is false (we are on page 1 or have no pages), the
# method does nothing – this is an additional safety check even
# though the button is normally disabled.
if self._page_idx > 0:
# 2. Decrements ``_page_idx`` by one.
self._page_idx -= 1
# 3. Calls ``_update_page()``, which re‑renders the HTML and updates
# the navigation footer (counter label and Prev/Next button states).
self._update_page()
def _next_page(self) -> None:
"""
Navigate to the next page of the current prayer.
This method is connected to the “Next ▶” button. It increments the
internal page index and refreshes the viewer, stopping at the last
page.
"""
# 1. The method first verifies that ``self._current_prayer`` is not
# ``None``, otherwise nothing happens.
if self._current_prayer:
# 2. It retrieves the list of pages and checks that ``self._page_idx < len(pages) - 1``.
# If the index is already at the last page, the method returns early. This check mirrors
# the button’s disabled state but offers an extra safety net.
pages = self._current_prayer.get("pages", [])
if self._page_idx < len(pages) - 1:
# 3. Increments ``_page_idx`` by one.
self._page_idx += 1
# 4. Calls ``_update_page()``, which re‑renders the HTML and updates
# the navigation footer (counter label and Prev/Next button states).
self._update_page()