bouquin 0.1.0__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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .main import main
bouquin/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
bouquin/db.py ADDED
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from sqlcipher3 import dbapi2 as sqlite
7
+
8
+
9
+ @dataclass
10
+ class DBConfig:
11
+ path: Path
12
+ key: str
13
+
14
+
15
+ class DBManager:
16
+ def __init__(self, cfg: DBConfig):
17
+ self.cfg = cfg
18
+ self.conn: sqlite.Connection | None = None
19
+
20
+ def connect(self) -> bool:
21
+ # Ensure parent dir exists
22
+ self.cfg.path.parent.mkdir(parents=True, exist_ok=True)
23
+ self.conn = sqlite.connect(str(self.cfg.path))
24
+ cur = self.conn.cursor()
25
+ cur.execute(f"PRAGMA key = '{self.cfg.key}';")
26
+ cur.execute("PRAGMA cipher_compatibility = 4;")
27
+ cur.execute("PRAGMA journal_mode = WAL;")
28
+ self.conn.commit()
29
+ try:
30
+ self._integrity_ok()
31
+ except Exception:
32
+ self.conn.close()
33
+ self.conn = None
34
+ return False
35
+ self._ensure_schema()
36
+ return True
37
+
38
+ def _integrity_ok(self) -> bool:
39
+ cur = self.conn.cursor()
40
+ cur.execute("PRAGMA cipher_integrity_check;")
41
+ rows = cur.fetchall()
42
+
43
+ # OK
44
+ if not rows:
45
+ return
46
+
47
+ # Not OK
48
+ details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None)
49
+ raise sqlite.IntegrityError(
50
+ "SQLCipher integrity check failed"
51
+ + (f": {details}" if details else f" ({len(rows)} issue(s) reported)")
52
+ )
53
+
54
+ def _ensure_schema(self) -> None:
55
+ cur = self.conn.cursor()
56
+ cur.execute(
57
+ """
58
+ CREATE TABLE IF NOT EXISTS entries (
59
+ date TEXT PRIMARY KEY, -- ISO yyyy-MM-dd
60
+ content TEXT NOT NULL
61
+ );
62
+ """
63
+ )
64
+ cur.execute("PRAGMA user_version = 1;")
65
+ self.conn.commit()
66
+
67
+ def get_entry(self, date_iso: str) -> str:
68
+ cur = self.conn.cursor()
69
+ cur.execute("SELECT content FROM entries WHERE date = ?;", (date_iso,))
70
+ row = cur.fetchone()
71
+ return row[0] if row else ""
72
+
73
+ def upsert_entry(self, date_iso: str, content: str) -> None:
74
+ cur = self.conn.cursor()
75
+ cur.execute(
76
+ """
77
+ INSERT INTO entries(date, content) VALUES(?, ?)
78
+ ON CONFLICT(date) DO UPDATE SET content = excluded.content;
79
+ """,
80
+ (date_iso, content),
81
+ )
82
+ self.conn.commit()
83
+
84
+ def dates_with_content(self) -> list[str]:
85
+ cur = self.conn.cursor()
86
+ cur.execute("SELECT date FROM entries WHERE TRIM(content) <> '';")
87
+ return [r[0] for r in cur.fetchall()]
88
+
89
+ def close(self) -> None:
90
+ if self.conn is not None:
91
+ self.conn.close()
92
+ self.conn = None
bouquin/highlighter.py ADDED
@@ -0,0 +1,112 @@
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/key_prompt.py ADDED
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from PySide6.QtWidgets import (
4
+ QDialog,
5
+ QVBoxLayout,
6
+ QLabel,
7
+ QLineEdit,
8
+ QPushButton,
9
+ QDialogButtonBox,
10
+ )
11
+
12
+
13
+ class KeyPrompt(QDialog):
14
+ def __init__(
15
+ self,
16
+ parent=None,
17
+ title: str = "Unlock database",
18
+ message: str = "Enter SQLCipher key",
19
+ ):
20
+ super().__init__(parent)
21
+ self.setWindowTitle(title)
22
+ v = QVBoxLayout(self)
23
+ v.addWidget(QLabel(message))
24
+ self.edit = QLineEdit()
25
+ self.edit.setEchoMode(QLineEdit.Password)
26
+ v.addWidget(self.edit)
27
+ toggle = QPushButton("Show")
28
+ toggle.setCheckable(True)
29
+ toggle.toggled.connect(
30
+ lambda c: self.edit.setEchoMode(
31
+ QLineEdit.Normal if c else QLineEdit.Password
32
+ )
33
+ )
34
+ v.addWidget(toggle)
35
+ bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
36
+ bb.accepted.connect(self.accept)
37
+ bb.rejected.connect(self.reject)
38
+ v.addWidget(bb)
39
+
40
+ def key(self) -> str:
41
+ return self.edit.text()
bouquin/main.py ADDED
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from PySide6.QtWidgets import QApplication
5
+
6
+ from .settings import APP_NAME, APP_ORG
7
+ from .main_window import MainWindow
8
+
9
+
10
+ def main():
11
+ app = QApplication(sys.argv)
12
+ app.setApplicationName(APP_NAME)
13
+ app.setOrganizationName(APP_ORG)
14
+ win = MainWindow(); win.show()
15
+ sys.exit(app.exec())
bouquin/main_window.py ADDED
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from PySide6.QtCore import QDate, QTimer, Qt
6
+ from PySide6.QtGui import QAction, QFont, QTextCharFormat
7
+ from PySide6.QtWidgets import (
8
+ QDialog,
9
+ QCalendarWidget,
10
+ QMainWindow,
11
+ QMessageBox,
12
+ QPlainTextEdit,
13
+ QSplitter,
14
+ QVBoxLayout,
15
+ QWidget,
16
+ QSizePolicy,
17
+ )
18
+
19
+ from .db import DBManager
20
+ from .settings import APP_NAME, load_db_config, save_db_config
21
+ from .key_prompt import KeyPrompt
22
+ from .highlighter import MarkdownHighlighter
23
+ from .settings_dialog import SettingsDialog
24
+
25
+
26
+ class MainWindow(QMainWindow):
27
+ def __init__(self):
28
+ super().__init__()
29
+ self.setWindowTitle(APP_NAME)
30
+ self.setMinimumSize(1000, 650)
31
+
32
+ self.cfg = load_db_config()
33
+ # Always prompt for the key (we never store it)
34
+ if not self._prompt_for_key_until_valid():
35
+ sys.exit(1)
36
+
37
+ # ---- UI: Left fixed panel (calendar) + right editor -----------------
38
+ self.calendar = QCalendarWidget()
39
+ self.calendar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
40
+ self.calendar.setGridVisible(True)
41
+ self.calendar.selectionChanged.connect(self._on_date_changed)
42
+
43
+ left_panel = QWidget()
44
+ left_layout = QVBoxLayout(left_panel)
45
+ left_layout.setContentsMargins(8, 8, 8, 8)
46
+ left_layout.addWidget(self.calendar, alignment=Qt.AlignTop)
47
+ left_layout.addStretch(1)
48
+ left_panel.setFixedWidth(self.calendar.sizeHint().width() + 16)
49
+
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())
54
+
55
+ split = QSplitter()
56
+ split.addWidget(left_panel)
57
+ split.addWidget(self.editor)
58
+ split.setStretchFactor(1, 1) # editor grows
59
+
60
+ container = QWidget()
61
+ lay = QVBoxLayout(container)
62
+ lay.addWidget(split)
63
+ self.setCentralWidget(container)
64
+
65
+ # Status bar for feedback
66
+ self.statusBar().showMessage("Ready", 800)
67
+
68
+ # Menu bar (File)
69
+ mb = self.menuBar()
70
+ file_menu = mb.addMenu("&File")
71
+ act_save = QAction("&Save", self)
72
+ act_save.setShortcut("Ctrl+S")
73
+ act_save.triggered.connect(lambda: self._save_current(explicit=True))
74
+ file_menu.addAction(act_save)
75
+ act_settings = QAction("&Settings", self)
76
+ act_settings.triggered.connect(self._open_settings)
77
+ file_menu.addAction(act_settings)
78
+ file_menu.addSeparator()
79
+ act_quit = QAction("&Quit", self)
80
+ act_quit.setShortcut("Ctrl+Q")
81
+ act_quit.triggered.connect(self.close)
82
+ file_menu.addAction(act_quit)
83
+
84
+ # Navigate menu with next/previous day
85
+ nav_menu = mb.addMenu("&Navigate")
86
+ act_prev = QAction("Previous Day", self)
87
+ act_prev.setShortcut("Ctrl+P")
88
+ act_prev.setShortcutContext(Qt.ApplicationShortcut)
89
+ act_prev.triggered.connect(lambda: self._adjust_day(-1))
90
+ nav_menu.addAction(act_prev)
91
+ self.addAction(act_prev)
92
+
93
+ act_next = QAction("Next Day", self)
94
+ act_next.setShortcut("Ctrl+N")
95
+ act_next.setShortcutContext(Qt.ApplicationShortcut)
96
+ act_next.triggered.connect(lambda: self._adjust_day(1))
97
+ nav_menu.addAction(act_next)
98
+ self.addAction(act_next)
99
+
100
+ # Autosave
101
+ self._dirty = False
102
+ self._save_timer = QTimer(self)
103
+ self._save_timer.setSingleShot(True)
104
+ self._save_timer.timeout.connect(self._save_current)
105
+ self.editor.textChanged.connect(self._on_text_changed)
106
+
107
+ # First load + mark dates with content
108
+ self._load_selected_date()
109
+ self._refresh_calendar_marks()
110
+
111
+ # --- DB lifecycle
112
+ def _try_connect(self) -> bool:
113
+ try:
114
+ self.db = DBManager(self.cfg)
115
+ ok = self.db.connect()
116
+ except Exception as e:
117
+ if str(e) == "file is not a database":
118
+ error = "The key is probably incorrect."
119
+ else:
120
+ error = str(e)
121
+ QMessageBox.critical(self, "Database Error", error)
122
+ return False
123
+ return ok
124
+
125
+ def _prompt_for_key_until_valid(self) -> bool:
126
+ while True:
127
+ dlg = KeyPrompt(self, message="Enter a key to unlock the notebook")
128
+ if dlg.exec() != QDialog.Accepted:
129
+ return False
130
+ self.cfg.key = dlg.key()
131
+ if self._try_connect():
132
+ return True
133
+
134
+ # --- Calendar marks to indicate text exists for htat day -----------------
135
+ def _refresh_calendar_marks(self):
136
+ fmt_bold = QTextCharFormat()
137
+ fmt_bold.setFontWeight(QFont.Weight.Bold)
138
+ # Clear previous marks
139
+ for d in getattr(self, "_marked_dates", set()):
140
+ self.calendar.setDateTextFormat(d, QTextCharFormat())
141
+ self._marked_dates = set()
142
+ try:
143
+ for date_iso in self.db.dates_with_content():
144
+ qd = QDate.fromString(date_iso, "yyyy-MM-dd")
145
+ if qd.isValid():
146
+ self.calendar.setDateTextFormat(qd, fmt_bold)
147
+ self._marked_dates.add(qd)
148
+ except Exception:
149
+ pass
150
+
151
+ # --- UI handlers ---------------------------------------------------------
152
+ def _current_date_iso(self) -> str:
153
+ d = self.calendar.selectedDate()
154
+ return f"{d.year():04d}-{d.month():02d}-{d.day():02d}"
155
+
156
+ def _load_selected_date(self):
157
+ date_iso = self._current_date_iso()
158
+ try:
159
+ text = self.db.get_entry(date_iso)
160
+ except Exception as e:
161
+ QMessageBox.critical(self, "Read Error", str(e))
162
+ return
163
+ self.editor.blockSignals(True)
164
+ self.editor.setPlainText(text)
165
+ self.editor.blockSignals(False)
166
+ self._dirty = False
167
+ # track which date the editor currently represents
168
+ self._active_date_iso = date_iso
169
+
170
+ def _on_text_changed(self):
171
+ self._dirty = True
172
+ self._save_timer.start(1200) # autosave after idle
173
+
174
+ def _adjust_day(self, delta: int):
175
+ """Move selection by delta days (negative for previous)."""
176
+ d = self.calendar.selectedDate().addDays(delta)
177
+ self.calendar.setSelectedDate(d)
178
+
179
+ def _on_date_changed(self):
180
+ """
181
+ When the calendar selection changes, save the previous day's note if dirty,
182
+ so we don't lose that text, then load the newly selected day.
183
+ """
184
+ # Stop pending autosave and persist current buffer if needed
185
+ try:
186
+ self._save_timer.stop()
187
+ except Exception:
188
+ pass
189
+ prev = getattr(self, "_active_date_iso", None)
190
+ if prev and self._dirty:
191
+ self._save_date(prev, explicit=False)
192
+ # Now load the newly selected date
193
+ self._load_selected_date()
194
+
195
+ def _save_date(self, date_iso: str, explicit: bool = False):
196
+ """
197
+ Save editor contents into the given date. Shows status on success.
198
+ explicit=True means user invoked Save: show feedback even if nothing changed.
199
+ """
200
+ if not self._dirty and not explicit:
201
+ return
202
+ text = self.editor.toPlainText()
203
+ try:
204
+ self.db.upsert_entry(date_iso, text)
205
+ except Exception as e:
206
+ QMessageBox.critical(self, "Save Error", str(e))
207
+ return
208
+ self._dirty = False
209
+ self._refresh_calendar_marks()
210
+ # Feedback in the status bar
211
+ from datetime import datetime as _dt
212
+
213
+ self.statusBar().showMessage(
214
+ f"Saved {date_iso} at {_dt.now().strftime('%H:%M:%S')}", 2000
215
+ )
216
+
217
+ def _save_current(self, explicit: bool = False):
218
+ # Delegate to _save_date for the currently selected date
219
+ self._save_date(self._current_date_iso(), explicit)
220
+
221
+ def _open_settings(self):
222
+ dlg = SettingsDialog(self.cfg, self)
223
+ if dlg.exec() == QDialog.Accepted:
224
+ new_cfg = dlg.config
225
+ if new_cfg.path != self.cfg.path:
226
+ # Save the new path to the notebook
227
+ self.cfg.path = new_cfg.path
228
+ save_db_config(self.cfg)
229
+ self.db.close()
230
+ # Prompt again for the key for the new path
231
+ if not self._prompt_for_key_until_valid():
232
+ QMessageBox.warning(
233
+ self, "Reopen failed", "Could not unlock database at new path."
234
+ )
235
+ return
236
+ self._load_selected_date()
237
+ self._refresh_calendar_marks()
238
+
239
+ def closeEvent(self, event): # noqa: N802
240
+ try:
241
+ self._save_current()
242
+ self.db.close()
243
+ except Exception:
244
+ pass
245
+ super().closeEvent(event)
bouquin/settings.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from PySide6.QtCore import QSettings, QStandardPaths
5
+
6
+ from .db import DBConfig
7
+
8
+ APP_ORG = "Bouquin"
9
+ APP_NAME = "Bouquin"
10
+
11
+
12
+ def default_db_path() -> Path:
13
+ base = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
14
+ return base / "notebook.db"
15
+
16
+
17
+ def get_settings() -> QSettings:
18
+ return QSettings(APP_ORG, APP_NAME)
19
+
20
+
21
+ def load_db_config() -> DBConfig:
22
+ s = get_settings()
23
+ path = Path(s.value("db/path", str(default_db_path())))
24
+ return DBConfig(path=path, key="")
25
+
26
+
27
+ def save_db_config(cfg: DBConfig) -> None:
28
+ s = get_settings()
29
+ s.setValue("db/path", str(cfg.path))
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from PySide6.QtWidgets import (
6
+ QDialog,
7
+ QFormLayout,
8
+ QHBoxLayout,
9
+ QVBoxLayout,
10
+ QWidget,
11
+ QLineEdit,
12
+ QPushButton,
13
+ QFileDialog,
14
+ QDialogButtonBox,
15
+ QSizePolicy,
16
+ )
17
+
18
+ from .db import DBConfig
19
+ from .settings import save_db_config
20
+
21
+
22
+ class SettingsDialog(QDialog):
23
+ def __init__(self, cfg: DBConfig, parent=None):
24
+ super().__init__(parent)
25
+ self.setWindowTitle("Settings")
26
+ self._cfg = DBConfig(path=cfg.path, key="")
27
+
28
+ form = QFormLayout()
29
+ form.setFieldGrowthPolicy(QFormLayout.ExpandingFieldsGrow)
30
+ self.setMinimumWidth(520)
31
+ self.setSizeGripEnabled(True)
32
+
33
+ self.path_edit = QLineEdit(str(self._cfg.path))
34
+ self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
35
+ browse_btn = QPushButton("Browse…")
36
+ browse_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
37
+ browse_btn.clicked.connect(self._browse)
38
+ path_row = QWidget()
39
+ h = QHBoxLayout(path_row)
40
+ h.setContentsMargins(0, 0, 0, 0)
41
+ h.addWidget(self.path_edit, 1)
42
+ h.addWidget(browse_btn, 0)
43
+ h.setStretch(0, 1)
44
+ h.setStretch(1, 0)
45
+ form.addRow("Database path", path_row)
46
+
47
+ bb = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
48
+ bb.accepted.connect(self._save)
49
+ bb.rejected.connect(self.reject)
50
+
51
+ v = QVBoxLayout(self)
52
+ v.addLayout(form)
53
+ v.addWidget(bb)
54
+
55
+ def _browse(self):
56
+ p, _ = QFileDialog.getSaveFileName(
57
+ self,
58
+ "Choose database file",
59
+ self.path_edit.text(),
60
+ "DB Files (*.db);;All Files (*)",
61
+ )
62
+ if p:
63
+ self.path_edit.setText(p)
64
+
65
+ def _save(self):
66
+ self._cfg = DBConfig(path=Path(self.path_edit.text()), key="")
67
+ save_db_config(self._cfg)
68
+ self.accept()
69
+
70
+ @property
71
+ def config(self) -> DBConfig:
72
+ return self._cfg