plesty.lib.ui

GUI framework: one home window, many rearrangeable sub-windows.

The framework is generic — a Panel is anything worth a sub-window. MonitorPanel shows a live view and DevicePanel drives an instrument; a run status board is the same contract again. Theme carries the plesty.net design tokens so every PLESTY GUI looks like the platform it belongs to.

The Qt-dependent names (Shell, PlotRenderer, run()) are resolved lazily, so import plesty.lib.ui works on a headless host without PySide6 installed; only using them requires the gui extra:

uv add "plesty-lib[gui]"

Submodules

Attributes

CONTROL_KINDS

DEFAULT_SKIP_KINDS

DOCK_AREAS

ASSETS

Classes

DeviceHandle

The four things a panel asks of a device, wherever the device is.

DeviceWorker

Runs a handle's calls on one thread and hands the results back by queue.

LocalHandle

A DeviceHandle over a device object in this process.

Result

One finished call, waiting to be applied on the GUI thread.

Control

One rendered control, as derived from the device's own declaration.

DevicePanel

Sub-window holding one device's controls.

MonitorPanel

Sub-window rendering one monitor.

PlotPanel

Sub-window showing one plot of a run.

Panel

Base class for the dockable units of a Shell.

Theme

Resolved design tokens plus the stylesheet built from them.

Functions

controls_for(, operations, skip_kinds)

Derive a device's controls from its serialized doc model.

Package Contents

class plesty.lib.ui.DeviceHandle

Bases: Protocol

The four things a panel asks of a device, wherever the device is.

DeviceTCPIPClient satisfies this as written; an in-process device reaches it through LocalHandle; a remote bench relays the same four calls through its agent.

describe() → dict[str, Any]

Return {"methods": [...], "doc": {...} | None} for the device.

Return type:

dict[str, Any]

query(key: str) → Any

Read configuration parameter key from the device.

Parameters:

key (str)

Return type:

Any

write(key: str, value: Any) → Any

Write value to configuration parameter key.

Parameters:
  • key (str)

  • value (Any)

Return type:

Any

call(op: str, **kwargs: Any) → Any

Run operation op with keyword arguments.

Parameters:
  • op (str)

  • kwargs (Any)

Return type:

Any

class plesty.lib.ui.DeviceWorker(handle: DeviceHandle, name: str = 'device')

Runs a handle’s calls on one thread and hands the results back by queue.

One worker per panel, not one per shell. A device server that has died answers nothing until its client times out, and a shared worker would make that one device stall every panel in the window; a worker of its own means the dead device’s panel goes stale and the rest keep their clock.

Requests are coalesced by tag: asking again for something already queued or in flight is dropped rather than appended. Without it a device slower than the shell’s refresh accumulates a poll backlog it can never work off, and the panel ends up showing readings from minutes ago.

Prepare a worker over handle; it starts on start().

Parameters:
  • handle (DeviceHandle) – What the worker calls.

  • name (str) – Used for the thread name and log lines.

handle
name = 'device'
_requests: queue.Queue[_Request | None]
_results: queue.Queue[Result]
_inflight: set[Any]
_lock
_thread: threading.Thread | None = None
start() → None

Start the worker thread; calling it twice is harmless.

Return type:

None

stop(timeout: float = JOIN_TIMEOUT_S) → None

Ask the worker to finish and wait briefly for it.

The thread is a daemon and the wait is short on purpose: a call already carries the device’s own timeout, and closing a window must not hang on an instrument that stopped answering.

Parameters:

timeout (float) – Seconds to wait for the thread to end.

Return type:

None

property running: bool

Whether the worker thread is alive.

Return type:

bool

submit(tag: Any, verb: str, *args: Any, **kwargs: Any) → bool

Queue a call, unless one with the same tag is already outstanding.

Parameters:
  • tag (Any) – Label the matching Result carries back, and the key requests are coalesced on.

  • verb (str) – "describe", "query", "write" or "call".

  • *args (Any) – Positional arguments for the verb.

  • **kwargs (Any) – Keyword arguments for the verb.

Returns:

True when the request was queued, False when an identical tag was already outstanding and this one was dropped.

Return type:

bool

results() → list[Result]

Take every finished call off the queue without blocking.

Returns:

The results that arrived since the last drain, in order.

Return type:

list[Result]

