# Shell Framework `plesty.lib.ui` is the GUI framework of the platform: **one home window that hosts any number of rearrangeable sub-windows**. It is deliberately generic — a live view is only the first kind of panel; a device control form or a run status board is the same contract in the same window. The framework is toolkit-free down to the last layer. `Panel`, `Theme`, and `MonitorPanel` import without a GUI installed; only the Qt backend needs the `gui` extra: ```bash uv add "plesty-lib[gui]" # PySide6 + pyqtgraph ``` ## The home window ```python from plesty.lib.ui import MonitorPanel, run run( [ MonitorPanel(spectrum, area="top", min_height=260), MonitorPanel(spectral_map, area="bottom", min_height=260), MonitorPanel(power_series, area="right", min_width=360), ], title="QD-FSS", interval_ms=500, ) ``` What the operator can then do, without any further code: - **Move** a panel to another edge, or drop it onto another panel to tab the two together. - **Resize** by dragging the handle between panels; panels nest in both directions, so any split is reachable. - **Drag a panel out** of the home window into a free-floating window — onto a second screen, for instance — and **drag it back in** later. The `⤢` button and a double click on the title bar do the same thing. - **Close** a panel and bring it back from the *Panels* menu. - **Keep the arrangement**: it is saved on exit and restored next time, per window title, under `~/.plesty/ui/`. *Layout → Reset to defaults* undoes an arrangement that went wrong. Every panel is ticked on the shell's clock (`interval_ms`), which is the only clock in the application: monitors drain their sources there, so nothing polls in the background. A panel that raises is caught, and the error is shown in that panel's title bar — one broken view never blinds the rest. ## Panels A `Panel` is anything worth a sub-window. The contract is small on purpose: ```python class RunStatusPanel(Panel): kind = "status" def build(self, parent): # called once; toolkit imports live here from PySide6.QtWidgets import QLabel self._label = QLabel("waiting…", parent) return self._label def tick(self): # called on the shell's clock self._label.setText(self.journal.status()["state"]) def status(self): # shown in the title bar return f"{self.journal.status()['completed']} steps" ``` Placement (`area`, `floating`, `closable`, `min_width`, `min_height`) is declared at construction and is only the *initial* arrangement — the operator owns it from there. `MonitorPanel` is the bridge to the [monitor framework](monitor.md): it attaches a `PlotRenderer` to a monitor when it is built, and ticks it afterwards. Constructing and describing it needs no GUI, so a shell's composition is testable headless. ## Theme Colours are data, not code. `Theme` reads `assets/palette.json` and renders `assets/shell.qss`, whose placeholders it substitutes at load time: ```python theme = Theme.load() theme.color("cyan") # "#22d3ee" — a design token, not a literal theme.series_color(2) # nth trace of a view theme.state_color("error") # "#fb7185" ``` The tokens mirror the plesty.net stylesheet and the canonical module colours, so the website, the generated reports, and the lab GUI stay one visual system. A rig that wants its own palette passes a JSON file to `Theme.load()`; nothing needs patching. Widget chrome belongs in the `.qss` asset, never in a Python string — the same rule the report and tutorial generators follow. ## Rendering `PlotRenderer` (pyqtgraph) draws the trace kinds a monitor may declare: | Kind | Drawn as | |---|---| | `line`, `scatter` | curve, optionally with markers | | `image` | heat map, scaled onto its physical axes via the payload's `x`/`y` | | `scalar`, `text` | caption line under the plot | pyqtgraph is used because a spectrum arriving every few hundred milliseconds for hours has to redraw without the operator noticing. ## Recording a run A measurement that took two hours is shown to a colleague as a video of the run building up. Recording belongs to the window, not to a view, so whatever is docked is captured — including a panel added or floated halfway through: ```python shell.start_recording("runs/qd_fss_20260804.mp4", fps=10) ... path = shell.stop_recording() # also called when the window closes ``` The *Record* menu does the same thing without code. MP4 needs the `record` extra (`uv add "plesty-lib[record]"`); with Pillow alone the recorder writes an animated GIF rather than nothing. `every=N` captures one frame per N refreshes, for a run whose rows arrive faster than a video needs them. ## Testing The toolkit-free half of the framework is tested like any other module. The Qt half runs on Qt's `offscreen` platform, so it needs no display — and is skipped where the `gui` extra is not installed, which is why CI (and any headless lab host) still runs the full suite: ```bash QT_QPA_PLATFORM=offscreen uv run pytest tests/test_ui.py ``` ## See also - [Monitor framework](monitor.md) — what `MonitorPanel` shows.