"""Detect installed Word / LibreOffice / OpenOffice for post-save table splits.""" from __future__ import annotations import os import shutil import sys from dataclasses import dataclass from pathlib import Path from .uno_cli import soffice_cli_path OFFICE_ENGINE_MODES = ("auto", "word", "libre", "openoffice") DEFAULT_OFFICE_ENGINE = "auto" @dataclass(frozen=True) class OfficeInstall: engine: str # "word" | "libre" | "openoffice" label: str soffice_path: str | None = None python_path: str | None = None def _program_dirs(kind: str) -> list[Path]: roots: list[Path] = [] for env in ("ProgramFiles", "ProgramFiles(x86)"): base = os.environ.get(env) if base: roots.append(Path(base)) if sys.platform != "win32": roots.extend( [ Path("/usr/lib/libreoffice"), Path("/usr/lib64/libreoffice"), Path("/opt/libreoffice"), Path("/opt/openoffice4"), Path("/Applications/LibreOffice.app/Contents"), ] ) candidates: list[Path] = [] if kind == "libre": for root in roots: candidates.extend( [ root / "LibreOffice" / "program", root / "libreoffice" / "program", root / "program", root / "MacOS", ] ) else: for root in roots: candidates.extend( [ root / "OpenOffice 4" / "program", root / "openoffice4" / "program", ] ) return candidates def _soffice_names() -> tuple[str, ...]: if sys.platform == "win32": # soffice.com = CLI/headless; soffice.exe opens the Start Center GUI. return ("soffice.com", "soffice.exe") return ("soffice", "soffice.bin") def _python_names() -> tuple[str, ...]: if sys.platform == "win32": return ("python.exe",) return ("python", "python3") def _find_soffice_install(kind: str) -> OfficeInstall | None: label = "LibreOffice" if kind == "libre" else "OpenOffice" seen: set[str] = set() for directory in _program_dirs(kind): for soffice_name in _soffice_names(): soffice = directory / soffice_name key = str(soffice.resolve()) if soffice.exists() else "" if not key or key in seen: continue seen.add(key) python_path = None for py_name in _python_names(): candidate = directory / py_name if candidate.is_file(): python_path = str(candidate) break if python_path is None: continue return OfficeInstall( engine=kind, label=label, soffice_path=soffice_cli_path(str(soffice)), python_path=python_path, ) which = shutil.which("soffice") or shutil.which("soffice.bin") if which: program_dir = Path(which).resolve().parent python_path = None for py_name in _python_names(): candidate = program_dir / py_name if candidate.is_file(): python_path = str(candidate) break if python_path: lower = str(program_dir).lower() detected = "openoffice" if "openoffice" in lower else "libre" if kind != detected: return None return OfficeInstall( engine=kind, label=label, soffice_path=soffice_cli_path(str(Path(which).resolve())), python_path=python_path, ) return None def detect_office_install(engine: str) -> OfficeInstall | None: """Return install info for a concrete engine id.""" if engine == "word": return _detect_word() if engine == "libre": return _find_soffice_install("libre") if engine == "openoffice": return _find_soffice_install("openoffice") return None def _detect_word() -> OfficeInstall | None: if sys.platform != "win32": return None try: import win32com.client # type: ignore except ImportError: return None word = None try: word = win32com.client.DispatchEx("Word.Application") return OfficeInstall(engine="word", label="Microsoft Word") except Exception: return None finally: if word is not None: try: word.Quit() except Exception: pass def resolve_office_engine(mode: str) -> tuple[str | None, OfficeInstall | None]: """ Resolve ``mode`` to a concrete engine id and optional install record. ``auto`` tries Word, then LibreOffice, then OpenOffice. """ if mode not in OFFICE_ENGINE_MODES: mode = DEFAULT_OFFICE_ENGINE if mode == "auto": for candidate in ("word", "libre", "openoffice"): install = detect_office_install(candidate) if install is not None: return candidate, install return None, None install = detect_office_install(mode) if install is None: return None, None return mode, install