Merge pull request 'feat(gui): Run Diagnostic flow on the Games page — 0.12.0' (#7) from feat/m6-steam-detection into main
release / release (push) Successful in 14s
release / release (push) Successful in 14s
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -5,6 +5,15 @@ All notable changes to RigDoctor are recorded here. Format follows
|
||||
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||
release tag (so the auto-updater, D18, can compare versions).
|
||||
|
||||
## [0.12.0] - 2026-05-22
|
||||
### Added
|
||||
- **Guided diagnostic in the GUI.** Each game on the **Games** page now has a **Run Diagnostic**
|
||||
button → a focused, game-tagged capture starts and a recording banner appears (live sample
|
||||
count, GPU-lost indicator) with **Finish & analyze** / **Discard**. Finishing opens a results
|
||||
dialog: the window-scoped capture summary (peak temps/power, events, last samples) plus the
|
||||
health findings as cards. The banner persists/restores if you navigate away and back while a
|
||||
capture is running. Shares `core/diagnostic.py` with the CLI (one flow, three front-ends).
|
||||
|
||||
## [0.11.0] - 2026-05-22
|
||||
### Added
|
||||
- **Guided diagnostic session (CLI) — the seed use case, end to end.** `rigdoctor diagnose
|
||||
|
||||
+6
-4
@@ -41,10 +41,12 @@ Ubuntu + NVIDIA first; `.deb` distribution (see `DECISIONS.md`).
|
||||
- [ ] M11 tray / menu-bar applet (QSystemTrayIcon: live M1 readouts + Run Diagnostic +
|
||||
supporting actions — D13)
|
||||
- [~] Guided diagnostic session (pick game → focused M3 capture → M4 scan → findings),
|
||||
shared by tray/GUI/CLI — *core + CLI done* (`core/diagnostic.py`, `rigdoctor diagnose
|
||||
start/status/finish`): tags a focused capture with the chosen game (own diagnostic log,
|
||||
window-scoped report) and combines the capture summary with the M4 findings. *Pending:*
|
||||
the GUI/tray "Run Diagnostic" button, and auto start/stop via the D12 wrapper/watcher.
|
||||
shared by tray/GUI/CLI — *core + CLI + GUI done* (`core/diagnostic.py`, `rigdoctor
|
||||
diagnose start/status/finish`, and a **Run Diagnostic** button per game on the GUI Games
|
||||
page → recording banner → results dialog with the capture summary + findings). Tags a
|
||||
focused capture with the chosen game (own diagnostic log, window-scoped report) and
|
||||
combines the capture summary with the M4 findings. *Pending:* the tray (M11) entry point,
|
||||
and auto start/stop via the D12 wrapper/watcher.
|
||||
- [ ] Logger trigger modes: always-on + game-launch (D12 — wrapper first:
|
||||
`rigdoctor wrap %command%` + global Steam compat-tool; zero-config watcher
|
||||
(Steam RunningAppID + /proc) and GameMode hook follow)
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rigdoctor"
|
||||
version = "0.11.0"
|
||||
version = "0.12.0"
|
||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||
|
||||
__version__ = "0.11.0"
|
||||
__version__ = "0.12.0"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Results view for a guided diagnostic session (M6/D12): capture summary + findings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from ..render import render_summary
|
||||
from .widgets import finding_card
|
||||
|
||||
|
||||
class DiagnosticDialog(QDialog):
|
||||
def __init__(self, result, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(f"Diagnostic — {result.game}" if result.game else "Diagnostic")
|
||||
self.resize(660, 680)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(20, 18, 20, 16)
|
||||
root.setSpacing(14)
|
||||
|
||||
title = QLabel(f"Diagnostic — {result.game}" if result.game else "Diagnostic")
|
||||
title.setObjectName("PageTitle")
|
||||
root.addWidget(title)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
scroll.setStyleSheet("background: transparent;")
|
||||
body = QWidget()
|
||||
col = QVBoxLayout(body)
|
||||
col.setContentsMargins(0, 0, 0, 0)
|
||||
col.setSpacing(10)
|
||||
col.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
# Capture window summary (peaks / events / last samples) — monospace for the columns.
|
||||
cap_head = QLabel("Capture")
|
||||
cap_head.setStyleSheet("font-weight: 700; background: transparent;")
|
||||
col.addWidget(cap_head)
|
||||
summary = QLabel(render_summary(result.summary))
|
||||
summary.setObjectName("Report")
|
||||
summary.setFont(QFont("monospace"))
|
||||
summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
summary.setWordWrap(False)
|
||||
summary.setStyleSheet(
|
||||
"background: #0d0f13; color: #cfd3da; border: 1px solid #2a2f39; "
|
||||
"border-radius: 8px; padding: 10px;"
|
||||
)
|
||||
col.addWidget(summary)
|
||||
|
||||
find_head = QLabel(f"Findings ({len(result.findings)})")
|
||||
find_head.setStyleSheet("font-weight: 700; background: transparent;")
|
||||
col.addWidget(find_head)
|
||||
if result.findings:
|
||||
for finding in result.findings:
|
||||
col.addWidget(finding_card(finding))
|
||||
else:
|
||||
none = QLabel("No findings.")
|
||||
none.setObjectName("Muted")
|
||||
col.addWidget(none)
|
||||
|
||||
scroll.setWidget(body)
|
||||
root.addWidget(scroll, 1)
|
||||
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch(1)
|
||||
close = QPushButton("Close")
|
||||
close.setObjectName("PrimaryButton")
|
||||
close.clicked.connect(self.accept)
|
||||
buttons.addWidget(close)
|
||||
root.addLayout(buttons)
|
||||
@@ -17,6 +17,7 @@ from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
@@ -24,10 +25,11 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..config import load_config, update_config
|
||||
from .diagnostic_dialog import DiagnosticDialog
|
||||
from .theme import ACCENT, GOOD, MUTED
|
||||
|
||||
|
||||
def _game_row(name: str, sublabel: str, size: str, is_new: bool) -> QFrame:
|
||||
def _game_row(name: str, sublabel: str, size: str, is_new: bool, on_diagnose=None) -> QFrame:
|
||||
card = QFrame()
|
||||
card.setObjectName("Card")
|
||||
h = QHBoxLayout(card)
|
||||
@@ -59,6 +61,13 @@ def _game_row(name: str, sublabel: str, size: str, is_new: bool) -> QFrame:
|
||||
size_label.setMinimumWidth(80)
|
||||
size_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
h.addWidget(size_label, 0)
|
||||
|
||||
if on_diagnose is not None:
|
||||
diag_btn = QPushButton("Run Diagnostic")
|
||||
diag_btn.setObjectName("ActionButton")
|
||||
diag_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
diag_btn.clicked.connect(lambda: on_diagnose(name))
|
||||
h.addWidget(diag_btn, 0)
|
||||
return card
|
||||
|
||||
|
||||
@@ -66,14 +75,17 @@ class GamesPage(QWidget):
|
||||
_libraries_ready = Signal(object) # list[dict(path, label, count, selected)]
|
||||
_scanned = Signal(object) # steam.ScanResult
|
||||
new_count_changed = Signal(int) # newly-installed game count (for the nav badge)
|
||||
_diag_done = Signal(object) # DiagnosticResult — focused capture analyzed
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setObjectName("Page")
|
||||
self._libraries_ready.connect(self._render_libraries)
|
||||
self._scanned.connect(self._render_games)
|
||||
self._diag_done.connect(self._on_diag_done)
|
||||
self._busy = False
|
||||
self._new_appids: set[str] = set()
|
||||
self._diag_game: str | None = None
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(20, 18, 20, 18)
|
||||
@@ -93,6 +105,30 @@ class GamesPage(QWidget):
|
||||
header.addWidget(self._rescan_btn)
|
||||
root.addLayout(header)
|
||||
|
||||
# In-progress diagnostic banner (hidden until a focused capture is running).
|
||||
self._banner = QFrame()
|
||||
self._banner.setObjectName("Card")
|
||||
self._banner.setStyleSheet(f"#Card {{ border: 1px solid {ACCENT}; }}")
|
||||
banner_h = QHBoxLayout(self._banner)
|
||||
banner_h.setContentsMargins(16, 10, 16, 10)
|
||||
banner_h.setSpacing(10)
|
||||
self._banner_label = QLabel("")
|
||||
self._banner_label.setStyleSheet(f"color: {ACCENT}; font-weight: 700; background: transparent;")
|
||||
banner_h.addWidget(self._banner_label, 1)
|
||||
self._finish_btn = QPushButton("Finish & analyze")
|
||||
self._finish_btn.setObjectName("ActionButton")
|
||||
self._finish_btn.clicked.connect(self._finish_diagnostic)
|
||||
banner_h.addWidget(self._finish_btn)
|
||||
self._discard_btn = QPushButton("Discard")
|
||||
self._discard_btn.clicked.connect(self._discard_diagnostic)
|
||||
banner_h.addWidget(self._discard_btn)
|
||||
self._banner.hide()
|
||||
root.addWidget(self._banner)
|
||||
|
||||
self._diag_timer = QTimer(self)
|
||||
self._diag_timer.setInterval(1000)
|
||||
self._diag_timer.timeout.connect(self._poll_diag)
|
||||
|
||||
# Libraries (opt-in checkboxes)
|
||||
lib_card = QFrame()
|
||||
lib_card.setObjectName("Card")
|
||||
@@ -233,9 +269,74 @@ class GamesPage(QWidget):
|
||||
os.path.basename(g.library.rstrip("/")) or g.library,
|
||||
steam.human_size(g.size_bytes),
|
||||
g.appid in new_appids,
|
||||
on_diagnose=self._start_diagnostic,
|
||||
))
|
||||
self._list.addStretch(1)
|
||||
|
||||
# --- guided diagnostic (M6/D12) ---------------------------------------------------
|
||||
|
||||
def _start_diagnostic(self, name: str) -> None:
|
||||
from ..core import diagnostic
|
||||
|
||||
if diagnostic.is_running():
|
||||
QMessageBox.information(
|
||||
self, "RigDoctor",
|
||||
"A capture is already running — finish or discard it first.")
|
||||
return
|
||||
if diagnostic.start(game=name) is None:
|
||||
QMessageBox.warning(self, "RigDoctor", "Couldn't start the capture.")
|
||||
return
|
||||
self._diag_game = name
|
||||
self._banner_label.setText(f"● Recording — {name} · starting…")
|
||||
self._finish_btn.setEnabled(True)
|
||||
self._discard_btn.setEnabled(True)
|
||||
self._banner.show()
|
||||
self._diag_timer.start()
|
||||
|
||||
def _poll_diag(self) -> None:
|
||||
from ..core import diagnostic
|
||||
|
||||
status = diagnostic.active()
|
||||
if not status:
|
||||
self._diag_timer.stop() # recorder exited on its own
|
||||
return
|
||||
samples = status.get("samples", 0)
|
||||
lost = " · GPU-lost seen" if status.get("gpu_lost") else ""
|
||||
game = status.get("game") or self._diag_game or ""
|
||||
self._banner_label.setText(f"● Recording — {game} · {samples} samples{lost}")
|
||||
|
||||
def _finish_diagnostic(self) -> None:
|
||||
self._diag_timer.stop()
|
||||
self._finish_btn.setEnabled(False)
|
||||
self._discard_btn.setEnabled(False)
|
||||
self._banner_label.setText("Analyzing… (running the health report)")
|
||||
threading.Thread(target=self._work_finish, daemon=True).start()
|
||||
|
||||
def _work_finish(self) -> None:
|
||||
from ..core import diagnostic
|
||||
|
||||
try:
|
||||
result = diagnostic.finish()
|
||||
except Exception:
|
||||
result = None
|
||||
self._diag_done.emit(result)
|
||||
|
||||
def _on_diag_done(self, result) -> None:
|
||||
self._banner.hide()
|
||||
self._finish_btn.setEnabled(True)
|
||||
self._discard_btn.setEnabled(True)
|
||||
if result is None:
|
||||
QMessageBox.warning(self, "RigDoctor", "The diagnostic couldn't be analyzed.")
|
||||
return
|
||||
DiagnosticDialog(result, self).exec()
|
||||
|
||||
def _discard_diagnostic(self) -> None:
|
||||
from ..core import reccontrol
|
||||
|
||||
self._diag_timer.stop()
|
||||
reccontrol.stop_background()
|
||||
self._banner.hide()
|
||||
|
||||
# --- nav badge integration --------------------------------------------------------
|
||||
|
||||
def showEvent(self, event) -> None: # noqa: N802 (Qt override)
|
||||
@@ -247,3 +348,13 @@ class GamesPage(QWidget):
|
||||
|
||||
threading.Thread(target=steam.acknowledge_new, daemon=True).start()
|
||||
self.new_count_changed.emit(0)
|
||||
|
||||
# Reflect a capture that's still running (e.g. started earlier, navigated back).
|
||||
from ..core import diagnostic
|
||||
|
||||
if diagnostic.is_running():
|
||||
status = diagnostic.active() or {}
|
||||
self._diag_game = status.get("game") or self._diag_game
|
||||
self._banner.show()
|
||||
if not self._diag_timer.isActive():
|
||||
self._diag_timer.start()
|
||||
|
||||
Reference in New Issue
Block a user