diff --git a/Architecture.org b/Architecture.org index 7ca0469..d3b4e77 100644 --- a/Architecture.org +++ b/Architecture.org @@ -35,7 +35,7 @@ le même dossier, synchronise son contenu, puis remplace la destination. Une interruption ne laisse donc pas un JSON partiellement écrit. Pour une modification concurrente de type lire-modifier-écrire, utiliser =atomic_update_json= : cet utilitaire protège l'opération complète avec -un verrou inter-processus Linux/Windows. +un verrou inter-processus Linux, Windows et macOS. * Convention des scripts standardisés diff --git a/Readme.org b/Readme.org index ac62736..d1517e1 100644 --- a/Readme.org +++ b/Readme.org @@ -37,13 +37,24 @@ labels contenant notamment =:= ne sont pas acceptés par Windows. *** Python 3.11 ou plus récent +Sous macOS avec Homebrew, installer Python et la version correspondante +de Tkinter avant de créer l'environnement virtuel. Par exemple : + +#+BEGIN_SRC bash +brew install python@3.13 python-tk@3.13 +#+END_SRC + +Utiliser alors =python3.13= à la place de =python= dans les commandes +de création de l'environnement si la commande non versionnée n'est pas +disponible. + Créer et activer un environnement virtuel est recommandé : #+BEGIN_SRC bash python -m venv .venv #+END_SRC -Sous Linux : +Sous Linux et macOS : #+BEGIN_SRC bash source .venv/bin/activate @@ -99,11 +110,35 @@ les installations Python qui ne l'incluent pas d'origine. Fermer puis rouvrir le terminal et le GUI après une modification de =PATH=. +**** macOS + +Avec Homebrew, installer Poppler et MacTeX : + +#+BEGIN_SRC bash +brew install poppler +brew install --cask mactex +#+END_SRC + +MacTeX fournit la distribution TeX Live complète utilisée par les +modèles de Copienator. Après son installation, rouvrir le terminal ou +exécuter : + +#+BEGIN_SRC bash +eval "$(/usr/libexec/path_helper)" +#+END_SRC + +Copienator recherche aussi les exécutables dans =/opt/homebrew/bin=, +=/usr/local/bin= et =/Library/TeX/texbin=, notamment lorsque le GUI ne +récupère pas le =PATH= du terminal. Ces instructions conviennent aux +Mac Intel et Apple Silicon sous macOS 11 ou plus récent. + *** Programme externe facultatif PDF Arranger permet d'ouvrir et de réorganiser plus facilement les PDF. Son absence est signalée comme facultative dans le diagnostic. Il doit fournir la commande =pdf-arranger= ou =pdfarranger= dans =PATH=. +Sous macOS, Copienator utilise automatiquement Aperçu comme solution de +repli. *** Accès à Gemini @@ -155,10 +190,12 @@ absent, vide ou ne contient que des commentaires. Ces automatismes ne se répètent pas lors d'un retour en arrière. Le bouton =Diagnostic…= vérifie les modules Python, Poppler, LaTeX, -PDF Arranger et la configuration Gemini. Sous Windows, les exécutables -externes doivent être accessibles depuis =PATH=. Quand la création de -liens symboliques ou physiques n'est pas autorisée, l'export et la -préparation de =A Rendre= utilisent automatiquement une copie normale. +PDF Arranger (ou Aperçu sous macOS) et la configuration Gemini. Sous +Windows, les exécutables externes doivent être accessibles depuis +=PATH=. Sous macOS, les emplacements standards de Homebrew et MacTeX +sont également inspectés. Quand la création de liens symboliques ou +physiques n'est pas autorisée, l'export et la préparation de =A Rendre= +utilisent automatiquement une copie normale. Les chemins des étapes personnelles peuvent être adaptés avec =CURRENT_SCORE_ODS_PATH=, =FINAL_SCORE_ODS_PATH=, diff --git a/Script.org b/Script.org index f7ebe47..2121d2a 100644 --- a/Script.org +++ b/Script.org @@ -119,11 +119,11 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~ 2. =python -m copienator review-labels Interro= Permet de vérifier visuellement les labels trouvés. - + Sous linux, on peut faire =e= pour ouvrir le fichier .json et + + Sous Linux et macOS, on peut faire =e= pour ouvrir le fichier .json et l'éditer a la main. + Quand un label est manquant, il est possible de cliquer sur l'image, ce qui copie les coordonnées dans le presse papier - (sous linux…), puis on peut l'ajouter à la main. + puis on peut l'ajouter à la main. + Utilisation de `_`, `|…` et `…|` : + `|…` n'est pas arrêté verticalement par son type opposé. + `…|` est stoppé horizontalement par le `|…` le plus proche. diff --git a/copienator/platform.py b/copienator/platform.py index 2c01830..b115443 100644 --- a/copienator/platform.py +++ b/copienator/platform.py @@ -8,6 +8,12 @@ import sys from pathlib import Path from typing import Literal +MACOS_EXECUTABLE_DIRECTORIES = ( + Path("/opt/homebrew/bin"), + Path("/usr/local/bin"), + Path("/Library/TeX/texbin"), +) + WINDOWS_RESERVED_NAMES = { "CON", "PRN", @@ -71,6 +77,36 @@ def safe_filename(value: str, fallback: str = "Unknown") -> str: return cleaned +def add_platform_executable_paths( + environment: dict[str, str], platform_name: str | None = None +) -> dict[str, str]: + """Expose common desktop-installed executables to child processes.""" + result = dict(environment) + if (platform_name or sys.platform) != "darwin": + return result + existing = result.get("PATH", "").split(os.pathsep) + additions = [str(path) for path in MACOS_EXECUTABLE_DIRECTORIES if path.is_dir()] + result["PATH"] = os.pathsep.join(dict.fromkeys([*additions, *existing])) + return result + + +def find_executable( + *candidates: str, platform_name: str | None = None +) -> str | None: + """Find a command in PATH or in standard macOS package locations.""" + for candidate in candidates: + executable = shutil.which(candidate) + if executable: + return executable + if (platform_name or sys.platform) == "darwin": + for directory in MACOS_EXECUTABLE_DIRECTORIES: + for candidate in candidates: + executable = directory / candidate + if executable.is_file() and os.access(executable, os.X_OK): + return str(executable) + return None + + def open_path(path: str | Path) -> None: """Open a file with the desktop's default application.""" target = str(Path(path).expanduser().resolve()) @@ -78,17 +114,26 @@ def open_path(path: str | Path) -> None: os.startfile(target) # type: ignore[attr-defined] elif sys.platform.startswith("linux"): subprocess.Popen(["xdg-open", target]) + elif sys.platform == "darwin": + opener = find_executable("open") or "/usr/bin/open" + subprocess.Popen([opener, target]) else: raise RuntimeError(f"Unsupported platform: {sys.platform}") def launch_pdf_arranger(path: str | Path) -> None: - executable = shutil.which("pdf-arranger") or shutil.which("pdfarranger") - if not executable: - raise FileNotFoundError( - "PDF Arranger is not installed or is not available in PATH." - ) - subprocess.Popen([executable, str(Path(path).expanduser().resolve())]) + target = str(Path(path).expanduser().resolve()) + executable = find_executable("pdf-arranger", "pdfarranger") + if executable: + subprocess.Popen([executable, target]) + return + if sys.platform == "darwin": + opener = find_executable("open") or "/usr/bin/open" + subprocess.Popen([opener, "-b", "com.apple.Preview", target]) + return + raise FileNotFoundError( + "PDF Arranger is not installed or is not available in PATH." + ) def replace_with_link_or_copy( @@ -118,4 +163,3 @@ def replace_with_link_or_copy( shutil.copy2(source_path, destination_path) return "copy" - diff --git a/copienator_gui/app.py b/copienator_gui/app.py index aef1736..f0fd8fc 100644 --- a/copienator_gui/app.py +++ b/copienator_gui/app.py @@ -8,7 +8,12 @@ from tkinter import filedialog, messagebox, ttk from typing import Any from copienator import ExitCode -from copienator.platform import WindowsLabelError, open_path, validate_windows_labels +from copienator.platform import ( + WindowsLabelError, + add_platform_executable_paths, + open_path, + validate_windows_labels, +) from .diagnostics import collect_diagnostics from .runner import ProcessRunner @@ -95,7 +100,7 @@ def plotting_shortcut_lines() -> list[str]: def build_runner_environment( base: dict[str, str], api_key: str, proxy: str, use_proxy: bool ) -> dict[str, str]: - environment = dict(base) + environment = add_platform_executable_paths(base) environment["PYTHONUNBUFFERED"] = "1" environment["PYTHONIOENCODING"] = "utf-8" if api_key.strip(): diff --git a/copienator_gui/diagnostics.py b/copienator_gui/diagnostics.py index ed5712e..bfb3cac 100644 --- a/copienator_gui/diagnostics.py +++ b/copienator_gui/diagnostics.py @@ -7,7 +7,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from copienator.platform import windows_filename_problems +from copienator.platform import find_executable, windows_filename_problems @dataclass(frozen=True) @@ -33,8 +33,10 @@ def collect_diagnostics( checks = [ DiagnosticCheck( "Système", - os.name == "nt" or sys.platform.startswith("linux"), - f"{sys.platform} — Linux et Windows sont pris en charge", + os.name == "nt" + or sys.platform.startswith("linux") + or sys.platform == "darwin", + f"{sys.platform} — Linux, Windows et macOS sont pris en charge", ), DiagnosticCheck("Python", True, sys.executable), ] @@ -65,7 +67,13 @@ def collect_diagnostics( ("PDF Arranger", ("pdf-arranger", "pdfarranger"), False), ) for label, candidates, required in programs: - executable = next((shutil.which(candidate) for candidate in candidates if shutil.which(candidate)), None) + executable = find_executable(*candidates) + if label == "PDF Arranger" and not executable and sys.platform == "darwin": + system_open = Path("/usr/bin/open") + opener = find_executable("open") or ( + str(system_open) if system_open.is_file() else None + ) + executable = f"{opener} (Aperçu)" if opener else None detail = executable or "Introuvable dans PATH" checks.append(DiagnosticCheck(label, executable is not None, detail, required))