Fix history pane, some small cleanups

This commit is contained in:
Miguel Jacq 2025-11-09 19:09:56 +11:00
parent f023224074
commit ab1af80d10
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
12 changed files with 47 additions and 114 deletions

View file

@ -1,3 +1,8 @@
# 0.2.1.1
* Fix history preview pane to be in markdown
* Some other code cleanups
# 0.2.1 # 0.2.1
* Introduce tabs! * Introduce tabs!

View file

@ -427,29 +427,6 @@ class DBManager:
cur.execute("SELECT sqlcipher_export('backup')") cur.execute("SELECT sqlcipher_export('backup')")
cur.execute("DETACH DATABASE backup") cur.execute("DETACH DATABASE backup")
def export_by_extension(self, file_path: str) -> None:
"""
Fallback catch-all that runs one of the above functions based on
the extension of the file name that was chosen by the user.
"""
entries = self.get_all_entries()
ext = os.path.splitext(file_path)[1].lower()
if ext == ".json":
self.export_json(entries, file_path)
elif ext == ".csv":
self.export_csv(entries, file_path)
elif ext == ".txt":
self.export_txt(entries, file_path)
elif ext in {".html", ".htm"}:
self.export_html(entries, file_path)
elif ext in {".sql", ".sqlite"}:
self.export_sql(file_path)
elif ext == ".md":
self.export_markdown(entries, file_path)
else:
raise ValueError(f"Unsupported extension: {ext}")
def compact(self) -> None: def compact(self) -> None:
""" """
Runs VACUUM on the db. Runs VACUUM on the db.

View file

