bouquin 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of bouquin might be problematic. Click here for more details.

bouquin/db.py CHANGED
@@ -100,6 +100,12 @@ class DBManager:
100
100
  )
101
101
  self.conn.commit()
102
102
 
103
+ def search_entries(self, text: str) -> list[str]:
104
+ cur = self.conn.cursor()
105
+ pattern = f"%{text}%"
106
+ cur.execute("SELECT * FROM entries WHERE TRIM(content) LIKE ?", (pattern,))
107
+ return [r for r in cur.fetchall()]
108
+
103
109
  def dates_with_content(self) -> list[str]:
104
110
  cur = self.conn.cursor()
105
111
  cur.execute("SELECT date FROM entries WHERE TRIM(content) <> '';")
bouquin/editor.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtGui import (
4
+ QColor,
5
+ QFont,
6
+ QFontDatabase,
7
+ QTextCharFormat,
8
+ QTextListFormat,
9
+ QTextBlockFormat,
10
+ )
11
+ from PySide6.QtCore import Slot
12
+ from PySide6.QtWidgets import QTextEdit
13
+
14
+
15
+ class Editor(QTextEdit):
16
+ def __init__(self):
17
+ super().__init__()
18
+ tab_w = 4 * self.fontMetrics().horizontalAdvance(" ")
19
+ self.setTabStopDistance(tab_w)
20
+
21
+ def merge_on_sel(self, fmt):
22
+ """
23
+ Sets the styling on the selected characters.
24
+ """
25
+ cursor = self.textCursor()
26
+ if not cursor.hasSelection():
27
+ cursor.select(cursor.SelectionType.WordUnderCursor)
28
+ cursor.mergeCharFormat(fmt)
29
+ self.mergeCurrentCharFormat(fmt)
30
+
31
+ @Slot(QFont.Weight)
32
+ def apply_weight(self, weight):
33
+ fmt = QTextCharFormat()
34
+ fmt.setFontWeight(weight)
35
+ self.merge_on_sel(fmt)
36
+
37
+ @Slot()
38
+ def apply_italic(self):
39
+ cur = self.currentCharFormat()
40
+ fmt = QTextCharFormat()
41
+ fmt.setFontItalic(not cur.fontItalic())
42
+ self.merge_on_sel(fmt)
43
+
44
+ @Slot()
45
+ def apply_underline(self):
46
+ cur = self.currentCharFormat()
47
+ fmt = QTextCharFormat()
48
+ fmt.setFontUnderline(not cur.fontUnderline())
49
+ self.merge_on_sel(fmt)
50
+
51
+ @Slot()
52
+ def apply_strikethrough(self):
53
+ cur = self.currentCharFormat()
54
+ fmt = QTextCharFormat()
55
+ fmt.setFontStrikeOut(not cur.fontStrikeOut())
56
+ self.merge_on_sel(fmt)
57
+
58
+ @Slot()
59
+ def apply_code(self):
60
+ c = self.textCursor()
61
+ if not c.hasSelection():
62
+ c.select(c.SelectionType.BlockUnderCursor)
63
+
64
+ bf = QTextBlockFormat()
65
+ bf.setLeftMargin(12)
66
+ bf.setRightMargin(12)
67
+ bf.setTopMargin(6)
68
+ bf.setBottomMargin(6)
69
+ bf.setBackground(QColor(245, 245, 245))
70
+ bf.setNonBreakableLines(True)
71
+
72
+ cf = QTextCharFormat()
73
+ mono = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
74
+ cf.setFont(mono)
75
+ cf.setFontFixedPitch(True)
76
+
77
+ # If the current block already looks like a code block, remove styling
78
+ cur_bf = c.blockFormat()
79
+ is_code = (
80
+ cur_bf.nonBreakableLines()
81
+ and cur_bf.background().color().rgb() == QColor(245, 245, 245).rgb()
82
+ )
83
+ if is_code:
84
+ # clear: margins/background/wrapping
85
+ bf = QTextBlockFormat()
86
+ cf = QTextCharFormat()
87
+
88
+ c.mergeBlockFormat(bf)
89
+ c.mergeBlockCharFormat(cf)
90
+
91
+ @Slot(int)
92
+ def apply_heading(self, size):
93
+ fmt = QTextCharFormat()
94
+ if size:
95
+ fmt.setFontWeight(QFont.Weight.Bold)
96
+ fmt.setFontPointSize(size)
97
+ else:
98
+ fmt.setFontWeight(QFont.Weight.Normal)
99
+ fmt.setFontPointSize(self.font().pointSizeF())
100
+ self.merge_on_sel(fmt)
101
+
102
+ def toggle_bullets(self):
103
+ c = self.textCursor()
104
+ lst = c.currentList()
105
+ if lst and lst.format().style() == QTextListFormat.Style.ListDisc:
106
+ lst.remove(c.block())
107
+ return
108
+ fmt = QTextListFormat()
109
+ fmt.setStyle(QTextListFormat.Style.ListDisc)
110
+ c.createList(fmt)
111
+
112
+ def toggle_numbers(self):
113
+ c = self.textCursor()
114
+ lst = c.currentList()
115
+ if lst and lst.format().style() == QTextListFormat.Style.ListDecimal:
116
+ lst.remove(c.block())
117
+ return
118
+ fmt = QTextListFormat()
119
+ fmt.setStyle(QTextListFormat.Style.ListDecimal)
120
+ c.createList(fmt)
bouquin/main.py CHANGED
@@ -11,5 +11,6 @@ def main():
11
11
  app = QApplication(sys.argv)
