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:
uv add "plesty-lib[gui]" # PySide6 + pyqtgraph
The home window
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:
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: 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:
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 |
|---|---|
|
curve, optionally with markers |
|
heat map, scaled onto its physical axes via the payload’s |
|
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:
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:
QT_QPA_PLATFORM=offscreen uv run pytest tests/test_ui.py
See also
Monitor framework — what
MonitorPanelshows.