bouquin 0.1.0__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 +25 -0
- bouquin/editor.py +120 -0
- bouquin/main.py +2 -1
- bouquin/main_window.py +67 -22
- bouquin/search.py +195 -0
- bouquin/settings_dialog.py +32 -2
- bouquin/toolbar.py +98 -0
- {bouquin-0.1.0.dist-info → bouquin-0.1.2.dist-info}/METADATA +7 -7
- bouquin-0.1.2.dist-info/RECORD +16 -0
- bouquin/highlighter.py +0 -112
- bouquin-0.1.0.dist-info/RECORD +0 -14
- {bouquin-0.1.0.dist-info → bouquin-0.1.2.dist-info}/LICENSE +0 -0
- {bouquin-0.1.0.dist-info → bouquin-0.1.2.dist-info}/WHEEL +0 -0
- {bouquin-0.1.0.dist-info → bouquin-0.1.2.dist-info}/entry_points.txt +0 -0
bouquin/db.py
CHANGED
|
@@ -64,6 +64,25 @@ class DBManager:
|
|
|
64
64
|
cur.execute("PRAGMA user_version = 1;")
|
|
65
65
|
self.conn.commit()
|
|
66
66
|
|
|
67
|
+
def rekey(self, new_key: str) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Change the SQLCipher passphrase in-place, then reopen the connection
|
|
70
|
+
with the new key to verify.
|
|
71
|
+
"""
|
|
72
|
+
if self.conn is None:
|
|
73
|
+
raise RuntimeError("Database is not connected")
|
|
74
|
+
cur = self.conn.cursor()
|
|
75
|
+
# Change the encryption key of the currently open database
|
|
76
|
+
cur.execute(f"PRAGMA rekey = '{new_key}';")
|
|
77
|
+
self.conn.commit()
|
|
78
|
+
|
|
79
|
+
# Close and reopen with the new key to verify and restore PRAGMAs
|
|
80
|
+
self.conn.close()
|
|
81
|
+
self.conn = None
|
|
82
|
+
self.cfg.key = new_key
|
|
83
|
+
if not self.connect():
|
|
84
|
+
raise sqlite.Error("Re-open failed after rekey")
|
|
85
|
+
|
|
67
86
|
def get_entry(self, date_iso: str) -> str:
|
|
68
87
|
cur = self.conn.cursor()
|
|
69
88
|
cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,))
|
|
@@ -81,6 +100,12 @@ class DBManager:
|
|
|
81
100
|
)
|
|
82
101
|
self.conn.commit()
|
|
83
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
|
+
|
|
84
109
|
def dates_with_content(self) -> list[str]:
|
|
85
110
|
cur = self.conn.cursor()
|
|
86
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
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
|
|
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
|
-
|
|
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 .
|
|
23
|
+
from .editor import Editor
|
|
21
24
|
from .key_prompt import KeyPrompt
|
|
22
|
-
from .
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,12 +90,13 @@ class MainWindow(QMainWindow):
|
|
|
67
90
|
|
|
68
91
|
# Menu bar (File)
|
|
69
92
|
mb = self.menuBar()
|
|
70
|
-
file_menu = mb.addMenu("&
|
|
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
|
-
act_settings = QAction("&
|
|
98
|
+
act_settings = QAction("S&ettings", self)
|
|
99
|
+
act_settings.setShortcut("Ctrl+E")
|
|
76
100
|
act_settings.triggered.connect(self._open_settings)
|
|
77
101
|
file_menu.addAction(act_settings)
|
|
78
102
|
file_menu.addSeparator()
|
|
@@ -81,7 +105,7 @@ class MainWindow(QMainWindow):
|
|
|
81
105
|
act_quit.triggered.connect(self.close)
|
|
82
106
|
file_menu.addAction(act_quit)
|
|
83
107
|
|
|
84
|
-
# Navigate menu with next/previous
|
|
108
|
+
# Navigate menu with next/previous/today
|
|
85
109
|
nav_menu = mb.addMenu("&Navigate")
|
|
86
110
|
act_prev = QAction("Previous Day", self)
|
|
87
111
|
act_prev.setShortcut("Ctrl+P")
|
|
@@ -97,6 +121,13 @@ class MainWindow(QMainWindow):
|
|
|
97
121
|
nav_menu.addAction(act_next)
|
|
98
122
|
self.addAction(act_next)
|
|
99
123
|
|
|
124
|
+
act_today = QAction("Today", self)
|
|
125
|
+
act_today.setShortcut("Ctrl+T")
|
|
126
|
+
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
|
127
|
+
act_today.triggered.connect(self._adjust_today)
|
|
128
|
+
nav_menu.addAction(act_today)
|
|
129
|
+
self.addAction(act_today)
|
|
130
|
+
|
|
100
131
|
# Autosave
|
|
101
132
|
self._dirty = False
|
|
102
133
|
self._save_timer = QTimer(self)
|
|
@@ -104,12 +135,14 @@ class MainWindow(QMainWindow):
|
|
|
104
135
|
self._save_timer.timeout.connect(self._save_current)
|
|
105
136
|
self.editor.textChanged.connect(self._on_text_changed)
|
|
106
137
|
|
|
107
|
-
# First load + mark dates with content
|
|
138
|
+
# First load + mark dates in calendar with content
|
|
108
139
|
self._load_selected_date()
|
|
109
140
|
self._refresh_calendar_marks()
|
|
110
141
|
|
|
111
|
-
# --- DB lifecycle
|
|
112
142
|
def _try_connect(self) -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Try to connect to the database.
|
|
145
|
+
"""
|
|
113
146
|
try:
|
|
114
147
|
self.db = DBManager(self.cfg)
|
|
115
148
|
ok = self.db.connect()
|
|
@@ -123,6 +156,9 @@ class MainWindow(QMainWindow):
|
|
|
123
156
|
return ok
|
|
124
157
|
|
|
125
158
|
def _prompt_for_key_until_valid(self) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
Prompt for the SQLCipher key.
|
|
161
|
+
"""
|
|
126
162
|
while True:
|
|
127
163
|
dlg = KeyPrompt(self, message="Enter a key to unlock the notebook")
|
|
128
164
|
if dlg.exec() != QDialog.Accepted:
|
|
@@ -131,8 +167,11 @@ class MainWindow(QMainWindow):
|
|
|
131
167
|
if self._try_connect():
|
|
132
168
|
return True
|
|
133
169
|
|
|
134
|
-
# --- Calendar marks to indicate text exists for htat day -----------------
|
|
135
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
|
+
"""
|
|
136
175
|
fmt_bold = QTextCharFormat()
|
|
137
176
|
fmt_bold.setFontWeight(QFont.Weight.Bold)
|
|
138
177
|
# Clear previous marks
|
|
@@ -153,15 +192,16 @@ class MainWindow(QMainWindow):
|
|
|
153
192
|
d = self.calendar.selectedDate()
|
|
154
193
|
return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
|
|
155
194
|
|
|
156
|
-
def _load_selected_date(self):
|
|
157
|
-
|
|
195
|
+
def _load_selected_date(self, date_iso=False):
|
|
196
|
+
if not date_iso:
|
|
197
|
+
date_iso = self._current_date_iso()
|
|
158
198
|
try:
|
|
159
199
|
text = self.db.get_entry(date_iso)
|
|
160
200
|
except Exception as e:
|
|
161
201
|
QMessageBox.critical(self, "Read Error", str(e))
|
|
162
202
|
return
|
|
163
203
|
self.editor.blockSignals(True)
|
|
164
|
-
self.editor.
|
|
204
|
+
self.editor.setHtml(text)
|
|
165
205
|
self.editor.blockSignals(False)
|
|
166
206
|
self._dirty = False
|
|
167
207
|
# track which date the editor currently represents
|
|
@@ -176,6 +216,11 @@ class MainWindow(QMainWindow):
|
|
|
176
216
|
d = self.calendar.selectedDate().addDays(delta)
|
|
177
217
|
self.calendar.setSelectedDate(d)
|
|
178
218
|
|
|
219
|
+
def _adjust_today(self):
|
|
220
|
+
"""Jump to today."""
|
|
221
|
+
today = QDate.currentDate()
|
|
222
|
+
self.calendar.setSelectedDate(today)
|
|
223
|
+
|
|
179
224
|
def _on_date_changed(self):
|
|
180
225
|
"""
|
|
181
226
|
When the calendar selection changes, save the previous day's note if dirty,
|
|
@@ -199,7 +244,7 @@ class MainWindow(QMainWindow):
|
|
|
199
244
|
"""
|
|
200
245
|
if not self._dirty and not explicit:
|
|
201
246
|
return
|
|
202
|
-
text = self.editor.
|
|
247
|
+
text = self.editor.toHtml()
|
|
203
248
|
try:
|
|
204
249
|
self.db.upsert_entry(date_iso, text)
|
|
205
250
|
except Exception as e:
|
|
@@ -219,7 +264,7 @@ class MainWindow(QMainWindow):
|
|
|
219
264
|
self._save_date(self._current_date_iso(), explicit)
|
|
220
265
|
|
|
221
266
|
def _open_settings(self):
|
|
222
|
-
dlg = SettingsDialog(self.cfg, self)
|
|
267
|
+
dlg = SettingsDialog(self.cfg, self.db, self)
|
|
223
268
|
if dlg.exec() == QDialog.Accepted:
|
|
224
269
|
new_cfg = dlg.config
|
|
225
270
|
if new_cfg.path != self.cfg.path:
|
|
@@ -236,7 +281,7 @@ class MainWindow(QMainWindow):
|
|
|
236
281
|
self._load_selected_date()
|
|
237
282
|
self._refresh_calendar_marks()
|
|
238
283
|
|
|
239
|
-
def closeEvent(self, event):
|
|
284
|
+
def closeEvent(self, event):
|
|
240
285
|
try:
|
|
241
286
|
self._save_current()
|
|
242
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
|
bouquin/settings_dialog.py
CHANGED
|
@@ -13,17 +13,20 @@ from PySide6.QtWidgets import (
|
|
|
13
13
|
QFileDialog,
|
|
14
14
|
QDialogButtonBox,
|
|
15
15
|
QSizePolicy,
|
|
16
|
+
QMessageBox,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
|
-
from .db import DBConfig
|
|
19
|
+
from .db import DBConfig, DBManager
|
|
19
20
|
from .settings import save_db_config
|
|
21
|
+
from .key_prompt import KeyPrompt
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class SettingsDialog(QDialog):
|
|
23
|
-
def __init__(self, cfg: DBConfig, parent=None):
|
|
25
|
+
def __init__(self, cfg: DBConfig, db: DBManager, parent=None):
|
|
24
26
|
super().__init__(parent)
|
|
25
27
|
self.setWindowTitle("Settings")
|
|
26
28
|
self._cfg = DBConfig(path=cfg.path, key="")
|
|
29
|
+
self._db = db
|
|
27
30
|
|
|
28
31
|
form = QFormLayout()
|
|
29
32
|
form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
|
|
@@ -44,12 +47,17 @@ class SettingsDialog(QDialog):
|
|
|
44
47
|
h.setStretch(1, 0)
|
|
45
48
|
form.addRow("Database path", path_row)
|
|
46
49
|
|
|
50
|
+
# Change key button
|
|
51
|
+
self.rekey_btn = QPushButton("Change key")
|
|
52
|
+
self.rekey_btn.clicked.connect(self._change_key)
|
|
53
|
+
|
|
47
54
|
bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
48
55
|
bb.accepted.connect(self._save)
|
|
49
56
|
bb.rejected.connect(self.reject)
|
|
50
57
|
|
|
51
58
|
v = QVBoxLayout(self)
|
|
52
59
|
v.addLayout(form)
|
|
60
|
+
v.addWidget(self.rekey_btn)
|
|
53
61
|
v.addWidget(bb)
|
|
54
62
|
|
|
55
63
|
def _browse(self):
|
|
@@ -67,6 +75,28 @@ class SettingsDialog(QDialog):
|
|
|
67
75
|
save_db_config(self._cfg)
|
|
68
76
|
self.accept()
|
|
69
77
|
|
|
78
|
+
def _change_key(self):
|
|
79
|
+
p1 = KeyPrompt(self, title="Change key", message="Enter new key")
|
|
80
|
+
if p1.exec() != QDialog.Accepted:
|
|
81
|
+
return
|
|
82
|
+
new_key = p1.key()
|
|
83
|
+
p2 = KeyPrompt(self, title="Change key", message="Re-enter new key")
|
|
84
|
+
if p2.exec() != QDialog.Accepted:
|
|
85
|
+
return
|
|
86
|
+
if new_key != p2.key():
|
|
87
|
+
QMessageBox.warning(self, "Key mismatch", "The two entries did not match.")
|
|
88
|
+
return
|
|
89
|
+
if not new_key:
|
|
90
|
+
QMessageBox.warning(self, "Empty key", "Key cannot be empty.")
|
|
91
|
+
return
|
|
92
|
+
try:
|
|
93
|
+
self._db.rekey(new_key)
|
|
94
|
+
QMessageBox.information(
|
|
95
|
+
self, "Key changed", "The database key was updated."
|
|
96
|
+
)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
QMessageBox.critical(self, "Error", f"Could not change key:\n{e}")
|
|
99
|
+
|
|
70
100
|
@property
|
|
71
101
|
def config(self) -> DBConfig:
|
|
72
102
|
return self._cfg
|
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.
|
|
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,18 +40,16 @@ 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
|
-
*
|
|
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
|
-
* Ability to change the SQLCipher key
|
|
53
53
|
* Export to other formats (plaintext, json, sql etc)
|
|
54
54
|
|
|
55
55
|
|
|
@@ -66,7 +66,7 @@ There is deliberately no network connectivity or syncing intended.
|
|
|
66
66
|
|
|
67
67
|
* Download the whl and run it
|
|
68
68
|
|
|
69
|
-
### From PyPi
|
|
69
|
+
### From PyPi/pip
|
|
70
70
|
|
|
71
71
|
* `pip install bouquin`
|
|
72
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)
|
bouquin-0.1.0.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
bouquin/__init__.py,sha256=-bBNFYOq80A2Egtpo5V5zWJtYOxQfRZFQ_feve5lkFU,23
|
|
2
|
-
bouquin/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
|
|
3
|
-
bouquin/db.py,sha256=0fCr--ko-StUMRPG96Nwzotq7a7xz1-9ZlmaEKs_2PA,2697
|
|
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=qWDM1ZJcPRHsX8oju_TaoLmBLGAwkaLcUIf44wSRLEQ,8830
|
|
8
|
-
bouquin/settings.py,sha256=bJYQXbTqX_r_DfOKuGnah6IVZLiNwZAuBuz2OgdhA_E,670
|
|
9
|
-
bouquin/settings_dialog.py,sha256=XW7SFg3_bD4rGj9D3jA65j7Jvq6049CxvRilfcGCKDU,2014
|
|
10
|
-
bouquin-0.1.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
11
|
-
bouquin-0.1.0.dist-info/METADATA,sha256=SFda5ee4tRfifEa10WNRbMG9uIhK5IrB1Pz4TbobBNI,2187
|
|
12
|
-
bouquin-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
13
|
-
bouquin-0.1.0.dist-info/entry_points.txt,sha256=d2C5Mc85suj1vWg_mmcfFuEBAYEkdwhZquusme5EWuQ,49
|
|
14
|
-
bouquin-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|