diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-20 12:34:04 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-20 12:34:04 +0200 |
| commit | f2bc23df84907a361e7d9fa20bf1bc8f7a916037 (patch) | |
| tree | 60135956d7e3d98397ad80cdafa643446e231f2a | |
| parent | eaeeb7349504d83cd3251daba4c557854c46cd00 (diff) | |
| download | sb-builder-f2bc23df84907a361e7d9fa20bf1bc8f7a916037.tar.gz sb-builder-f2bc23df84907a361e7d9fa20bf1bc8f7a916037.zip | |
Keep column widths, splitters and window size across runs
QSettings in state.conf next to config.toml, saved on close: every table
header (widths, sort column) and splitter of the tabs, and the window
geometry. Tests point it at a temp file, never the user's own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | sb_builder/__main__.py | 1 | ||||
| -rw-r--r-- | sb_builder/mainwindow.py | 39 | ||||
| -rw-r--r-- | tests/test_gui.py | 17 |
4 files changed, 54 insertions, 6 deletions
@@ -28,6 +28,9 @@ Work in progress. See `docs/superpowers/specs/` for the design. - VM: `/etc/sb-builder/config.toml`, see `remote/config.example.toml` Every key is optional. `File > Reload config` rereads the GUI config. +Column widths, sort order, splitter positions and the window size are kept +across runs in `~/.config/sb-builder/state.conf` (delete it to reset them; on +Wayland the compositor may still pick the window size). ## Usage diff --git a/sb_builder/__main__.py b/sb_builder/__main__.py index 7571bd6..239b405 100644 --- a/sb_builder/__main__.py +++ b/sb_builder/__main__.py @@ -34,7 +34,6 @@ def main(): app.setDesktopFileName("sb-builder") # Wayland: match sb-builder.desktop cfg, err = config.load() w = MainWindow(cfg, Remote(cfg)) - w.resize(1100, 700) w.show() if err: QMessageBox.warning(w, "Config error", f"{err}\nUsing defaults.") diff --git a/sb_builder/mainwindow.py b/sb_builder/mainwindow.py index c6ea02e..f63a4ba 100644 --- a/sb_builder/mainwindow.py +++ b/sb_builder/mainwindow.py @@ -16,13 +16,14 @@ """Main window: tabs, status bar, connection polling, VM control, notifications.""" +import os import time -from PyQt6.QtCore import QMetaType, QObject, QSize, Qt, QTimer, pyqtSignal, pyqtSlot +from PyQt6.QtCore import QSettings, QMetaType, QObject, QSize, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt6.QtDBus import QDBusArgument, QDBusConnection, QDBusInterface, QDBusMessage from PyQt6.QtGui import QIcon, QKeySequence, QShortcut -from PyQt6.QtWidgets import (QLabel, QMainWindow, QMessageBox, QPushButton, QStyle, QTabWidget, - QToolBar, QToolButton) +from PyQt6.QtWidgets import (QHeaderView, QLabel, QMainWindow, QMessageBox, QPushButton, QSplitter, + QStyle, QTabWidget, QToolBar, QToolButton) from . import VERSION, config from .dashboard import Dashboard @@ -79,8 +80,11 @@ class Notifier(QObject): class MainWindow(QMainWindow): - def __init__(self, cfg, remote, notifier=None): + def __init__(self, cfg, remote, notifier=None, state=None): super().__init__() + # window size, column widths, splitters: kept across runs, apart from config.toml + self.state = QSettings(state or os.path.join(os.path.dirname(config.path()), "state.conf"), + QSettings.Format.IniFormat) self.cfg, self.remote = cfg, remote self.notifier = notifier or Notifier(self) self.notifier.clicked.connect(self.open_job) @@ -156,6 +160,8 @@ class MainWindow(QMainWindow): self.stats_timer = QTimer(self, timeout=self.poll_slow) self._start_timers() QTimer.singleShot(0, self.poll_jobs) + self.resize(1100, 700) + self._restore_view() # ── polling ───────────────────────────────────────────────────────────── @@ -400,6 +406,31 @@ class MainWindow(QMainWindow): if jid: self.open_jobs().select(jid) + # ── view state ────────────────────────────────────────────────────────── + + def _views(self): + """(key, widget) of every table header and splitter in the tabs.""" + # ponytail: keyed by tab class and creation order; a reordered layout + # restores the wrong widths once, then saves the right ones + for tab in (self.dashboard, self.packages_tab, self.hints_tab, self.results_tab, self.jobs_tab): + for kind in (QHeaderView, QSplitter): + for n, w in enumerate(tab.findChildren(kind)): + yield f"{type(tab).__name__}/{kind.__name__}{n}", w + + def _restore_view(self): + if self.state.value("geometry"): + self.restoreGeometry(self.state.value("geometry")) + for key, w in self._views(): + if self.state.value(key): + w.restoreState(self.state.value(key)) + + def closeEvent(self, event): + self.state.setValue("geometry", self.saveGeometry()) + for key, w in self._views(): + self.state.setValue(key, w.saveState()) + self.state.sync() + super().closeEvent(event) + def reload_config(self): cfg, err = config.load() if err: diff --git a/tests/test_gui.py b/tests/test_gui.py index d5a1128..bd08e7f 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -21,6 +21,7 @@ os.environ["QT_QPA_PLATFORM"] = "offscreen" # never the real display, even if t import importlib.machinery # noqa: E402 import importlib.util # noqa: E402 +import shutil # noqa: E402 import sys # noqa: E402 import tempfile # noqa: E402 import time # noqa: E402 @@ -305,7 +306,11 @@ class SmokeTest(unittest.TestCase): "truncated": True, "mtime": 0, "resource": "peak memory 3308M\n"}, None), **answers}) - w = MainWindow(config.load("/nonexistent")[0], remote, FakeNotifier()) + if not hasattr(self, "state"): + tmp = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, tmp) + self.state = os.path.join(tmp, "state.conf") # never the user's own + w = MainWindow(config.load("/nonexistent")[0], remote, FakeNotifier(), self.state) self.addCleanup(w.deleteLater) self.addCleanup(lambda: w.jobs_tab and w.jobs_tab.stop()) return w, remote @@ -663,6 +668,16 @@ class SmokeTest(unittest.TestCase): self.assertIn(("job-start", "hint-new", "curl", "nuclei"), remote.calls) self.assertIs(w.tabs.currentWidget(), w.hints_tab) # starting a job stays put + def test_view_state_kept_across_runs(self): + w, _ = self.window() + w.packages_tab.table.setColumnWidth(1, 333) + w.close() # saves + w2, _ = self.window() # same state file + self.assertEqual(w2.packages_tab.table.columnWidth(1), 333) + # the size itself can't be checked here: restoreGeometry clamps to offscreen's 800x600 screen + self.assertTrue(w2.state.value("geometry")) + self.assertEqual(w2.hints_tab.table.columnWidth(1), 220) # untouched: code default + def test_tab_shortcuts_wrap(self): w, _ = self.window() w.poll_jobs() |
