diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-20 12:13:00 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-20 12:13:00 +0200 |
| commit | d4cae7a87fd30341b5a936aa848e1fdc019a379e (patch) | |
| tree | 4a8a21966595468046f1b39b345a49a03c76f25e /sb_builder/jobs.py | |
| parent | c8306d6c87a40bb800241cccd90831e3893550b5 (diff) | |
| download | sb-builder-d4cae7a87fd30341b5a936aa848e1fdc019a379e.tar.gz sb-builder-d4cae7a87fd30341b5a936aa848e1fdc019a379e.zip | |
One frame for every tab: ribbons of actions, next steps as dialogs
Tabs are permanent and load on first show. The ribbon shows the current
tab's actions in workflow order, a small ribbon under it the view
controls; inapplicable actions are disabled with the reason as tooltip.
Starting a job stays on the tab; the Jobs tab reads (running), then shows
the end status. When a preview this session started ends, its log opens
with Run it (publish box) or Apply shadowing; Not now loses nothing, Run
it in Jobs reopens it. New hint moves to the Packages ribbon and acts on
the ticked packages without a hint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'sb_builder/jobs.py')
| -rw-r--r-- | sb_builder/jobs.py | 154 |
1 files changed, 102 insertions, 52 deletions
diff --git a/sb_builder/jobs.py b/sb_builder/jobs.py index b413597..111d804 100644 --- a/sb_builder/jobs.py +++ b/sb_builder/jobs.py @@ -14,18 +14,19 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Jobs tab: job list, live log through job-follow, start and kill.""" +"""Jobs tab: job list, live log through job-follow, start and kill; the next-step dialog.""" import codecs import re from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFontDatabase, QTextCursor -from PyQt6.QtWidgets import (QCheckBox, QHBoxLayout, QInputDialog, QLineEdit, QListWidget, - QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, QPushButton, - QSplitter, QToolButton, QVBoxLayout, QWidget) +from PyQt6.QtWidgets import (QCheckBox, QDialog, QHBoxLayout, QInputDialog, QLineEdit, + QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, + QPushButton, QSplitter, QVBoxLayout, QWidget) -from .fmt import describe +from .fmt import describe, job_title +from .ui import SP, action, enable ANSI = re.compile(rb"\x1b\[[0-9;?]*[A-Za-z]") SHADOW_PLAN = re.compile(r"^(un)?shadow: ", re.M) @@ -33,10 +34,88 @@ ACTIVE = ("starting", "running") BAD = ("failed", "killed", "lost") +def is_preview(j): + """A finished preview whose next step is a real run: slackrepo --preview, sbpull/sbshadow --no.""" + return j["status"] == "ok" and ( + (j["kind"] == "slackrepo" and "--preview" in j["args"]) + or (j["kind"] in ("sbpull", "sbshadow") and "--no" in j["args"])) + + +class NextStep(QDialog): + """After a preview: its whole log, then Run it (or Apply shadowing) / Not now. + + run(job-start args) asks for the real run; the caller starts it. For a + shadowing preview, loaded(has_plan) tells whether there is anything to apply. + """ + + run = pyqtSignal(list) + loaded = pyqtSignal(bool) + + def __init__(self, remote, job, publish, parent=None): + super().__init__(parent) + self.job = job + self.shadow = job["kind"] != "slackrepo" + self.setWindowTitle("Preview: " + job_title(dict(job, args=[ + a for a in job["args"] if a not in ("--preview", "--no")]))) + self.resize(900, 600) + self.decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") + self.log = QPlainTextEdit(readOnly=True) + self.log.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)) + self.publish = QCheckBox("publish", checked=publish, visible=not self.shadow) + self.publish.setToolTip("Push to the mirror when the build ends; unticked, use Publish later") + self.run_btn = QPushButton("Apply shadowing" if self.shadow else "Run it", default=True) + self.run_btn.clicked.connect(self._run) + later = QPushButton("Not now") + later.clicked.connect(self.reject) + bar = QHBoxLayout() + bar.addWidget(self.publish) + bar.addStretch() + bar.addWidget(later) + bar.addWidget(self.run_btn) + layout = QVBoxLayout(self) + layout.addWidget(self.log) + layout.addLayout(bar) + self.proc = p = remote.follow(job["id"], 0) # the job has ended: the stream closes by itself + p.readyReadStandardOutput.connect(self._chunk) + p.finished.connect(self._ended) + + def _chunk(self): + data = bytes(self.proc.readAllStandardOutput()) + self.log.appendPlainText(self.decoder.decode(ANSI.sub(b"", data)).rstrip("\n")) + + def _ended(self, *_): + self._chunk() + self.proc.deleteLater() + if not self.has_plan(): + self.run_btn.setEnabled(False) + self.run_btn.setToolTip("no shadow: or unshadow: lines, nothing to apply") + self.loaded.emit(self.has_plan()) + + def has_plan(self): + return not self.shadow or bool(SHADOW_PLAN.search(self.log.toPlainText())) + + def confirm(self, question): + return QMessageBox.question(self, "Confirm", question) == QMessageBox.StandardButton.Yes + + def _run(self): + if self.shadow: + if not self.confirm("Apply the shadowing plan to the repo (sbshadow --yes)?"): + return + self.run.emit(["sbshadow", "--yes"]) + else: + args = [a for a in self.job["args"] if a != "--preview"] + if not self.publish.isChecked(): + args.insert(1, "--no-publish") + self.run.emit(["slackrepo", *args]) + self.accept() + + class JobsTab(QWidget): refresh_requested = pyqtSignal() + started = pyqtSignal(str) # id of a job this GUI started + preview_requested = pyqtSignal(dict) # a finished preview to take to its next step - def __init__(self, remote, publish=True, parent=None): + def __init__(self, remote, parent=None): super().__init__(parent) self.remote = remote self.pending = () @@ -46,25 +125,17 @@ class JobsTab(QWidget): self.proc = None self.decoder = codecs.getincrementaldecoder("utf-8")(errors="replace") - self.start_menu = QMenu(self) - start = QToolButton(text="Start", popupMode=QToolButton.ToolButtonPopupMode.InstantPopup) - start.setMenu(self.start_menu) - self.apply_btn = QPushButton("Apply shadowing") - self.apply_btn.clicked.connect(lambda: self.confirm_start( - "Apply the shadowing plan to the repo (sbshadow --yes)?", "sbshadow", "--yes")) - self.apply_btn.hide() - self.publish_box = QCheckBox("publish", checked=publish) - self.publish_box.setToolTip("Push to the mirror when the build ends; " - "unticked, use Publish later") - self.run_btn = QPushButton("Run it") - self.run_btn.clicked.connect(self.run_preview) - self.kill_btn = QPushButton("Kill") - self.kill_btn.clicked.connect(self.kill) - bar = QHBoxLayout() - for w in (start, self.apply_btn, self.run_btn, self.publish_box): - bar.addWidget(w) - bar.addStretch() - bar.addWidget(self.kill_btn) + self.pull_menu = QMenu(self) + self.act_pull = action(self, "Pull overlay", "folder-download", SP.SP_ArrowDown) + self.act_pull.setMenu(self.pull_menu) + self.act_run = action(self, "Run it", "media-playback-start", SP.SP_MediaPlay, + lambda: self.preview_requested.emit(self.jobs[self.current])) + self.act_kill = action(self, "Kill", "process-stop", SP.SP_BrowserStop, self.kill) + self.actions = [self.act_pull, + action(self, "Check shadowing", "edit-find", SP.SP_FileDialogContentsView, + lambda: self.start("sbshadow", "--no")), + self.act_run, self.act_kill] + self.view_actions = [] self.list = QListWidget() self.list.currentItemChanged.connect(self._on_select) @@ -76,19 +147,14 @@ class JobsTab(QWidget): split.addWidget(self.log) split.setSizes([300, 700]) layout = QVBoxLayout(self) - layout.addLayout(bar) layout.addWidget(split) self.set_overlays([]) def set_overlays(self, overlays): - m = self.start_menu - m.clear() + self.pull_menu.clear() for o in overlays: - m.addAction(f"sbpull {o} (preview)", lambda o=o: self.start("sbpull", o, "--no")) - m.addSeparator() - m.addAction("sbshadow (preview)", lambda: self.start("sbshadow", "--no")) - m.addAction("sbshadow (apply)…", lambda: self.confirm_start( - "Run sbshadow --yes on the repo?", "sbshadow", "--yes")) + self.pull_menu.addAction(f"{o} (preview)", lambda o=o: self.start("sbpull", o, "--no")) + enable(self.act_pull, None if overlays else "no overlays known yet") def set_jobs(self, jobs): self.jobs = {j["id"]: j for j in jobs} @@ -140,23 +206,12 @@ class JobsTab(QWidget): self.pending = args self.remote.call(self._started, "job-start", *args) - def confirm_start(self, question, *args): - if QMessageBox.question(self, "Confirm", question) == QMessageBox.StandardButton.Yes: - self.start(*args) - def kill(self): if QMessageBox.question(self, "Kill job", f"Send SIGTERM to {self.current}?") \ == QMessageBox.StandardButton.Yes: self.remote.call(lambda _d, err: err and QMessageBox.warning(self, "Kill failed", err), "job-kill", self.current) - def run_preview(self): - """Start the previewed slackrepo run for real.""" - args = [a for a in self.jobs[self.current]["args"] if a != "--preview"] - if not self.publish_box.isChecked(): - args.insert(1, "--no-publish") - self.start_signed("slackrepo", *args) - def ask_passphrase(self, retry): text, ok = QInputDialog.getText( self, "Signing key", ("Wrong passphrase, try again." if retry else @@ -206,6 +261,7 @@ class JobsTab(QWidget): self.select(data["busy"]) return self.select(data["id"]) + self.started.emit(data["id"]) self.refresh_requested.emit() def _on_select(self, item, _prev): @@ -243,11 +299,5 @@ class JobsTab(QWidget): def _update_buttons(self): j = self.jobs.get(self.current) - self.kill_btn.setEnabled(bool(j) and j["status"] == "running") - self.apply_btn.setVisible( - bool(j) and j["status"] == "ok" and "--no" in j["args"] - and bool(SHADOW_PLAN.search(self.log.toPlainText()))) - preview = bool(j) and j["kind"] == "slackrepo" and j["status"] == "ok" \ - and "--preview" in j["args"] - self.run_btn.setVisible(preview) - self.publish_box.setVisible(preview) + enable(self.act_kill, None if j and j["status"] == "running" else "select a running job") + enable(self.act_run, None if j and is_preview(j) else "select a finished preview") |
