aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md2
-rw-r--r--docs/design/2026-10-20-sb-builder-ui-design.md2
-rw-r--r--sb_builder/packages.py9
-rw-r--r--sb_builder/repository.py18
-rw-r--r--sb_builder/ui.py7
-rw-r--r--tests/test_gui.py6
6 files changed, 24 insertions, 20 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 2718ff7..155b496 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -58,7 +58,7 @@ Every tab follows these (design: `docs/design/2026-10-20-sb-builder-ui-design.md
2. Main ribbon: the tab's actions (`tab.actions`) in workflow order. Small
ribbon: view controls (`tab.view_actions`). Build them with
`sb_builder/ui.py` `action()`. Nothing actionable lives inside a view
- except navigation links.
+ except navigation buttons to another tab (`ui.nav_button()`).
3. Change actions act on ticked rows, view actions on the highlighted row. A
row click toggles its tick; Ctrl+A / Ctrl+Shift+A select visible / clear.
4. An inapplicable action is disabled with `ui.enable(action, reason)`, the
diff --git a/docs/design/2026-10-20-sb-builder-ui-design.md b/docs/design/2026-10-20-sb-builder-ui-design.md
index 8a6844d..826c6f9 100644
--- a/docs/design/2026-10-20-sb-builder-ui-design.md
+++ b/docs/design/2026-10-20-sb-builder-ui-design.md
@@ -132,7 +132,7 @@ A new section, binding for every tab:
1. Tabs are permanent; each loads lazily on first show.
2. Main ribbon: the tab's actions in workflow order. Small ribbon: view
- controls. Nothing actionable lives inside a view except navigation links.
+ controls. Nothing actionable lives inside a view except navigation buttons.
3. Change actions act on ticked rows, view actions on the highlighted row.
A row click toggles its tick; Ctrl+A / Ctrl+Shift+A select visible /
clear.
diff --git a/sb_builder/packages.py b/sb_builder/packages.py
index a3d7126..dbbbd30 100644
--- a/sb_builder/packages.py
+++ b/sb_builder/packages.py
@@ -27,7 +27,7 @@ from PyQt6.QtWidgets import (QHBoxLayout, QLabel, QMessageBox, QScrollArea, QSpl
from . import hints as hintlib
from .dashboard import STATUS
from .fmt import NA, fmt_time
-from .ui import SP, action, chip, enable, group_label, search_field
+from .ui import SP, action, chip, enable, group_label, nav_button, search_field
OVERLAYS = ("personal", "pentesting")
STATUSES = ("ok", "failed", "never", "outdated", "newer")
@@ -303,14 +303,11 @@ class PackagesTab(QWidget):
alignment=Qt.AlignmentFlag.AlignTop)
self.detail.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.detail.setContentsMargins(12, 8, 12, 8)
- # navigation, not an action: a link in the pane (UI guidelines)
- self.open_hint_btn = QLabel('<a href="#">Open in Hints</a>')
- self.open_hint_btn.setContentsMargins(12, 0, 12, 8)
- self.open_hint_btn.linkActivated.connect(lambda _l: self.hints_requested.emit(self.current()["name"]))
+ self.open_hint_btn = nav_button("Open in Hints", lambda: self.hints_requested.emit(self.current()["name"]))
pane = QWidget()
pane_layout = QVBoxLayout(pane)
+ pane_layout.addWidget(self.open_hint_btn, alignment=Qt.AlignmentFlag.AlignLeft)
pane_layout.addWidget(self.detail)
- pane_layout.addWidget(self.open_hint_btn)
pane_layout.addStretch()
scroll = QScrollArea(widgetResizable=True)
scroll.setWidget(pane)
diff --git a/sb_builder/repository.py b/sb_builder/repository.py
index 1eae22e..29fd5f4 100644
--- a/sb_builder/repository.py
+++ b/sb_builder/repository.py
@@ -30,7 +30,7 @@ from PyQt6.QtWidgets import QHBoxLayout, QLabel, QScrollArea, QSplitter, QTableV
from .dashboard import CARD_STYLE, Tile
from .fmt import NA, fmt_age, fmt_bytes, fmt_time
from .packages import OVERLAYS, table
-from .ui import SP, action, chip, group_label, search_field
+from .ui import SP, action, chip, group_label, nav_button, search_field
PKGFILE = re.compile(r"^(?P<name>.+)-(?P<version>[^-]+)-(?P<arch>[^-]+)-(?P<build>[^-]+)\.t[gx]z$")
# category/pkg/file: text; a long one is indented, its text on the next line
@@ -195,7 +195,8 @@ class Filter(QSortFilterProxyModel):
def detail_html(r):
e = html.escape
out = [f"<h3>{e(r['name'])} {e(r['version'])}</h3>",
- f"<p style='white-space:pre-wrap'>{e(r['desc'])}</p>" if r["desc"] else "",
+ # slack-desc wraps at 70 columns: reflow, blank lines keep paragraphs
+ "".join(f"<p>{e(' '.join(p.split()))}</p>" for p in r["desc"].split("\n\n")),
table([("file", r["file"]), ("size", fmt_bytes(r["size"])),
("uncompressed", fmt_bytes(r["usize"])), ("built", fmt_time(r["mtime"])),
("published", "yes" if r["published"] else "no, not in PACKAGES.TXT")])]
@@ -236,21 +237,20 @@ class RepositoryTab(QWidget):
self.table.setSelectionMode(QTableView.SelectionMode.SingleSelection)
self.table.verticalHeader().hide()
self.table.horizontalHeader().setStretchLastSection(True)
- for col, width in enumerate((220, 120, 80, 110, 90, 150)):
+ for col, width in enumerate((220, 120, 80, 110, 90, 170)):
self.table.setColumnWidth(col, width)
self.table.selectionModel().currentRowChanged.connect(self.show_detail)
self.detail = QLabel(textFormat=Qt.TextFormat.RichText, wordWrap=True,
alignment=Qt.AlignmentFlag.AlignTop)
self.detail.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
self.detail.setContentsMargins(12, 8, 12, 8)
- # navigation, not an action: a link in the pane (UI guidelines)
- self.link = QLabel('<a href="#">Open in Packages</a>', visible=False)
- self.link.setContentsMargins(12, 0, 12, 8)
- self.link.linkActivated.connect(lambda _l: self.packages_requested.emit(self.current()["name"]))
+ self.open_packages_btn = nav_button("Open in Packages",
+ lambda: self.packages_requested.emit(self.current()["name"]))
+ self.open_packages_btn.hide()
pane = QWidget()
pane_layout = QVBoxLayout(pane)
+ pane_layout.addWidget(self.open_packages_btn, alignment=Qt.AlignmentFlag.AlignLeft)
pane_layout.addWidget(self.detail)
- pane_layout.addWidget(self.link)
pane_layout.addStretch()
scroll = QScrollArea(widgetResizable=True)
scroll.setWidget(pane)
@@ -323,4 +323,4 @@ class RepositoryTab(QWidget):
def show_detail(self, current, _previous):
row = current.data(Qt.ItemDataRole.UserRole) if current.isValid() else None
self.detail.setText(detail_html(row) if row else "")
- self.link.setVisible(bool(row))
+ self.open_packages_btn.setVisible(bool(row))
diff --git a/sb_builder/ui.py b/sb_builder/ui.py
index 3f02515..2321566 100644
--- a/sb_builder/ui.py
+++ b/sb_builder/ui.py
@@ -81,6 +81,13 @@ def group_label(text):
return label
+def nav_button(text, slot):
+ """A detail pane's jump to another tab: navigation, so it lives in the view."""
+ b = QPushButton(QIcon.fromTheme("go-next", QApplication.style().standardIcon(SP.SP_ArrowForward)), text)
+ b.clicked.connect(lambda _checked=False: slot())
+ return b
+
+
def search_field(placeholder):
q = QLineEdit(placeholderText=placeholder, clearButtonEnabled=True)
icon = QIcon.fromTheme("edit-find", QApplication.style().standardIcon(SP.SP_FileDialogContentsView))
diff --git a/tests/test_gui.py b/tests/test_gui.py
index 08df339..52e4ea7 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -655,13 +655,13 @@ class SmokeTest(unittest.TestCase):
self.assertEqual(tab.proxy.rowCount(), 1)
tab.table.setCurrentIndex(tab.proxy.index(0, 0))
self.assertIn("Updated for version 1.0.", tab.detail.text())
- tab.link.linkActivated.emit("#")
+ tab.open_packages_btn.click()
self.assertIs(w.tabs.currentWidget(), w.packages_tab)
self.assertEqual(w.packages_tab.query.text(), "foo")
tab.table.setCurrentIndex(tab.proxy.index(0, 0))
tab.reload() # the reset drops the current row: no stale pane or link
self.wait(lambda: tab.stats.text() != "loading…")
- self.assertTrue(tab.link.isHidden())
+ self.assertTrue(tab.open_packages_btn.isHidden())
self.assertEqual(tab.detail.text(), "")
def test_repository_missing(self):
@@ -865,7 +865,7 @@ class SmokeTest(unittest.TestCase):
tab = w.open_packages()
tab.table.setCurrentIndex(tab.proxy.index(1, 1)) # foo, has a hint
self.assertTrue(tab.open_hint_btn.isVisibleTo(tab))
- tab.open_hint_btn.linkActivated.emit("#")
+ tab.open_hint_btn.click()
self.assertIs(w.tabs.currentWidget(), w.hints_tab)
tab.model.checked.update({"foo"})
tab.update_count()