Compare commits

...

7 commits

Author SHA1 Message Date
81cf878ffd
0.5.2
Some checks failed
Lint / test (push) Successful in 33s
Trivy / test (push) Successful in 23s
CI / test (push) Failing after 5m24s
2025-11-27 09:57:49 +11:00
73aa536a32
Small README tweak 2025-11-27 09:57:39 +11:00
219b358569
remove problematic interactive test 2025-11-27 09:57:26 +11:00
5a54d809ed
Fix HTML export of markdown (with newlines, tables and other styling preserved) 2025-11-27 09:57:14 +11:00
917e51aa0b
Use same alarm icon for the Reminders sidebar widget 2025-11-27 09:56:29 +11:00
576dc435ef
Adjust History icon and reorder toolbar items. Try to address checkbox/bullet size issues (again) 2025-11-27 09:45:15 +11:00
fdc72a1146
Update icon again to remove background 2025-11-27 08:55:28 +11:00
11 changed files with 108 additions and 67 deletions

View file

@ -1,3 +1,10 @@
# 0.5.2
* Update icon again to remove background
* Adjust History icon and reorder toolbar items
* Try to address checkbox/bullet size issues (again)
* Fix HTML export of markdown (with newlines, tables and other styling preserved)
# 0.5.1
* Try to address Noto Sans font issue that works for both numbers and checkbox/bullets.

View file

@ -6,7 +6,7 @@
## Introduction
Bouquin ("Book-ahn") is a notebook and planner application written in Python, PyQt and SQLCipher.
Bouquin ("Book-ahn") is a notebook and planner application written in Python, Qt and SQLCipher.
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
search, reminders and time logging for those of us who need to keep track of not just TODOs, but

View file

@ -5,6 +5,7 @@ import datetime as _dt
import hashlib
import html
import json
import markdown
import re
from dataclasses import dataclass
@ -440,14 +441,33 @@ class DBManager:
'<html lang="en">',
'<meta charset="utf-8">',
f"<title>{html.escape(title)}</title>",
"<style>body{font:16px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;padding:24px;max-width:900px;margin:auto;}",
"article{padding:16px 0;border-bottom:1px solid #ddd;} time{font-weight:600;color:#333;} section{margin-top:8px;}</style>",
"<style>"
"body{font:16px/1.5 system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;"
"padding:24px;max-width:900px;margin:auto;}"
"article{padding:16px 0;border-bottom:1px solid #ddd;}"
"article header time{font-weight:600;color:#333;}"
"section{margin-top:8px;}"
"table{border-collapse:collapse;margin-top:8px;}"
"th,td{border:1px solid #ddd;padding:4px 8px;text-align:left;}"
"</style>",
"<body>",
f"<h1>{html.escape(title)}</h1>",
]
for d, c in entries:
body_html = markdown.markdown(
c,
extensions=[
"extra",
"nl2br",
],
output_format="html5",
)
parts.append(
f"<article><header><time>{html.escape(d)}</time></header><section>{c}</section></article>"
f"<article>"
f"<header><time>{html.escape(d)}</time></header>"
f"<section>{body_html}</section>"
f"</article>"
)
parts.append("</body></html>")

View file

@ -4,21 +4,10 @@
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Rounded square background tile -->
<rect
x="0"
y="0"
width="512"
height="512"
rx="112"
ry="112"
fill="#0F172A"
/>
<!-- Book cover -->
<rect
x="116"
y="92"
y="76"
width="280"
height="360"
rx="48"
@ -29,7 +18,7 @@
<!-- Book spine -->
<rect
x="116"
y="92"
y="76"
width="64"
height="360"
rx="40"
@ -39,14 +28,14 @@
<!-- Folded page corner (top-right triangle) -->
<path
d="M396 92 L356 92 L396 132 Z"
d="M396 76 L356 76 L396 116 Z"
fill="#FEF9C3"
/>
<!-- Keyhole: circular top -->
<circle
cx="256"
cy="260"
cy="256"
r="34"
fill="#0F172A"
/>

Before

Width:  |  Height:  |  Size: 887 B

After

Width:  |  Height:  |  Size: 733 B

Before After
Before After

View file

@ -119,6 +119,22 @@ class MarkdownEditor(QTextEdit):
self._apply_code_block_spacing()
QTimer.singleShot(0, self._update_code_block_row_backgrounds)
def setFont(self, font: QFont) -> None: # type: ignore[override]
"""
Ensure that whenever the base editor font changes, our highlighter
re-computes checkbox / bullet formats.
"""
# Keep qfont in sync
self.qfont = QFont(font)
super().setFont(self.qfont)
# If the highlighter is already attached, let it rebuild its formats
highlighter = getattr(self, "highlighter", None)
if highlighter is not None:
refresh = getattr(highlighter, "refresh_for_font_change", None)
if callable(refresh):
refresh()
def showEvent(self, e):
super().showEvent(e)
# First time the widget is shown, Qt may rebuild layout once more.

View file

