From b47006bc2236010f4d5e00aa105d210f877acc40 Mon Sep 17 00:00:00 2001 From: Jessey van Offeren Date: Thu, 21 May 2026 20:25:52 +0200 Subject: [PATCH] fix(share): terminal caret position; remove GUI Inventory tab (use CLI) - The shared terminal caret now sits at the real cursor (row and column) instead of the start of the line. - Remove the GUI Inventory tab; `rigdoctor inventory` (CLI) covers it. Inventory is still collected for the relay guest view (so a remote helper sees the host's hardware) and via launch elevation. Deletes gui/inventory_page.py. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 8 ++ pyproject.toml | 2 +- src/rigdoctor/__init__.py | 2 +- src/rigdoctor/gui/inventory_page.py | 151 --------------------------- src/rigdoctor/gui/main_window.py | 13 +-- src/rigdoctor/gui/terminal_widget.py | 3 +- 6 files changed, 17 insertions(+), 162 deletions(-) delete mode 100644 src/rigdoctor/gui/inventory_page.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9d1d1..3262b0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ 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.7.2] - 2026-05-21 +### Changed +- Removed the GUI **Inventory** tab — use the CLI `rigdoctor inventory` instead. (Inventory is + still collected for the relay guest view, so a remote helper still sees the host's hardware.) +### Fixed +- Shared terminal caret now sits at the real cursor position (row **and** column) instead of + the start of the line. + ## [0.7.1] - 2026-05-21 ### Fixed - Shared terminal: a guest who joined **after** the host enabled the terminal stayed read-only. diff --git a/pyproject.toml b/pyproject.toml index fe0a1bd..39fc91e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rigdoctor" -version = "0.7.1" +version = "0.7.2" 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 345ec8c..613139b 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.7.1" +__version__ = "0.7.2" diff --git a/src/rigdoctor/gui/inventory_page.py b/src/rigdoctor/gui/inventory_page.py deleted file mode 100644 index 26c5c68..0000000 --- a/src/rigdoctor/gui/inventory_page.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Inventory page (M5 in the GUI): system inventory with copy/save + admin re-collect.""" - -from __future__ import annotations - -import os -import threading - -from PySide6.QtCore import Qt, QTimer, Signal -from PySide6.QtWidgets import ( - QApplication, - QFileDialog, - QFrame, - QGridLayout, - QHBoxLayout, - QLabel, - QPushButton, - QScrollArea, - QVBoxLayout, - QWidget, -) - -from ..core import inventory -from .theme import MUTED - - -def _section_card(section) -> QFrame: - card = QFrame() - card.setObjectName("Card") - layout = QVBoxLayout(card) - layout.setContentsMargins(16, 12, 16, 12) - layout.setSpacing(6) - title = QLabel(section.title) - title.setStyleSheet("font-weight: 700; background: transparent;") - layout.addWidget(title) - grid = QGridLayout() - grid.setColumnStretch(1, 1) - grid.setHorizontalSpacing(14) - grid.setVerticalSpacing(4) - for row, (key, value) in enumerate(section.items): - k = QLabel(key) - k.setObjectName("Muted") - v = QLabel(value) - v.setWordWrap(True) - v.setStyleSheet("background: transparent;") - grid.addWidget(k, row, 0) - grid.addWidget(v, row, 1) - layout.addLayout(grid) - return card - - -class InventoryPage(QWidget): - _result = Signal(object) # list[Section] - - def __init__(self) -> None: - super().__init__() - self.setObjectName("Page") - self._sections: list = [] - self._result.connect(self._render) - - root = QVBoxLayout(self) - root.setContentsMargins(20, 18, 20, 18) - root.setSpacing(16) - - header = QHBoxLayout() - title = QLabel("Inventory") - title.setObjectName("PageTitle") - header.addWidget(title) - header.addStretch(1) - self._status = QLabel("") - self._status.setObjectName("Muted") - header.addWidget(self._status) - self._copy_btn = QPushButton("Copy Markdown") - self._copy_btn.clicked.connect(self._copy) - header.addWidget(self._copy_btn) - self._save_btn = QPushButton("Save…") - self._save_btn.clicked.connect(self._save) - header.addWidget(self._save_btn) - self._refresh_btn = QPushButton("Refresh") - self._refresh_btn.setObjectName("PrimaryButton") - self._refresh_btn.clicked.connect(self._run) - header.addWidget(self._refresh_btn) - root.addLayout(header) - - self._scroll = scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scroll.setStyleSheet("background: transparent;") - self._container = QWidget() - self._list = QVBoxLayout(self._container) - self._list.setContentsMargins(0, 0, 0, 0) - self._list.setSpacing(12) - self._list.setAlignment(Qt.AlignmentFlag.AlignTop) - scroll.setWidget(self._container) - root.addWidget(scroll, 1) - - QTimer.singleShot(300, self._run) - - def _run(self) -> None: - self._busy("Collecting…") - threading.Thread(target=self._work, daemon=True).start() - - def _work(self) -> None: - try: - sections = inventory.collect() - except Exception: - sections = [] - self._result.emit(sections) - - def _busy(self, text: str) -> None: - self._status.setText(text) - for b in (self._refresh_btn, self._copy_btn, self._save_btn): - b.setEnabled(False) - - def _render(self, sections) -> None: - self._refresh_btn.setEnabled(True) - self._copy_btn.setEnabled(True) - self._save_btn.setEnabled(True) - if sections is None: # collection failed — keep current - self._status.setText("collection failed") - return - if sections == self._sections: # unchanged — don't rebuild (would jump scroll) - self._status.setText("") - return - - scroll_pos = self._scroll.verticalScrollBar().value() - self._sections = sections - while self._list.count(): - item = self._list.takeAt(0) - w = item.widget() - if w is not None: - w.deleteLater() - for section in sections: - self._list.addWidget(_section_card(section)) - self._list.addStretch(1) - self._status.setText("") - # restore scroll after the layout settles so re-renders don't yank to the top - QTimer.singleShot(0, lambda: self._scroll.verticalScrollBar().setValue(scroll_pos)) - - def _copy(self) -> None: - if self._sections: - QApplication.clipboard().setText(inventory.render_markdown(self._sections)) - self._status.setText("copied as Markdown") - - def _save(self) -> None: - if not self._sections: - return - path, _ = QFileDialog.getSaveFileName(self, "Save inventory", "rigdoctor-inventory.md", "Markdown (*.md)") - if path: - with open(path, "w", encoding="utf-8") as f: - f.write(inventory.render_markdown(self._sections)) - self._status.setText(f"saved {os.path.basename(path)}") diff --git a/src/rigdoctor/gui/main_window.py b/src/rigdoctor/gui/main_window.py index 6cc0fe9..fda6eca 100644 --- a/src/rigdoctor/gui/main_window.py +++ b/src/rigdoctor/gui/main_window.py @@ -29,7 +29,6 @@ from ..config import load_config from ..core import alerts, elevation, updates from .dashboard import Dashboard from .health_page import HealthPage -from .inventory_page import InventoryPage from .notifications_page import NotificationsPage from .recorder_page import RecorderPage from .setup_page import SetupPage @@ -37,7 +36,7 @@ from .share_page import SharePage from .theme import ACCENT, GOOD, MUTED from .worker import SamplerWorker -_NAV_ITEMS = ["Dashboard", "Logs", "Health", "Setup", "Inventory", "Notifications", "Share"] +_NAV_ITEMS = ["Dashboard", "Logs", "Health", "Setup", "Notifications", "Share"] class MainWindow(QMainWindow): @@ -68,7 +67,6 @@ class MainWindow(QMainWindow): self.recorder_page = RecorderPage() self.health_page = HealthPage() self.setup_page = SetupPage() - self.inventory_page = InventoryPage() self.notifications_page = NotificationsPage() self.notifications_page.changed.connect(self._apply_alert_settings) self.share_page = SharePage() @@ -76,9 +74,8 @@ class MainWindow(QMainWindow): self._stack.addWidget(self.recorder_page) # 1 Logs self._stack.addWidget(self.health_page) # 2 Health self._stack.addWidget(self.setup_page) # 3 Setup - self._stack.addWidget(self.inventory_page) # 4 Inventory - self._stack.addWidget(self.notifications_page) # 5 Notifications - self._stack.addWidget(self.share_page) # 6 Share + self._stack.addWidget(self.notifications_page) # 4 Notifications + self._stack.addWidget(self.share_page) # 5 Share content_layout.addWidget(self._stack) layout.addWidget(self._build_sidebar()) @@ -228,9 +225,9 @@ class MainWindow(QMainWindow): self._elevated.emit() def _on_elevated(self) -> None: - # Re-run Health and Inventory now that root-only data (SMART/dmidecode) is available. + # Re-run Health now that root-only SMART data is available. (dmidecode is still + # collected and used by the relay guest view + the CLI `rigdoctor inventory`.) self.health_page._run() - self.inventory_page._run() def _apply_alert_settings(self) -> None: cfg = load_config() diff --git a/src/rigdoctor/gui/terminal_widget.py b/src/rigdoctor/gui/terminal_widget.py index 6242d27..cb2dab6 100644 --- a/src/rigdoctor/gui/terminal_widget.py +++ b/src/rigdoctor/gui/terminal_widget.py @@ -40,10 +40,11 @@ class TerminalView(QPlainTextEdit): def _render(self) -> None: self.setPlainText("\n".join(self._screen.display)) - # Follow the terminal cursor so output (e.g. `ls -la`) stays in view. + # Place the caret at the terminal's actual cursor (row, col) and keep it in view. cursor = self.textCursor() cursor.movePosition(QTextCursor.MoveOperation.Start) cursor.movePosition(QTextCursor.MoveOperation.Down, QTextCursor.MoveMode.MoveAnchor, self._screen.cursor.y) + cursor.movePosition(QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.MoveAnchor, self._screen.cursor.x) self.setTextCursor(cursor) self.ensureCursorVisible()