exlab_wizard.tray#

Tray application package. Backend Spec §4.3.2.

Public API:

class exlab_wizard.tray.AutostartManager(*, executable_path=None, filesystem_root=None, platform=None)[source]#

Bases: object

Per-platform autostart register / unregister / is_registered.

Construction-time parameters:

  • executable_path – absolute path to ExLab-Wizard-Tray. The production launcher resolves sys.executable (PyInstaller bundles set sys.executable to the launcher binary).

  • filesystem_root – root directory for per-user files. Defaults to Path.home() in production; tests inject tmp_path. The env-var override EXLAB_AUTOSTART_ROOT wins over the constructor default.

  • platform – override the OS dispatch, used by tests to exercise every branch on a single host.

Parameters:
property executable_path: str#

Return the absolute path the autostart record points at.

is_registered()[source]#

Return True iff a per-platform autostart record exists.

Return type:

bool

property platform: Platform#

Return the platform dispatch this manager is configured for.

register()[source]#

Create the per-platform autostart record. Idempotent.

Return type:

None

unregister()[source]#

Remove the per-platform autostart record. Idempotent.

Return type:

None

class exlab_wizard.tray.NotificationBus(*, notifier=None, is_window_foregrounded=None, coalescing_window=5.0)[source]#

Bases: object

Coalescing layer over notify().

The launcher constructs one bus and threads it through to the components that emit notification-eligible events (plugin host, NAS-sync). Components call emit(); the bus handles coalescing, foreground suppression, and the actual plyer call.

Parameters:
cancel_all()[source]#

Cancel every active timer without firing the notifications.

Return type:

None

emit(*, kind, title, message)[source]#

Submit a notification trigger.

If the window is foregrounded the call is dropped silently. Otherwise the trigger is added to its coalescing bucket; the first trigger in a window arms a timer that fires the actual notification at window end.

Parameters:
Return type:

None

flush_pending()[source]#

Drain every active bucket synchronously. Returns the bucket count.

Used by tests and by the quit coordinator to make sure no in-flight coalesce-buckets are dropped on shutdown.

Return type:

int

class exlab_wizard.tray.QuitCoordinator(*, server_runner, window_launcher, session_store, nas_sync, on_force_quit_prompt=None, timeout_seconds=30.0, sigterm_timeout_seconds=5.0, poll_interval_seconds=0.1)[source]#

Bases: object

Drive the §4.3.2 graceful-shutdown protocol.

Construction-time dependencies are kept loosely typed so the coordinator can integrate with whatever stub fixtures unit tests pass. The runtime contract is documented per parameter.

Parameters:
async quit(*, sigterm=False)[source]#

Run the graceful-shutdown protocol.

sigterm=True reduces the wait window to sigterm_timeout (5 s by default) because the OS will hard-kill the process shortly anyway.

Parameters:

sigterm (bool)

Return type:

None

class exlab_wizard.tray.ServerRunner(*, app, state_dir)[source]#

Bases: object

Starts uvicorn on a free localhost port and tracks its state file.

Backend Spec §4.3.2 + §15.3.1.

Parameters:
property is_running: bool#

Return True while the worker thread is alive.

property port: int#

Return the port the server is bound to.

Raises RuntimeError when called before start().

start()[source]#

Pick a port, launch uvicorn in a worker thread, write server.json.

Returns the chosen port. Idempotent in the sense that calling start() a second time before stop() raises RuntimeError – the runner manages exactly one server.

Return type:

int

property state_file: Path#

Return the absolute path of the server.json state file.

stop()[source]#

Signal uvicorn to exit, join the worker thread, delete server.json.

Idempotent: a second call is a no-op.

Return type:

None

class exlab_wizard.tray.StatusSnapshot(active_sessions=0, sync_queue_depth=0, input_required_count=0)[source]#

Bases: object

Immutable view over the §4.3.2 status inputs.

Parameters:
  • active_sessions (int)

  • sync_queue_depth (int)

  • input_required_count (int)

active_sessions: int#
input_required_count: int#
sync_queue_depth: int#
class exlab_wizard.tray.StatusTicker(*, session_store=None, nas_sync=None, on_update=None, interval_seconds=5.0)[source]#

Bases: object