@ -6,6 +6,7 @@ from PySide6.QtGui import (
QColor,
QFont,
QFontDatabase,
QFontMetrics,
QGuiApplication,
QPalette,
QSyntaxHighlighter,
@ -33,6 +34,14 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self._setup_formats()
self.rehighlight()
def refresh_for_font_change(self) -> None:
"""
Called when the editor's base font changes (zoom / settings).
It rebuilds any formats that depend on the editor font metrics.
"""
self._setup_formats()
self.rehighlight()
def _setup_formats(self):
"""Setup text formats for different markdown elements."""
@ -110,8 +119,21 @@ class MarkdownHighlighter(QSyntaxHighlighter):
# Use Symbols font for checkbox and bullet glyphs if present
if self._editor is not None and hasattr(self._editor, "symbols_font_family"):
base_size = self._editor.qfont.pointSize()
symbols_font = QFont(self._editor.symbols_font_family, base_size)
base_font = QFont(self._editor.qfont) # copy of editor font
symbols_font = QFont(self._editor.symbols_font_family)
symbols_font.setPointSizeF(base_font.pointSizeF())
base_metrics = QFontMetrics(base_font)
sym_metrics = QFontMetrics(symbols_font)
# If Symbols glyphs are noticeably shorter than the text,
# scale them up so the visual heights roughly match.
if sym_metrics.height() > 0:
ratio = base_metrics.height() / sym_metrics.height()
if ratio > 1.05: # more than ~5% smaller
ratio = min(ratio, 1.4) # Oh, Tod, Tod. Don't overdo it.
symbols_font.setPointSizeF(symbols_font.pointSizeF() * ratio)
self.checkbox_format.setFont(symbols_font)
self.bullet_format.setFont(symbols_font)

View file

@ -191,7 +191,7 @@ class UpcomingRemindersWidget(QFrame):
self.toggle_btn.clicked.connect(self._on_toggle)
self.add_btn = QToolButton()
self.add_btn.setIcon(self.style().standardIcon(QStyle.SP_FileDialogNewFolder))
self.add_btn.setText("")
self.add_btn.setToolTip("Add Reminder")
self.add_btn.setAutoRaise(True)
self.add_btn.clicked.connect(self._add_reminder)

View file

@ -100,6 +100,17 @@ class ToolBar(QToolBar):
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
# Images
self.actInsertImg = QAction("📸", self)
self.actInsertImg.setToolTip(strings._("insert_images"))
self.actInsertImg.setShortcut("Ctrl+Shift+I")
self.actInsertImg.triggered.connect(self.insertImageRequested)
# History button
self.actHistory = QAction("🔁", self)
self.actHistory.setToolTip(strings._("history"))
self.actHistory.triggered.connect(self.historyRequested)
# Alarm / reminder
self.actAlarm = QAction("", self)
self.actAlarm.setToolTip(strings._("toolbar_alarm"))
@ -115,17 +126,6 @@ class ToolBar(QToolBar):
self.actTable.setToolTip(strings._("toolbar_insert_table"))
self.actTable.triggered.connect(self.tableRequested)
# Images
self.actInsertImg = QAction("📸", self)
self.actInsertImg.setToolTip(strings._("insert_images"))
self.actInsertImg.setShortcut("Ctrl+Shift+I")
self.actInsertImg.triggered.connect(self.insertImageRequested)
# History button
self.actHistory = QAction("", self)
self.actHistory.setToolTip(strings._("history"))
self.actHistory.triggered.connect(self.historyRequested)
# Set exclusive buttons in QActionGroups
self.grpHeadings = QActionGroup(self)
self.grpHeadings.setExclusive(True)
@ -162,10 +162,10 @@ class ToolBar(QToolBar):
self.actBullets,
self.actNumbers,
self.actCheckboxes,
self.actAlarm,
self.actTimer,
self.actTable,
self.actInsertImg,
self.actAlarm,
self.actTimer,
self.actHistory,
]
)
@ -195,7 +195,7 @@ class ToolBar(QToolBar):
self._style_letter_button(self.actTable, "")
# History
self._style_letter_button(self.actHistory, "")
self._style_letter_button(self.actHistory, "🔁")
def _style_letter_button(
self,

17
poetry.lock generated
View file

@ -307,6 +307,21 @@ files = [
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "markdown"
version = "3.10"
description = "Python implementation of John Gruber's Markdown."
optional = false
python-versions = ">=3.10"
files = [
{file = "markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c"},
{file = "markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e"},
]
[package.extras]
docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "packaging"
version = "25.0"
@ -744,4 +759,4 @@ zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.14"
content-hash = "d5fd8ea759b6bd3f23336930bdce9241659256ed918ec31746787cc86e817235"
content-hash = "86db2b4d6498ce9eba7fa9aedf9ce58fec6a63542b5f3bdb3cf6c4067e5b87df"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "bouquin"
version = "0.5.1"
version = "0.5.2"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md"
@ -14,6 +14,7 @@ python = ">=3.10,<3.14"
pyside6 = ">=6.8.1,<7.0.0"
sqlcipher3-wheels = "^0.5.5.post0"
requests = "^2.32.5"
markdown = "^3.10"
[tool.poetry.scripts]
bouquin = "bouquin.__main__:main"

View file

@ -765,35 +765,6 @@ def test_reminder_not_today_skipped(qtbot, fresh_db):
assert len(triggered_texts) == 0
def test_reminder_context_menu_single_item(qtbot, fresh_db):
"""Test context menu for a single reminder item."""
reminder = Reminder(
id=None,
text="Test reminder",
reminder_type=ReminderType.ONCE,
time_str="14:30",
date_iso=date.today().isoformat(),
active=True,
)
fresh_db.save_reminder(reminder)
reminders_widget = UpcomingRemindersWidget(fresh_db)
qtbot.addWidget(reminders_widget)
reminders_widget.show()
# Refresh to populate the list
reminders_widget.refresh()
# Select the first item
if reminders_widget.reminder_list.count() > 0:
reminders_widget.reminder_list.setCurrentRow(0)
# Show context menu (won't actually display in tests)
reminders_widget._show_reminder_context_menu(
reminders_widget.reminder_list.pos()
)
def test_reminder_context_menu_no_selection(qtbot, fresh_db):
"""Test context menu with no selection returns early."""
reminders_widget = UpcomingRemindersWidget(fresh_db)