Any fool can criticize, complain, and condemn— and most fools do. But it takes character and self-control to be understanding and forgiving, Dale Carnegie
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.

class DiaryTab(QWidget):
"""
DiaryTab is a QWidget that implements a two‑pane prayer journal:
* Left pane: a vertical list of past entries (displayed by date, newest first) and a button to create today’s entry.
* Right pane: a text editor for writing in Markdown, with a toggle button that switches between the editor and a rendered preview using QWebEngineView.
The journal is stored as one Markdown file per calendar day inside a dedicated directory, e.g.,
~/dotfiles/myArch/config/bible/prayer_diary/2026-04-30.md
Pressing "New entry" loads or creates today's file. Ctrl+S saves (wired in MainWindow).
Markdown preview uses the `markdown` package if installed,
otherwise falls back to a minimal inline regex converter (no extra deps).
"""
def __init__(self):
super().__init__()
# the currently selected date in ISO format (e.g., "2026-05-09").
# It is updated whenever a new entry is loaded (either today’s or a historical one).
# It determines which file is read/written.
self._date = datetime.date.today().isoformat()
# _preview : bool – True when the preview pane is visible and the editor is hidden; False otherwise.
self._preview = False
# The root layout is a QHBoxLayout containing two columns: left and right.
root = QHBoxLayout(self)
root.setContentsMargins(14, 14, 14, 14)
root.setSpacing(14)
# ── Left list ─────────────────────────────────────────────────────
left = QVBoxLayout()
# Title label — “Prayer Journal” in ACCENT colour, bold, size 12px.
lbl = QLabel("Prayer Journal")
lbl.setStyleSheet(f"color:{ACCENT};font-weight:bold;font-size:12px;")
left.addWidget(lbl)
# self.elist (QListWidget) — fixed width of 190 pixels.
# It displays one item per existing .md file sorted reverse‑chronologically (newest first).
self.elist = QListWidget()
self.elist.setFixedWidth(190)
# Clicking an item calls _load() with the displayed date string.
# 1. User clicks an item in the left list.
# 2. _load() reads the corresponding file and puts its content in the editor.
# 3. Preview is automatically turned off.
# 4. User can edit and save historical entries just like today’s.
self.elist.itemClicked.connect(
lambda _: self._load(self.elist.currentItem().text())
)
left.addWidget(self.elist, 1)
# “+ New entry” button — calls _new() to load or create today’s file.
new_btn = QPushButton("+ New entry")
new_btn.clicked.connect(self._new)
left.addWidget(new_btn)
root.addLayout(left)
# ── Right editor ──────────────────────────────────────────────────
# The right column is itself a QVBoxLayout containing:
right = QVBoxLayout()
right.setSpacing(8)
# Toolbar (QHBoxLayout):
toolbar = QHBoxLayout()
# date_lbl — shows the currently loaded date, styled in GOLD and bold.
self.date_lbl = QLabel()
self.date_lbl.setStyleSheet(f"color:{GOLD};font-weight:bold;")
toolbar.addWidget(self.date_lbl)
toolbar.addStretch()
# Preview toggle button — shows “👁 Preview” when editing, “✏ Edit” when previewing.
self.prev_btn = QPushButton("👁 Preview")
# Save button — shows “💾 Save”
self.save_btn = QPushButton("💾 Save")
# Preview toggle button is connected to _toggle_preview().
# Clicking “👁 Preview” calls _toggle_preview(), which calls _set_preview(True).
# The editor is hidden and the rendered HTML is displayed.
# Clicking “✏ Edit” returns to the editor.
self.prev_btn.clicked.connect(self._toggle_preview)
# Save button is connected to self.save().
# Writes the current editor contents to the file corresponding to self._date.
# It also refreshes the list so today’s entry appears in gold.
# Temporary “✅ Saved” appears on the button.
self.save_btn.clicked.connect(self.save)
toolbar.addWidget(self.prev_btn)
toolbar.addWidget(self.save_btn)
right.addLayout(toolbar)
# Editor area — a QTextEdit (self.editor) with:
self.editor = QTextEdit()
# Monospace font “Hack” at 13 pt.
self.editor.setFont(QFont("Hack", 13))
# Placeholder text that gives a suggested Markdown template.
self.editor.setPlaceholderText(
"Write your prayer here... Markdown is supported.\n\n"
"# Date\n## Prayer\n## Gratitude\n## Reflection"
)
# This widget fills all available space (stretch factor = 1).
right.addWidget(self.editor, 1)
# Preview area — a QWebEngineView (self.preview_view).
self.preview_view = QWebEngineView()
self.preview_view.hide() # It is hidden by default.
right.addWidget(self.preview_view, 1)
# It is also given a stretch factor of 1,
# so when shown it occupies exactly the same space as the editor.
root.addLayout(right, 1)
# After building the layout, two methods are called:
self._refresh_list() # It populates the left list with all existing .md entries (files).
self._new() # It loads today’s file (or creates a blank template) and sets the editor accordingly.
# Editor shows the content; preview is hidden.
# ── Internal helpers ──────────────────────────────────────────────────
def _path(self, date_str: str) -> Path:
"""
Returns the full pathlib.Path to the Markdown file for a given ISO date string.
This is the single point where the mapping from a date string to a file is defined.
Changing it (e.g., to use subfolders) only requires modifying this method.
"""
# DIARY_DIR : pathlib.Path it the directory where all .md diary files live. Typically something like:
# DIARY_DIR = Path.home() / "dotfiles/myArch/config/bible/prayer_diary"
return DIARY_DIR / (date_str + ".md")
def _refresh_list(self) -> None:
"""
Rebuilds the entire contents of self.elist.
This method is called:
1. During __init__.
2. After a successful save (save()).
It ensures the list always reflects the current contents of the filesystem.
"""
# 1. Clears the list.
self.elist.clear()
# 2. Finds all .md files in DIARY_DIR using Path.glob("*.md").
# 3. Sorts them reverse alphabetically (because ISO dates sort chronologically, reverse gives newest first).
for p in sorted(DIARY_DIR.glob("*.md"), reverse=True):
# 4. For each file, creates a QListWidgetItem with the stem (the filename without extension),
# i.e., the date string.
item = QListWidgetItem(p.stem)
# 5. If the stem equals today’s date in ISO format, the item’s foreground colour is set to GOLD.
if p.stem == datetime.date.today().isoformat():
item.setForeground(QColor(GOLD))
self.elist.addItem(item)
def _new(self) -> None:
"""Load today's entry diary or create a blank template."""
# 1. Sets self._date to today’s ISO string.
self._date = datetime.date.today().isoformat()
# 2. Computes the full pathlib.Path to the Markdown file via the internal helper _path.
path = self._path(self._date)
# 3. If the file already exists...
if path.exists():
# ... its entire text is loaded into the editor via setPlainText().
self.editor.setPlainText(path.read_text("utf-8"))
else:
# 4. If the file does not exist, a template is inserted
# A first‑level heading with today’s formatted date, e.g., "# Saturday, May 09, 2026"
# Sections: ## Prayer, ## Gratitude, ## Reflection, each followed by blank lines.
# The template is written to the editor only; the file is not created on disk until the user explicitly saves.
fmt = datetime.date.today().strftime("%A, %B %d, %Y")
self.editor.setPlainText(
f"# {fmt}\n\n"
"## Prayer\n\n\n\n"
"## Gratitude\n\n\n\n"
"## Reflection\n\n"
)
# The date label is updated.
self.date_lbl.setText(" " + self._date)
# Preview mode is turned off by calling _set_preview(False).
# Therefore, the user always sees the editor after clicking “New entry”.
self._set_preview(False)
def _load(self, date_str: str) -> None:
"""
Loads an existing entry for an arbitrary date (typically triggered by clicking an item in the list).
"""
# 1. Sets self._date to the given date string.
self._date = date_str
# 2. Reads the file if it exists, otherwise clears the editor (empty string).
path = self._path(date_str)
self.editor.setPlainText(
path.read_text("utf-8") if path.exists() else ""
)
# 3. Updates the date label.
self.date_lbl.setText(" " + date_str)
# 4. Preview mode is turned off by calling _set_preview(False).
self._set_preview(False)
# ── Public ────────────────────────────────────────────────────────────
def save(self) -> None:
"""
Writes the current editor contents to the file corresponding to self._date.
"""
# 1. Retrieves raw plain text from the editor.
text = self.editor.toPlainText()
# 2. If the text is completely empty (after stripping whitespace), the method returns early.
# This prevents overwriting (maybe by user error) a previously saved entry with an accidental blank save.
if not text.strip():
return
# 3. The text is written to the file using UTF‑8 encoding (write_text).
self._path(self._date).write_text(text, "utf-8")
# 4. The left list is refreshed so any new file (if this was the first save of today) appears.
self._refresh_list()
orig = self.save_btn.text()
# 5. A visual feedback is given: the save button temporarily changes to “✅ Saved” for 2 seconds,
# then reverts to “💾 Save”. This is achieved with a QTimer.singleShot.
self.save_btn.setText("✅ Saved") # This happens immediately. The user sees the green checkmark.
QTimer.singleShot(2000, lambda: self.save_btn.setText(orig))
# Then, we schedule a one‑time timer. It simply tells Qt: “Please call this function 2000 ms from now.”
# After 2 seconds, Qt executes the callback, self.save_btn.setText(orig),
# and the button returns to “💾 Save”.
def _toggle_preview(self) -> None:
""" It simply toggles the preview state."""
self._set_preview(not self._preview)
def _set_preview(self, on: bool) -> None:
"""
Activates or deactivates preview mode and updates the UI accordingly.
"""
# 1. Sets self._preview = on.
self._preview = on
# 2. If turning preview ON:
if on:
# Converts the editor’s plain text to HTML using _render(): self._render(self.editor.toPlainText())
# Calls self.preview_view.setHtml() to display it.
self.preview_view.setHtml(self._render(self.editor.toPlainText()))
# Hides the QTextEdit (self.editor).
self.editor.hide()
# Shows the QWebEngineView.
self.preview_view.show()
# Changes the button text to “✏ Edit”.
self.prev_btn.setText("✏ Edit")
# 3. If turning preview OFF:
else:
# Hides the preview.
self.preview_view.hide()
# Shows the editor.
self.editor.show()
# Restores the button text to “👁 Preview”.
# The editor and preview occupy the same grid cell (both have stretch factor 1 inside the right layout)
# so toggling visibility swaps them without altering the overall layout.
self.prev_btn.setText("👁 Preview")
@staticmethod
def _render(md: str) -> str:
"""
Convert Markdown text to a styled HTML page for the preview pane.
It converts a raw Markdown string into a complete HTML document,
suitable for display in a QWebEngineView.
Uses the `markdown` package (pip install markdown) if available for
full CommonMark support. Falls back to a minimal regex converter that
handles the headings/bold/italic/blockquote/hr used in prayer diaries.
"""
# The Markdown → HTML conversion itself has two code paths:
try:
# 1. Preferred path – using the markdown package
import markdown
# extra extension: adds support for tables, fenced code blocks, definition lists, footnotes, etc.
# nl2br extension: causes newlines inside paragraphs to be converted to <br> tags,
# matching the typical behaviour users expect from a plain‑text editor
body = markdown.markdown(md, extensions=["extra", "nl2br"])
except ImportError:
# 2. Fallback path – minimal inline regex converter
# If markdown is not available, a handwritten regex‑based converter handles the subset of
# Markdown most likely to appear in a prayer diary:
import html as _h
# scape HTML – html.escape(md) prevents raw <script> or other tags from being interpreted.
t = _h.escape(md)
# Headings – lines starting with #, ##, or ### are converted to <h1>, <h2>, <h3> respectively.
t = re.sub(r"^### (.+)$", r"<h3>\1</h3>", t, flags=re.MULTILINE)
t = re.sub(r"^## (.+)$", r"<h2>\1</h2>", t, flags=re.MULTILINE)
t = re.sub(r"^# (.+)$", r"<h1>\1</h1>", t, flags=re.MULTILINE)
# Horizontal rules – lines consisting solely of --- become <hr>.
t = re.sub(r"^---$", "<hr>", t, flags=re.MULTILINE)
# Blockquotes – lines starting with > become <blockquote>...</blockquote>.
t = re.sub(r"^gt; (.+)$", r"<blockquote>\1</blockquote>",
t, flags=re.MULTILINE)
# Bold – text between ** is wrapped in <strong>.
t = re.sub(r"^\*\*(.+?)\*\*", r"<strong>\1</strong>", t)
# Italic – text between single * is wrapped in <em>.
t = re.sub(r"^\*(.+?)\*", r"<em>\1</em>", t)
# Inline code – text between backticks ` is wrapped in <code>.
t = re.sub(r"^`(.+?)`", r"<code>\1</code>", t)
# Paragraphs – double newlines (\n\n) are replaced with </p><p>,
# and single newlines become <br>. Finally the whole fragment is wrapped in <p>...</p>
# so that even a single block of text is treated as a paragraph.
body = (
"<p>"
+ t.replace("\n\n", "</p><p>").replace("\n", "<br>")
+ "</p>"
)
# Both paths return a complete HTML string:
return (
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
f"<style>{DIARY_CSS}</style></head><body>{body}</body></html>"
)
# ══════════════════════════════════════════════════════════════════════════════
# MAIN WINDOW
# ══════════════════════════════════════════════════════════════════════════════
# MainWindow inherits from QMainWindow and serves as the top‑level container for the entire Prayer Companion application.
# It arranges four functional areas in a tabbed interface, provides a persistent status bar,
# and wires a global keyboard shortcut that works from any tab.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# This is a mandatory call to the base QMainWindow initialiser.
self.setWindowTitle("Prayer Companion")
# Sets the text that appears in the title bar of the window.
# This will also be the default name used by the operating system’s taskbar / window manager.
self.resize(1100, 750)
# Gives the window an initial size of 1100 pixels wide by 750 pixels high.
# This is not fixed; the user can freely resize the window afterwards.
# It is chosen to comfortably accommodate the diary’s two‑pane layout without feeling cramped.
# Creating the central tab widget
self.diary_tab = DiaryTab()
# The diary tab is stored as an instance attribute because later a keyboard shortcut needs to call
# its save() method directly, regardless of which tab is currently visible.
self.tabs = QTabWidget()
# Creates a tab container. Each tab holds one full‑featured widget.
# Four tabs are added, each with a decorative emoji and text:
self.tabs.addTab(VerseTab(), " 📖 Verse ")
# A tab for reading or searching Bible verses.
self.tabs.addTab(VideoTab(), " 🙏 Prayer ")
# A video‑based prayer or meditation resource.
self.tabs.addTab(PrayerOnlineTab(), " 🌐 Online ")
# An integrated web view for online prayer content.
self.tabs.addTab(self.diary_tab, " 📔 Diary ")
# The local Markdown prayer journal described earlier.
self.setCentralWidget(self.tabs)
# Makes the QTabWidget occupy the entire central area of the QMainWindow.
# The main window automatically manages any toolbar, status bar, and dock widgets around the central widget.
# Status bar
# A QStatusBar is created and displayed at the bottom of the window.
status = QStatusBar()
# The message shows today’s full date (e.g., “Sunday, May 10, 2026”) followed by a centred dot and the
# application name. The date is generated dynamically using datetime.date.today(),
# so it always reflects the current day when the application is launched.
status.showMessage(
" "
+ datetime.date.today().strftime("%A, %B %d, %Y")
+ " · Prayer Companion"
)
# Note: The date shown in the status bar is generated only once at startup.
# It does not update at midnight. If the application is kept running for several days,
# the status bar will show the original launch date.
# This is a minor limitation that could be addressed by adding a timer
# that refreshes the status bar every minute
# It installs this status bar into the main window. The status bar is persistent
# and can later be updated by other parts of the application if needed.
self.setStatusBar(status)
# Global keyboard shortcut
# Ctrl+S saves the diary from any tab
sc = QShortcut(QKeySequence("Ctrl+S"), self)
# Creates a shortcut that listens for Ctrl+S while the main window has focus.
sc.activated.connect(self.diary_tab.save)
# When activated, it calls diary_tab.save() directly,
# bypassing the need for the diary tab to be the currently visible widget.
# The shortcut is tied to the MainWindow instance (self),
# so it works as long as the main window is the active window.
# The diary_tab.save() method already handles the feedback (temporarily changing
# the button text to “✅ Saved”), so the user gets visual confirmation even
# if the diary tab is not visible.
# ══════════════════════════════════════════════════════════════════════════════
# APPLICATION ENTRY POINT
# ══════════════════════════════════════════════════════════════════════════════
def main() -> None:
app = QApplication(sys.argv) # Creates the Qt application object.
app.setApplicationName("Prayer Companion")
# Sets the application’s name.
app.setStyleSheet(STYLE)
# Applies the global stylesheet string STYLE to every widget in the application.
# This constant defines colours, fonts, borders, etc., that give the application a consistent look and feel.
win = MainWindow()
# Instantiates the main window (which itself creates all tabs, status bar, and shortcuts).
win.show()
# Makes the main window visible on screen.
# No call to showFullScreen() or showMaximized(),
# so it appears at the size set by resize(1100, 750) and at the window manager’s default position.
sys.exit(app.exec())
# Enters the Qt event loop.
# The application remains running until the user closes the main window.
# app.exec() returns an exit code that is passed to sys.exit() for a clean shutdown.
# Standard Python practice that prevents the main() function from running if the file is imported as a module.
# It makes the code import‑safe, though in this application the file is almost certainly executed directly.
if __name__ == "__main__":
main()