12
12
  app.setApplicationName(APP_NAME)
13
13
  app.setOrganizationName(APP_ORG)
14
- win = MainWindow(); win.show()
14
+ win = MainWindow()
15
+ win.show()
15
16
  sys.exit(app.exec())
bouquin/main_window.py CHANGED
@@ -3,24 +3,29 @@ from __future__ import annotations
3
3
  import sys
4
4
 
5
5
  from PySide6.QtCore import QDate, QTimer, Qt
6
- from PySide6.QtGui import QAction, QFont, QTextCharFormat
6
+ from PySide6.QtGui import (
7
+ QAction,
8
+ QFont,
9
+ QTextCharFormat,
10
+ )
7
11
  from PySide6.QtWidgets import (
8
- QDialog,
9
12
  QCalendarWidget,
13
+ QDialog,
10
14
  QMainWindow,
11
15
  QMessageBox,
12
- QPlainTextEdit,
16
+ QSizePolicy,
13
17
  QSplitter,
14
18
  QVBoxLayout,
15
19
  QWidget,
16
- QSizePolicy,
17
20
  )
18
21
 
19
22
  from .db import DBManager
20
- from .settings import APP_NAME, load_db_config, save_db_config
23
+ from .editor import Editor
21
24
  from .key_prompt import KeyPrompt
22
- from .highlighter import MarkdownHighlighter
25
+ from .search import Search
26
+ from .settings import APP_NAME, load_db_config, save_db_config
23
27
  from .settings_dialog import SettingsDialog
28
+ from .toolbar import ToolBar
24
29
 
25
30
 
26
31
  class MainWindow(QMainWindow):
@@ -40,17 +45,35 @@ class MainWindow(QMainWindow):
40
45
  self.calendar.setGridVisible(True)
41
46
  self.calendar.selectionChanged.connect(self._on_date_changed)
42
47
 
48
+ self.search = Search(self.db)
49
+ self.search.openDateRequested.connect(self._load_selected_date)
50
+
51
+ # Lock the calendar to the left panel at the top to stop it stretching
52
+ # when the main window is resized.
43
53
  left_panel = QWidget()
44
54
  left_layout = QVBoxLayout(left_panel)
45
55
  left_layout.setContentsMargins(8, 8, 8, 8)
46
56
  left_layout.addWidget(self.calendar, alignment=Qt.AlignTop)
57
+ left_layout.addWidget(self.search, alignment=Qt.AlignBottom)
47
58
  left_layout.addStretch(1)
48
59
  left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
49
60
 
50
- self.editor = QPlainTextEdit()
51
- tab_w = 4 * self.editor.fontMetrics().horizontalAdvance(" ")
52
- self.editor.setTabStopDistance(tab_w)
53
- self.highlighter = MarkdownHighlighter(self.editor.document())
61
+ # This is the note-taking editor
62
+ self.editor = Editor()
63
+
64
+ # Toolbar for controlling styling
65
+ tb = ToolBar()
66
+ self.addToolBar(tb)
67
+ # Wire toolbar intents to editor methods
68
+ tb.boldRequested.connect(self.editor.apply_weight)
69
+ tb.italicRequested.connect(self.editor.apply_italic)
70
+ tb.underlineRequested.connect(self.editor.apply_underline)
71
+ tb.strikeRequested.connect(self.editor.apply_strikethrough)
72
+ tb.codeRequested.connect(self.editor.apply_code)
73
+ tb.headingRequested.connect(self.editor.apply_heading)
74
+ tb.bulletsRequested.connect(self.editor.toggle_bullets)
75
+ tb.numbersRequested.connect(self.editor.toggle_numbers)
76
+ tb.alignRequested.connect(self.editor.setAlignment)
54
77
 
