aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-20 12:34:04 +0200
committerDanilo M. <danix@danix.xyz>2026-10-20 12:34:04 +0200
commitf2bc23df84907a361e7d9fa20bf1bc8f7a916037 (patch)
tree60135956d7e3d98397ad80cdafa643446e231f2a
parenteaeeb7349504d83cd3251daba4c557854c46cd00 (diff)
downloadsb-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.md3
-rw-r--r--sb_builder/__main__.py1
-rw-r--r--sb_builder/mainwindow.py39
-rw-r--r--tests/test_gui.py17
4 files changed, 54 insertions, 6 deletions
diff --git a/README.md b/README.md
index ca3613f..de8b22d 100644
--- a/README.md
+++ b/README.md
@@ -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()