property pending: int

How many calls are queued or in flight.

Return type:

int

_run() → None

Take requests until asked to stop.

Return type:

None

_answer(request: _Request) → Result

Run one request and wrap whatever it does into a Result.

Every exception is caught: a panel reports a device that refused a write in its title bar, and a worker thread that died would instead leave the panel silently frozen.

Parameters:

request (_Request) – The queued call.

Returns:

The result to hand back.

Return type:

Result

class plesty.lib.ui.LocalHandle(device: Any)

A DeviceHandle over a device object in this process.

The device already answers query() and write(); what it does not have is the other two, because in-process callers use doc_model() and call operations as attributes. Going through the same serialized doc payload the wire uses is deliberate — a module’s own GUI then exercises exactly what the console and a remote client will see, so a hole in the payload shows up at the bench and not only across a network.

Wrap device.

Parameters:

device (Any) – A BaseDeviceSyncModel.

device
describe() → dict[str, Any]

Return the device’s methods and its serialized doc model.

Returns:

The same shape the device server’s describe reply carries.

Return type:

dict[str, Any]

query(key: str) → Any

Read parameter key.

Parameters:

key (str) – Configuration key.

Returns:

The value the device reported.

Return type:

Any

write(key: str, value: Any) → Any

Write value to parameter key.

Parameters:
  • key (str) – Configuration key.

  • value (Any) – The value to set.

Returns:

Whatever the device’s write returns.

Return type:

Any

call(op: str, **kwargs: Any) → Any

Run operation op.

Parameters:
  • op (str) – Operation name.

  • **kwargs (Any) – The operation’s arguments.

Returns:

The operation’s result.

Return type:

Any

class plesty.lib.ui.Result

One finished call, waiting to be applied on the GUI thread.

Variables:
  • tag – What the panel asked for, as it labelled the request.

  • value – The return value, or None when the call raised.

  • error – The exception message, or "" when the call succeeded.

  • duration – How long the call took, in milliseconds.

tag: Any
value: Any = None
error: str = ''
duration: float = 0.0
property ok: bool

Whether the call returned rather than raised.

Return type:

bool

plesty.lib.ui.CONTROL_KINDS: tuple[str, Ellipsis] = ('number', 'choice', 'toggle', 'text', 'readout', 'action')
plesty.lib.ui.DEFAULT_SKIP_KINDS: tuple[str, Ellipsis] = ('lifecycle',)
class plesty.lib.ui.Control

One rendered control, as derived from the device’s own declaration.

Variables:
  • key – Configuration key or operation name.

  • kind – One of CONTROL_KINDS.

  • label – Text shown beside the control.

  • dtype – Declared type name, as it crossed the wire.

  • group – Configuration group the parameter belongs to.

  • unit – Physical unit, shown as the editor’s suffix.

  • minimum – Lower bound, or None when the device declares none.

  • maximum – Upper bound, or None when the device declares none.

  • options – Allowed values, which make the control a drop-down.

  • default – Declared default.

  • tooltip – The declaration’s description.

  • read_only – The device reports it; the operator cannot set it.

  • write_only – The operator sets it; it cannot be read back, so it is never polled.

  • arguments – For an action, the operation’s arguments as controls of their own — the same rules, one level down.

  • outputs – For an action, what it returns — name, type and unit — which is what lets a result be shown as a reading rather than as the mapping it arrives in.

  • op_kind – For an action, its OpKind kind.

key: str
kind: str
label: str
dtype: str = 'Any'
group: str = 'default'
unit: str | None = None
minimum: float | None = None
maximum: float | None = None
options: tuple[Any, Ellipsis] | None = None
default: Any = None
tooltip: str = ''
read_only: bool = False
write_only: bool = False
arguments: tuple[Control, Ellipsis] = ()
outputs: tuple[Control, Ellipsis] = ()
op_kind: str = ''
required: bool = True
property types: frozenset[str]

The types this declaration allows, a union split into its members.

None is not among them: it is not a type an editor can offer, and what is left is what the control has to be able to send. Switch on this rather than on dtype, which is one string and may be a union.

Return type:

frozenset[str]

property omittable: bool

Whether leaving this argument alone means sending nothing.

