JustToThePoint English Website Version
JustToThePoint en español

Bible GUI. A Desktop Devotional Companion 3

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.

Geometrical analysis

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 &lt;script&gt; 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()
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.