Polls snapshot_status() on a fixed cadence.

On every tick the formatted string is computed; if it differs from the previous label the on_update callback is invoked with the new label. The callback is the tray menu’s “set status text” hook; tests pass a recording callable.

Parameters:
start()[source]#

Spawn the ticker thread. Idempotent.

Return type:

None

stop()[source]#

Signal the ticker to exit. Idempotent.

Return type:

None

tick_once()[source]#

Run a single tick synchronously. Returns the new label.

Tests call this directly so they don’t have to wait for a real 5-second interval.

Return type:

str

class exlab_wizard.tray.TrayApp(server_runner, window_launcher, quit_coordinator, status_ticker, notification_bus, autostart, icon=None)[source]#

Bases: object

Bundle of long-lived tray components.

The launcher constructs one and calls run(); tests build one with stub components and exercise shutdown() / open() individually.

Parameters:
autostart: AutostartManager#
icon: Any = None#
notification_bus: NotificationBus#
open_window()[source]#

Spawn or focus the window subprocess.

Return type:

None

quit_coordinator: QuitCoordinator#
request_quit()[source]#

Trigger the graceful-shutdown protocol on the icon thread.

Return type:

None

run(*, run_loop=None)[source]#

Start the server, build the icon, run the pystray loop.

run_loop is injected by tests so they don’t actually call Icon.run (which would block on a real backend). Returns the exit code.

Parameters:

run_loop (Callable[[], None] | None)

Return type:

int

server_runner: ServerRunner#
shutdown()[source]#

Tear down sub-components synchronously.

Return type:

None

start_server()[source]#

Start the in-process FastAPI server. Returns the bound port.

Return type:

int

status_ticker: StatusTicker#
window_launcher: WindowLauncher#
class exlab_wizard.tray.WindowLauncher(*, window_executable=None, state_dir)[source]#

Bases: object

Spawn exlab-wizard-window as a subprocess, track its PID.

On a re-open request with an existing live child, focuses the existing window (best-effort) rather than spawning a duplicate (Backend §4.1: “single-instance window”).

Parameters:
close()[source]#

Terminate the live window subprocess, if any. Idempotent.

Return type:

None

property is_alive: bool#

Return True while the window subprocess is running.

open()[source]#

Spawn a window subprocess, or focus the existing one.

Return type:

None

property pid: int | None#

Return the live window’s PID, or None if no window is up.

exlab_wizard.tray.build_icon(*, on_open, on_quit, status_provider, pystray_module=None, icon_image=None, icon_name='exlab-wizard', title='ExLab-Wizard')[source]#

Build the pystray icon with the §4.1 menu.

status_provider is invoked every time pystray re-renders the menu (pystray supports lazily-evaluated text via callable labels) so the operator sees live status without a separate refresh notification.

Parameters:
Return type:

Any

exlab_wizard.tray.format_status(snapshot)[source]#

Return the menu-label string for the §4.3.2 formatter.

Parameters:

snapshot (StatusSnapshot)

Return type:

str

exlab_wizard.tray.notify(*, title, message, notifier=None)[source]#

Fire a plyer notification (or an injected stub).

notifier defaults to plyer.notification.notify which the plyer wheel auto-resolves to the platform-appropriate backend (UNUserNotificationCenter / ToastNotificationManager / libnotify). Tests pass a recording callable.

Parameters:
Return type:

None

exlab_wizard.tray.snapshot_status(*, session_store=None, nas_sync=None)[source]#

Build a StatusSnapshot from live component references.

Each component is read defensively so the launcher can pass None (setup-incomplete state) without the formatter crashing. The function reads active_sessions and input_required-style counts from session_store and in_flight_jobs / queue_depth from nas_sync.

Parameters:
  • session_store (Any)

  • nas_sync (Any)

Return type:

StatusSnapshot

Modules

autostart

Per-platform autostart registration.

icon

pystray icon construction.

main

exlab-wizard-tray console_scripts entry point.

notifications

OS notifications via plyer with 5-second coalescing.

quit_coordinator

Graceful tray shutdown coordinator.

server_runner

Programmatic uvicorn launcher with atomic server.json writes.

status

Status-submenu rendering.

window_launcher

Spawn / focus the on-demand window subprocess.