diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..07944f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# 0.1.1 + + * Add ability to change the key + * Add ability to jump to today's date + * Add shortcut for Settings (Ctrl+E) so as not to collide with Ctrl+S (Save) + +# 0.1.0 + + * Initial release. diff --git a/README.md b/README.md index 8b20e14..b874668 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ There is deliberately no network connectivity or syncing intended. * Search * Taxonomy/tagging - * Ability to change the SQLCipher key * Export to other formats (plaintext, json, sql etc) diff --git a/bouquin/db.py b/bouquin/db.py index 1ea60fa..15cc4c9 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -64,6 +64,25 @@ class DBManager: cur.execute("PRAGMA user_version = 1;") self.conn.commit() + def rekey(self, new_key: str) -> None: + """ + Change the SQLCipher passphrase in-place, then reopen the connection + with the new key to verify. + """ + if self.conn is None: + raise RuntimeError("Database is not connected") + cur = self.conn.cursor() + # Change the encryption key of the currently open database + cur.execute(f"PRAGMA rekey = '{new_key}';") + self.conn.commit() + + # Close and reopen with the new key to verify and restore PRAGMAs + self.conn.close() + self.conn = None + self.cfg.key = new_key + if not self.connect(): + raise sqlite.Error("Re-open failed after rekey") + def get_entry(self, date_iso: str) -> str: cur = self.conn.cursor() cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,)) diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 394ccb9..6b0451c 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -72,7 +72,8 @@ class MainWindow(QMainWindow): act_save.setShortcut("Ctrl+S") act_save.triggered.connect(lambda: self._save_current(explicit=True)) file_menu.addAction(act_save) - act_settings = QAction("&Settings", self) + act_settings = QAction("S&ettings", self) + act_save.setShortcut("Ctrl+E") act_settings.triggered.connect(self._open_settings) file_menu.addAction(act_settings) file_menu.addSeparator() @@ -97,6 +98,13 @@ class MainWindow(QMainWindow): nav_menu.addAction(act_next) self.addAction(act_next) + act_today = QAction("Today", self) + act_today.setShortcut("Ctrl+T") + act_today.setShortcutContext(Qt.ApplicationShortcut) + act_today.triggered.connect(self._adjust_today) + nav_menu.addAction(act_today) + self.addAction(act_today) + # Autosave self._dirty = False self._save_timer = QTimer(self) @@ -176,6 +184,11 @@ class MainWindow(QMainWindow): d = self.calendar.selectedDate().addDays(delta) self.calendar.setSelectedDate(d) + def _adjust_today(self): + """Jump to today.""" + today = QDate.currentDate() + self.calendar.setSelectedDate(today) + def _on_date_changed(self): """ When the calendar selection changes, save the previous day's note if dirty, @@ -219,7 +232,7 @@ class MainWindow(QMainWindow): self._save_date(self._current_date_iso(), explicit) def _open_settings(self): - dlg = SettingsDialog(self.cfg, self) + dlg = SettingsDialog(self.cfg, self.db, self) if dlg.exec() == QDialog.Accepted: new_cfg = dlg.config if new_cfg.path != self.cfg.path: diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index 790c4e0..ca2514c 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -13,17 +13,20 @@ from PySide6.QtWidgets import ( QFileDialog, QDialogButtonBox, QSizePolicy, + QMessageBox, ) -from .db import DBConfig +from .db import DBConfig, DBManager from .settings import save_db_config +from .key_prompt import KeyPrompt class SettingsDialog(QDialog): - def __init__(self, cfg: DBConfig, parent=None): + def __init__(self, cfg: DBConfig, db: DBManager, parent=None): super().__init__(parent) self.setWindowTitle("Settings") self._cfg = DBConfig(path=cfg.path, key="") + self._db = db form = QFormLayout() form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) @@ -44,12 +47,17 @@ class SettingsDialog(QDialog): h.setStretch(1, 0) form.addRow("Database path", path_row) + # Change key button + self.rekey_btn = QPushButton("Change key") + self.rekey_btn.clicked.connect(self._change_key) + bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) bb.accepted.connect(self._save) bb.rejected.connect(self.reject) v = QVBoxLayout(self) v.addLayout(form) + v.addWidget(self.rekey_btn) v.addWidget(bb) def _browse(self): @@ -67,6 +75,26 @@ class SettingsDialog(QDialog): save_db_config(self._cfg) self.accept() + def _change_key(self): + p1 = KeyPrompt(self, title="Change key", message="Enter new key") + if p1.exec() != QDialog.Accepted: + return + new_key = p1.key() + p2 = KeyPrompt(self, title="Change key", message="Re-enter new key") + if p2.exec() != QDialog.Accepted: + return + if new_key != p2.key(): + QMessageBox.warning(self, "Key mismatch", "The two entries did not match.") + return + if not new_key: + QMessageBox.warning(self, "Empty key", "Key cannot be empty.") + return + try: + self._db.rekey(new_key) + QMessageBox.information(self, "Key changed", "The database key was updated.") + except Exception as e: + QMessageBox.critical(self, "Error", f"Could not change key:\n{e}") + @property def config(self) -> DBConfig: return self._cfg diff --git a/pyproject.toml b/pyproject.toml index 1be51b7..2be1386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.1.0" +version = "0.1.1" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md"