Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc84bbda88 | |||
| 75a4da7af3 |
@@ -5,6 +5,18 @@ 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.19.0] - 2026-05-22
|
||||||
|
### Added
|
||||||
|
- **System-tray applet (M11, D13).** A tray icon whose menu shows live **CPU / GPU temp** and
|
||||||
|
**memory used/total**, a **status line** (Normal / Hot / GPU not responding), and is led by a
|
||||||
|
**Run Diagnostic** submenu (pick a detected game → the guided session), plus **Open dashboard**,
|
||||||
|
**Start/Stop recording**, **Snapshot (copy)**, and **Quit**. It reuses the dashboard's sample
|
||||||
|
stream (no extra sampling). With a tray present, **closing the window hides to the tray** (Quit
|
||||||
|
exits); `rigdoctor-gui --tray` starts hidden for autostart. Needs a tray host — on GNOME the
|
||||||
|
AppIndicator extension; degrades to a no-op if none is available. Completes the Desktop UI bundle.
|
||||||
|
- **GUI smoke tests**: construct `MainWindow` headless and exercise the tray, so a startup crash
|
||||||
|
fails the build (closes the gap that let the 0.18.0 import regression ship).
|
||||||
|
|
||||||
## [0.18.2] - 2026-05-22
|
## [0.18.2] - 2026-05-22
|
||||||
### Fixed
|
### Fixed
|
||||||
- **GUI wouldn't start** (0.18.0 regression): the recording indicator used a wrong relative
|
- **GUI wouldn't start** (0.18.0 regression): the recording indicator used a wrong relative
|
||||||
|
|||||||
+9
-5
@@ -16,7 +16,7 @@ Status: ⬜ not started · 🟦 designing · 🟨 in progress · ✅ done
|
|||||||
| M5 | System inventory | Diagnostics | none (opt: lm-sensors, dmidecode) | all | P1 | ✅ |
|
| M5 | System inventory | Diagnostics | none (opt: lm-sensors, dmidecode) | all | P1 | ✅ |
|
||||||
| M6 | Gaming env checks | Diagnostics | none | all | P2 | 🟨 |
|
| M6 | Gaming env checks | Diagnostics | none | all | P2 | 🟨 |
|
||||||
| M10 | Desktop GUI | Desktop UI | **python3-pyside6** | all | P2 | ✅ |
|
| M10 | Desktop GUI | Desktop UI | **python3-pyside6** | all | P2 | ✅ |
|
||||||
| M11 | Tray / menu-bar applet | Desktop UI | **python3-pyside6** (+ AppIndicator on GNOME) | all | P2 | ⬜ |
|
| M11 | Tray / menu-bar applet | Desktop UI | **python3-pyside6** (+ AppIndicator on GNOME) | all | P2 | ✅ |
|
||||||
| M9 | Installer | (meta) | none | all | P1 | 🟨 |
|
| M9 | Installer | (meta) | none | all | P1 | 🟨 |
|
||||||
| M12 | Session sharing / remote assist | Sharing | none (Tier 3: tmate/sshx) | all | P3 | 🟨 |
|
| M12 | Session sharing / remote assist | Sharing | none (Tier 3: tmate/sshx) | all | P3 | 🟨 |
|
||||||
| M13 | Auto-update | (core) | none (stdlib; user-local file swap) | all | P3 | ✅ |
|
| M13 | Auto-update | (core) | none (stdlib; user-local file swap) | all | P3 | ✅ |
|
||||||
@@ -73,10 +73,14 @@ Status: ⬜ not started · 🟦 designing · 🟨 in progress · ✅ done
|
|||||||
nav, a live dashboard (circular gauges + collapsible per-subsystem cards, temperature-
|
nav, a live dashboard (circular gauges + collapsible per-subsystem cards, temperature-
|
||||||
colored values), and a **Recording/Logs page** with full M3 controls (start/stop/status +
|
colored values), and a **Recording/Logs page** with full M3 controls (start/stop/status +
|
||||||
post-crash report). Health/Inventory remain placeholders until M4/M5. GUI-first per D17.
|
post-crash report). Health/Inventory remain placeholders until M4/M5. GUI-first per D17.
|
||||||
- **M11 Tray applet** — `QSystemTrayIcon` menu-bar applet. Dropdown shows live M1 readouts
|
- **M11 Tray applet** — `QSystemTrayIcon` menu-bar applet. *Implemented (`gui/tray.py`, D13):*
|
||||||
(CPU temp, GPU temp, memory used/total, status dot) and is led by a **Run Diagnostic**
|
the menu shows live M1 readouts (CPU temp, GPU temp, memory used/total) + a status line
|
||||||
action (the guided diagnostic session), plus Open dashboard / Start-Stop recording /
|
(Normal / Hot / GPU not responding), led by a **Run Diagnostic** submenu (per detected game →
|
||||||
Snapshot / Quit (D13). Optional; shares the Qt dependency with M10.
|
the guided session), plus Open dashboard / Start-Stop recording / Snapshot-copy / Quit. It
|
||||||
|
shares the dashboard's sample stream (no extra sampling) and drives the existing MainWindow
|
||||||
|
flows. With a tray present, closing the window **hides to the tray** (Quit exits); `rigdoctor-gui
|
||||||
|
--tray` starts hidden for autostart. Optional; shares the Qt dependency with M10. *Needs a tray
|
||||||
|
host* — on GNOME that means the AppIndicator extension; degrades to no-op if none is available.
|
||||||
- **M9 Installer** — interactive wizard layered on the `.deb` (D8); apt-first dependency
|
- **M9 Installer** — interactive wizard layered on the `.deb` (D8); apt-first dependency
|
||||||
resolution; enables the logger service and trigger mode. *Implemented (first cut):* distro/
|
resolution; enables the logger service and trigger mode. *Implemented (first cut):* distro/
|
||||||
package-manager/GPU detection (`core/sysenv`), an optional-component catalog (`core/catalog`),
|
package-manager/GPU detection (`core/sysenv`), an optional-component catalog (`core/catalog`),
|
||||||
|
|||||||
+5
-3
@@ -37,9 +37,11 @@ Ubuntu + NVIDIA first; `.deb` distribution (see `DECISIONS.md`).
|
|||||||
- [ ] SMART integration (smartmontools if present)
|
- [ ] SMART integration (smartmontools if present)
|
||||||
|
|
||||||
## Phase 4 — Desktop UI & installer
|
## Phase 4 — Desktop UI & installer
|
||||||
- [ ] M10 desktop GUI (PySide6: dashboard, log browser, report viewer, logger controls)
|
- [x] M10 desktop GUI (PySide6: dashboard w/ history graphs, logs, health, games, environment,
|
||||||
- [ ] M11 tray / menu-bar applet (QSystemTrayIcon: live M1 readouts + Run Diagnostic +
|
inventory, setup, notifications, share)
|
||||||
supporting actions — D13)
|
- [x] M11 tray / menu-bar applet (`gui/tray.py`: live CPU/GPU temp + memory readouts, status
|
||||||
|
line, Run Diagnostic submenu per game, Open dashboard / Start-Stop recording / Snapshot /
|
||||||
|
Quit — D13; close-to-tray, `--tray` autostart). Needs a tray host (AppIndicator on GNOME).
|
||||||
- [~] Guided diagnostic session (pick game → focused M3 capture → M4 scan → findings),
|
- [~] Guided diagnostic session (pick game → focused M3 capture → M4 scan → findings),
|
||||||
shared by tray/GUI/CLI — *core + CLI + GUI done* (`core/diagnostic.py`, `rigdoctor
|
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
|
diagnose start/status/finish`, and a **Run Diagnostic** button per game on the GUI Games
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rigdoctor"
|
name = "rigdoctor"
|
||||||
version = "0.18.2"
|
version = "0.19.0"
|
||||||
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,3 +1,3 @@
|
|||||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||||
|
|
||||||
__version__ = "0.18.2"
|
__version__ = "0.19.0"
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
|
|
||||||
interval = float(load_config().get("interval", 1.0))
|
interval = float(load_config().get("interval", 1.0))
|
||||||
window = MainWindow(interval=interval)
|
window = MainWindow(interval=interval)
|
||||||
|
# `--tray` starts hidden to the system tray (for autostart); if no tray is available,
|
||||||
|
# fall back to showing the window so the app is never invisible-and-unreachable.
|
||||||
|
args = argv if argv is not None else sys.argv
|
||||||
|
if "--tray" in args and window.tray_available():
|
||||||
|
window.start_minimized_note()
|
||||||
|
else:
|
||||||
window.show()
|
window.show()
|
||||||
return app.exec()
|
return app.exec()
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import html
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QProcess, QTimer, Signal
|
from PySide6.QtCore import Qt, QProcess, QTimer, Signal
|
||||||
from PySide6.QtGui import QTextDocument
|
from PySide6.QtGui import QIcon, QTextDocument
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QButtonGroup,
|
QButtonGroup,
|
||||||
@@ -20,6 +21,7 @@ from PySide6.QtWidgets import (
|
|||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QStackedWidget,
|
QStackedWidget,
|
||||||
|
QSystemTrayIcon,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
@@ -38,9 +40,11 @@ from .recorder_page import RecorderPage
|
|||||||
from .setup_page import SetupPage
|
from .setup_page import SetupPage
|
||||||
from .share_page import SharePage
|
from .share_page import SharePage
|
||||||
from .theme import ACCENT, CRIT, GOOD, MUTED, TEXT
|
from .theme import ACCENT, CRIT, GOOD, MUTED, TEXT
|
||||||
|
from .tray import TrayIcon
|
||||||
from .worker import SamplerWorker
|
from .worker import SamplerWorker
|
||||||
|
|
||||||
_NAV_ITEMS = ["Dashboard", "Logs", "Health", "Games", "Environment", "Inventory", "Setup", "Notifications", "Share"]
|
_NAV_ITEMS = ["Dashboard", "Logs", "Health", "Games", "Environment", "Inventory", "Setup", "Notifications", "Share"]
|
||||||
|
_ICON = Path(__file__).parent / "assets" / "rigdoctor.svg"
|
||||||
|
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
@@ -136,6 +140,22 @@ class MainWindow(QMainWindow):
|
|||||||
self._rec_timer.start()
|
self._rec_timer.start()
|
||||||
self._update_recording()
|
self._update_recording()
|
||||||
|
|
||||||
|
# System-tray applet (M11) — optional; only when the desktop offers a tray. When
|
||||||
|
# present, closing the window hides to the tray instead of quitting.
|
||||||
|
self._tray = None
|
||||||
|
self._quitting = False
|
||||||
|
self._tray_hint_shown = False
|
||||||
|
if QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
icon = self.windowIcon() if not self.windowIcon().isNull() else QIcon(str(_ICON))
|
||||||
|
self._tray = TrayIcon(
|
||||||
|
self, icon,
|
||||||
|
gpu_alert=float(cfg.get("gpu_temp_alert", 90.0)),
|
||||||
|
cpu_alert=float(cfg.get("cpu_temp_alert", 95.0)),
|
||||||
|
)
|
||||||
|
self._worker.sampled.connect(self._tray.update_sample)
|
||||||
|
self._tray.show()
|
||||||
|
QApplication.instance().setQuitOnLastWindowClosed(False)
|
||||||
|
|
||||||
def _build_sidebar(self) -> QFrame:
|
def _build_sidebar(self) -> QFrame:
|
||||||
bar = QFrame()
|
bar = QFrame()
|
||||||
bar.setObjectName("Sidebar")
|
bar.setObjectName("Sidebar")
|
||||||
@@ -262,6 +282,44 @@ class MainWindow(QMainWindow):
|
|||||||
self.health_page._run()
|
self.health_page._run()
|
||||||
self.inventory_page._run()
|
self.inventory_page._run()
|
||||||
|
|
||||||
|
# --- tray-driven actions (M11) ----------------------------------------------------
|
||||||
|
|
||||||
|
def show_page(self, name: str) -> None:
|
||||||
|
"""Bring the window forward on a given page (used by the tray)."""
|
||||||
|
if name in self._nav_buttons:
|
||||||
|
self._stack.setCurrentIndex(_NAV_ITEMS.index(name))
|
||||||
|
self._nav_buttons[name].setChecked(True)
|
||||||
|
self.showNormal()
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
|
|
||||||
|
def show_dashboard(self) -> None:
|
||||||
|
self.show_page("Dashboard")
|
||||||
|
|
||||||
|
def tray_available(self) -> bool:
|
||||||
|
return self._tray is not None
|
||||||
|
|
||||||
|
def start_minimized_note(self) -> None:
|
||||||
|
"""Started hidden to the tray (autostart) — let the user know it's there."""
|
||||||
|
if self._tray is not None:
|
||||||
|
self._tray_hint_shown = True
|
||||||
|
self._tray.showMessage(
|
||||||
|
"RigDoctor", "Running in the tray — right-click the icon for actions.",
|
||||||
|
QSystemTrayIcon.MessageIcon.Information, 4000,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run_diagnostic(self, name: str, appid: str) -> None:
|
||||||
|
self.show_page("Games")
|
||||||
|
self.games_page._start_diagnostic(name, appid)
|
||||||
|
|
||||||
|
def quit_app(self) -> None:
|
||||||
|
self._quitting = True
|
||||||
|
self._worker.stop()
|
||||||
|
self.share_page.shutdown()
|
||||||
|
if self._tray is not None:
|
||||||
|
self._tray.hide()
|
||||||
|
QApplication.instance().quit()
|
||||||
|
|
||||||
def _update_recording(self) -> None:
|
def _update_recording(self) -> None:
|
||||||
from ..core import diagnostic
|
from ..core import diagnostic
|
||||||
|
|
||||||
@@ -359,6 +417,19 @@ class MainWindow(QMainWindow):
|
|||||||
self._update_label.setText("up-to-date")
|
self._update_label.setText("up-to-date")
|
||||||
|
|
||||||
def closeEvent(self, event) -> None: # noqa: N802 (Qt override)
|
def closeEvent(self, event) -> None: # noqa: N802 (Qt override)
|
||||||
|
# With a tray, closing the window hides it (the app keeps running for the tray
|
||||||
|
# readouts + any capture); Quit from the tray menu exits for real.
|
||||||
|
if self._tray is not None and not self._quitting:
|
||||||
|
event.ignore()
|
||||||
|
self.hide()
|
||||||
|
if not self._tray_hint_shown:
|
||||||
|
self._tray_hint_shown = True
|
||||||
|
self._tray.showMessage(
|
||||||
|
"RigDoctor",
|
||||||
|
"Still running in the tray — right-click the icon for actions or Quit.",
|
||||||
|
QSystemTrayIcon.MessageIcon.Information, 5000,
|
||||||
|
)
|
||||||
|
return
|
||||||
self._worker.stop()
|
self._worker.stop()
|
||||||
self.share_page.shutdown()
|
self.share_page.shutdown()
|
||||||
super().closeEvent(event)
|
super().closeEvent(event)
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""System-tray applet (M11, D13): live readouts + quick actions over the shared engine.
|
||||||
|
|
||||||
|
A QSystemTrayIcon whose menu shows at-a-glance CPU/GPU temp + memory and a status dot, led
|
||||||
|
by **Run Diagnostic** (the guided session), plus Open dashboard / Start-Stop recording /
|
||||||
|
Snapshot / Quit. It consumes the same sample stream as the dashboard (no extra sampling) and
|
||||||
|
drives the existing MainWindow flows — one engine, another front-end.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from PySide6.QtWidgets import QApplication, QMenu, QSystemTrayIcon
|
||||||
|
|
||||||
|
from ..core import reccontrol
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_temp(sample):
|
||||||
|
for r in sample.readings:
|
||||||
|
if r.source == "gpu" and r.metric == "temp" and r.label == "" and r.value is not None:
|
||||||
|
return r.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _cpu_temp(sample):
|
||||||
|
temps = [r for r in sample.readings if r.source == "cpu" and r.metric == "temp" and r.value is not None]
|
||||||
|
for r in temps:
|
||||||
|
low = r.label.lower()
|
||||||
|
if low.startswith("package") or "tctl" in low or "tdie" in low:
|
||||||
|
return r.value
|
||||||
|
return max((r.value for r in temps), default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _memory(sample):
|
||||||
|
used = total = pct = None
|
||||||
|
for r in sample.readings:
|
||||||
|
if r.source == "memory":
|
||||||
|
if r.metric == "used":
|
||||||
|
used = r.value
|
||||||
|
elif r.metric == "total":
|
||||||
|
total = r.value
|
||||||
|
elif r.metric == "used_pct":
|
||||||
|
pct = r.value
|
||||||
|
return used, total, pct
|
||||||
|
|
||||||
|
|
||||||
|
def _gpu_lost(sample) -> bool:
|
||||||
|
return any(r.source == "gpu" and r.metric == "status" and r.label == "query-timeout"
|
||||||
|
for r in sample.readings)
|
||||||
|
|
||||||
|
|
||||||
|
class TrayIcon(QSystemTrayIcon):
|
||||||
|
def __init__(self, window, icon, gpu_alert: float = 90.0, cpu_alert: float = 95.0) -> None:
|
||||||
|
super().__init__(icon, window)
|
||||||
|
self._window = window
|
||||||
|
self._gpu_alert = gpu_alert
|
||||||
|
self._cpu_alert = cpu_alert
|
||||||
|
self._last = None
|
||||||
|
self.setToolTip("RigDoctor")
|
||||||
|
|
||||||
|
menu = QMenu()
|
||||||
|
self._status_act = self._readout(menu, "● starting…")
|
||||||
|
self._cpu_act = self._readout(menu, "CPU temp: —")
|
||||||
|
self._gpu_act = self._readout(menu, "GPU temp: —")
|
||||||
|
self._mem_act = self._readout(menu, "Memory: —")
|
||||||
|
menu.addSeparator()
|
||||||
|
self._diag_menu = menu.addMenu("Run Diagnostic")
|
||||||
|
self._diag_menu.aboutToShow.connect(self._rebuild_diag_menu)
|
||||||
|
menu.addAction("Open dashboard", self._window.show_dashboard)
|
||||||
|
self._rec_act = menu.addAction("Start recording", self._toggle_record)
|
||||||
|
menu.addAction("Snapshot (copy)", self._snapshot)
|
||||||
|
menu.addSeparator()
|
||||||
|
menu.addAction("Quit", self._window.quit_app)
|
||||||
|
menu.aboutToShow.connect(self._refresh_actions)
|
||||||
|
self.setContextMenu(menu)
|
||||||
|
self.activated.connect(self._on_activated)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _readout(menu: QMenu, text: str):
|
||||||
|
act = menu.addAction(text)
|
||||||
|
act.setEnabled(False) # display-only line
|
||||||
|
return act
|
||||||
|
|
||||||
|
def _on_activated(self, reason) -> None:
|
||||||
|
if reason in (QSystemTrayIcon.ActivationReason.Trigger,
|
||||||
|
QSystemTrayIcon.ActivationReason.DoubleClick):
|
||||||
|
self._window.show_dashboard()
|
||||||
|
|
||||||
|
def update_sample(self, sample) -> None:
|
||||||
|
self._last = sample
|
||||||
|
cpu, gpu = _cpu_temp(sample), _gpu_temp(sample)
|
||||||
|
used, total, pct = _memory(sample)
|
||||||
|
self._cpu_act.setText(f"CPU temp: {cpu:.0f} °C" if cpu is not None else "CPU temp: —")
|
||||||
|
self._gpu_act.setText(f"GPU temp: {gpu:.0f} °C" if gpu is not None else "GPU temp: —")
|
||||||
|
if used is not None and total is not None:
|
||||||
|
extra = f" ({pct:.0f}%)" if pct is not None else ""
|
||||||
|
self._mem_act.setText(f"Memory: {used:.1f} / {total:.1f} GB{extra}")
|
||||||
|
else:
|
||||||
|
self._mem_act.setText("Memory: —")
|
||||||
|
|
||||||
|
if _gpu_lost(sample):
|
||||||
|
self._status_act.setText("● GPU not responding")
|
||||||
|
elif (gpu is not None and gpu >= self._gpu_alert) or (cpu is not None and cpu >= self._cpu_alert):
|
||||||
|
self._status_act.setText("● Hot — over alert threshold")
|
||||||
|
else:
|
||||||
|
self._status_act.setText("● Normal")
|
||||||
|
|
||||||
|
bits = []
|
||||||
|
if cpu is not None:
|
||||||
|
bits.append(f"CPU {cpu:.0f}°C")
|
||||||
|
if gpu is not None:
|
||||||
|
bits.append(f"GPU {gpu:.0f}°C")
|
||||||
|
self.setToolTip("RigDoctor" + (" — " + " ".join(bits) if bits else ""))
|
||||||
|
|
||||||
|
def _refresh_actions(self) -> None:
|
||||||
|
self._rec_act.setText("Stop recording" if reccontrol.running_pid() else "Start recording")
|
||||||
|
|
||||||
|
def _toggle_record(self) -> None:
|
||||||
|
if reccontrol.running_pid():
|
||||||
|
reccontrol.stop_background()
|
||||||
|
else:
|
||||||
|
reccontrol.start_background()
|
||||||
|
|
||||||
|
def _rebuild_diag_menu(self) -> None:
|
||||||
|
from ..core import steam
|
||||||
|
|
||||||
|
self._diag_menu.clear()
|
||||||
|
games = steam.cached_games()
|
||||||
|
if not games:
|
||||||
|
self._diag_menu.addAction("Open Games to pick a game…",
|
||||||
|
lambda: self._window.show_page("Games"))
|
||||||
|
return
|
||||||
|
for g in games[:20]:
|
||||||
|
self._diag_menu.addAction(
|
||||||
|
g.name,
|
||||||
|
lambda _checked=False, name=g.name, appid=g.appid: self._window.run_diagnostic(name, appid),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _snapshot(self) -> None:
|
||||||
|
if self._last is None:
|
||||||
|
return
|
||||||
|
from ..render import render_snapshot
|
||||||
|
|
||||||
|
QApplication.clipboard().setText(render_snapshot(self._last))
|
||||||
|
self.showMessage("RigDoctor", "Snapshot copied to clipboard.",
|
||||||
|
QSystemTrayIcon.MessageIcon.Information, 4000)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""GUI smoke tests: construct the real widgets so a startup crash fails the build.
|
||||||
|
|
||||||
|
These run headless (offscreen) and skip cleanly if PySide6 isn't installed (the core/CLI
|
||||||
|
test suite stays Qt-free). Constructing MainWindow is the check that would have caught the
|
||||||
|
0.18.0 bad-import regression that broke launch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PySide6.QtGui import QIcon
|
||||||
|
from PySide6.QtWidgets import QApplication, QWidget
|
||||||
|
HAVE_QT = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_QT = False
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(HAVE_QT, "PySide6 not installed")
|
||||||
|
class GuiSmokeTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.app = QApplication.instance() or QApplication([])
|
||||||
|
|
||||||
|
def test_main_window_constructs(self):
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from rigdoctor.core import updates
|
||||||
|
from rigdoctor.gui import main_window as mw
|
||||||
|
|
||||||
|
# Avoid construction side effects: no pkexec elevation, no network update check.
|
||||||
|
with mock.patch("rigdoctor.core.elevation.available", return_value=False), \
|
||||||
|
mock.patch.object(updates, "update_state", return_value=(updates.UP_TO_DATE, None, "")):
|
||||||
|
window = mw.MainWindow()
|
||||||
|
try:
|
||||||
|
self.assertEqual(len(window._nav_buttons), len(mw._NAV_ITEMS))
|
||||||
|
finally:
|
||||||
|
window._worker.stop()
|
||||||
|
|
||||||
|
def test_tray_readouts_update(self):
|
||||||
|
from rigdoctor.core.sample import Reading, Sample
|
||||||
|
from rigdoctor.gui.tray import TrayIcon
|
||||||
|
|
||||||
|
class StubWindow(QWidget):
|
||||||
|
def show_dashboard(self): ...
|
||||||
|
def show_page(self, name): ...
|
||||||
|
def run_diagnostic(self, name, appid): ...
|
||||||
|
def quit_app(self): ...
|
||||||
|
|
||||||
|
tray = TrayIcon(StubWindow(), QIcon())
|
||||||
|
tray.update_sample(Sample(time.time(), [
|
||||||
|
Reading("gpu", "temp", 72.0, "°C", ""),
|
||||||
|
Reading("cpu", "temp", 65.0, "°C", "Package id 0"),
|
||||||
|
Reading("memory", "used", 14.2, "GB"),
|
||||||
|
Reading("memory", "total", 31.0, "GB"),
|
||||||
|
Reading("memory", "used_pct", 46.0, "%"),
|
||||||
|
]))
|
||||||
|
self.assertIn("72", tray._gpu_act.text())
|
||||||
|
self.assertIn("65", tray._cpu_act.text())
|
||||||
|
self.assertIn("14.2 / 31.0 GB", tray._mem_act.text())
|
||||||
|
self.assertEqual(tray._status_act.text(), "● Normal")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user