From 43bbe971eb855ece25b9e6b3e28307cba8a3b0e3 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 2 Nov 2025 11:44:22 +1100 Subject: [PATCH] Add ability to save the key to avoid being prompted for it --- README.md | 4 +- bouquin/editor.py | 14 ++++--- bouquin/main_window.py | 18 +++++---- bouquin/settings.py | 4 +- bouquin/settings_dialog.py | 77 +++++++++++++++++++++++++++++++++++--- bouquin/toolbar.py | 2 +- bouquin/url_highlighter.py | 25 ------------- 7 files changed, 98 insertions(+), 46 deletions(-) delete mode 100644 bouquin/url_highlighter.py diff --git a/README.md b/README.md index 3307543..94d0648 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. @@ -35,6 +35,8 @@ There is deliberately no network connectivity or syncing intended. ## 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 diff --git a/bouquin/editor.py b/bouquin/editor.py index 1f7e132..eb3b664 100644 --- a/bouquin/editor.py +++ b/bouquin/editor.py @@ -1,7 +1,6 @@ from __future__ import annotations from PySide6.QtGui import ( - QBrush, QColor, QDesktopServices, QFont, @@ -14,7 +13,6 @@ from PySide6.QtGui import ( from PySide6.QtCore import Qt, QUrl, Signal, Slot, QRegularExpression from PySide6.QtWidgets import QTextEdit -from .url_highlighter import UrlHighlighter class Editor(QTextEdit): linkActivated = Signal(str) @@ -55,13 +53,13 @@ class Editor(QTextEdit): while it.hasNext(): m = it.next() start = block.position() + m.capturedStart() - end = start + m.capturedLength() + end = start + m.capturedLength() cur.setPosition(start) cur.setPosition(end, QTextCursor.KeepAnchor) fmt = cur.charFormat() - if fmt.isAnchor(): # already linkified; skip + if fmt.isAnchor(): # already linkified; skip continue href = m.captured(0) @@ -110,7 +108,7 @@ class Editor(QTextEdit): # When pressing Enter/return key, insert first, then neutralise the empty block’s inline format if key in (Qt.Key_Return, Qt.Key_Enter): - super().keyPressEvent(e) # create the new (possibly empty) paragraph + super().keyPressEvent(e) # create the new (possibly empty) paragraph # If we're on an empty block, clear the insertion char format so the # *next* Enter will create another new line (not consume the press to reset formatting). @@ -156,7 +154,11 @@ class Editor(QTextEdit): def apply_weight(self): cur = self.currentCharFormat() fmt = QTextCharFormat() - weight = QFont.Weight.Normal if cur.fontWeight() == QFont.Weight.Bold else QFont.Weight.Bold + weight = ( + QFont.Weight.Normal + if cur.fontWeight() == QFont.Weight.Bold + else QFont.Weight.Bold + ) fmt.setFontWeight(weight) self.merge_on_sel(fmt) diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 8a03852..0f90197 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -26,7 +26,7 @@ from .db import DBManager from .editor import Editor from .key_prompt import KeyPrompt from .search import Search -from .settings import APP_NAME, load_db_config, save_db_config +from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config from .settings_dialog import SettingsDialog from .toolbar import ToolBar @@ -44,9 +44,12 @@ class MainWindow(QMainWindow): else: first_time = False - # Always prompt for the key (we never store it) - if not self._prompt_for_key_until_valid(first_time): - sys.exit(1) + # Prompt for the key unless it is found in config + if not self.cfg.key: + if not self._prompt_for_key_until_valid(first_time): + sys.exit(1) + else: + self._try_connect() # ---- UI: Left fixed panel (calendar) + right editor ----------------- self.calendar = QCalendarWidget() @@ -149,7 +152,7 @@ class MainWindow(QMainWindow): self._refresh_calendar_marks() # Restore window position from settings - self.settings = QSettings(APP_NAME) + self.settings = QSettings(APP_ORG, APP_NAME) self._restore_window_position() def _try_connect(self) -> bool: @@ -328,12 +331,13 @@ class MainWindow(QMainWindow): return False def _move_to_cursor_screen_center(self): - screen = QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen() + screen = ( + QGuiApplication.screenAt(QCursor.pos()) or QGuiApplication.primaryScreen() + ) r = screen.availableGeometry() # Center the window in that screen’s available area self.move(r.center() - self.rect().center()) - def closeEvent(self, event): try: # Save window position diff --git a/bouquin/settings.py b/bouquin/settings.py index 508e12f..ec45094 100644 --- a/bouquin/settings.py +++ b/bouquin/settings.py @@ -21,9 +21,11 @@ def get_settings() -> QSettings: def load_db_config() -> DBConfig: s = get_settings() path = Path(s.value("db/path", str(default_db_path()))) - return DBConfig(path=path, key="") + key = s.value("db/key", "") + return DBConfig(path=path, key=key) def save_db_config(cfg: DBConfig) -> None: s = get_settings() s.setValue("db/path", str(cfg.path)) + s.setValue("db/key", str(cfg.key)) diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index d739630..70ae8f6 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -3,8 +3,12 @@ from __future__ import annotations from pathlib import Path from PySide6.QtWidgets import ( + QCheckBox, QDialog, QFormLayout, + QFrame, + QGroupBox, + QLabel, QHBoxLayout, QVBoxLayout, QWidget, @@ -15,9 +19,12 @@ from PySide6.QtWidgets import ( QSizePolicy, QMessageBox, ) +from PySide6.QtCore import Qt, Slot +from PySide6.QtGui import QPalette + from .db import DBConfig, DBManager -from .settings import save_db_config +from .settings import load_db_config, save_db_config from .key_prompt import KeyPrompt @@ -27,10 +34,11 @@ class SettingsDialog(QDialog): self.setWindowTitle("Settings") self._cfg = DBConfig(path=cfg.path, key="") self._db = db + self.key = "" form = QFormLayout() form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow) - self.setMinimumWidth(520) + self.setMinimumWidth(560) self.setSizeGripEnabled(True) self.path_edit = QLineEdit(str(self._cfg.path)) @@ -47,18 +55,65 @@ class SettingsDialog(QDialog): h.setStretch(1, 0) form.addRow("Database path", path_row) + # Encryption settings + enc_group = QGroupBox("Encryption") + enc = QVBoxLayout(enc_group) + enc.setContentsMargins(12, 8, 12, 12) + enc.setSpacing(6) + + # Checkbox to remember key + self.save_key_btn = QCheckBox("Remember key") + current_settings = load_db_config() + if current_settings.key: + self.save_key_btn.setChecked(True) + else: + self.save_key_btn.setChecked(False) + self.save_key_btn.setCursor(Qt.PointingHandCursor) + self.save_key_btn.toggled.connect(self.save_key_btn_clicked) + enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft) + + # Explanation for remembering key + self.save_key_label = QLabel( + "If you don't want to be prompted for your encryption key, check this to remember it. " + "WARNING: the key is saved to disk and could be recoverable if your disk is compromised." + ) + self.save_key_label.setWordWrap(True) + self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + # make it look secondary + pal = self.save_key_label.palette() + pal.setColor(self.save_key_label.foregroundRole(), pal.color(QPalette.Mid)) + self.save_key_label.setPalette(pal) + + exp_row = QHBoxLayout() + exp_row.setContentsMargins(24, 0, 0, 0) # indent to line up under the checkbox + exp_row.addWidget(self.save_key_label) + enc.addLayout(exp_row) + + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setFrameShadow(QFrame.Sunken) + enc.addWidget(line) + # Change key button self.rekey_btn = QPushButton("Change key") + self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.rekey_btn.clicked.connect(self._change_key) + enc.addWidget(self.rekey_btn, 0, Qt.AlignLeft) + # Put the group into the form so it spans the full width nicely + form.addRow(enc_group) + + # Buttons bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) bb.accepted.connect(self._save) bb.rejected.connect(self.reject) + # Root layout (adjust margins/spacing a bit) v = QVBoxLayout(self) + v.setContentsMargins(12, 12, 12, 12) + v.setSpacing(10) v.addLayout(form) - v.addWidget(self.rekey_btn) - v.addWidget(bb) + v.addWidget(bb, 0, Qt.AlignRight) def _browse(self): p, _ = QFileDialog.getSaveFileName( @@ -71,7 +126,7 @@ class SettingsDialog(QDialog): self.path_edit.setText(p) def _save(self): - self._cfg = DBConfig(path=Path(self.path_edit.text()), key="") + self._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key) save_db_config(self._cfg) self.accept() @@ -97,6 +152,18 @@ class SettingsDialog(QDialog): except Exception as e: QMessageBox.critical(self, "Error", f"Could not change key:\n{e}") + @Slot(bool) + def save_key_btn_clicked(self, checked: bool): + if checked: + p1 = KeyPrompt( + self, title="Enter your key", message="Enter the encryption key" + ) + if p1.exec() != QDialog.Accepted: + return + self.key = p1.key() + self._cfg = DBConfig(path=Path(self.path_edit.text()), key=self.key) + save_db_config(self._cfg) + @property def config(self) -> DBConfig: return self._cfg diff --git a/bouquin/toolbar.py b/bouquin/toolbar.py index 0951f20..c796ac8 100644 --- a/bouquin/toolbar.py +++ b/bouquin/toolbar.py @@ -1,7 +1,7 @@ from __future__ import annotations from PySide6.QtCore import Signal, Qt -from PySide6.QtGui import QFont, QAction +from PySide6.QtGui import QAction from PySide6.QtWidgets import QToolBar diff --git a/bouquin/url_highlighter.py b/bouquin/url_highlighter.py deleted file mode 100644 index 1fd1af6..0000000 --- a/bouquin/url_highlighter.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -import re -from PySide6.QtGui import QSyntaxHighlighter, QTextCharFormat -from PySide6.QtCore import Qt, QRegularExpression - -class UrlHighlighter(QSyntaxHighlighter): - def __init__(self, doc): - super().__init__(doc) - self.rx = QRegularExpression(r"(https?://[^\s<>\"]+|www\.[^\s<>\"]+)") - - def highlightBlock(self, text: str): - it = self.rx.globalMatch(text) - while it.hasNext(): - m = it.next() - href = m.captured(0) - if href.startswith("www."): - href = "https://" + href - - fmt = QTextCharFormat() - fmt.setAnchor(True) - fmt.setAnchorHref(href) - fmt.setFontUnderline(True) - fmt.setForeground(Qt.blue) - self.setFormat(m.capturedStart(0), m.capturedLength(0), fmt)