@ -21,9 +21,8 @@ from PySide6.QtWidgets import (
class FindBar(QWidget): class FindBar(QWidget):
"""Widget for finding text in the Editor""" """Widget for finding text in the Editor"""
closed = ( # emitted when the bar is hidden (Esc/✕), so caller can refocus editor
Signal() closed = Signal()
) # emitted when the bar is hidden (Esc/✕), so caller can refocus editor
def __init__( def __init__(
self, self,
@ -45,7 +44,7 @@ class FindBar(QWidget):
layout.addWidget(QLabel("Find:")) layout.addWidget(QLabel("Find:"))
self.edit = QLineEdit(self) self.edit = QLineEdit(self)
self.edit.setPlaceholderText("Type to search") self.edit.setPlaceholderText("Type to search")
layout.addWidget(self.edit) layout.addWidget(self.edit)
self.case = QCheckBox("Match case", self) self.case = QCheckBox("Match case", self)
@ -79,7 +78,7 @@ class FindBar(QWidget):
@property @property
def editor(self) -> QTextEdit | None: def editor(self) -> QTextEdit | None:
"""Get the current editor (no side effects).""" """Get the current editor"""
return self._editor_getter() return self._editor_getter()
# ----- Public API ----- # ----- Public API -----

View file

@ -118,9 +118,9 @@ class HistoryDialog(QDialog):
return local.strftime("%Y-%m-%d %H:%M:%S %Z") return local.strftime("%Y-%m-%d %H:%M:%S %Z")
def _load_versions(self): def _load_versions(self):
self._versions = self._db.list_versions( # [{id,version_no,created_at,note,is_current}]
self._date self._versions = self._db.list_versions(self._date)
) # [{id,version_no,created_at,note,is_current}]
self._current_id = next( self._current_id = next(
(v["id"] for v in self._versions if v["is_current"]), None (v["id"] for v in self._versions if v["is_current"]), None
) )
@ -152,13 +152,8 @@ class HistoryDialog(QDialog):
self.btn_revert.setEnabled(False) self.btn_revert.setEnabled(False)
return return
sel_id = item.data(Qt.UserRole) sel_id = item.data(Qt.UserRole)
# Preview selected as plain text (markdown)
sel = self._db.get_version(version_id=sel_id) sel = self._db.get_version(version_id=sel_id)
# Show markdown as plain text with monospace font for better readability self.preview.setMarkdown(sel["content"])
self.preview.setPlainText(sel["content"])
self.preview.setStyleSheet(
"font-family: Consolas, Menlo, Monaco, monospace; font-size: 13px;"
)
# Diff vs current (textual diff) # Diff vs current (textual diff)
cur = self._db.get_version(version_id=self._current_id) cur = self._db.get_version(version_id=self._current_id)
self.diff.setHtml(_colored_unified_diff_html(cur["content"], sel["content"])) self.diff.setHtml(_colored_unified_diff_html(cur["content"], sel["content"]))

View file

@ -808,7 +808,7 @@ class MainWindow(QMainWindow):
def _adjust_today(self): def _adjust_today(self):
"""Jump to today.""" """Jump to today."""
today = QDate.currentDate() today = QDate.currentDate()
self.calendar.setSelectedDate(today) self._create_new_tab(today)
def _load_yesterday_todos(self): def _load_yesterday_todos(self):
try: try:
@ -1090,7 +1090,7 @@ If you want an encrypted backup, choose Backup instead of Export.
elif selected_filter.startswith("SQL"): elif selected_filter.startswith("SQL"):
self.db.export_sql(filename) self.db.export_sql(filename)
else: else:
self.db.export_by_extension(filename) raise ValueError("Unrecognised extension!")
QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}") QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}")
except Exception as e: except Exception as e:

View file

@ -112,7 +112,7 @@ class MarkdownHighlighter(QSyntaxHighlighter):
self.setCurrentBlockState(1 if in_code_block else 0) self.setCurrentBlockState(1 if in_code_block else 0)
# Format the fence markers - but keep them somewhat visible for editing # Format the fence markers - but keep them somewhat visible for editing
# Use code format instead of syntax format so cursor is visible # Use code format instead of syntax format so cursor is visible
self.setFormat(0, len(text), self.code_block_format) self.setFormat(0, len(text), self.code_format)
return return
if in_code_block: if in_code_block:
@ -258,13 +258,13 @@ class MarkdownEditor(QTextEdit):
# Transform only this line: # Transform only this line:
# - "TODO " at start (with optional indent) -> "- ☐ " # - "TODO " at start (with optional indent) -> "- ☐ "
# - "- [ ] " -> "- ☐ " and "- [x] " -> "- ☑ " # - "- [ ] " -> " ☐ " and "- [x] " -> " ☑ "
def transform_line(s: str) -> str: def transform_line(s: str) -> str:
s = s.replace("- [x] ", f"- {self._CHECK_CHECKED_DISPLAY} ") s = s.replace("- [x] ", f"{self._CHECK_CHECKED_DISPLAY} ")
s = s.replace("- [ ] ", f"- {self._CHECK_UNCHECKED_DISPLAY} ") s = s.replace("- [ ] ", f"{self._CHECK_UNCHECKED_DISPLAY} ")
s = re.sub( s = re.sub(
r"^([ \t]*)TODO\b[:\-]?\s+", r"^([ \t]*)TODO\b[:\-]?\s+",
lambda m: f"{m.group(1)}- {self._CHECK_UNCHECKED_DISPLAY} ", lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
s, s,
) )
return s return s
@ -293,8 +293,8 @@ class MarkdownEditor(QTextEdit):
text = self._extract_images_to_markdown() text = self._extract_images_to_markdown()
# Convert Unicode checkboxes back to markdown syntax # Convert Unicode checkboxes back to markdown syntax
text = text.replace(f"- {self._CHECK_CHECKED_DISPLAY} ", "- [x] ") text = text.replace(f"{self._CHECK_CHECKED_DISPLAY} ", "- [x] ")
text = text.replace(f"- {self._CHECK_UNCHECKED_DISPLAY} ", "- [ ] ") text = text.replace(f"{self._CHECK_UNCHECKED_DISPLAY} ", "- [ ] ")
return text return text
@ -336,15 +336,15 @@ class MarkdownEditor(QTextEdit):
"""Load markdown text into the editor (convert markdown checkboxes to Unicode).""" """Load markdown text into the editor (convert markdown checkboxes to Unicode)."""
# Convert markdown checkboxes to Unicode for display # Convert markdown checkboxes to Unicode for display
display_text = markdown_text.replace( display_text = markdown_text.replace(
"- [x] ", f"- {self._CHECK_CHECKED_DISPLAY} " "- [x] ", f"{self._CHECK_CHECKED_DISPLAY} "
) )
display_text = display_text.replace( display_text = display_text.replace(
"- [ ] ", f"- {self._CHECK_UNCHECKED_DISPLAY} " "- [ ] ", f"{self._CHECK_UNCHECKED_DISPLAY} "
) )
# Also convert any plain 'TODO ' at the start of a line to an unchecked checkbox # Also convert any plain 'TODO ' at the start of a line to an unchecked checkbox
display_text = re.sub( display_text = re.sub(
r"(?m)^([ \t]*)TODO\s", r"(?m)^([ \t]*)TODO\s",
lambda m: f"{m.group(1)}- {self._CHECK_UNCHECKED_DISPLAY} ", lambda m: f"{m.group(1)}\n{self._CHECK_UNCHECKED_DISPLAY} ",
display_text, display_text,
) )
@ -425,10 +425,10 @@ class MarkdownEditor(QTextEdit):
line = line.lstrip() line = line.lstrip()
# Checkbox list (Unicode display format) # Checkbox list (Unicode display format)
if line.startswith(f"- {self._CHECK_UNCHECKED_DISPLAY} ") or line.startswith( if line.startswith(f"{self._CHECK_UNCHECKED_DISPLAY} ") or line.startswith(
f"- {self._CHECK_CHECKED_DISPLAY} " f"{self._CHECK_CHECKED_DISPLAY} "
): ):
return ("checkbox", f"- {self._CHECK_UNCHECKED_DISPLAY} ") return ("checkbox", f"{self._CHECK_UNCHECKED_DISPLAY} ")
# Bullet list # Bullet list
if re.match(r"^[-*+]\s", line): if re.match(r"^[-*+]\s", line):
@ -533,19 +533,19 @@ class MarkdownEditor(QTextEdit):
# Check if clicking on a checkbox line # Check if clicking on a checkbox line
if ( if (
f"- {self._CHECK_UNCHECKED_DISPLAY} " in line f"{self._CHECK_UNCHECKED_DISPLAY} " in line
or f"- {self._CHECK_CHECKED_DISPLAY} " in line or f"{self._CHECK_CHECKED_DISPLAY} " in line
): ):
# Toggle the checkbox # Toggle the checkbox
if f"- {self._CHECK_UNCHECKED_DISPLAY} " in line: if f"{self._CHECK_UNCHECKED_DISPLAY} " in line:
new_line = line.replace( new_line = line.replace(
f"- {self._CHECK_UNCHECKED_DISPLAY} ", f"{self._CHECK_UNCHECKED_DISPLAY} ",
f"- {self._CHECK_CHECKED_DISPLAY} ", f"{self._CHECK_CHECKED_DISPLAY} ",
) )
else: else:
new_line = line.replace( new_line = line.replace(
f"- {self._CHECK_CHECKED_DISPLAY} ", f"{self._CHECK_CHECKED_DISPLAY} ",
f"- {self._CHECK_UNCHECKED_DISPLAY} ", f"{self._CHECK_UNCHECKED_DISPLAY} ",
) )
cursor.insertText(new_line) cursor.insertText(new_line)
@ -745,18 +745,18 @@ class MarkdownEditor(QTextEdit):
# Check if already has checkbox (Unicode display format) # Check if already has checkbox (Unicode display format)
if ( if (
f"- {self._CHECK_UNCHECKED_DISPLAY} " in line f"{self._CHECK_UNCHECKED_DISPLAY} " in line
or f"- {self._CHECK_CHECKED_DISPLAY} " in line or f"{self._CHECK_CHECKED_DISPLAY} " in line
): ):
# Remove checkbox - use raw string to avoid escape sequence warning # Remove checkbox - use raw string to avoid escape sequence warning
new_line = re.sub( new_line = re.sub(
rf"^\s*-\s*[{self._CHECK_UNCHECKED_DISPLAY}{self._CHECK_CHECKED_DISPLAY}]\s+", rf"^\s*[{self._CHECK_UNCHECKED_DISPLAY}{self._CHECK_CHECKED_DISPLAY}]\s+",
"", "",
line, line,
) )
else: else:
# Add checkbox (Unicode display format) # Add checkbox (Unicode display format)
new_line = f"- {self._CHECK_UNCHECKED_DISPLAY} " + line.lstrip() new_line = f"{self._CHECK_UNCHECKED_DISPLAY} " + line.lstrip()
cursor.insertText(new_line) cursor.insertText(new_line)

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "bouquin" name = "bouquin"
version = "0.2.1" version = "0.2.1.1"
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
authors = ["Miguel Jacq <mig@mig5.net>"] authors = ["Miguel Jacq <mig@mig5.net>"]
readme = "README.md" readme = "README.md"

View file

@ -1,6 +1,8 @@
import pytest
import json, csv import json, csv
import datetime as dt import datetime as dt
from bouquin.db import DBManager
def _today(): def _today():
return dt.date.today().isoformat() return dt.date.today().isoformat()
@ -57,7 +59,7 @@ def test_dates_with_content_and_search(fresh_db):
assert any(d == _tomorrow() for d, _ in hits) assert any(d == _tomorrow() for d, _ in hits)
def test_get_all_entries_and_export_by_extension(fresh_db, tmp_path): def test_get_all_entries_and_export(fresh_db, tmp_path):
for i in range(3): for i in range(3):
d = (dt.date.today() - dt.timedelta(days=i)).isoformat() d = (dt.date.today() - dt.timedelta(days=i)).isoformat()
fresh_db.save_new_version(d, _entry(f"note {i}"), f"note {i}") fresh_db.save_new_version(d, _entry(f"note {i}"), f"note {i}")
@ -93,18 +95,12 @@ def test_get_all_entries_and_export_by_extension(fresh_db, tmp_path):
fresh_db.export_sqlcipher(str(sqlc_path)) fresh_db.export_sqlcipher(str(sqlc_path))
assert sqlc_path.exists() and sqlc_path.read_bytes() assert sqlc_path.exists() and sqlc_path.read_bytes()
for path in [json_path, csv_path, txt_path, md_path, html_path, sql_path]:
path.unlink(missing_ok=True)
fresh_db.export_by_extension(str(path))
assert path.exists()
def test_rekey_and_reopen(fresh_db, tmp_db_cfg): def test_rekey_and_reopen(fresh_db, tmp_db_cfg):
fresh_db.save_new_version(_today(), _entry("secure"), "before rekey") fresh_db.save_new_version(_today(), _entry("secure"), "before rekey")
fresh_db.rekey("new-key-123") fresh_db.rekey("new-key-123")
fresh_db.close() fresh_db.close()
from bouquin.db import DBManager
tmp_db_cfg.key = "new-key-123" tmp_db_cfg.key = "new-key-123"
db2 = DBManager(tmp_db_cfg) db2 = DBManager(tmp_db_cfg)
@ -116,12 +112,3 @@ def test_rekey_and_reopen(fresh_db, tmp_db_cfg):
def test_compact_and_close_dont_crash(fresh_db): def test_compact_and_close_dont_crash(fresh_db):
fresh_db.compact() fresh_db.compact()
fresh_db.close() fresh_db.close()
import pytest
def test_export_by_extension_unsupported(fresh_db, tmp_path):
p = tmp_path / "export.xyz"
with pytest.raises(ValueError):
fresh_db.export_by_extension(str(p))

View file

@ -3,7 +3,7 @@ import pytest
from PySide6.QtGui import QTextCursor from PySide6.QtGui import QTextCursor
from bouquin.markdown_editor import MarkdownEditor from bouquin.markdown_editor import MarkdownEditor
from bouquin.theme import ThemeManager, ThemeConfig, Theme from bouquin.theme import ThemeManager, ThemeConfig, Theme
from bouquin.find_bar import FindBar
@pytest.fixture @pytest.fixture
def editor(app, qtbot): def editor(app, qtbot):
@ -14,9 +14,6 @@ def editor(app, qtbot):
return ed return ed
from bouquin.find_bar import FindBar
@pytest.mark.gui @pytest.mark.gui
def test_findbar_basic_navigation(qtbot, editor): def test_findbar_basic_navigation(qtbot, editor):
editor.from_markdown("alpha\nbeta\nalpha\nGamma\n") editor.from_markdown("alpha\nbeta\nalpha\nGamma\n")
@ -42,7 +39,6 @@ def test_findbar_basic_navigation(qtbot, editor):
def test_show_bar_seeds_selection(qtbot, editor): def test_show_bar_seeds_selection(qtbot, editor):
from PySide6.QtGui import QTextCursor
editor.from_markdown("alpha beta") editor.from_markdown("alpha beta")
c = editor.textCursor() c = editor.textCursor()

View file

@ -8,7 +8,6 @@ from bouquin.key_prompt import KeyPrompt
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication
@pytest.mark.gui @pytest.mark.gui
def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db): def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
s = get_settings() s = get_settings()
@ -52,10 +51,6 @@ def test_main_window_loads_and_saves(qtbot, app, tmp_db_cfg, fresh_db):
def test_load_yesterday_todos_moves_items(qtbot, app, tmp_db_cfg, fresh_db): def test_load_yesterday_todos_moves_items(qtbot, app, tmp_db_cfg, fresh_db):
from PySide6.QtCore import QDate
from bouquin.theme import ThemeManager, ThemeConfig, Theme
from bouquin.settings import get_settings
s = get_settings() s = get_settings()
s.setValue("db/path", str(tmp_db_cfg.path)) s.setValue("db/path", str(tmp_db_cfg.path))
s.setValue("db/key", tmp_db_cfg.key) s.setValue("db/key", tmp_db_cfg.key)
@ -66,7 +61,6 @@ def test_load_yesterday_todos_moves_items(qtbot, app, tmp_db_cfg, fresh_db):
fresh_db.save_new_version(y, "- [ ] carry me\n- [x] done", "seed") fresh_db.save_new_version(y, "- [ ] carry me\n- [x] done", "seed")
themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
from bouquin.main_window import MainWindow
w = MainWindow(themes=themes) w = MainWindow(themes=themes)
qtbot.addWidget(w) qtbot.addWidget(w)

