linux-kernel-manager 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.
- linux_kernel_manager-0.1.2.dist-info/METADATA +271 -0
- linux_kernel_manager-0.1.2.dist-info/RECORD +45 -0
- linux_kernel_manager-0.1.2.dist-info/WHEEL +4 -0
- linux_kernel_manager-0.1.2.dist-info/entry_points.txt +3 -0
- linux_kernel_manager-0.1.2.dist-info/licenses/LICENSE +675 -0
- lkm/__init__.py +2 -0
- lkm/cli/__init__.py +0 -0
- lkm/cli/main.py +332 -0
- lkm/cli/output.py +57 -0
- lkm/core/__init__.py +0 -0
- lkm/core/backends/__init__.py +54 -0
- lkm/core/backends/apk.py +46 -0
- lkm/core/backends/apt.py +77 -0
- lkm/core/backends/base.py +81 -0
- lkm/core/backends/dnf.py +44 -0
- lkm/core/backends/nix.py +195 -0
- lkm/core/backends/pacman.py +78 -0
- lkm/core/backends/portage.py +107 -0
- lkm/core/backends/xbps.py +139 -0
- lkm/core/backends/zypper.py +44 -0
- lkm/core/kernel.py +95 -0
- lkm/core/manager.py +243 -0
- lkm/core/providers/__init__.py +62 -0
- lkm/core/providers/base.py +88 -0
- lkm/core/providers/distro.py +139 -0
- lkm/core/providers/gentoo.py +68 -0
- lkm/core/providers/liquorix.py +80 -0
- lkm/core/providers/lkf_build.py +355 -0
- lkm/core/providers/local_file.py +104 -0
- lkm/core/providers/mainline.py +106 -0
- lkm/core/providers/nixos.py +76 -0
- lkm/core/providers/void.py +69 -0
- lkm/core/providers/xanmod.py +81 -0
- lkm/core/system.py +235 -0
- lkm/gui/__init__.py +0 -0
- lkm/gui/app.py +94 -0
- lkm/gui/kernel_model.py +98 -0
- lkm/gui/main_window.py +385 -0
- lkm/gui/widgets/__init__.py +0 -0
- lkm/gui/widgets/gentoo_compile_dialog.py +132 -0
- lkm/gui/widgets/kernel_view.py +121 -0
- lkm/gui/widgets/lkf_build_dialog.py +370 -0
- lkm/gui/widgets/log_panel.py +78 -0
- lkm/gui/widgets/note_dialog.py +38 -0
- lkm/qt.py +122 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""
|
|
2
|
+
lkf Build dialog — GUI front-end for the lkf build pipeline.
|
|
3
|
+
|
|
4
|
+
Two modes:
|
|
5
|
+
Profile mode — pick a remix.toml from the discovered profile list and
|
|
6
|
+
run `lkf remix --file <profile>`.
|
|
7
|
+
Custom mode — specify version, flavor, arch, compiler flags manually
|
|
8
|
+
and run `lkf build`.
|
|
9
|
+
|
|
10
|
+
The dialog streams lkf output live via a QThread worker, then optionally
|
|
11
|
+
installs the resulting package through the system backend.
|
|
12
|
+
|
|
13
|
+
This dialog is modelled on GentooCompileDialog and uses the same LogPanel
|
|
14
|
+
and worker pattern.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from lkm.core.providers.lkf_build import LkfBuildProvider
|
|
21
|
+
from lkm.gui.widgets.log_panel import LogPanel
|
|
22
|
+
from lkm.qt import (
|
|
23
|
+
QCheckBox,
|
|
24
|
+
QComboBox,
|
|
25
|
+
QDialog,
|
|
26
|
+
QFileDialog,
|
|
27
|
+
QGroupBox,
|
|
28
|
+
QHBoxLayout,
|
|
29
|
+
QLabel,
|
|
30
|
+
QLineEdit,
|
|
31
|
+
QMessageBox,
|
|
32
|
+
QPushButton,
|
|
33
|
+
QSizePolicy,
|
|
34
|
+
QTabWidget,
|
|
35
|
+
QThread,
|
|
36
|
+
QVBoxLayout,
|
|
37
|
+
QWidget,
|
|
38
|
+
Signal,
|
|
39
|
+
Slot,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# Worker thread
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
class _BuildWorker(QThread):
|
|
47
|
+
line_ready = Signal(str)
|
|
48
|
+
finished_ok = Signal(str) # emits path to output package (may be "")
|
|
49
|
+
finished_err = Signal(str)
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
provider: LkfBuildProvider,
|
|
54
|
+
mode: str, # "profile" | "custom"
|
|
55
|
+
profile_path: str = "",
|
|
56
|
+
version: str = "",
|
|
57
|
+
flavor: str = "mainline",
|
|
58
|
+
arch: str = "",
|
|
59
|
+
llvm: bool = False,
|
|
60
|
+
lto: str = "none",
|
|
61
|
+
output_fmt: str = "deb",
|
|
62
|
+
) -> None:
|
|
63
|
+
super().__init__()
|
|
64
|
+
self._provider = provider
|
|
65
|
+
self._mode = mode
|
|
66
|
+
self._profile = profile_path
|
|
67
|
+
self._version = version
|
|
68
|
+
self._flavor = flavor
|
|
69
|
+
self._arch = arch
|
|
70
|
+
self._llvm = llvm
|
|
71
|
+
self._lto = lto
|
|
72
|
+
self._output_fmt = output_fmt
|
|
73
|
+
|
|
74
|
+
def run(self) -> None:
|
|
75
|
+
try:
|
|
76
|
+
if self._mode == "profile":
|
|
77
|
+
gen = self._provider.build_only(self._profile)
|
|
78
|
+
else:
|
|
79
|
+
gen = self._provider.build_custom(
|
|
80
|
+
version=self._version,
|
|
81
|
+
flavor=self._flavor,
|
|
82
|
+
arch=self._arch or None,
|
|
83
|
+
llvm=self._llvm,
|
|
84
|
+
lto=self._lto,
|
|
85
|
+
output_fmt=self._output_fmt,
|
|
86
|
+
)
|
|
87
|
+
for line in gen:
|
|
88
|
+
self.line_ready.emit(line)
|
|
89
|
+
|
|
90
|
+
# Locate the output package
|
|
91
|
+
from lkm.core.providers.lkf_build import _find_output_package
|
|
92
|
+
pkg = _find_output_package(
|
|
93
|
+
self._provider.output_dir,
|
|
94
|
+
self._version,
|
|
95
|
+
)
|
|
96
|
+
self.finished_ok.emit(str(pkg) if pkg else "")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
self.finished_err.emit(str(e))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Dialog
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
class LkfBuildDialog(QDialog):
|
|
106
|
+
"""
|
|
107
|
+
Full-featured build dialog for the lkf pipeline.
|
|
108
|
+
|
|
109
|
+
Opened from the Build tab in the main window.
|
|
110
|
+
On successful build, emits build_succeeded(pkg_path) so the main window
|
|
111
|
+
can offer to install the result.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
build_succeeded = Signal(str) # absolute path to the output package
|
|
115
|
+
|
|
116
|
+
def __init__(self, provider: LkfBuildProvider, parent=None) -> None:
|
|
117
|
+
super().__init__(parent)
|
|
118
|
+
self._provider = provider
|
|
119
|
+
self._worker: _BuildWorker | None = None
|
|
120
|
+
self.setWindowTitle("Build Kernel with lkf")
|
|
121
|
+
self.setMinimumSize(720, 580)
|
|
122
|
+
self._setup_ui()
|
|
123
|
+
self._populate_profiles()
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# UI construction
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def _setup_ui(self) -> None:
|
|
130
|
+
root = QVBoxLayout(self)
|
|
131
|
+
|
|
132
|
+
self._tabs = QTabWidget()
|
|
133
|
+
self._tabs.addTab(self._make_profile_tab(), "Profile")
|
|
134
|
+
self._tabs.addTab(self._make_custom_tab(), "Custom")
|
|
135
|
+
root.addWidget(self._tabs)
|
|
136
|
+
|
|
137
|
+
# Output dir label
|
|
138
|
+
out_row = QHBoxLayout()
|
|
139
|
+
out_row.addWidget(QLabel("Output dir:"))
|
|
140
|
+
self._out_label = QLabel(str(self._provider.output_dir))
|
|
141
|
+
self._out_label.setWordWrap(True)
|
|
142
|
+
out_row.addWidget(self._out_label, 1)
|
|
143
|
+
change_btn = QPushButton("Change…")
|
|
144
|
+
change_btn.clicked.connect(self._change_output_dir)
|
|
145
|
+
out_row.addWidget(change_btn)
|
|
146
|
+
root.addLayout(out_row)
|
|
147
|
+
|
|
148
|
+
# Log
|
|
149
|
+
self._log = LogPanel()
|
|
150
|
+
root.addWidget(self._log)
|
|
151
|
+
|
|
152
|
+
# Buttons
|
|
153
|
+
btn_row = QHBoxLayout()
|
|
154
|
+
btn_row.addStretch()
|
|
155
|
+
self._build_btn = QPushButton("Build")
|
|
156
|
+
self._build_btn.setDefault(True)
|
|
157
|
+
self._build_btn.clicked.connect(self._start_build)
|
|
158
|
+
btn_row.addWidget(self._build_btn)
|
|
159
|
+
|
|
160
|
+
self._install_btn = QPushButton("Build && Install")
|
|
161
|
+
self._install_btn.clicked.connect(self._start_build_and_install)
|
|
162
|
+
btn_row.addWidget(self._install_btn)
|
|
163
|
+
|
|
164
|
+
self._close_btn = QPushButton("Close")
|
|
165
|
+
self._close_btn.clicked.connect(self.reject)
|
|
166
|
+
btn_row.addWidget(self._close_btn)
|
|
167
|
+
root.addLayout(btn_row)
|
|
168
|
+
|
|
169
|
+
self._pending_install = False
|
|
170
|
+
|
|
171
|
+
def _make_profile_tab(self) -> QWidget:
|
|
172
|
+
w = QWidget()
|
|
173
|
+
layout = QVBoxLayout(w)
|
|
174
|
+
|
|
175
|
+
row = QHBoxLayout()
|
|
176
|
+
row.addWidget(QLabel("Profile:"))
|
|
177
|
+
self._profile_combo = QComboBox()
|
|
178
|
+
self._profile_combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
179
|
+
row.addWidget(self._profile_combo)
|
|
180
|
+
|
|
181
|
+
browse_btn = QPushButton("Browse…")
|
|
182
|
+
browse_btn.clicked.connect(self._browse_profile)
|
|
183
|
+
row.addWidget(browse_btn)
|
|
184
|
+
layout.addLayout(row)
|
|
185
|
+
|
|
186
|
+
# Profile details (read-only)
|
|
187
|
+
self._profile_detail = QLabel("")
|
|
188
|
+
self._profile_detail.setWordWrap(True)
|
|
189
|
+
layout.addWidget(self._profile_detail)
|
|
190
|
+
layout.addStretch()
|
|
191
|
+
|
|
192
|
+
self._profile_combo.currentIndexChanged.connect(self._update_profile_detail)
|
|
193
|
+
return w
|
|
194
|
+
|
|
195
|
+
def _make_custom_tab(self) -> QWidget:
|
|
196
|
+
w = QWidget()
|
|
197
|
+
layout = QVBoxLayout(w)
|
|
198
|
+
|
|
199
|
+
# Version
|
|
200
|
+
row1 = QHBoxLayout()
|
|
201
|
+
row1.addWidget(QLabel("Version:"))
|
|
202
|
+
self._version_edit = QLineEdit()
|
|
203
|
+
self._version_edit.setPlaceholderText("e.g. 6.12")
|
|
204
|
+
row1.addWidget(self._version_edit)
|
|
205
|
+
layout.addLayout(row1)
|
|
206
|
+
|
|
207
|
+
# Flavor + arch
|
|
208
|
+
row2 = QHBoxLayout()
|
|
209
|
+
row2.addWidget(QLabel("Flavor:"))
|
|
210
|
+
self._flavor_combo = QComboBox()
|
|
211
|
+
for f in ["mainline", "xanmod", "cachyos", "zen", "rt", "tkg", "android", "custom"]:
|
|
212
|
+
self._flavor_combo.addItem(f)
|
|
213
|
+
row2.addWidget(self._flavor_combo)
|
|
214
|
+
|
|
215
|
+
row2.addWidget(QLabel("Arch:"))
|
|
216
|
+
self._arch_combo = QComboBox()
|
|
217
|
+
for a in ["(host)", "x86_64", "aarch64", "arm", "riscv64"]:
|
|
218
|
+
self._arch_combo.addItem(a)
|
|
219
|
+
row2.addWidget(self._arch_combo)
|
|
220
|
+
layout.addLayout(row2)
|
|
221
|
+
|
|
222
|
+
# Compiler options
|
|
223
|
+
compiler_box = QGroupBox("Compiler")
|
|
224
|
+
cb_layout = QHBoxLayout(compiler_box)
|
|
225
|
+
self._llvm_cb = QCheckBox("Clang/LLVM")
|
|
226
|
+
cb_layout.addWidget(self._llvm_cb)
|
|
227
|
+
cb_layout.addWidget(QLabel("LTO:"))
|
|
228
|
+
self._lto_combo = QComboBox()
|
|
229
|
+
for lto in ["none", "thin", "full"]:
|
|
230
|
+
self._lto_combo.addItem(lto)
|
|
231
|
+
cb_layout.addWidget(self._lto_combo)
|
|
232
|
+
cb_layout.addStretch()
|
|
233
|
+
layout.addWidget(compiler_box)
|
|
234
|
+
|
|
235
|
+
# Output format
|
|
236
|
+
row3 = QHBoxLayout()
|
|
237
|
+
row3.addWidget(QLabel("Output format:"))
|
|
238
|
+
self._fmt_combo = QComboBox()
|
|
239
|
+
for fmt in ["deb", "rpm", "pkg.tar.zst", "tar.gz", "efi-unified", "android-boot"]:
|
|
240
|
+
self._fmt_combo.addItem(fmt)
|
|
241
|
+
row3.addWidget(self._fmt_combo)
|
|
242
|
+
row3.addStretch()
|
|
243
|
+
layout.addLayout(row3)
|
|
244
|
+
|
|
245
|
+
layout.addStretch()
|
|
246
|
+
return w
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# Profile helpers
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def _populate_profiles(self) -> None:
|
|
253
|
+
from lkm.core.providers.lkf_build import _find_lkf_root, _find_profiles
|
|
254
|
+
profiles = _find_profiles(_find_lkf_root())
|
|
255
|
+
if not profiles:
|
|
256
|
+
self._profile_combo.addItem("No profiles found", "")
|
|
257
|
+
self._build_btn.setEnabled(self._tabs.currentIndex() != 0)
|
|
258
|
+
return
|
|
259
|
+
for p in profiles:
|
|
260
|
+
from lkm.core.providers.lkf_build import _parse_profile_name, _parse_profile_version
|
|
261
|
+
label = f"{_parse_profile_name(p)} ({_parse_profile_version(p)})"
|
|
262
|
+
self._profile_combo.addItem(label, str(p))
|
|
263
|
+
|
|
264
|
+
def _update_profile_detail(self, _: int) -> None:
|
|
265
|
+
path = self._profile_combo.currentData()
|
|
266
|
+
if not path:
|
|
267
|
+
self._profile_detail.setText("")
|
|
268
|
+
return
|
|
269
|
+
try:
|
|
270
|
+
text = Path(path).read_text()
|
|
271
|
+
# Show first 8 lines of the TOML
|
|
272
|
+
preview = "\n".join(text.splitlines()[:8])
|
|
273
|
+
self._profile_detail.setText(f"<pre>{preview}</pre>")
|
|
274
|
+
except OSError:
|
|
275
|
+
self._profile_detail.setText("")
|
|
276
|
+
|
|
277
|
+
def _browse_profile(self) -> None:
|
|
278
|
+
path, _ = QFileDialog.getOpenFileName(
|
|
279
|
+
self, "Select remix.toml", str(Path.home()), "TOML files (*.toml)"
|
|
280
|
+
)
|
|
281
|
+
if path:
|
|
282
|
+
self._profile_combo.insertItem(0, Path(path).name, path)
|
|
283
|
+
self._profile_combo.setCurrentIndex(0)
|
|
284
|
+
|
|
285
|
+
def _change_output_dir(self) -> None:
|
|
286
|
+
d = QFileDialog.getExistingDirectory(
|
|
287
|
+
self, "Select output directory", str(self._provider.output_dir)
|
|
288
|
+
)
|
|
289
|
+
if d:
|
|
290
|
+
self._provider._output_dir = Path(d)
|
|
291
|
+
self._out_label.setText(d)
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------
|
|
294
|
+
# Build actions
|
|
295
|
+
# ------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def _start_build(self) -> None:
|
|
298
|
+
self._pending_install = False
|
|
299
|
+
self._run_build()
|
|
300
|
+
|
|
301
|
+
def _start_build_and_install(self) -> None:
|
|
302
|
+
self._pending_install = True
|
|
303
|
+
self._run_build()
|
|
304
|
+
|
|
305
|
+
def _run_build(self) -> None:
|
|
306
|
+
if self._worker and self._worker.isRunning():
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
self._log.clear()
|
|
310
|
+
self._log.show_panel()
|
|
311
|
+
self._build_btn.setEnabled(False)
|
|
312
|
+
self._install_btn.setEnabled(False)
|
|
313
|
+
|
|
314
|
+
mode = "profile" if self._tabs.currentIndex() == 0 else "custom"
|
|
315
|
+
|
|
316
|
+
if mode == "profile":
|
|
317
|
+
profile = self._profile_combo.currentData()
|
|
318
|
+
if not profile:
|
|
319
|
+
QMessageBox.warning(self, "No profile", "Select a profile first.")
|
|
320
|
+
self._build_btn.setEnabled(True)
|
|
321
|
+
self._install_btn.setEnabled(True)
|
|
322
|
+
return
|
|
323
|
+
self._worker = _BuildWorker(
|
|
324
|
+
provider=self._provider,
|
|
325
|
+
mode="profile",
|
|
326
|
+
profile_path=profile,
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
version = self._version_edit.text().strip()
|
|
330
|
+
if not version:
|
|
331
|
+
QMessageBox.warning(self, "No version", "Enter a kernel version.")
|
|
332
|
+
self._build_btn.setEnabled(True)
|
|
333
|
+
self._install_btn.setEnabled(True)
|
|
334
|
+
return
|
|
335
|
+
arch_text = self._arch_combo.currentText()
|
|
336
|
+
arch = "" if arch_text == "(host)" else arch_text
|
|
337
|
+
self._worker = _BuildWorker(
|
|
338
|
+
provider=self._provider,
|
|
339
|
+
mode="custom",
|
|
340
|
+
version=version,
|
|
341
|
+
flavor=self._flavor_combo.currentText(),
|
|
342
|
+
arch=arch,
|
|
343
|
+
llvm=self._llvm_cb.isChecked(),
|
|
344
|
+
lto=self._lto_combo.currentText(),
|
|
345
|
+
output_fmt=self._fmt_combo.currentText(),
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
self._worker.line_ready.connect(self._log.append)
|
|
349
|
+
self._worker.finished_ok.connect(self._on_build_done)
|
|
350
|
+
self._worker.finished_err.connect(self._on_build_error)
|
|
351
|
+
self._worker.start()
|
|
352
|
+
|
|
353
|
+
@Slot(str)
|
|
354
|
+
def _on_build_done(self, pkg_path: str) -> None:
|
|
355
|
+
self._log.append("\n✓ Build complete.\n")
|
|
356
|
+
if pkg_path:
|
|
357
|
+
self._log.append(f"Package: {pkg_path}\n")
|
|
358
|
+
self._build_btn.setEnabled(True)
|
|
359
|
+
self._install_btn.setEnabled(True)
|
|
360
|
+
|
|
361
|
+
if pkg_path:
|
|
362
|
+
self.build_succeeded.emit(pkg_path)
|
|
363
|
+
if self._pending_install:
|
|
364
|
+
self.accept() # close dialog; main window handles install
|
|
365
|
+
|
|
366
|
+
@Slot(str)
|
|
367
|
+
def _on_build_error(self, msg: str) -> None:
|
|
368
|
+
self._log.append(f"\n✗ Build failed: {msg}\n")
|
|
369
|
+
self._build_btn.setEnabled(True)
|
|
370
|
+
self._install_btn.setEnabled(True)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Collapsible live log panel — shared by the kernel view and build dialogs."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from lkm.qt import (
|
|
5
|
+
QFont,
|
|
6
|
+
QHBoxLayout,
|
|
7
|
+
QPushButton,
|
|
8
|
+
QSizePolicy,
|
|
9
|
+
QTextEdit,
|
|
10
|
+
QVBoxLayout,
|
|
11
|
+
QWidget,
|
|
12
|
+
Slot,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LogPanel(QWidget):
|
|
17
|
+
"""
|
|
18
|
+
A collapsible text area that streams log output.
|
|
19
|
+
|
|
20
|
+
Call append(line) from any thread via a Signal connection.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, parent=None) -> None:
|
|
24
|
+
super().__init__(parent)
|
|
25
|
+
self._setup_ui()
|
|
26
|
+
|
|
27
|
+
def _setup_ui(self) -> None:
|
|
28
|
+
layout = QVBoxLayout(self)
|
|
29
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
30
|
+
|
|
31
|
+
# Toggle bar
|
|
32
|
+
bar = QHBoxLayout()
|
|
33
|
+
self._toggle_btn = QPushButton("▼ Log")
|
|
34
|
+
self._toggle_btn.setFlat(True)
|
|
35
|
+
self._toggle_btn.setCheckable(True)
|
|
36
|
+
self._toggle_btn.setChecked(True)
|
|
37
|
+
self._toggle_btn.clicked.connect(self._toggle)
|
|
38
|
+
bar.addWidget(self._toggle_btn)
|
|
39
|
+
|
|
40
|
+
self._clear_btn = QPushButton("Clear")
|
|
41
|
+
self._clear_btn.setFlat(True)
|
|
42
|
+
self._clear_btn.clicked.connect(self.clear)
|
|
43
|
+
bar.addWidget(self._clear_btn)
|
|
44
|
+
bar.addStretch()
|
|
45
|
+
layout.addLayout(bar)
|
|
46
|
+
|
|
47
|
+
# Log text area
|
|
48
|
+
self._text = QTextEdit()
|
|
49
|
+
self._text.setReadOnly(True)
|
|
50
|
+
self._text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
|
51
|
+
font = QFont("Monospace")
|
|
52
|
+
font.setStyleHint(QFont.StyleHint.TypeWriter)
|
|
53
|
+
font.setPointSize(9)
|
|
54
|
+
self._text.setFont(font)
|
|
55
|
+
self._text.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
56
|
+
layout.addWidget(self._text)
|
|
57
|
+
|
|
58
|
+
@Slot(str)
|
|
59
|
+
def append(self, text: str) -> None:
|
|
60
|
+
self._text.insertPlainText(text)
|
|
61
|
+
self._text.ensureCursorVisible()
|
|
62
|
+
|
|
63
|
+
def clear(self) -> None:
|
|
64
|
+
self._text.clear()
|
|
65
|
+
|
|
66
|
+
def show_panel(self) -> None:
|
|
67
|
+
self._text.setVisible(True)
|
|
68
|
+
self._toggle_btn.setChecked(True)
|
|
69
|
+
self._toggle_btn.setText("▼ Log")
|
|
70
|
+
|
|
71
|
+
def hide_panel(self) -> None:
|
|
72
|
+
self._text.setVisible(False)
|
|
73
|
+
self._toggle_btn.setChecked(False)
|
|
74
|
+
self._toggle_btn.setText("▶ Log")
|
|
75
|
+
|
|
76
|
+
def _toggle(self, checked: bool) -> None:
|
|
77
|
+
self._text.setVisible(checked)
|
|
78
|
+
self._toggle_btn.setText("▼ Log" if checked else "▶ Log")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Note editing dialog."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from lkm.qt import (
|
|
5
|
+
QDialog,
|
|
6
|
+
QDialogButtonBox,
|
|
7
|
+
QLabel,
|
|
8
|
+
QTextEdit,
|
|
9
|
+
QVBoxLayout,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NoteDialog(QDialog):
|
|
14
|
+
|
|
15
|
+
def __init__(self, kernel_name: str, current_note: str = "", parent=None) -> None:
|
|
16
|
+
super().__init__(parent)
|
|
17
|
+
self.setWindowTitle(f"Note — {kernel_name}")
|
|
18
|
+
self.setMinimumWidth(400)
|
|
19
|
+
self._setup_ui(current_note)
|
|
20
|
+
|
|
21
|
+
def _setup_ui(self, current_note: str) -> None:
|
|
22
|
+
layout = QVBoxLayout(self)
|
|
23
|
+
layout.addWidget(QLabel("Note:"))
|
|
24
|
+
self._edit = QTextEdit()
|
|
25
|
+
self._edit.setPlainText(current_note)
|
|
26
|
+
self._edit.setMinimumHeight(80)
|
|
27
|
+
layout.addWidget(self._edit)
|
|
28
|
+
|
|
29
|
+
btns = QDialogButtonBox(
|
|
30
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
31
|
+
)
|
|
32
|
+
btns.accepted.connect(self.accept)
|
|
33
|
+
btns.rejected.connect(self.reject)
|
|
34
|
+
layout.addWidget(btns)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def note(self) -> str:
|
|
38
|
+
return self._edit.toPlainText().strip()
|
lkm/qt.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PySide6 / PyQt6 compatibility shim.
|
|
3
|
+
|
|
4
|
+
Import Qt symbols from here rather than directly from PySide6 or PyQt6.
|
|
5
|
+
The binding is selected by the LKM_QT environment variable, falling back
|
|
6
|
+
to PySide6 then PyQt6.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
_binding = os.environ.get("LKM_QT", "")
|
|
13
|
+
|
|
14
|
+
if _binding == "PyQt6":
|
|
15
|
+
_use_pyqt6 = True
|
|
16
|
+
elif _binding == "PySide6":
|
|
17
|
+
_use_pyqt6 = False
|
|
18
|
+
else:
|
|
19
|
+
try:
|
|
20
|
+
import PySide6 # noqa: F401
|
|
21
|
+
_use_pyqt6 = False
|
|
22
|
+
except ImportError:
|
|
23
|
+
_use_pyqt6 = True
|
|
24
|
+
|
|
25
|
+
if _use_pyqt6:
|
|
26
|
+
from PyQt6.QtCore import (
|
|
27
|
+
QAbstractTableModel,
|
|
28
|
+
QModelIndex,
|
|
29
|
+
QSortFilterProxyModel,
|
|
30
|
+
Qt,
|
|
31
|
+
QThread,
|
|
32
|
+
QTimer,
|
|
33
|
+
)
|
|
34
|
+
from PyQt6.QtCore import (
|
|
35
|
+
pyqtSignal as Signal,
|
|
36
|
+
)
|
|
37
|
+
from PyQt6.QtCore import (
|
|
38
|
+
pyqtSlot as Slot,
|
|
39
|
+
)
|
|
40
|
+
from PyQt6.QtGui import QColor, QFont, QIcon
|
|
41
|
+
from PyQt6.QtWidgets import (
|
|
42
|
+
QAbstractItemView,
|
|
43
|
+
QAction,
|
|
44
|
+
QApplication,
|
|
45
|
+
QCheckBox,
|
|
46
|
+
QComboBox,
|
|
47
|
+
QDialog,
|
|
48
|
+
QDialogButtonBox,
|
|
49
|
+
QFileDialog,
|
|
50
|
+
QGroupBox,
|
|
51
|
+
QHBoxLayout,
|
|
52
|
+
QHeaderView,
|
|
53
|
+
QLabel,
|
|
54
|
+
QLineEdit,
|
|
55
|
+
QMainWindow,
|
|
56
|
+
QMenu,
|
|
57
|
+
QMessageBox,
|
|
58
|
+
QPushButton,
|
|
59
|
+
QSizePolicy,
|
|
60
|
+
QSpinBox,
|
|
61
|
+
QSplitter,
|
|
62
|
+
QStatusBar,
|
|
63
|
+
QTableView,
|
|
64
|
+
QTabWidget,
|
|
65
|
+
QTextEdit,
|
|
66
|
+
QToolBar,
|
|
67
|
+
QVBoxLayout,
|
|
68
|
+
QWidget,
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
from PySide6.QtCore import (
|
|
72
|
+
QAbstractTableModel,
|
|
73
|
+
QModelIndex,
|
|
74
|
+
QSortFilterProxyModel,
|
|
75
|
+
Qt,
|
|
76
|
+
QThread,
|
|
77
|
+
QTimer,
|
|
78
|
+
Signal,
|
|
79
|
+
Slot,
|
|
80
|
+
)
|
|
81
|
+
from PySide6.QtGui import QAction, QColor, QFont, QIcon
|
|
82
|
+
from PySide6.QtWidgets import (
|
|
83
|
+
QAbstractItemView,
|
|
84
|
+
QApplication,
|
|
85
|
+
QCheckBox,
|
|
86
|
+
QComboBox,
|
|
87
|
+
QDialog,
|
|
88
|
+
QDialogButtonBox,
|
|
89
|
+
QFileDialog,
|
|
90
|
+
QGroupBox,
|
|
91
|
+
QHBoxLayout,
|
|
92
|
+
QHeaderView,
|
|
93
|
+
QLabel,
|
|
94
|
+
QLineEdit,
|
|
95
|
+
QMainWindow,
|
|
96
|
+
QMenu,
|
|
97
|
+
QMessageBox,
|
|
98
|
+
QPushButton,
|
|
99
|
+
QSizePolicy,
|
|
100
|
+
QSpinBox,
|
|
101
|
+
QSplitter,
|
|
102
|
+
QStatusBar,
|
|
103
|
+
QTableView,
|
|
104
|
+
QTabWidget,
|
|
105
|
+
QTextEdit,
|
|
106
|
+
QToolBar,
|
|
107
|
+
QVBoxLayout,
|
|
108
|
+
QWidget,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
"QApplication", "QMainWindow", "QWidget", "QDialog",
|
|
113
|
+
"QVBoxLayout", "QHBoxLayout", "QLabel", "QComboBox",
|
|
114
|
+
"QPushButton", "QCheckBox", "QDialogButtonBox",
|
|
115
|
+
"QSizePolicy", "QTabWidget", "QTextEdit", "QLineEdit",
|
|
116
|
+
"QSpinBox", "QGroupBox", "QSplitter", "QToolBar",
|
|
117
|
+
"QStatusBar", "QMessageBox", "QFileDialog", "QAction",
|
|
118
|
+
"QTableView", "QHeaderView", "QAbstractItemView", "QMenu",
|
|
119
|
+
"Qt", "QThread", "QAbstractTableModel", "QModelIndex",
|
|
120
|
+
"QSortFilterProxyModel", "Signal", "Slot", "QTimer",
|
|
121
|
+
"QFont", "QColor", "QIcon",
|
|
122
|
+
]
|