"""Progress UI for instance-sync operations. Two implementations sharing the ``Progress`` protocol: - :class:`HeadlessProgress` — no window; prints to stdout/stderr. Used when ``--no-gui`` is set OR when Qt isn't available (the only graphical path is Qt; there is no tkinter fallback). - :class:`QtProgressWindow` (in :mod:`cloud_sync.ui_qt`) — Qt modal window with the Prism-dark Steam-style layout. The factory :func:`make_progress` picks Qt → Headless. Qt requires PySide6 or PyQt6 to be importable. Install via ``pip install 'cloud-sync[qt]'`` or directly ``pip install PySide6``. """ from __future__ import annotations import sys from typing import Callable, Protocol class Progress(Protocol): """Interface for status + cancellation reporting during a sync run.""" def set_status(self, msg: str) -> None: ... def is_cancelled(self) -> bool: ... def run_with(self, worker: Callable[[], int], title: str) -> int: ... class HeadlessProgress: """No-op progress. Status messages go to stdout, errors to stderr.""" def set_status(self, msg: str) -> None: print(f"instance-sync: {msg}", flush=True) def is_cancelled(self) -> bool: return False def run_with(self, worker: Callable[[], int], title: str) -> int: self.set_status(title) return worker() def make_progress(headless: bool) -> Progress: """Return the best Progress impl for the runtime + flags. Order: 1. Qt window (PySide6 or PyQt6) — preferred when available. 2. HeadlessProgress — fallback when ``--no-gui`` is set or Qt is missing. Logs to stdout/stderr. """ if headless: return HeadlessProgress() try: from .ui_qt import QtProgressWindow return QtProgressWindow() except ImportError: print( "instance-sync: Qt (PySide6/PyQt6) not installed; " "running headless. Install with: pip install PySide6", file=sys.stderr, ) return HeadlessProgress() except Exception as e: # noqa: BLE001 print( f"instance-sync: Qt init failed ({e}); falling back to headless", file=sys.stderr, ) return HeadlessProgress()