Merge pull request 'fix(gui): readable Environment dropdowns and action buttons — 0.10.1' (#5) from feat/m6-steam-detection into main
release / release (push) Successful in 14s

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2026-05-22 06:22:40 +00:00
6 changed files with 55 additions and 11 deletions
+15
View File
@@ -5,6 +5,21 @@ All notable changes to RigDoctor are recorded here. Format follows
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git (`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
release tag (so the auto-updater, D18, can compare versions). release tag (so the auto-updater, D18, can compare versions).
## [0.10.2] - 2026-05-22
### Changed
- When an Environment **Apply**/**Install** fails, the status now shows the **real reason**
(cancelled at the password prompt vs. the system rejecting the change, e.g. a BIOS/kernel-
locked PCIe ASPM policy) instead of a vague "cancelled, or needs privileges".
## [0.10.1] - 2026-05-22
### Fixed
- **Environment-page contrast.** The combo-box **drop-down list** was rendering light-on-light
(the popup view is a separate widget the theme didn't cover) — now dark with readable text.
- The **Install / Apply** buttons on findings were hard to read (the accent fill didn't paint
reliably inside the finding cards, leaving dim dark-on-dark text). They're now an outlined
style — bright accent text on the dark card, filling accent on hover — readable regardless,
and given a minimum height so the row can't crush them.
## [0.10.0] - 2026-05-22 ## [0.10.0] - 2026-05-22
### Added ### Added
- **Actionable Environment page (M6) — install & apply, not just advice.** Findings that - **Actionable Environment page (M6) — install & apply, not just advice.** Findings that
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "rigdoctor" name = "rigdoctor"
version = "0.10.0" version = "0.10.2"
description = "Modular hardware monitoring & crash diagnostics for Linux gamers." description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
+1 -1
View File
@@ -1,3 +1,3 @@
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
__version__ = "0.10.0" __version__ = "0.10.2"
+18 -7
View File
@@ -19,9 +19,20 @@ from PySide6.QtWidgets import (
from .widgets import finding_card from .widgets import finding_card
def _fail_reason(out: str) -> str:
"""Turn the failed command's output into a short, human reason."""
low = (out or "").lower()
if "not authorized" in low or "dismissed" in low or "authentication" in low:
return "cancelled at the password prompt"
if "operation not permitted" in low or "invalid argument" in low or "permission denied" in low:
return "the system rejected the change (it may be locked by BIOS/kernel)"
last = next((ln.strip() for ln in reversed((out or "").splitlines()) if ln.strip()), "")
return (last[:80] or "no privileges, or cancelled")
class EnvironmentPage(QWidget): class EnvironmentPage(QWidget):
_result = Signal(object) # list[Finding] _result = Signal(object) # list[Finding]
_action_done = Signal(object) # (label, rc) — install or apply finished _action_done = Signal(object) # (label, rc, output) — install or apply finished
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
@@ -117,8 +128,8 @@ class EnvironmentPage(QWidget):
def _work_install(self, component) -> None: def _work_install(self, component) -> None:
from ..core import installer from ..core import installer
rc, _out = installer.install_packages(list(component.apt)) rc, out = installer.install_packages(list(component.apt))
self._action_done.emit((component.name, rc)) self._action_done.emit((component.name, rc, out))
def _apply(self, fix_id: str, value: str) -> None: def _apply(self, fix_id: str, value: str) -> None:
if self._busy: if self._busy:
@@ -131,15 +142,15 @@ class EnvironmentPage(QWidget):
def _work_apply(self, fix_id: str, value: str) -> None: def _work_apply(self, fix_id: str, value: str) -> None:
from ..core import fixes from ..core import fixes
rc, _out = fixes.apply(fix_id, value) rc, out = fixes.apply(fix_id, value)
self._action_done.emit((value, rc)) self._action_done.emit((value, rc, out))
def _on_action_done(self, result) -> None: def _on_action_done(self, result) -> None:
label, rc = result label, rc, out = result
self._busy = False self._busy = False
if rc == 0: if rc == 0:
self._status.setText(f"{label} applied — re-checking…") self._status.setText(f"{label} applied — re-checking…")
self._run() # re-run so the finding reflects the new state self._run() # re-run so the finding reflects the new state
else: else:
self._run_btn.setEnabled(True) self._run_btn.setEnabled(True)
self._status.setText(f"'{label}' failed (cancelled, or needs privileges)") self._status.setText(f"'{label}' failed {_fail_reason(out)}")
+18
View File
@@ -104,6 +104,15 @@ QPushButton#PrimaryButton {{ background: {ACCENT}; color: #06222e; border: none;
QPushButton#PrimaryButton:hover {{ background: #5cc8fb; }} QPushButton#PrimaryButton:hover {{ background: #5cc8fb; }}
QPushButton#PrimaryButton:disabled {{ background: #27424f; color: #5f7c8a; }} QPushButton#PrimaryButton:disabled {{ background: #27424f; color: #5f7c8a; }}
/* Inline per-finding action buttons (Install / Apply). Outlined: bright accent text on the
dark card so it stays readable regardless of fill painting; fills accent on hover. */
QPushButton#ActionButton {{
background: transparent; color: {ACCENT}; border: 1px solid {ACCENT};
border-radius: 8px; padding: 6px 16px; font-weight: 700; min-height: 18px;
}}
QPushButton#ActionButton:hover {{ background: {ACCENT}; color: #06222e; }}
QPushButton#ActionButton:disabled {{ color: {MUTED}; border-color: {CARD_BORDER}; }}
QDoubleSpinBox, QSpinBox {{ QDoubleSpinBox, QSpinBox {{
background: #262b34; color: {TEXT}; border: 1px solid {CARD_BORDER}; background: #262b34; color: {TEXT}; border: 1px solid {CARD_BORDER};
border-radius: 6px; padding: 4px 6px; border-radius: 6px; padding: 4px 6px;
@@ -150,4 +159,13 @@ QLineEdit:focus, QPlainTextEdit:focus, QAbstractSpinBox:focus, QComboBox:focus {
border: 1px solid {ACCENT}; border: 1px solid {ACCENT};
}} }}
QLineEdit:disabled, QPlainTextEdit:disabled, QAbstractSpinBox:disabled {{ color: {MUTED}; }} QLineEdit:disabled, QPlainTextEdit:disabled, QAbstractSpinBox:disabled {{ color: {MUTED}; }}
/* The combo-box drop-down list is a separate popup view — unstyled it renders
light-on-light (same Fusion trap as the closed control above). */
QComboBox QAbstractItemView {{
background: {CARD}; color: {TEXT};
border: 1px solid {CARD_BORDER}; outline: 0;
selection-background-color: {ACCENT}; selection-color: #06222e;
}}
QComboBox QAbstractItemView::item {{ padding: 5px 8px; min-height: 22px; }}
""" """
+2 -2
View File
@@ -66,7 +66,7 @@ def finding_card(finding, on_install=None, on_apply=None) -> QFrame:
row = QHBoxLayout() row = QHBoxLayout()
row.addStretch(1) row.addStretch(1)
btn = QPushButton(f"Install {component.name}") btn = QPushButton(f"Install {component.name}")
btn.setObjectName("PrimaryButton") btn.setObjectName("ActionButton")
btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setCursor(Qt.CursorShape.PointingHandCursor)
btn.clicked.connect(lambda: on_install(component)) btn.clicked.connect(lambda: on_install(component))
row.addWidget(btn) row.addWidget(btn)
@@ -83,7 +83,7 @@ def finding_card(finding, on_install=None, on_apply=None) -> QFrame:
combo.setCurrentText(tunable.current) combo.setCurrentText(tunable.current)
combo.setCursor(Qt.CursorShape.PointingHandCursor) combo.setCursor(Qt.CursorShape.PointingHandCursor)
apply_btn = QPushButton("Apply") apply_btn = QPushButton("Apply")
apply_btn.setObjectName("PrimaryButton") apply_btn.setObjectName("ActionButton")
apply_btn.setCursor(Qt.CursorShape.PointingHandCursor) apply_btn.setCursor(Qt.CursorShape.PointingHandCursor)
apply_btn.clicked.connect(lambda: on_apply(tunable.id, combo.currentText())) apply_btn.clicked.connect(lambda: on_apply(tunable.id, combo.currentText()))
row.addWidget(name) row.addWidget(name)