diff --git a/CHANGELOG.md b/CHANGELOG.md
index 07944f2..1fe1e6f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,19 @@
+# 0.1.3
+
+ * Fix bold toggle
+ * Improvements to preview size in search results
+ * Make URLs highlighted and clickable (Ctrl+click)
+ * Explain the purpose of the encryption key for first-time use
+ * Support saving the encryption key to the settings file to avoid being prompted (off by default)
+ * Abbreviated toolbar symbols to keep things tidier. Add tooltips
+ * Add ability to export the database to different formats
+
+# 0.1.2
+
+ * Switch from Markdown to HTML via QTextEdit, with a toolbar
+ * Add search ability
+ * Fix Settings shortcut and change nav menu from 'File' to 'Application'
+
# 0.1.1
* Add ability to change the key
diff --git a/README.md b/README.md
index b874668..4bf621a 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a dr
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
To increase security, the SQLCipher key is requested when the app is opened, and is not written
-to disk.
+to disk unless the user configures it to be in the settings.
There is deliberately no network connectivity or syncing intended.
@@ -19,23 +19,21 @@ There is deliberately no network connectivity or syncing intended.
## Features
+ * Data is encrypted at rest
+ * Encryption key is prompted for and never stored, unless user chooses to via Settings
* Every 'page' is linked to the calendar day
- * Basic markdown
- * Automatic periodic saving (or explicitly save)
- * Navigating from one day to the next automatically saves
- * Basic keyboard shortcuts
- * Transparent integrity checking of the database when it opens
-
-
-## Yet to do
-
+ * Text is HTML with basic styling
* Search
- * Taxonomy/tagging
- * Export to other formats (plaintext, json, sql etc)
+ * Automatic periodic saving (or explicitly save)
+ * Transparent integrity checking of the database when it opens
+ * Rekey the database (change the password)
+ * Export the database to json, txt, html or csv
## How to install
+Make sure you have `libxcb-cursor0` installed (it may be called something else on non-Debian distributions).
+
### From source
* Clone this repo or download the tarball from the releases page
@@ -47,7 +45,7 @@ There is deliberately no network connectivity or syncing intended.
* Download the whl and run it
-### From PyPi
+### From PyPi/pip
* `pip install bouquin`
diff --git a/bouquin/db.py b/bouquin/db.py
index 15cc4c9..90aca09 100644
--- a/bouquin/db.py
+++ b/bouquin/db.py
@@ -1,9 +1,16 @@
from __future__ import annotations
+import csv
+import html
+import json
+import os
+
from dataclasses import dataclass
from pathlib import Path
-
from sqlcipher3 import dbapi2 as sqlite
+from typing import List, Sequence, Tuple
+
+Entry = Tuple[str, str]
@dataclass
@@ -21,9 +28,9 @@ class DBManager:
# Ensure parent dir exists
self.cfg.path.parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite.connect(str(self.cfg.path))
+ self.conn.row_factory = sqlite.Row
cur = self.conn.cursor()
cur.execute(f"PRAGMA key = '{self.cfg.key}';")
- cur.execute("PRAGMA cipher_compatibility = 4;")
cur.execute("PRAGMA journal_mode = WAL;")
self.conn.commit()
try:
@@ -100,11 +107,119 @@ class DBManager:
)
self.conn.commit()
+ def search_entries(self, text: str) -> list[str]:
+ cur = self.conn.cursor()
+ pattern = f"%{text}%"
+ return cur.execute(
+ "SELECT * FROM entries WHERE TRIM(content) LIKE ?", (pattern,)
+ ).fetchall()
+
def dates_with_content(self) -> list[str]:
cur = self.conn.cursor()
cur.execute("SELECT date FROM entries WHERE TRIM(content) <> '';")
return [r[0] for r in cur.fetchall()]
+ def get_all_entries(self) -> List[Entry]:
+ cur = self.conn.cursor()
+ rows = cur.execute("SELECT date, content FROM entries ORDER BY date").fetchall()
+ return [(row["date"], row["content"]) for row in rows]
+
+ def export_json(
+ self, entries: Sequence[Entry], file_path: str, pretty: bool = True
+ ) -> None:
+ data = [{"date": d, "content": c} for d, c in entries]
+ with open(file_path, "w", encoding="utf-8") as f:
+ if pretty:
+ json.dump(data, f, ensure_ascii=False, indent=2)
+ else:
+ json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
+
+ def export_csv(self, entries: Sequence[Entry], file_path: str) -> None:
+ # utf-8-sig adds a BOM so Excel opens as UTF-8 by default.
+ with open(file_path, "w", encoding="utf-8-sig", newline="") as f:
+ writer = csv.writer(f)
+ writer.writerow(["date", "content"]) # header
+ writer.writerows(entries)
+
+ def export_txt(
+ self,
+ entries: Sequence[Entry],
+ file_path: str,
+ separator: str = "\n\n— — — — —\n\n",
+ strip_html: bool = True,
+ ) -> None:
+ import re, html as _html
+
+ # Precompiled patterns
+ STYLE_SCRIPT_RE = re.compile(r"(?is)<(script|style)[^>]*>.*?\1>")
+ COMMENT_RE = re.compile(r"", re.S)
+ BR_RE = re.compile(r"(?i)
")
+ BLOCK_END_RE = re.compile(r"(?i)(p|div|section|article|li|h[1-6])\\s*>")
+ TAG_RE = re.compile(r"<[^>]+>")
+ WS_ENDS_RE = re.compile(r"[ \\t]+\\n")
+ MULTINEWLINE_RE = re.compile(r"\\n{3,}")
+
+ def _strip(s: str) -> str:
+ # 1) Remove ",
+ "