An optional argument the device gives no default for has an answer already — its own — and the panel has no way to show one. A spin box always holds a number, so an untouched one said 0, and 0 is a real value: wait_settled read it as a zero-second budget and timed out at once. Such an argument is shown as unset and left out of the call instead.

Return type:

bool

property repeatable: bool

Whether holding this control should keep asking the device.

Any readable parameter. An operation only when the module declares it REPEATABLE_OP_KINDS — a reading or an acquisition, never a move or a stop.

Return type:

bool

property readable: bool

Whether polling this control makes sense.

Return type:

bool

property writable: bool

Whether the operator can send this control anywhere.

Return type:

bool

class plesty.lib.ui.DevicePanel(handle: plesty.lib.ui.device_link.DeviceHandle, *, doc: dict[str, Any] | None = None, include: Sequence[str] | None = None, exclude: Iterable[str] = (), operations: Sequence[str] | None = None, skip_kinds: Iterable[str] = DEFAULT_SKIP_KINDS, writable: bool = False, poll_s: float = DEFAULT_POLL_S, theme: plesty.lib.ui.theme.Theme | None = None, **kwargs: Any)

Bases: plesty.lib.ui.panel.Panel

Sub-window holding one device’s controls.

Constructing the panel touches neither a toolkit nor the device: it is built when a shell adds it, and it asks the device what it offers on its own worker thread, so a slow or absent instrument delays nothing. Until that answer arrives the panel says so.

The panel starts read-only. It may be pointed at an instrument an experiment is driving, and a stray write into a running measurement is worse than a click: set_writable() is what a Take control toggle calls.

Wrap handle into a dockable control panel.

Parameters:
  • handle (plesty.lib.ui.device_link.DeviceHandle) – What the panel drives — see DeviceHandle.

  • doc (Optional[dict[str, Any]]) – The device’s serialized doc model when the caller already has it (a console that just called describe); None makes the panel fetch it.

  • include (Optional[Sequence[str]]) – Parameter keys to show, in order.

  • exclude (Iterable[str]) – Parameter keys to leave out.

  • operations (Optional[Sequence[str]]) – Operations to offer, in order.

  • skip_kinds (Iterable[str]) – Operation kinds left out by default.

  • writable (bool) – Start with writes and operations enabled. The default is read-only.

  • poll_s (float) – Seconds between re-reads of the device.

  • theme (Optional[plesty.lib.ui.theme.Theme]) – Palette; None loads the packaged one.

  • **kwargs (Any) – Forwarded to Panel.

kind: ClassVar[str] = 'device'

Panel category, used for layout keys and styling hooks.

handle
theme = None
writable = False
poll_s = 1.0
live: dict[str, dict[str, Any]]
controls: list[Control] = []
values: dict[str, Any]
worker
form: Any = None
_selection
_doc = None
_note = ''
_tone = 'idle'
_latency = 0.0
_polled_at = 0.0
_selection_kwargs() → dict[str, Any]

Return the selection arguments controls_for() was configured with.

Return type:

dict[str, Any]

open() → None

Start the worker and ask the device what it offers.

Separate from build() so the device-facing half of the panel — which is all of the interesting behaviour — can be driven with no display and no toolkit: open it, tick() it, and read controls and values.

Return type:

None

build(parent: Any = None) → Any

Create the form, then open the device.

Parameters:

parent (Any) – The parent widget provided by the shell.

Returns:

The form widget.

Return type:

Any

tick() → None

Apply whatever the worker finished, then poll if polling is on.

Both halves run on the shell’s clock and neither blocks: this is the only place results cross back to the GUI thread, and the coalescing in DeviceWorker is what keeps a slow device from building a backlog it can never work off.

Polling is off until it is asked for — see set_live().

Return type:

None

control(key: str) → Control | None

The control named key, or None.

Parameters:

key (str)

Return type:

Optional[Control]

_read(key: str, arguments: dict[str, Any] | None = None) → None

Ask the device about one control: a parameter’s value, or an operation.

Parameters:
  • key (str)

  • arguments (Optional[dict[str, Any]])

Return type:

None

read_once(key: str | None = None, arguments: Any = None) → None

Ask the device once: for key, or for every value the panel shows.

Parameters:
  • key (Optional[str]) – The control to read; None reads every readable one.

  • arguments (Any) – For an operation, what to call it with.

Return type:

None

set_live(key: str, live: bool, arguments: Any = None) → None

