plesty.lib.ui.qt.recorder

Recording a shell: one captured frame per refresh, encoded on stop.

A measurement that took two hours is shown to a colleague, a supervisor, or a paper reviewer as a video of the run building up — the same arrangement of panels the operator watched, at whatever speed makes it readable. Recording therefore belongs to the window, not to any one view: whatever is docked gets captured, including panels added halfway through.

Capture is a window grab on the GUI thread, which is cheap and, more importantly, needs no cooperation from the panels. Frames are size-locked to the first capture, so resizing or floating a panel mid-recording cannot corrupt the stream — the resized window is letterboxed into the frame it started with.

MP4 is written through imageio (with imageio-ffmpeg) when both are installed, and an animated GIF through Pillow otherwise, so a recording is always produced:

uv add "plesty-lib[record]"

Attributes

DEFAULT_FPS

Classes

Recorder

Capture a window while active; encode a video file on stop.

Functions

_to_array(→ numpy.ndarray)

Grab widget as an RGB array.

_fit(→ numpy.ndarray)

Letterbox frame into shape, so every frame of a video matches.

Module Contents

plesty.lib.ui.qt.recorder.DEFAULT_FPS = 10
plesty.lib.ui.qt.recorder._to_array(widget: PySide6.QtWidgets.QWidget) numpy.ndarray

Grab widget as an RGB array.

Parameters:

widget (PySide6.QtWidgets.QWidget) – The window (or panel) to capture.

Returns:

The pixels as a (height, width, 3) uint8 array.

Return type:

numpy.ndarray

plesty.lib.ui.qt.recorder._fit(frame: numpy.ndarray, shape: tuple[int, int]) numpy.ndarray

Letterbox frame into shape, so every frame of a video matches.

Parameters:
  • frame (numpy.ndarray)

  • shape (tuple[int, int])

Return type:

numpy.ndarray

class plesty.lib.ui.qt.recorder.Recorder(widget: PySide6.QtWidgets.QWidget, path: str | pathlib.Path, *, fps: int = DEFAULT_FPS, every: int = 1)

Capture a window while active; encode a video file on stop.

shell.start_recording("run.mp4", fps=10)
...                                  # the shell captures on every tick
path = shell.stop_recording()        # → run.mp4, or run.gif without ffmpeg

Prepare a recording of widget.

Parameters:
  • widget (PySide6.QtWidgets.QWidget) – The window to capture — normally the shell itself.

  • path (str | pathlib.Path) – Destination file; the suffix is corrected to what the available encoder can actually write.

  • fps (int) – Frame rate of the encoded video.

  • every (int) – Capture one frame per every refreshes, for a run whose rows arrive faster than a video needs them.

Raises:

ValueError – If fps or every is not positive.

widget
path
fps = 10
every = 1
active = False
frames: list[numpy.ndarray] = []
_shape: tuple[int, int] | None = None
_seen = 0
start() None

Begin capturing; a restart discards what was captured before.

Return type:

None

capture() None

Capture one frame, honouring every; ignored when inactive.

Return type:

None

stop() pathlib.Path | None

Stop capturing and encode what was captured.

Returns:

The written file, or None when nothing was captured — a recording that never saw a frame writes no file rather than an empty one.

Return type:

Optional[pathlib.Path]

_encode() pathlib.Path

Write the captured frames, preferring MP4 and falling back to GIF.

Return type:

pathlib.Path

_encode_gif() pathlib.Path

Write an animated GIF, the fallback that needs only Pillow.

Return type:

pathlib.Path

describe() dict[str, Any]

Return what has been captured so far.

Returns:

Whether the recorder is active, the frame count, and the target.

Return type:

dict[str, Any]