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, update_checker=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_releases_page()[source]#

Open the GitHub releases page in the operator’s browser.

Wired to the “Check for updates…” tray item (Design Spec §15.6 / §15.8 item 3). Any browser-launch failure is logged and swallowed so the tray menu callback never crashes the icon thread.

Return type:

None

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#
update_checker: Any = None#
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', on_check_update=None)[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.

When on_check_update is supplied (Design Spec §15.6 / §15.8 item 3) a “Check for updates…” item is inserted between the separator and Quit, giving the order Open, Status, —, Check for updates…, Quit. When it is None the menu is exactly as before.

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.

dependencies

Production AppDependencies factory.

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.

storage_secret

Per-installation NiceGUI storage_secret token.

window_launcher

Spawn / focus the on-demand window subprocess.