Start or stop re-reading one control on the shell’s clock.

Parameters:
  • key (str) – The control to keep reading.

  • live (bool) – Whether to keep reading it.

  • arguments (Any) – For an operation, what to keep calling it with. Taken once, when it starts: an operation repeating on a timer should go on asking the same question, not whatever the form happens to hold when the timer next fires.

Return type:

None

is_live(key: str) → bool

Whether key is being re-read.

Parameters:

key (str)

Return type:

bool

status() → str

Return the line shown in the panel’s title bar.

Returns:

What the panel is doing, or what went wrong.

Return type:

str

tone() → str

Return the state colour for the panel’s dot.

Returns:

error when the last call failed, warning when the device offered no schema, running once controls are on screen.

Return type:

str

close() → None

Stop the worker thread.

Return type:

None

describe() → dict[str, Any]

Return the panel description including what it drives.

Returns:

The panel’s placement plus its controls and whether it is writable.

Return type:

dict[str, Any]

set_writable(writable: bool) → None

Enable or disable every editor and button.

Parameters:

writable (bool) – Whether the operator may drive the device.

Return type:

None

request_write(key: str, value: Any) → None

Send a parameter change to the device.

Parameters:
  • key (str) – Configuration key.

  • value (Any) – The value the operator entered.

Return type:

None

request_call(op: str, arguments: dict[str, Any] | None = None) → None

Run an operation on the device.

Parameters:
  • op (str) – Operation name.

  • arguments (Optional[dict[str, Any]]) – The operation’s arguments.

Return type:

None

_apply(result: plesty.lib.ui.device_link.Result) → None

Fold one finished call into the panel’s state and its form.

Parameters:

result (plesty.lib.ui.device_link.Result) – What the worker handed back.

Return type:

None

_receive_doc(describe: Any) → None

Derive the controls from a describe reply and fill the form.

Parameters:

describe (Any) – The reply, or None.

Return type:

None

_fail(result: plesty.lib.ui.device_link.Result) → None

Record a failed call in the title bar.

Parameters:

result (plesty.lib.ui.device_link.Result) – The failed result.

Return type:

None

plesty.lib.ui.controls_for(doc: dict[str, Any] | None, *, include: Sequence[str] | None = None, exclude: Iterable[str] = (), operations: Sequence[str] | None = None, skip_kinds: Iterable[str] = DEFAULT_SKIP_KINDS) → list[Control]

Derive a device’s controls from its serialized doc model.

This is the whole schema-to-widget mapping, and it is deliberately free of any toolkit: it is where the bounds, units and options come from, so it is where a drifting panel would be caught, and a plain test can catch it.

Parameters:
  • doc (Optional[dict[str, Any]]) – A to_dict() payload — from a describe reply or from the device itself. None (an older server, or a device whose model failed to build) yields no controls rather than an error.

  • include (Optional[Sequence[str]]) – Parameter keys to show, in this order. None shows every declared parameter. A grouped parameter is named as the device addresses it, group.parameter.

  • exclude (Iterable[str]) – Parameter keys to leave out, applied after include.

  • operations (Optional[Sequence[str]]) – Operation names to offer, in this order. None offers every operation whose kind is not in skip_kinds; naming one overrides that, so an operation can always be asked for explicitly.

  • skip_kinds (Iterable[str]) – Operation kinds left out when operations is None.

Returns:

The parameters in declaration order, then the operations.

Return type:

list[Control]

class plesty.lib.ui.MonitorPanel(monitor: plesty.lib.monitor.base_monitor.Monitor, *, theme: plesty.lib.ui.theme.Theme | None = None, **kwargs: Any)

Bases: plesty.lib.ui.panel.Panel

Sub-window rendering one monitor.

The panel is toolkit-free until it is built: constructing and describing it needs no GUI, which is what lets a headless test cover a shell’s composition.

Wrap monitor into a dockable panel.

Parameters:
kind: ClassVar[str] = 'monitor'

Panel category, used for layout keys and styling hooks.

monitor
theme = None
renderer: Any = None
build(parent: Any = None) → Any

Create the plot widget and attach it to the monitor.

Parameters:

parent (Any) – The parent widget provided by the shell.

Returns:

The plot widget.

Return type:

Any

tick() → None

