177 lines
5.2 KiB
Python
177 lines
5.2 KiB
Python
"""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
|