From 69f9cfea41020f08978fdf1a1ca70b53b73c1f71 Mon Sep 17 00:00:00 2001 From: Labyricorn Date: Fri, 14 Aug 2026 10:05:48 -0700 Subject: [PATCH] commit --- README.md | 8 ++ .../contents.lr | 19 ---- scripts/project_editor.py | 104 +++++++++++++++++- 3 files changed, 111 insertions(+), 20 deletions(-) delete mode 100644 content/blog/this-is-a-blog-example-delete-me-later/contents.lr diff --git a/README.md b/README.md index 0ff20d7..ca40ddb 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,14 @@ editor directs the user to copy the standalone `scripts/devlog_editor.py` into the project checkout, initialize and author the devlog, publish the changes, and recheck. It never edits, commits, or pushes an external project repository. +Project Editor requires the Lektor Python package because it uses the site's +authoritative importer directly. If the launching Python cannot import Lektor, +the editor automatically checks `LABYRICORN_LEKTOR_PYTHON`, a project-local +`.venv`, common pipx Lektor environments, and the environment containing a +`lektor` command. It relaunches itself with the first runtime that can import +both Lektor and Tkinter. Set `LABYRICORN_LEKTOR_PYTHON` to an explicit Python +executable when Lektor is installed somewhere else. + ## Production filesystem and permissions ```text diff --git a/content/blog/this-is-a-blog-example-delete-me-later/contents.lr b/content/blog/this-is-a-blog-example-delete-me-later/contents.lr deleted file mode 100644 index d67948a..0000000 --- a/content/blog/this-is-a-blog-example-delete-me-later/contents.lr +++ /dev/null @@ -1,19 +0,0 @@ -_model: entry ---- -title: This is a blog example, delete me later ---- -date: 2026-08-13 ---- -author: Christopher Chambers ---- -tags: ai, configuration, html, taggy ---- -kicker: Blog ---- -summary: Summary ---- -published_urls: - ---- -body: -Body (markdown) woohoo! diff --git a/scripts/project_editor.py b/scripts/project_editor.py index 12d5f2f..930b524 100644 --- a/scripts/project_editor.py +++ b/scripts/project_editor.py @@ -12,6 +12,7 @@ from pathlib import Path import re import shutil import subprocess +import sys import tempfile import threading from typing import Any @@ -33,6 +34,104 @@ class ProjectEditorError(Exception): """An expected project-editor problem suitable for display to a user.""" +def _python_has_lektor(executable: Path) -> bool: + if not executable.is_file(): + return False + options: dict[str, Any] = { + "capture_output": True, + "text": True, + "timeout": 10, + "check": False, + } + if os.name == "nt": + options["creationflags"] = subprocess.CREATE_NO_WINDOW + try: + result = subprocess.run( + [str(executable), "-c", "import lektor, tkinter"], + **options, + ) + except (OSError, subprocess.SubprocessError): + return False + return result.returncode == 0 + + +def _find_lektor_python(site_root: Path) -> Path | None: + """Locate a Python runtime that can import both Lektor and Tkinter.""" + candidates: list[Path] = [] + configured = os.environ.get("LABYRICORN_LEKTOR_PYTHON", "").strip() + if configured: + candidates.append(Path(configured).expanduser()) + if os.name == "nt": + candidates.extend( + ( + site_root / ".venv" / "Scripts" / "python.exe", + Path(os.environ.get("LOCALAPPDATA", "")) + / "pipx" + / "pipx" + / "venvs" + / "lektor" + / "Scripts" + / "python.exe", + Path.home() + / ".local" + / "pipx" + / "venvs" + / "lektor" + / "Scripts" + / "python.exe", + ) + ) + else: + candidates.extend( + ( + site_root / ".venv" / "bin" / "python", + Path.home() / ".local" / "pipx" / "venvs" / "lektor" / "bin" / "python", + ) + ) + lektor_command = shutil.which("lektor") + if lektor_command: + candidates.append( + Path(lektor_command).resolve().parent + / ("python.exe" if os.name == "nt" else "python") + ) + + seen: set[Path] = set() + for candidate in candidates: + candidate = candidate.resolve() + if candidate in seen: + continue + seen.add(candidate) + if _python_has_lektor(candidate): + return candidate + return None + + +def _relaunch_with_lektor(site_root: Path) -> bool: + try: + import lektor # noqa: F401 + except ModuleNotFoundError: + pass + else: + return False + if os.environ.get("LABYRICORN_LEKTOR_RELAUNCHED") == "1": + return False + executable = _find_lektor_python(site_root) + if executable is None: + return False + gui_executable = executable + if os.name == "nt": + pythonw = executable.with_name("pythonw.exe") + if pythonw.is_file(): + gui_executable = pythonw + child_environment = {**os.environ, "LABYRICORN_LEKTOR_RELAUNCHED": "1"} + subprocess.Popen( + [str(gui_executable), str(Path(__file__).resolve()), *sys.argv[1:]], + cwd=site_root, + env=child_environment, + ) + return True + + def _load_project_importer() -> Any: try: return importlib.import_module("project_sources") @@ -691,8 +790,11 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--site-root", type=Path, default=Path(__file__).resolve().parents[1]) args = parser.parse_args() + site_root = args.site_root.resolve() + if _relaunch_with_lektor(site_root): + return 0 try: - run_gui(ProjectSourceRegistry(args.site_root)) + run_gui(ProjectSourceRegistry(site_root)) except ProjectEditorError as exc: parser.exit(1, f"project-editor: ERROR: {exc}\n") return 0