Drain the monitor’s source; every new frame redraws immediately.

Return type:

None

status() → str

Return the monitor’s status line.

Returns:

The text the monitor wants shown in the title bar.

Return type:

str

close() → None

Detach the renderer and close the monitor’s source.

Return type:

None

describe() → dict[str, Any]

Return the panel description including the monitor it shows.

Returns:

The panel’s placement plus describe().

Return type:

dict[str, Any]

class plesty.lib.ui.PlotPanel(plot: plesty.lib.monitor.plots.Plot, data: Any, *, theme: plesty.lib.ui.theme.Theme | None = None, **kwargs: Any)

Bases: plesty.lib.ui.panel.Panel

Sub-window showing one plot of a run.

Toolkit-free until it is built, like every panel: constructing and describing it needs no GUI, which is what lets a headless test cover a viewer’s composition.

Show plot over data.

Parameters:
kind = 'plot'

Panel category, used for layout keys and styling hooks.

plot
data
theme = None
renderer: Any = None
_traces: _Traces | None = None
_shape: tuple[Any, Ellipsis] = ()
_own_widget: Any = None
_failure = ''
_drawing: concurrent.futures.Future[list[plesty.lib.monitor.plots.Drawable]] | None = None
build(parent: Any = None) → Any

Create the widget this panel shows.

A plot that overrides build() supplies its own widget and is never drawn as a plot — the escape hatch for a view that is a table or an image browser rather than a curve.

Parameters:

parent (Any) – The parent widget provided by the shell.

Returns:

The widget.

Return type:

Any

tick() → None

Show what the last draw produced, and set the next one going.

The work a draw does is not this thread’s to do. Reading what has committed and asking the plot what to draw are a file read and, for a view that fits its lines, a least-squares solve per spectrum — and this runs on the thread that answers clicks and repaints. Done here it stopped a console following a live run for seconds at a time, which is a window that looks hung while the measurement it is showing is perfectly fine.

So a tick renders whatever the drawing thread has finished and then asks it for the next, and the window is never inside a draw. A panel has at most one in flight: ticks that arrive while one is running start nothing, which is also what keeps a slow plot from queueing up work it will never catch up with. The cost is that a drawing appears on the tick after the one that asked for it — a poll interval late, on a view that is watching a measurement unfold.

Return type:

None

_begin_drawing() → None

Ask the drawing thread for the next drawing, if it is free of this panel.

Return type:

None

_compute_drawing() → list[plesty.lib.monitor.plots.Drawable]

Poll the data and ask the plot what to draw. Runs off the GUI thread.

The poll is shared: whichever panel gets here first brings the store up to date and the rest find it already so, which is why one run is read once however many plots watch it.

Return type:

list[plesty.lib.monitor.plots.Drawable]

_show_finished_drawing() → None

Render the drawing the thread has finished, if it has finished one.

Return type:

None

status() → str

Return what the title bar shows beside the plot’s name.

Returns:

How far the run has got, which is the same question for every plot of it, so no plot has to answer it — or why this view stopped drawing, which is the one thing only this panel knows.

Return type:

str

close() → None

Drop the renderer; the store is shared and is not closed here.

Return type:

None

describe() → dict[str, Any]

Return the panel’s placement plus what it draws.

Returns:

The panel description, with the plot’s title and class.

Return type:

dict[str, Any]

plesty.lib.ui.DOCK_AREAS: tuple[str, Ellipsis] = ('left', 'right', 'top', 'bottom')
class plesty.lib.ui.Panel(*, name: str | None = None, title: str | None = None, area: str = 'right', floating: bool = False, closable: bool = True, weight: float = 1.0, min_width: int | None = None, min_height: int | None = None)

Bases: abc.ABC

Base class for the dockable units of a Shell.

Subclasses implement build() and usually tick(). The panel owns its widget; the shell owns where the widget sits, and remembers that placement between sessions through name.

Configure how the shell should place this panel.

Parameters:
  • name (Optional[str]) – Stable identifier used as the layout key; must be unique within a shell. Defaults to the class name.

  • title (Optional[str]) – Title shown in the panel’s bar; defaults to name.

  • area (str) – Initial docking area — one of DOCK_AREAS.

  • floating (bool) – Start as a free-floating window instead of docked.

  • closable (bool) – Allow the operator to close the panel (it can always be brought back from the shell’s Panels menu).

  • weight (float) – Share of the area’s space this panel starts with, relative to its neighbours: a plot 3, a readout strip 1. Only the initial arrangement — the operator resizes from there, and a saved layout overrides it.

  • min_width (Optional[int]) – Minimum widget width in pixels.

  • min_height (Optional[int]) – Minimum widget height in pixels.

