From 73f347449ea44949406ebcaba4f154e622276d79 Mon Sep 17 00:00:00 2001 From: Jessey van Offeren Date: Fri, 22 May 2026 08:40:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(gui):=20explain=20Run=20Diagnostic=20+=20o?= =?UTF-8?q?ffer=20to=20launch=20the=20game=20=E2=80=94=200.13.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recording banner gave no guidance, so it wasn't clear what to do after clicking Run Diagnostic. - Start dialog now spells out the flow: play the game, reproduce the crash, then Finish & analyze (data survives a hard freeze + reboot), with "Launch game & start" (steam.launch_game via steam:// appid URL) or "Start without launching". - Recording banner now states the next step, not just a sample count. - steam.launch_game(appid): best-effort Steam launch (steam / xdg-open). - Fix: escape "&" in button labels (Qt mnemonic) so "Finish & analyze" shows correctly instead of "Finish _analyze". Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 11 +++++++ pyproject.toml | 2 +- src/rigdoctor/__init__.py | 2 +- src/rigdoctor/core/steam.py | 20 +++++++++++++ src/rigdoctor/gui/games_page.py | 52 +++++++++++++++++++++++++++------ 5 files changed, 76 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66abc40..c3f51bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ 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.13.0] - 2026-05-22 +### Added +- **Run Diagnostic now explains itself and can launch the game.** Clicking Run Diagnostic shows + what to do — *play the game, reproduce the crash, then Finish & analyze* (and that data + survives a hard freeze + reboot) — and offers **Launch game & start** (asks Steam to run it by + appid) or **Start without launching**. The recording banner now spells out the next step + instead of just showing a sample count. +### Fixed +- Button labels containing "&" (e.g. "Finish & analyze") rendered as "Finish _analyze" because + Qt treated the "&" as a keyboard mnemonic — now escaped so the ampersand shows literally. + ## [0.12.0] - 2026-05-22 ### Added - **Guided diagnostic in the GUI.** Each game on the **Games** page now has a **Run Diagnostic** diff --git a/pyproject.toml b/pyproject.toml index 8fa6fcf..f603bd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rigdoctor" -version = "0.12.0" +version = "0.13.0" description = "Modular hardware monitoring & crash diagnostics for Linux gamers." readme = "README.md" requires-python = ">=3.11" diff --git a/src/rigdoctor/__init__.py b/src/rigdoctor/__init__.py index 5e0f1c7..26dfd23 100644 --- a/src/rigdoctor/__init__.py +++ b/src/rigdoctor/__init__.py @@ -1,3 +1,3 @@ """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" -__version__ = "0.12.0" +__version__ = "0.13.0" diff --git a/src/rigdoctor/core/steam.py b/src/rigdoctor/core/steam.py index d9bb4f5..56f4b17 100644 --- a/src/rigdoctor/core/steam.py +++ b/src/rigdoctor/core/steam.py @@ -15,6 +15,8 @@ from __future__ import annotations import json import os +import shutil +import subprocess import time from dataclasses import asdict, dataclass from pathlib import Path @@ -351,6 +353,24 @@ def acknowledge_new() -> None: # --- formatting ----------------------------------------------------------------------- +def launch_game(appid: str) -> bool: + """Best-effort: ask Steam to launch a game by appid (steam:// URL). Non-blocking.""" + if not appid: + return False + url = f"steam://rungameid/{appid}" + for cmd in (["steam", url], ["xdg-open", url]): + if shutil.which(cmd[0]): + try: + subprocess.Popen( + cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, start_new_session=True, + ) + return True + except (OSError, subprocess.SubprocessError): + continue + return False + + def human_size(num_bytes: int) -> str: if num_bytes <= 0: return "—" diff --git a/src/rigdoctor/gui/games_page.py b/src/rigdoctor/gui/games_page.py index d79765b..62e87a9 100644 --- a/src/rigdoctor/gui/games_page.py +++ b/src/rigdoctor/gui/games_page.py @@ -29,7 +29,7 @@ from .diagnostic_dialog import DiagnosticDialog from .theme import ACCENT, GOOD, MUTED -def _game_row(name: str, sublabel: str, size: str, is_new: bool, on_diagnose=None) -> QFrame: +def _game_row(name: str, sublabel: str, size: str, is_new: bool, appid: str = "", on_diagnose=None) -> QFrame: card = QFrame() card.setObjectName("Card") h = QHBoxLayout(card) @@ -66,7 +66,7 @@ def _game_row(name: str, sublabel: str, size: str, is_new: bool, on_diagnose=Non diag_btn = QPushButton("Run Diagnostic") diag_btn.setObjectName("ActionButton") diag_btn.setCursor(Qt.CursorShape.PointingHandCursor) - diag_btn.clicked.connect(lambda: on_diagnose(name)) + diag_btn.clicked.connect(lambda: on_diagnose(name, appid)) h.addWidget(diag_btn, 0) return card @@ -113,9 +113,10 @@ class GamesPage(QWidget): banner_h.setContentsMargins(16, 10, 16, 10) banner_h.setSpacing(10) self._banner_label = QLabel("") + self._banner_label.setWordWrap(True) 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 = QPushButton("Finish && analyze") # && → literal & (not a mnemonic) self._finish_btn.setObjectName("ActionButton") self._finish_btn.clicked.connect(self._finish_diagnostic) banner_h.addWidget(self._finish_btn) @@ -269,29 +270,59 @@ class GamesPage(QWidget): os.path.basename(g.library.rstrip("/")) or g.library, steam.human_size(g.size_bytes), g.appid in new_appids, + appid=g.appid, on_diagnose=self._start_diagnostic, )) self._list.addStretch(1) # --- guided diagnostic (M6/D12) --------------------------------------------------- - def _start_diagnostic(self, name: str) -> None: - from ..core import diagnostic + def _start_diagnostic(self, name: str, appid: str = "") -> None: + from ..core import diagnostic, steam if diagnostic.is_running(): QMessageBox.information( self, "RigDoctor", "A capture is already running — finish or discard it first.") return + + # Tell the user what the flow actually is, and offer to launch the game for them. + box = QMessageBox(self) + box.setIcon(QMessageBox.Icon.Information) + box.setWindowTitle(f"Run Diagnostic — {name}") + box.setText(f"Record a focused diagnostic while you play {name}?") + box.setInformativeText( + "RigDoctor will capture sensors in the background. Then:\n\n" + "1. Play the game and try to reproduce the freeze / black screen / crash.\n" + "2. When you're done — or after a hard freeze and reboot — come back here and " + "click “Finish & analyze”.\n\n" + "Your readings are saved continuously, so even a hard lock won't lose them." + ) + launch_btn = box.addButton("Launch game && start", QMessageBox.ButtonRole.AcceptRole) + start_btn = box.addButton("Start without launching", QMessageBox.ButtonRole.ActionRole) + box.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) + if not appid: + launch_btn.setEnabled(False) # no appid → can't ask Steam to launch it + box.exec() + clicked = box.clickedButton() + if clicked not in (launch_btn, start_btn): + return + if diagnostic.start(game=name) is None: QMessageBox.warning(self, "RigDoctor", "Couldn't start the capture.") return + launched = steam.launch_game(appid) if clicked is launch_btn else False 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() + self._poll_diag() + if clicked is launch_btn and not launched: + QMessageBox.information( + self, "RigDoctor", + "Recording started, but couldn't launch the game automatically — " + "launch it yourself, then click “Finish & analyze” when you're done.") def _poll_diag(self) -> None: from ..core import diagnostic @@ -301,9 +332,12 @@ class GamesPage(QWidget): 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}") + lost = " · ⚠ GPU-lost detected" if status.get("gpu_lost") else "" + game = status.get("game") or self._diag_game or "your game" + self._banner_label.setText( + f"● Recording {game} — play it and reproduce the problem, then click " + f"“Finish & analyze”. ({samples} samples{lost})" + ) def _finish_diagnostic(self) -> None: self._diag_timer.stop()