55
78
  split = QSplitter()
56
79
  split.addWidget(left_panel)
@@ -67,13 +90,13 @@ class MainWindow(QMainWindow):
67
90
 
68
91
  # Menu bar (File)
69
92
  mb = self.menuBar()
70
- file_menu = mb.addMenu("&File")
93
+ file_menu = mb.addMenu("&Application")
71
94
  act_save = QAction("&Save", self)
72
95
  act_save.setShortcut("Ctrl+S")
73
96
  act_save.triggered.connect(lambda: self._save_current(explicit=True))
74
97
  file_menu.addAction(act_save)
75
98
  act_settings = QAction("S&ettings", self)
76
- act_save.setShortcut("Ctrl+E")
99
+ act_settings.setShortcut("Ctrl+E")
77
100
  act_settings.triggered.connect(self._open_settings)
78
101
  file_menu.addAction(act_settings)
79
102
  file_menu.addSeparator()
@@ -82,7 +105,7 @@ class MainWindow(QMainWindow):
82
105
  act_quit.triggered.connect(self.close)
83
106
  file_menu.addAction(act_quit)
84
107
 
85
- # Navigate menu with next/previous day
108
+ # Navigate menu with next/previous/today
86
109
  nav_menu = mb.addMenu("&Navigate")
87
110
  act_prev = QAction("Previous Day", self)
88
111
  act_prev.setShortcut("Ctrl+P")
@@ -112,12 +135,14 @@ class MainWindow(QMainWindow):
112
135
  self._save_timer.timeout.connect(self._save_current)
113
136
  self.editor.textChanged.connect(self._on_text_changed)
114
137
 
115
- # First load + mark dates with content
138
+ # First load + mark dates in calendar with content
116
139
  self._load_selected_date()
117
140
  self._refresh_calendar_marks()
118
141
 
119
- # --- DB lifecycle
120
142
  def _try_connect(self) -> bool:
143
+ """
144
+ Try to connect to the database.
145
+ """
121
146
  try:
122
147
  self.db = DBManager(self.cfg)
123
148
  ok = self.db.connect()
@@ -131,6 +156,9 @@ class MainWindow(QMainWindow):
131
156
  return ok
132
157
 
133
158
  def _prompt_for_key_until_valid(self) -> bool:
159
+ """
160
+ Prompt for the SQLCipher key.
161
+ """
134
162
  while True:
135
163
  dlg = KeyPrompt(self, message="Enter a key to unlock the notebook")
136
164
  if dlg.exec() != QDialog.Accepted:
@@ -139,8 +167,11 @@ class MainWindow(QMainWindow):
139
167
  if self._try_connect():
140
168
  return True
141
169
 
142
- # --- Calendar marks to indicate text exists for htat day -----------------
143
170
  def _refresh_calendar_marks(self):
171
+ """
172
+ Sets a bold marker on the day to indicate that text exists
173
+ for that day.
174
+ """
144
175
  fmt_bold = QTextCharFormat()
145
176
  fmt_bold.setFontWeight(QFont.Weight.Bold)
146
177
  # Clear previous marks
@@ -161,15 +192,16 @@ class MainWindow(QMainWindow):
161
192
  d = self.calendar.selectedDate()
162
193
  return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
163
194
 
164
- def _load_selected_date(self):
165
- date_iso = self._current_date_iso()
195
+ def _load_selected_date(self, date_iso=False):
196
+ if not date_iso:
197
+ date_iso = self._current_date_iso()
166
198
  try:
167
199
  text = self.db.get_entry(date_iso)
168
200
  except Exception as e:
169
201
  QMessageBox.critical(self, "Read Error", str(e))
170
202
  return
171
203
  self.editor.blockSignals(True)
172
- self.editor.setPlainText(text)
204
+ self.editor.setHtml(text)
173
205
  self.editor.blockSignals(False)
174
206
  self._dirty = False
175
207
  # track which date the editor currently represents