Raises:

ValueError – If area is not a known docking area.

kind: ClassVar[str] = 'panel'

Panel category, used for layout keys and styling hooks.

name = 'Panel'
title = 'Panel'
area = 'right'
floating = False
closable = True
weight = 1.0
min_width = None
min_height = None
_widget: Any = None
abstractmethod build(parent: Any = None) → Any

Create and return the panel’s widget.

Called once, when the panel is added to a shell. Toolkit imports belong inside this method, so that constructing a panel — and describing it, and testing it — needs no GUI installed.

Parameters:

parent (Any) – The parent widget the shell provides.

Returns:

The widget to show inside the sub-window.

Return type:

Any

tick() → None

Advance the panel on the shell’s clock.

Called at the shell’s refresh interval on the GUI thread. Implementations must return quickly: drain a source, redraw, and nothing more.

Return type:

None

status() → str

Return the one-line status shown in the panel’s title bar.

Returns:

The status text; empty by default.

Return type:

str

tone() → str

Return the state colour of the panel’s dot.

One of the theme’s state names — running, idle, warning, error. The default reads the panel as working whenever it has something to say, which is right for a view that only ever draws. A panel that can fail while still running — a device that stopped answering, a source that went stale — overrides this, because a red dot is how the operator learns from across the room.

Returns:

The state name.

Return type:

str

close() → None

Release whatever the panel holds; called when the shell shuts down.

Return type:

None

property widget: Any

The built widget, or None before the panel was added to a shell.

Return type:

Any

ensure_widget(parent: Any = None) → Any

Build the widget on first use and return it.

Parameters:

parent (Any) – The parent widget passed to build().

Returns:

The panel’s widget.

Return type:

Any

describe() → dict[str, Any]

Return a JSON-serializable description of this panel.

Returns:

Identity and placement, as stored in a saved layout.

Return type:

dict[str, Any]

plesty.lib.ui.ASSETS = 'plesty.lib.ui.assets'
class plesty.lib.ui.Theme

Resolved design tokens plus the stylesheet built from them.

Variables:
  • name – Theme identifier, e.g. "plesty-dark".

  • tokens – Token name → hex colour.

  • series – Token names cycled for successive traces of a view.

  • colormap – Hex stops of the image/heatmap colour map, dark to bright.

  • state – Semantic state name → token name (running, idle, error, …).

name: str = 'plesty-dark'
tokens: dict[str, str]
series: list[str] = []
colormap: list[str] = []
state: dict[str, str]
classmethod load(palette: str | pathlib.Path | None = None) → Theme

Load the packaged palette, or a custom one from disk.

Parameters:

palette (Optional[str | pathlib.Path]) – Path to a JSON palette with the same keys as the packaged palette.json; None loads the packaged one.

Returns:

The resolved theme.

Return type:

Theme

color(token: str, default: str | None = None) → str

Return the hex colour of a design token.

Parameters:
  • token (str) – Token name ("cyan", "violet", "muted", …) or a literal #rrggbb value, which is passed through.

  • default (Optional[str]) – Returned when the token is unknown; without it an unknown token resolves to a glaring magenta rather than failing silently mid-experiment.

Returns:

The hex colour.

Return type:

str

series_color(index: int) → str

Return the colour of the index-th trace of a view.

Parameters:

index (int) – Zero-based trace index; wraps around the series list.

Returns:

The hex colour.

Return type:

str

state_color(state: str) → str

Return the colour marking a run or device state.

Parameters:

state (str) – State name, e.g. "running", "error", "idle".

Returns:

The hex colour; unknown states fall back to the muted text colour.

Return type:

str

stylesheet(source: str | pathlib.Path | None = None) → str

Return the Qt stylesheet with every {{token}} substituted.

Parameters:

source (Optional[str | pathlib.Path]) – Path to a custom .qss template; None uses the packaged shell.qss.

Returns:

The rendered stylesheet.

Return type:

str