mac support

This commit is contained in:
2026-08-26 13:45:15 +02:00
parent 15d1319374
commit a9897606ac
6 changed files with 115 additions and 21 deletions
+1 -1
View File
@@ -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 interruption ne laisse donc pas un JSON partiellement écrit. Pour
une modification concurrente de type lire-modifier-écrire, utiliser une modification concurrente de type lire-modifier-écrire, utiliser
=atomic_update_json= : cet utilitaire protège l'opération complète avec =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 * Convention des scripts standardisés
+42 -5
View File
@@ -37,13 +37,24 @@ labels contenant notamment =:= ne sont pas acceptés par Windows.
*** Python 3.11 ou plus récent *** 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é : Créer et activer un environnement virtuel est recommandé :
#+BEGIN_SRC bash #+BEGIN_SRC bash
python -m venv .venv python -m venv .venv
#+END_SRC #+END_SRC
Sous Linux : Sous Linux et macOS :
#+BEGIN_SRC bash #+BEGIN_SRC bash
source .venv/bin/activate 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 Fermer puis rouvrir le terminal et le GUI après une modification de
=PATH=. =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 *** Programme externe facultatif
PDF Arranger permet d'ouvrir et de réorganiser plus facilement les PDF Arranger permet d'ouvrir et de réorganiser plus facilement les
PDF. Son absence est signalée comme facultative dans le diagnostic. Il PDF. Son absence est signalée comme facultative dans le diagnostic. Il
doit fournir la commande =pdf-arranger= ou =pdfarranger= dans =PATH=. doit fournir la commande =pdf-arranger= ou =pdfarranger= dans =PATH=.
Sous macOS, Copienator utilise automatiquement Aperçu comme solution de
repli.
*** Accès à Gemini *** 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. se répètent pas lors d'un retour en arrière.
Le bouton =Diagnostic…= vérifie les modules Python, Poppler, LaTeX, Le bouton =Diagnostic…= vérifie les modules Python, Poppler, LaTeX,
PDF Arranger et la configuration Gemini. Sous Windows, les exécutables PDF Arranger (ou Aperçu sous macOS) et la configuration Gemini. Sous
externes doivent être accessibles depuis =PATH=. Quand la création de Windows, les exécutables externes doivent être accessibles depuis
liens symboliques ou physiques n'est pas autorisée, l'export et la =PATH=. Sous macOS, les emplacements standards de Homebrew et MacTeX
préparation de =A Rendre= utilisent automatiquement une copie normale. 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 Les chemins des étapes personnelles peuvent être adaptés avec
=CURRENT_SCORE_ODS_PATH=, =FINAL_SCORE_ODS_PATH=, =CURRENT_SCORE_ODS_PATH=, =FINAL_SCORE_ODS_PATH=,
+2 -2
View File
@@ -119,11 +119,11 @@ Optional : Set proxy with ~export HTTPS_PROXY="http://10.0.0.1:3128"~
2. =python -m copienator review-labels Interro= 2. =python -m copienator review-labels Interro=
Permet de vérifier visuellement les labels trouvés. 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. l'éditer a la main.
+ Quand un label est manquant, il est possible de cliquer sur + Quand un label est manquant, il est possible de cliquer sur
l'image, ce qui copie les coordonnées dans le presse papier 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 `…|` : + Utilisation de `_`, `|…` et `…|` :
+ `|…` n'est pas arrêté verticalement par son type opposé. + `|…` n'est pas arrêté verticalement par son type opposé.
+ `…|` est stoppé horizontalement par le `|…` le plus proche. + `…|` est stoppé horizontalement par le `|…` le plus proche.
+51 -7
View File
@@ -8,6 +8,12 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Literal from typing import Literal
MACOS_EXECUTABLE_DIRECTORIES = (
Path("/opt/homebrew/bin"),
Path("/usr/local/bin"),
Path("/Library/TeX/texbin"),
)
WINDOWS_RESERVED_NAMES = { WINDOWS_RESERVED_NAMES = {
"CON", "CON",
"PRN", "PRN",
@@ -71,6 +77,36 @@ def safe_filename(value: str, fallback: str = "Unknown") -> str:
return cleaned 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: def open_path(path: str | Path) -> None:
"""Open a file with the desktop's default application.""" """Open a file with the desktop's default application."""
target = str(Path(path).expanduser().resolve()) target = str(Path(path).expanduser().resolve())
@@ -78,17 +114,26 @@ def open_path(path: str | Path) -> None:
os.startfile(target) # type: ignore[attr-defined] os.startfile(target) # type: ignore[attr-defined]
elif sys.platform.startswith("linux"): elif sys.platform.startswith("linux"):
subprocess.Popen(["xdg-open", target]) subprocess.Popen(["xdg-open", target])
elif sys.platform == "darwin":
opener = find_executable("open") or "/usr/bin/open"
subprocess.Popen([opener, target])
else: else:
raise RuntimeError(f"Unsupported platform: {sys.platform}") raise RuntimeError(f"Unsupported platform: {sys.platform}")
def launch_pdf_arranger(path: str | Path) -> None: def launch_pdf_arranger(path: str | Path) -> None:
executable = shutil.which("pdf-arranger") or shutil.which("pdfarranger") target = str(Path(path).expanduser().resolve())
if not executable: executable = find_executable("pdf-arranger", "pdfarranger")
raise FileNotFoundError( if executable:
"PDF Arranger is not installed or is not available in PATH." subprocess.Popen([executable, target])
) return
subprocess.Popen([executable, str(Path(path).expanduser().resolve())]) 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( def replace_with_link_or_copy(
@@ -118,4 +163,3 @@ def replace_with_link_or_copy(
shutil.copy2(source_path, destination_path) shutil.copy2(source_path, destination_path)
return "copy" return "copy"
+7 -2
View File
@@ -8,7 +8,12 @@ from tkinter import filedialog, messagebox, ttk
from typing import Any from typing import Any
from copienator import ExitCode 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 .diagnostics import collect_diagnostics
from .runner import ProcessRunner from .runner import ProcessRunner
@@ -95,7 +100,7 @@ def plotting_shortcut_lines() -> list[str]:
def build_runner_environment( def build_runner_environment(
base: dict[str, str], api_key: str, proxy: str, use_proxy: bool base: dict[str, str], api_key: str, proxy: str, use_proxy: bool
) -> dict[str, str]: ) -> dict[str, str]:
environment = dict(base) environment = add_platform_executable_paths(base)
environment["PYTHONUNBUFFERED"] = "1" environment["PYTHONUNBUFFERED"] = "1"
environment["PYTHONIOENCODING"] = "utf-8" environment["PYTHONIOENCODING"] = "utf-8"
if api_key.strip(): if api_key.strip():
+12 -4
View File
@@ -7,7 +7,7 @@ import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from copienator.platform import windows_filename_problems from copienator.platform import find_executable, windows_filename_problems
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -33,8 +33,10 @@ def collect_diagnostics(
checks = [ checks = [
DiagnosticCheck( DiagnosticCheck(
"Système", "Système",
os.name == "nt" or sys.platform.startswith("linux"), os.name == "nt"
f"{sys.platform} — Linux et Windows sont pris en charge", 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), DiagnosticCheck("Python", True, sys.executable),
] ]
@@ -65,7 +67,13 @@ def collect_diagnostics(
("PDF Arranger", ("pdf-arranger", "pdfarranger"), False), ("PDF Arranger", ("pdf-arranger", "pdfarranger"), False),
) )
for label, candidates, required in programs: 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" detail = executable or "Introuvable dans PATH"
checks.append(DiagnosticCheck(label, executable is not None, detail, required)) checks.append(DiagnosticCheck(label, executable is not None, detail, required))