Files
cimtechniques-service-suite/cim_suite/shell/launcher.py
Andy 63169a7644 feat: add versioning and changelog system
Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:31:22 -04:00

147 lines
4.7 KiB
Python

"""The landing page: a cable panel + a grid of module cards.
The cable panel resolves the active service cable; module cards stay disabled until
a source is selected. Available cards emit ``moduleChosen`` when Open is clicked.
"""
from __future__ import annotations
from collections.abc import Callable, Sequence
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import (
QFrame,
QGridLayout,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
import cim_suite
from cim_suite.core.module import Module
from cim_suite.core.transport.port_scan import DetectedPort, scan_ports
from cim_suite.core.ui.theme import Space
from .cable_panel import CablePanel
from .whats_new import WhatsNewDialog
_COLUMNS = 3
class _Card(QFrame):
def __init__(self, module: Module) -> None:
super().__init__()
self.setObjectName("ModuleCard")
self.setProperty("available", "true" if module.available else "false")
layout = QVBoxLayout(self)
layout.setContentsMargins(Space.LG, Space.LG, Space.LG, Space.LG)
layout.setSpacing(Space.SM)
title = QLabel(module.title)
title.setObjectName("ModuleCardTitle")
title.setWordWrap(True)
summary = QLabel(module.summary)
summary.setObjectName("ModuleCardSummary")
summary.setWordWrap(True)
layout.addWidget(title)
layout.addWidget(summary)
layout.addStretch(1)
if module.available:
self.open_button = QPushButton("Open")
self.open_button.setProperty("variant", "primary")
layout.addWidget(self.open_button, alignment=Qt.AlignmentFlag.AlignLeft)
else:
badge = QLabel("Coming soon")
badge.setObjectName("ModuleCardBadge")
layout.addWidget(badge, alignment=Qt.AlignmentFlag.AlignLeft)
self.open_button = None
class LauncherView(QScrollArea):
"""Emits ``moduleChosen`` with the module when an available card's Open is clicked.
Module Open buttons are disabled until the cable panel reports a selected source.
"""
moduleChosen = Signal(object)
def __init__(
self,
modules: Sequence[Module],
*,
include_simulator: bool = False,
scan_fn: Callable[..., list[DetectedPort]] = scan_ports,
) -> None:
super().__init__()
self.setWidgetResizable(True)
self._scan_fn = scan_fn
self._open_buttons: list[QPushButton] = []
page = QWidget()
page.setObjectName("LauncherPage")
outer = QVBoxLayout(page)
outer.setContentsMargins(Space.XL, Space.LG, Space.XL, Space.XL)
outer.setSpacing(Space.MD)
header = QHBoxLayout()
heading = QLabel("CIMTechniques Service Suite")
heading.setObjectName("LauncherHeading")
header.addWidget(heading)
header.addStretch(1)
version = QLabel(f"v{cim_suite.__version__}")
version.setObjectName("LauncherVersion")
header.addWidget(version, alignment=Qt.AlignmentFlag.AlignVCenter)
self._whats_new_btn = QPushButton("What's new")
self._whats_new_btn.setObjectName("WhatsNewButton")
self._whats_new_btn.clicked.connect(self._show_whats_new)
header.addWidget(self._whats_new_btn, alignment=Qt.AlignmentFlag.AlignVCenter)
outer.addLayout(header)
self._cable = CablePanel(include_simulator=include_simulator, scan_fn=scan_fn)
self._cable.sourceChanged.connect(self._on_source_changed)
outer.addWidget(self._cable)
grid = QGridLayout()
grid.setSpacing(Space.LG)
for i, module in enumerate(modules):
card = _Card(module)
if card.open_button is not None:
self._open_buttons.append(card.open_button)
card.open_button.clicked.connect(
lambda _=False, m=module: self.moduleChosen.emit(m)
)
grid.addWidget(card, i // _COLUMNS, i % _COLUMNS)
outer.addLayout(grid)
outer.addStretch(1)
self.setWidget(page)
self._set_cards_enabled(False)
# --- cable selection ---------------------------------------------------
def active_source(self) -> str:
return self._cable.active_source()
def rescan(self) -> int:
# Keep the panel's scan source in sync (tests reassign self._scan_fn).
self._cable.set_scan_fn(self._scan_fn)
return self._cable.rescan()
def _show_whats_new(self) -> None:
WhatsNewDialog(self).exec()
def _on_source_changed(self, source: str) -> None:
self._set_cards_enabled(bool(source))
def _set_cards_enabled(self, enabled: bool) -> None:
for btn in self._open_buttons:
btn.setEnabled(enabled)