@@ -212,7 +244,7 @@ class MainWindow(QMainWindow):
212
244
  """
213
245
  if not self._dirty and not explicit:
214
246
  return
215
- text = self.editor.toPlainText()
247
+ text = self.editor.toHtml()
216
248
  try:
217
249
  self.db.upsert_entry(date_iso, text)
218
250
  except Exception as e:
@@ -249,7 +281,7 @@ class MainWindow(QMainWindow):
249
281
  self._load_selected_date()
250
282
  self._refresh_calendar_marks()
251
283
 
252
- def closeEvent(self, event): # noqa: N802
284
+ def closeEvent(self, event):
253
285
  try:
254
286
  self._save_current()
255
287
  self.db.close()
bouquin/search.py ADDED
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Iterable, Tuple
5
+
6
+ from PySide6.QtCore import Qt, Signal
7
+ from PySide6.QtGui import QFont, QTextCharFormat, QTextCursor, QTextDocument
8
+ from PySide6.QtWidgets import (
9
+ QLabel,
10
+ QLineEdit,
11
+ QListWidget,
12
+ QListWidgetItem,
13
+ QHBoxLayout,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ )
17
+
18
+ # type: rows are (date_iso, content)
19
+ Row = Tuple[str, str]
20
+
21
+
22
+ class Search(QWidget):
23
+ """Encapsulates the search UI + logic and emits a signal when a result is chosen."""
24
+
25
+ openDateRequested = Signal(str)
26
+
27
+ def __init__(self, db, parent: QWidget | None = None):
28
+ super().__init__(parent)
29
+ self._db = db
30
+
31
+ self.search = QLineEdit()
32
+ self.search.setPlaceholderText("Search for notes here")
33
+ self.search.textChanged.connect(self._search)
34
+
35
+ self.results = QListWidget()
36
+ self.results.setUniformItemSizes(False)
37
+ self.results.setSelectionMode(self.results.SelectionMode.SingleSelection)
38
+ self.results.itemClicked.connect(self._open_selected)
39
+ self.results.hide()
40
+
41
+ lay = QVBoxLayout(self)
42
+ lay.setContentsMargins(0, 0, 0, 0)
43
+ lay.setSpacing(6)
44
+ lay.addWidget(self.search)
45
+ lay.addWidget(self.results)
46
+
47
+ def _open_selected(self, item: QListWidgetItem):
48
+ date_str = item.data(Qt.ItemDataRole.UserRole)
49
+ if date_str:
50
+ self.openDateRequested.emit(date_str)
51
+
52
+ def _search(self, text: str):
53
+ """
54
+ Search for the supplied text in the database.
55
+ For all rows found, populate the results widget with a clickable preview.
56
+ """
57
+ q = text.strip()
58
+ if not q:
59
+ self.results.clear()
60
+ self.results.hide()
61
+ return
62
+
63
+ try:
64
+ rows: Iterable[Row] = self._db.search_entries(q)
65
+ except Exception:
66
+ # be quiet on DB errors here; caller can surface if desired
67
+ rows = []
68
+
69
+ self._populate_results(q, rows)
70
+
71
+ def _populate_results(self, query: str, rows: Iterable[Row]):
72
+ self.results.clear()
73
+ rows = list(rows)
74
+ if not rows:
75
+ self.results.hide()
76
+ return
77
+
78
+ self.results.show()
79
+
80
+ for date_str, content in rows:
81
+ # Build an HTML fragment around the match and whether to show ellipses
82
+ frag_html, left_ell, right_ell = self._make_html_snippet(
83
+ content, query, radius=60, maxlen=180
84
+ )
85
+
86
+ # ---- Per-item widget: date on top, preview row below (with ellipses) ----
87
+ container = QWidget()
88
+ outer = QVBoxLayout(container)
89
+ outer.setContentsMargins(8, 6, 8, 6)
90
+ outer.setSpacing(2)
91
+
92
+ # Date label (plain text)
93
+ date_lbl = QLabel(date_str)
94
+ date_lbl.setTextFormat(Qt.TextFormat.PlainText)
95
+ date_f = date_lbl.font()
96
+ date_f.setPointSizeF(date_f.pointSizeF() - 1)
97
+ date_lbl.setFont(date_f)
98
+ date_lbl.setStyleSheet("color:#666;")
99
+ outer.addWidget(date_lbl)
100
+
101
+ # Preview row with optional ellipses
102
+ row = QWidget()
103
+ h = QHBoxLayout(row)
104
+ h.setContentsMargins(0, 0, 0, 0)
105
+ h.setSpacing(4)
106
+
107
+ if left_ell:
108
+ left = QLabel("…")
109
+ left.setStyleSheet("color:#888;")
110
+ h.addWidget(left, 0, Qt.AlignmentFlag.AlignTop)
111
+
112
+ preview = QLabel()
113
+ preview.setTextFormat(Qt.TextFormat.RichText)
114
+ preview.setWordWrap(True)
115
+ preview.setOpenExternalLinks(True) # keep links in your HTML clickable
116
+ preview.setText(
117
+ frag_html
118
+ if frag_html
119
+ else "<span style='color:#888'>(no preview)</span>"
120
+ )
121
+ h.addWidget(preview, 1)
122
+
123
+ if right_ell:
124
+ right = QLabel("…")
125
+ right.setStyleSheet("color:#888;")
126
+ h.addWidget(right, 0, Qt.AlignmentFlag.AlignBottom)
127
+
128
+ outer.addWidget(row)
129
+
130
+ # ---- Add to list ----
131
+ item = QListWidgetItem()
132
+ item.setData(Qt.ItemDataRole.UserRole, date_str)
133
+ item.setSizeHint(container.sizeHint())
134
+
135
+ self.results.addItem(item)
136
+ self.results.setItemWidget(item, container)
137
+
138
+ # --- Snippet/highlight helpers -----------------------------------------
139
+ def _make_html_snippet(self, html_src: str, query: str, *, radius=60, maxlen=180):
140
+ doc = QTextDocument()
141
+ doc.setHtml(html_src)
142
+ plain = doc.toPlainText()
143
+ if not plain:
144
+ return "", False, False
145
+
146
+ tokens = [t for t in re.split(r"\s+", query.strip()) if t]
147
+ L = len(plain)
148
+
149
+ # Find first occurrence (phrase first, then earliest token)
150
+ idx, mlen = -1, 0
151
+ if tokens:
152
+ lower = plain.lower()
153
+ phrase = " ".join(tokens).lower()
154
+ j = lower.find(phrase)
155
+ if j >= 0:
156
+ idx, mlen = j, len(phrase)
157
+ else:
158
+ for t in tokens:
159
+ tj = lower.find(t.lower())
160
+ if tj >= 0 and (idx < 0 or tj < idx):
161
+ idx, mlen = tj, len(t)
162
+ # Compute window
163
+ if idx < 0:
164
+ start, end = 0, min(L, maxlen)
165
+ else:
166
+ start = max(0, min(idx - radius, max(0, L - maxlen)))
167
+ end = min(L, max(idx + mlen + radius, start + maxlen))
168
+
169
+ # Bold all token matches that fall inside [start, end)
170
+ if tokens:
171
+ lower = plain.lower()
172
+ fmt = QTextCharFormat()
173
+ fmt.setFontWeight(QFont.Weight.Bold)
174
+ for t in tokens:
175
+ t_low = t.lower()
176
+ pos = start
177
+ while True:
178
+ k = lower.find(t_low, pos)
179
+ if k == -1 or k >= end:
180
+ break
181
+ c = QTextCursor(doc)
182
+ c.setPosition(k)
183
+ c.setPosition(k + len(t), QTextCursor.MoveMode.KeepAnchor)
184
+ c.mergeCharFormat(fmt)
185
+ pos = k + len(t)
186
+
187
+ # Select the window and export as HTML fragment
188
+ c = QTextCursor(doc)
189
+ c.setPosition(start)
190
+ c.setPosition(end, QTextCursor.MoveMode.KeepAnchor)
191
+ fragment_html = (
192
+ c.selection().toHtml()
193
+ ) # preserves original styles + our bolding
194
+
195
+ return fragment_html, start > 0, end < L
@@ -91,7 +91,9 @@ class SettingsDialog(QDialog):
91
91
  return
92
92
  try:
93
93
  self._db.rekey(new_key)
94
- QMessageBox.information(self, "Key changed", "The database key was updated.")
94
+ QMessageBox.information(
95
+ self, "Key changed", "The database key was updated."
96
+ )
95
97
  except Exception as e:
96
98
  QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
97
99
 
bouquin/toolbar.py ADDED
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtCore import Signal, Qt
4
+ from PySide6.QtGui import QFont, QAction
5
+ from PySide6.QtWidgets import QToolBar
6
+
7
+
8
+ class ToolBar(QToolBar):
9
+ boldRequested = Signal(QFont.Weight)
10
+ italicRequested = Signal()
11
+ underlineRequested = Signal()
12
+ strikeRequested = Signal()
13
+ codeRequested = Signal()
14
+ headingRequested = Signal(int)
15
+ bulletsRequested = Signal()
16
+ numbersRequested = Signal()
17
+ alignRequested = Signal(Qt.AlignmentFlag)
18
+
19
+ def __init__(self, parent=None):
20
+ super().__init__("Format", parent)
21
+ self._build_actions()
22
+
23
+ def _build_actions(self):
24
+ # Bold
25
+ bold = QAction("Bold", self)
26
+ bold.setShortcut("Ctrl+B")
27
+ bold.triggered.connect(lambda: self.boldRequested.emit(QFont.Weight.Bold))
28
+
29
+ italic = QAction("Italic", self)
30
+ italic.setShortcut("Ctrl+I")
31
+ italic.triggered.connect(self.italicRequested)
32
+
33
+ underline = QAction("Underline", self)
34
+ underline.setShortcut("Ctrl+U")
35
+ underline.triggered.connect(self.underlineRequested)
36
+
37
+ strike = QAction("Strikethrough", self)
38
+ strike.setShortcut("Ctrl+-")
39
+ strike.triggered.connect(self.strikeRequested)
40
+
41
+ code = QAction("<code>", self)
42
+ code.setShortcut("Ctrl+`")
43
+ code.triggered.connect(self.codeRequested)
44
+
45
+ # Headings
46
+ h1 = QAction("H1", self)
47
+ h1.setShortcut("Ctrl+1")
48
+ h2 = QAction("H2", self)
49
+ h2.setShortcut("Ctrl+2")
50
+ h3 = QAction("H3", self)
51
+ h3.setShortcut("Ctrl+3")
52
+ normal = QAction("Normal", self)
53
+ normal.setShortcut("Ctrl+P")
54
+
55
+ h1.triggered.connect(lambda: self.headingRequested.emit(24))
56
+ h2.triggered.connect(lambda: self.headingRequested.emit(18))
57
+ h3.triggered.connect(lambda: self.headingRequested.emit(14))
58
+ normal.triggered.connect(lambda: self.headingRequested.emit(0))
59
+
60
+ # Lists
61
+ bullets = QAction("• Bullets", self)
62
+ bullets.triggered.connect(self.bulletsRequested)
63
+ numbers = QAction("1. Numbered", self)
64
+ numbers.triggered.connect(self.numbersRequested)
65
+
66
+ # Alignment
67
+ left = QAction("Align Left", self)
68
+ center = QAction("Align Center", self)
69
+ right = QAction("Align Right", self)
70
+
71
+ left.triggered.connect(
72
+ lambda: self.alignRequested.emit(Qt.AlignmentFlag.AlignLeft)
73
+ )
74
+ center.triggered.connect(
75
+ lambda: self.alignRequested.emit(Qt.AlignmentFlag.AlignHCenter)
76
+ )
77
+ right.triggered.connect(
78
+ lambda: self.alignRequested.emit(Qt.AlignmentFlag.AlignRight)
79
+ )
80
+
81
+ self.addActions(
82
+ [
83
+ bold,
84
+ italic,
85
+ underline,
86
+ strike,
87
+ code,
88
+ h1,
89
+ h2,
90
+ h3,
91
+ normal,
92
+ bullets,
93
+ numbers,
94
+ left,
95
+ center,
96
+ right,
97
+ ]
98
+ )
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bouquin
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
5
+ Home-page: https://git.mig5.net/mig5/bouquin
5
6
  License: GPL-3.0-or-later
6
7
  Author: Miguel Jacq
7
8
  Author-email: mig@mig5.net
@@ -14,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Requires-Dist: pyside6 (>=6.8.1,<7.0.0)
16
17
  Requires-Dist: sqlcipher3-wheels (>=0.5.5.post0,<0.6.0)
18
+ Project-URL: Repository, https://git.mig5.net/mig5/bouquin
17
19
  Description-Content-Type: text/markdown
18
20
 
19
21
  # Bouquin
@@ -38,16 +40,15 @@ There is deliberately no network connectivity or syncing intended.
38
40
  ## Features
39
41
 
40
42
  * Every 'page' is linked to the calendar day
41
- * Basic markdown
43
+ * Text is HTML with basic styling
44
+ * Search
42
45
  * Automatic periodic saving (or explicitly save)
43
- * Navigating from one day to the next automatically saves
44
- * Basic keyboard shortcuts
45
46
  * Transparent integrity checking of the database when it opens
47
+ * Rekey the database (change the password)
46
48
 
47
49
 
48
50
  ## Yet to do
49
51
 
50
- * Search
51
52
  * Taxonomy/tagging
52
53
  * Export to other formats (plaintext, json, sql etc)
53
54
 
@@ -65,7 +66,7 @@ There is deliberately no network connectivity or syncing intended.
65
66
 
66
67
  * Download the whl and run it
67
68
 
68
- ### From PyPi
69
+ ### From PyPi/pip
69
70
 
70
71
  * `pip install bouquin`
71
72
 
@@ -0,0 +1,16 @@
1
+ bouquin/__init__.py,sha256=-bBNFYOq80A2Egtpo5V5zWJtYOxQfRZFQ_feve5lkFU,23
2
+ bouquin/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ bouquin/db.py,sha256=LlKf_AzaJpzgN3cjxUshsHLybaIATgfQF1g9G92yYEw,3641
4
+ bouquin/editor.py,sha256=HY5ASmSTiwb_pQzEdqyMBhKFOojw1bppuCk4FacE660,3540
5
+ bouquin/key_prompt.py,sha256=RNrW0bN4xnwDGeBlgbmFaBSs_2iQyYrBYpKOQhe4E0c,1092
6
+ bouquin/main.py,sha256=u7Wm5-9LRZDKkzKkK0W6P4oTtDorrrmtwIJWmQCqsRs,351
7
+ bouquin/main_window.py,sha256=LOu80m5r6bg-tjY1R-Ol5H4bLUCVJbOR6nN2ykN7Q1M,10363
8
+ bouquin/search.py,sha256=uTHkxsKrcWqVpXEbOMqCkqrAfVsQvIvgvDV6YNH06lA,6516
9
+ bouquin/settings.py,sha256=bJYQXbTqX_r_DfOKuGnah6IVZLiNwZAuBuz2OgdhA_E,670
10
+ bouquin/settings_dialog.py,sha256=HV7IERazYBjvMXyVkm9FmZqu3gVHlceNrfFaW_fQJHE,3150
11
+ bouquin/toolbar.py,sha256=jPsix5f8VErO-P_cjRo_ZWHPW7KGGAAlHbC5S-2uStg,3050
12
+ bouquin-0.1.2.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ bouquin-0.1.2.dist-info/METADATA,sha256=XLFFF4yUWZVjEVAliPz4XdP_qP1yUkhtr7CBGYCgSy0,2230
14
+ bouquin-0.1.2.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
15
+ bouquin-0.1.2.dist-info/entry_points.txt,sha256=d2C5Mc85suj1vWg_mmcfFuEBAYEkdwhZquusme5EWuQ,49
16
+ bouquin-0.1.2.dist-info/RECORD,,
bouquin/highlighter.py DELETED
@@ -1,112 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from PySide6.QtGui import QFont, QTextCharFormat, QSyntaxHighlighter, QColor
5
-
6
-
7
- class MarkdownHighlighter(QSyntaxHighlighter):
8
- ST_NORMAL = 0
9
- ST_CODE = 1
10
-
11
- FENCE = re.compile(r"^```")
12
-
13
- def __init__(self, document):
14
- super().__init__(document)
15
-
16
- base_size = document.defaultFont().pointSizeF() or 12.0
17
-
18
- # Monospace for code
19
- self.mono = QFont("Monospace")
20
- self.mono.setStyleHint(QFont.TypeWriter)
21
-
22
- # Light, high-contrast scheme for code
23
- self.col_bg = QColor("#eef2f6") # light code bg
24
- self.col_fg = QColor("#1f2328") # dark text
25
-
26
- # Formats
27
- self.fmt_h = [QTextCharFormat() for _ in range(6)]
28
- for i, f in enumerate(self.fmt_h, start=1):
29
- f.setFontWeight(QFont.Weight.Bold)
30
- f.setFontPointSize(base_size + (7 - i))
31
- self.fmt_bold = QTextCharFormat()
32
- self.fmt_bold.setFontWeight(QFont.Weight.Bold)
33
- self.fmt_italic = QTextCharFormat()
34
- self.fmt_italic.setFontItalic(True)
35
- self.fmt_quote = QTextCharFormat()
36
- self.fmt_quote.setForeground(QColor("#6a737d"))
37
- self.fmt_link = QTextCharFormat()
38
- self.fmt_link.setFontUnderline(True)
39
- self.fmt_list = QTextCharFormat()
40
- self.fmt_list.setFontWeight(QFont.Weight.DemiBold)
41
- self.fmt_strike = QTextCharFormat()
42
- self.fmt_strike.setFontStrikeOut(True)
43
-
44
- # Uniform code style
45
- self.fmt_code = QTextCharFormat()
46
- self.fmt_code.setFont(self.mono)
47
- self.fmt_code.setFontPointSize(max(6.0, base_size - 1))
48
- self.fmt_code.setBackground(self.col_bg)
49
- self.fmt_code.setForeground(self.col_fg)
50
-
51
- # Simple patterns
52
- self.re_heading = re.compile(r"^(#{1,6}) +.*$")
53
- self.re_bold = re.compile(r"\*\*(.+?)\*\*|__(.+?)__")
54
- self.re_italic = re.compile(r"\*(?!\*)(.+?)\*|_(?!_)(.+?)_")
55
- self.re_strike = re.compile(r"~~(.+?)~~")
56
- self.re_inline_code = re.compile(r"`([^`]+)`")
57
- self.re_link = re.compile(r"\[([^\]]+)\]\(([^)]+)\)")
58
- self.re_list = re.compile(r"^ *(?:[-*+] +|[0-9]+[.)] +)")
59
- self.re_quote = re.compile(r"^> ?.*$")
60
-
61
- def highlightBlock(self, text: str) -> None:
62
- prev = self.previousBlockState()
63
- in_code = prev == self.ST_CODE
64
-
65
- if in_code:
66
- # Entire line is code
67
- self.setFormat(0, len(text), self.fmt_code)
68
- if self.FENCE.match(text):
69
- self.setCurrentBlockState(self.ST_NORMAL)
70
- else:
71
- self.setCurrentBlockState(self.ST_CODE)
72
- return
73
-
74
- # Starting/ending a fenced block?
75
- if self.FENCE.match(text):
76
- self.setFormat(0, len(text), self.fmt_code)
77
- self.setCurrentBlockState(self.ST_CODE)
78
- return
79
-
80
- # --- Normal markdown styling ---
81
- m = self.re_heading.match(text)
82
- if m:
83
- level = min(len(m.group(1)), 6)
84
- self.setFormat(0, len(text), self.fmt_h[level - 1])
85
- self.setCurrentBlockState(self.ST_NORMAL)
86
- return
87
-
88
- m = self.re_list.match(text)
89
- if m:
90
- self.setFormat(m.start(), m.end() - m.start(), self.fmt_list)
91
-
92
- if self.re_quote.match(text):
93
- self.setFormat(0, len(text), self.fmt_quote)
94
-
95
- for m in self.re_inline_code.finditer(text):
96
- self.setFormat(m.start(), m.end() - m.start(), self.fmt_code)
97
-
98
- for m in self.re_bold.finditer(text):
99
- self.setFormat(m.start(), m.end() - m.start(), self.fmt_bold)
100
-
101
- for m in self.re_italic.finditer(text):
102
- self.setFormat(m.start(), m.end() - m.start(), self.fmt_italic)
103
-
104
- for m in self.re_strike.finditer(text):
105
- self.setFormat(m.start(), m.end() - m.start(), self.fmt_strike)
106
-
107
- for m in self.re_link.finditer(text):
108
- start = m.start(1) - 1
109
- length = len(m.group(1)) + 2
110
- self.setFormat(start, length, self.fmt_link)
111
-
112
- self.setCurrentBlockState(self.ST_NORMAL)
@@ -1,14 +0,0 @@
1
- bouquin/__init__.py,sha256=-bBNFYOq80A2Egtpo5V5zWJtYOxQfRZFQ_feve5lkFU,23
2
- bouquin/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
- bouquin/db.py,sha256=-RCWeStZD-eZ2TEUaUTj67cBvq30q2aiSmLoP-K67Jk,3396
4
- bouquin/highlighter.py,sha256=UPP4G4jdN7U8y1c3nk9zTswkHHJw0Tpl3PX6obZrSG0,4077
5
- bouquin/key_prompt.py,sha256=RNrW0bN4xnwDGeBlgbmFaBSs_2iQyYrBYpKOQhe4E0c,1092
6
- bouquin/main.py,sha256=tx59cJZnGgHQ1UHQbdlYgaC36_L0ulyKaOoy6oURXVA,348
7
- bouquin/main_window.py,sha256=OQQ1BhPp3F3ULVrtjeTsKTraZZDzRECjYCORrRpWmis,9291
8
- bouquin/settings.py,sha256=bJYQXbTqX_r_DfOKuGnah6IVZLiNwZAuBuz2OgdhA_E,670
9
- bouquin/settings_dialog.py,sha256=NA40-RvXvM8SND-o5K6b-yKMrShY6NQnQuDAxUoCzyI,3120
10
- bouquin-0.1.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
11
- bouquin-0.1.1.dist-info/METADATA,sha256=j1CnzeNbB-bzfHaXN9rS66RDaG3-q4Ic6erwzKjqxBw,2148
12
- bouquin-0.1.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
13
- bouquin-0.1.1.dist-info/entry_points.txt,sha256=d2C5Mc85suj1vWg_mmcfFuEBAYEkdwhZquusme5EWuQ,49
14
- bouquin-0.1.1.dist-info/RECORD,,