aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-20 12:52:16 +0200
committerDanilo M. <danix@danix.xyz>2026-10-20 12:52:16 +0200
commit81604635b5a15ce36d14c04931ace058552ee2f3 (patch)
treea4daa1626da006ba50eb205e096adae603407066
parentc7acbfc7d9146a4440f45f3efd4246935c2ffd19 (diff)
downloadsb-builder-81604635b5a15ce36d14c04931ace058552ee2f3.tar.gz
sb-builder-81604635b5a15ce36d14c04931ace058552ee2f3.zip
Hints: Phantom deps dialog
The list with Add and Remove, suggestions (required names no package of the tree has, with who needs them) to tick and add, and Apply to hints, which starts fix-current; its end opens the diff dialog with Revert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS.md10
-rw-r--r--README.md9
-rw-r--r--sb_builder/hints_tab.py117
-rw-r--r--tests/test_gui.py40
4 files changed, 163 insertions, 13 deletions
diff --git a/AGENTS.md b/AGENTS.md
index f55d952..9f92d71 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -33,12 +33,12 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is
- Hints are parsed (`shlex`), never sourced. `sb_builder/hints.py` mirrors
slackrepo's own rules (`../GITHUB/slackrepo/libexec/functions.d/parsefunctions.sh`);
check that file when changing it.
-- Hint editing lives in the helper (`hint-update`, `hint-new` jobs run the
- helper's own `_hint` command): text edits, `.bak` before each
+- Hint editing lives in the helper (`hint-update`, `hint-new`, `fix-current`
+ jobs run the helper's own `_hint` command): text edits, `.bak` before each
write, a final `SBB-RESULT {json}` log line read back with `job-result`.
- It replaces mkhint, which stays on the VM for neovim and openvino
- (bundled deps, `manifest_packages`) and for the `DELREQUIRES` hints
- sbshadow regenerates with `mkhint -F` after every sync, until phase 4b.
+ The phantom deps list is the helper's `phantom_deps_file`; sbshadow runs
+ `sbb-remote fix-current-now` after each sync. mkhint stays on the VM only
+ for neovim (bundled deps, `manifest_packages`), until phase 4b part 2.
- Jobs never get input. Any tool run as a job must be non-interactive
(`--yes`/`--no` style flags). If a tool needs a flag it lacks, add it in
that tool's own repo (e.g. `slackrepo_setup` for sbshadow) rather than
diff --git a/README.md b/README.md
index de8b22d..0d42aa6 100644
--- a/README.md
+++ b/README.md
@@ -73,8 +73,13 @@ reopens it.
**No download** on, `DOWNLOAD="no"` instead; a hint sbshadow wrote,
`DELREQUIRES` only, gets the `.info` version lines added). **Delete hint**
removes the ticked packages' hints, each kept as a `.bak`, revertable from
- the dialog that follows. neovim and openvino are refused for now: their
- bundled deps still need mkhint on the VM.
+ the dialog that follows. **Phantom deps…** keeps the list of deps
+ -current ships, which SBo packages still require (SBo drops a package once
+ it is official): add names, or tick suggestions, the required names no
+ package of the tree has; **Apply to hints** writes them into the
+ `DELREQUIRES` of every package that requires them (sbshadow does the same
+ after each sync). neovim is refused for now: its bundled deps still need
+ mkhint on the VM.
- **Results**: slackrepo's last build attempt per package, newest first,
filterable by result. **Show build log** (or double-click) opens the end of
the log of the package's last attempt (slackrepo keeps one per command:
diff --git a/sb_builder/hints_tab.py b/sb_builder/hints_tab.py
index fbf8a70..ca16354 100644
--- a/sb_builder/hints_tab.py
+++ b/sb_builder/hints_tab.py
@@ -21,9 +21,9 @@ import os
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QColor, QFontDatabase, QKeySequence, QShortcut, QSyntaxHighlighter
-from PyQt6.QtWidgets import (QDialog, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
- QMessageBox, QPlainTextEdit, QPushButton, QSplitter, QTableWidget,
- QTableWidgetItem, QVBoxLayout, QWidget)
+from PyQt6.QtWidgets import (QAbstractItemView, QDialog, QHBoxLayout, QLabel, QLineEdit,
+ QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, QPushButton,
+ QSplitter, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)
from . import upstream
from .dashboard import STATUS
@@ -34,7 +34,7 @@ STATES = ("outdated", "current", "no stanza", "not checked", "nvchecker error")
STATE_COLOR = {"outdated": STATUS["aborted"], "current": STATUS["ok"],
"nvchecker error": STATUS["failed"]}
COLUMNS = ("", "package", "hint", ".info", "upstream", "state")
-HINT_JOBS = ("hint-update", "hint-new")
+HINT_JOBS = ("hint-update", "hint-new", "fix-current")
class HintsTab(QWidget):
@@ -78,7 +78,10 @@ class HintsTab(QWidget):
self.act_update = action(self, "Update selected", "document-save", SP.SP_DialogSaveButton,
self.update_selected)
self.act_delete = action(self, "Delete hint", "edit-delete", SP.SP_TrashIcon, self.delete)
- self.actions = [self.act_check, self.act_update, self.act_delete]
+ self.actions = [self.act_check, self.act_update, self.act_delete,
+ action(self, "Phantom deps…", "view-list-details", SP.SP_FileDialogListView,
+ self.phantom)]
+ self.phantom_dialog = None
self.no_dl = action(self, "No download", "network-offline", SP.SP_DriveNetIcon, checkable=True)
self.no_dl.setToolTip('Update selected writes DOWNLOAD="no": slackrepo uses the source '
"already in its cache")
@@ -212,6 +215,14 @@ class HintsTab(QWidget):
self.start_requested.emit(["hint-update", *self.pairs()]
+ (["--no-dl"] if self.no_dl.isChecked() else []))
+ def phantom(self):
+ if self.phantom_dialog is None:
+ self.phantom_dialog = d = PhantomDialog(self.remote, self)
+ d.apply_requested.connect(lambda: self.start_requested.emit(["fix-current"]))
+ d.finished.connect(lambda _r: setattr(self, "phantom_dialog", None))
+ d.show()
+ self.phantom_dialog.raise_()
+
def confirm(self, title, text):
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
@@ -332,3 +343,99 @@ class HintResultDialog(QDialog):
if pkgs:
self.build_requested.emit(pkgs)
self.accept()
+
+
+class PhantomDialog(QDialog):
+ """The -current phantom deps: the list, suggestions from the tree, Apply = fix-current."""
+
+ apply_requested = pyqtSignal()
+
+ def __init__(self, remote, parent=None):
+ super().__init__(parent)
+ self.remote = remote
+ self.setWindowTitle("Phantom deps: shipped by -current, DELREQUIREd from SBo packages")
+ self.resize(900, 600)
+ self.deps = QListWidget(selectionMode=QAbstractItemView.SelectionMode.ExtendedSelection)
+ self.name = QLineEdit(placeholderText="package name")
+ self.name.returnPressed.connect(self.add_name)
+ add = QPushButton("Add")
+ add.clicked.connect(self.add_name)
+ remove = QPushButton("Remove")
+ remove.clicked.connect(self.remove_selected)
+ self.suggestions = QListWidget()
+ self.suggestions.itemClicked.connect(lambda item: item.setCheckState(
+ Qt.CheckState.Unchecked if item.checkState() == Qt.CheckState.Checked else Qt.CheckState.Checked))
+ add_ticked = QPushButton("Add ticked")
+ add_ticked.clicked.connect(self.add_ticked)
+ apply = QPushButton("Apply to hints", default=True)
+ apply.setToolTip("Start fix-current: the listed deps go into DELREQUIRES of the "
+ "packages that require them")
+ apply.clicked.connect(self.apply)
+ close = QPushButton("Close")
+ close.clicked.connect(self.accept)
+
+ left = QVBoxLayout()
+ left.addWidget(QLabel("<b>Listed</b>"))
+ left.addWidget(self.deps)
+ left.addWidget(self.name)
+ row = QHBoxLayout()
+ row.addWidget(add)
+ row.addWidget(remove)
+ left.addLayout(row)
+ right = QVBoxLayout()
+ right.addWidget(QLabel("<b>Suggested</b>: required, but no package of the tree has them"))
+ right.addWidget(self.suggestions)
+ right.addWidget(add_ticked)
+ lists = QHBoxLayout()
+ lists.addLayout(left, 1)
+ lists.addLayout(right, 2)
+ bar = QHBoxLayout()
+ bar.addStretch()
+ bar.addWidget(close)
+ bar.addWidget(apply)
+ layout = QVBoxLayout(self)
+ layout.addLayout(lists, 1)
+ layout.addLayout(bar)
+ self.reload()
+
+ def reload(self):
+ self.remote.call(self._loaded, "phantom-list")
+
+ def _loaded(self, data, err):
+ if err:
+ QMessageBox.warning(self, "Phantom deps", err)
+ return
+ self.deps.clear()
+ self.deps.addItems(data["deps"])
+ self.suggestions.clear()
+ for s in data["suggestions"]:
+ needed = s["needed_by"]
+ more = f" (+{len(needed) - 5})" if len(needed) > 5 else ""
+ item = QListWidgetItem(f"{s['name']} needed by {', '.join(needed[:5])}{more}")
+ item.setData(Qt.ItemDataRole.UserRole, s["name"])
+ item.setToolTip(", ".join(needed))
+ item.setFlags(Qt.ItemFlag.ItemIsEnabled) # the click handler toggles: no double toggle
+ item.setCheckState(Qt.CheckState.Unchecked)
+ self.suggestions.addItem(item)
+
+ def _change(self, cmd, names):
+ if names:
+ self.remote.call(lambda _d, err: self._loaded(None, err) if err else self.reload(),
+ cmd, *names)
+
+ def add_name(self):
+ name = self.name.text().strip()
+ self.name.clear()
+ self._change("phantom-add", [name] if name else [])
+
+ def add_ticked(self):
+ self._change("phantom-add", [
+ self.suggestions.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.suggestions.count())
+ if self.suggestions.item(i).checkState() == Qt.CheckState.Checked])
+
+ def remove_selected(self):
+ self._change("phantom-remove", [i.text() for i in self.deps.selectedItems()])
+
+ def apply(self):
+ self.apply_requested.emit()
+ self.accept()
diff --git a/tests/test_gui.py b/tests/test_gui.py
index ca51c3c..a25be37 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -36,6 +36,7 @@ from PyQt6.QtWidgets import QApplication # noqa: E402
from sb_builder import VERSION, config # noqa: E402
from sb_builder import hints as hintlib # noqa: E402
+from sb_builder import hints_tab # noqa: E402
from sb_builder import packages as pk # noqa: E402
from sb_builder.mainwindow import MainWindow # noqa: E402
from sb_builder.remote import DOWN, MISSING, parse # noqa: E402
@@ -534,7 +535,7 @@ class SmokeTest(unittest.TestCase):
"Dashboard": (["Refresh"], []),
"Packages": (["Build", "Update", "Rebuild", "Remove", "Update all", "New hint", "Publish"],
["Select visible", "Clear", "Refresh"]),
- "Hints": (["Check upstream", "Update selected", "Delete hint"],
+ "Hints": (["Check upstream", "Update selected", "Delete hint", "Phantom deps…"],
["Select visible", "Clear", "No download", "Refresh"]),
"Results": (["Show build log"], ["Refresh"]),
"Jobs": (["Pull overlay", "Check shadowing", "Run it", "Kill"], []),
@@ -620,6 +621,43 @@ class SmokeTest(unittest.TestCase):
self.assertEqual(len(w.hint_dialogs), 1) # Revert lives there
self.assertIn("-A=1", w.hint_dialogs[0].diff.toPlainText())
+ def test_phantom_deps_dialog(self):
+ listed = ["rust-opt"]
+
+ def change(args, _stdin):
+ if args[0] == "phantom-add":
+ listed.extend(n for n in args[1:] if n not in listed)
+ else:
+ listed[:] = [n for n in listed if n not in args[1:]]
+ return {"deps": list(listed)}, None
+ w, remote = self.window(**{
+ "phantom-list": lambda args, _stdin: ({"deps": list(listed), "suggestions": [
+ {"name": n, "needed_by": ["marco", "steam"]} for n in ("jq", "zenity") if n not in listed]}, # sorted, as the helper
+ None),
+ "phantom-add": change, "phantom-remove": change,
+ "job-start": ({"id": "20260101-000001-fix-current"}, None)})
+ w.poll_jobs()
+ tab = w.open_hints()
+ tab.actions[-1].trigger()
+ d = tab.phantom_dialog
+ self.assertEqual(d.deps.item(0).text(), "rust-opt")
+ self.assertEqual(d.suggestions.count(), 2)
+ self.assertEqual(d.suggestions.item(1).text(), "zenity needed by marco, steam")
+ d.suggestions.itemClicked.emit(d.suggestions.item(1)) # tick zenity by its row
+ d.add_ticked()
+ self.assertIn(("phantom-add", "zenity"), remote.calls)
+ self.assertEqual([d.deps.item(i).text() for i in range(d.deps.count())], ["rust-opt", "zenity"])
+ self.assertEqual(d.suggestions.count(), 1) # zenity left the suggestions
+ d.name.setText("spdlog")
+ d.add_name()
+ d.deps.item(0).setSelected(True)
+ d.remove_selected()
+ self.assertEqual(listed, ["zenity", "spdlog"])
+ d.apply()
+ self.assertIn(("job-start", "fix-current"), remote.calls)
+ self.assertIsNone(tab.phantom_dialog)
+ self.assertIn("fix-current", hints_tab.HINT_JOBS) # its end opens the diff dialog
+
def test_hint_job_end_opens_diff(self):
job = dict(JOB, id="20260101-000000-hint-update", kind="hint-update", args=["nuclei=3.12.0"])
w, remote = self.window(jobs=({"jobs": [job]}, None),