import argparse import os import sys def parse_args(argv=None): parser = argparse.ArgumentParser( prog="scanslicer", description="ScanSlicer Qt UI (Wayland/X11)", ) parser.add_argument( "path", nargs="?", help="Optional input directory containing images", ) parser.add_argument( "--backend", choices=("auto", "wayland", "x11"), default="auto", help="Display backend selection (default: auto)", ) return parser.parse_args(argv) def _detect_backend_from_qpa_env(): qpa_value = os.environ.get("QT_QPA_PLATFORM", "").strip().lower() if not qpa_value: return None tokens = [token.strip() for token in qpa_value.split(";") if token.strip()] for token in tokens: if token == "wayland": return "wayland" if token in ("xcb", "x11"): return "x11" return None def resolve_runtime_backend(backend): if backend in ("wayland", "x11"): return backend env_backend = _detect_backend_from_qpa_env() if env_backend is not None: return env_backend session_type = os.environ.get("XDG_SESSION_TYPE", "").strip().lower() if session_type == "wayland": return "wayland" if session_type == "x11": return "x11" runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip() wayland_display = os.environ.get("WAYLAND_DISPLAY", "").strip() if runtime_dir and wayland_display and os.path.exists(os.path.join(runtime_dir, wayland_display)): return "wayland" if os.environ.get("DISPLAY", "").strip(): return "x11" # Fall back to Wayland when auto-detection has no clear signal. return "wayland" def configure_qt_backend(backend): if backend == "wayland": os.environ["QT_QPA_PLATFORM"] = "wayland" # Prevent accidental X11/XWayland routing in mixed environments. os.environ.pop("DISPLAY", None) elif backend == "x11": os.environ["QT_QPA_PLATFORM"] = "xcb" else: raise RuntimeError(f"Unsupported backend '{backend}'.") is_appimage = bool(os.environ.get("APPIMAGE")) # In sandboxed desktop runtimes (Flatpak/Snap), prefer portal integration # for file dialogs and desktop services. sandboxed = bool( os.environ.get("FLATPAK_ID") or os.environ.get("SNAP") ) if sandboxed: os.environ.setdefault("QT_QPA_PLATFORMTHEME", "xdgdesktopportal") elif is_appimage: # AppImage is not a strict sandbox and some portal backends can emit: # "Connection already associated with an application ID". # Prefer GTK theme integration and avoid forced portal registration. os.environ.setdefault("QT_QPA_PLATFORMTHEME", "gtk3") os.environ.setdefault("QT_NO_XDG_DESKTOP_PORTAL", "1") # In containers, prefer GTK platform theme so Qt can follow host theme # settings when available (for example dark/light mode). if os.path.exists("/.dockerenv"): os.environ.setdefault("QT_QPA_PLATFORMTHEME", "gtk3") def validate_wayland_environment(): runtime_dir = os.environ.get("XDG_RUNTIME_DIR", "").strip() display_name = os.environ.get("WAYLAND_DISPLAY", "").strip() if not runtime_dir or not display_name: raise RuntimeError( "Missing Wayland runtime environment. " "Set XDG_RUNTIME_DIR and WAYLAND_DISPLAY." ) socket_path = os.path.join(runtime_dir, display_name) if not os.path.exists(socket_path): raise RuntimeError( f"Wayland socket not found: {socket_path}" ) def validate_x11_environment(): display_name = os.environ.get("DISPLAY", "").strip() if not display_name: raise RuntimeError( "Missing X11 runtime environment. Set DISPLAY or use --backend wayland." ) def main(argv=None): args = parse_args(sys.argv[1:] if argv is None else argv) runtime_backend = None try: runtime_backend = resolve_runtime_backend(args.backend) configure_qt_backend(runtime_backend) if runtime_backend == "wayland": validate_wayland_environment() elif runtime_backend == "x11": validate_x11_environment() from .main import run_app return run_app(args.path) except Exception as exc: selected = os.environ.get("QT_QPA_PLATFORM", "") print( f"Failed to start ScanSlicer Qt UI (backend={runtime_backend or args.backend}, QT_QPA_PLATFORM={selected}).", file=sys.stderr, ) print(str(exc), file=sys.stderr) if runtime_backend == "x11": print( "X11 hint: ensure DISPLAY is set and an X server is available.", file=sys.stderr, ) else: print( "Wayland hint: ensure XDG_RUNTIME_DIR and WAYLAND_DISPLAY are set and the Wayland socket is mounted.", file=sys.stderr, ) return 1 if __name__ == "__main__": raise SystemExit(main())