View file

@ -1,10 +1,11 @@
import pytest import pytest
from bouquin.db import DBManager, DBConfig
from bouquin.key_prompt import KeyPrompt
from bouquin.settings_dialog import SettingsDialog from bouquin.settings_dialog import SettingsDialog
from bouquin.theme import ThemeManager, ThemeConfig, Theme from bouquin.theme import ThemeManager, ThemeConfig, Theme
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
@pytest.mark.gui @pytest.mark.gui
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path): def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path):
# Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...)) # Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
@ -38,12 +39,6 @@ def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path)
def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app): def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox
from bouquin.key_prompt import KeyPrompt
from bouquin.theme import ThemeManager, ThemeConfig, Theme
from PySide6.QtWidgets import QWidget
parent = QWidget() parent = QWidget()
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT)) parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
@ -81,12 +76,6 @@ def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app):
def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app): def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
from bouquin.key_prompt import KeyPrompt
from bouquin.db import DBManager, DBConfig
from bouquin.theme import ThemeManager, ThemeConfig, Theme
cfg = DBConfig( cfg = DBConfig(
path=tmp_path / "iso.db", path=tmp_path / "iso.db",
key="oldkey", key="oldkey",
@ -129,12 +118,6 @@ def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app):
def test_change_key_success(qtbot, tmp_path, app): def test_change_key_success(qtbot, tmp_path, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
from bouquin.key_prompt import KeyPrompt
from bouquin.db import DBManager, DBConfig
from bouquin.theme import ThemeManager, ThemeConfig, Theme
cfg = DBConfig( cfg = DBConfig(
path=tmp_path / "iso2.db", path=tmp_path / "iso2.db",
key="oldkey", key="oldkey",

View file

@ -2,6 +2,7 @@ import pytest
from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QWidget
from bouquin.markdown_editor import MarkdownEditor from bouquin.markdown_editor import MarkdownEditor
from bouquin.theme import ThemeManager, ThemeConfig, Theme from bouquin.theme import ThemeManager, ThemeConfig, Theme
from bouquin.toolbar import ToolBar
@pytest.fixture @pytest.fixture
@ -12,10 +13,6 @@ def editor(app, qtbot):
ed.show() ed.show()
return ed return ed
from bouquin.toolbar import ToolBar
@pytest.mark.gui @pytest.mark.gui
def test_toolbar_signals_and_styling(qtbot, editor): def test_toolbar_signals_and_styling(qtbot, editor):
host = QWidget() host = QWidget()