Files
ETPlot/gallery/cli.py
T
2026-05-08 14:42:01 +02:00

301 lines
11 KiB
Python

"""
Command-line interface for gallery generation.
Provides a CLI entry point for gallery generation and config management.
Shell autocomplete: add the following line to your .bashrc / .zshrc:
eval "$(register-python-argcomplete gallery)"
"""
import argparse
import os
import sys
from pathlib import Path
import argcomplete
from gallery import generate
from gallery.config import ConfigManager, GalleryConfig, GallerySource, default_config_path
_WELCOME = """\
Gallery - Scientific Plot Gallery Generator
===========================================
Generates responsive static HTML galleries from collections of PDFs and HTMLs.
Getting started:
1. Set your web output directory:
gallery config set paths.web_folder /path/to/your/public_html
2. Add one or more plot sources:
gallery config add-source --path /path/to/plots
3. Generate your gallery:
gallery generate
Config commands:
gallery config list Show all settings
gallery config get <key> Get a single value (e.g. gallery.png_dpi)
gallery config set <key> <value> Update a setting (e.g. paths.web_folder /my/web)
gallery config add-source --path P Add a plot source (--name defaults to dir name)
gallery config remove-source <name> Remove a plot source
gallery config sources List configured sources
gallery config path Show config file location
Generate commands:
gallery generate Generate gallery from config
gallery generate --config myconfig.yaml Use a custom config file
gallery generate --clean Clean and regenerate everything
gallery generate --source /path/to/plots Regenerate one source only
gallery generate --verbose Print detailed output
Shell autocomplete (run once after install):
gallery install-completion
Config file: {config_path}
"""
def _is_configured(config_path: Path) -> bool:
"""Return True if web_folder is set to a non-empty value."""
try:
web_folder = ConfigManager(config_path).get("paths.web_folder")
return bool(web_folder and str(web_folder).strip())
except Exception:
return False
def _source_names(prefix, parsed_args, **kwargs):
"""Autocomplete helper: return configured source names."""
try:
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
return [s["name"] for s in ConfigManager(config_path).list_sources()]
except Exception:
return []
def _config_keys(prefix, parsed_args, **kwargs):
"""Autocomplete helper: return known dot-notation config keys."""
try:
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
data = ConfigManager(config_path).list_all()
keys = []
for section, values in data.items():
if isinstance(values, dict):
for k in values:
keys.append(f"{section}.{k}")
else:
keys.append(section)
return [k for k in keys if k.startswith(prefix)]
except Exception:
return []
# ---------------------------------------------------------------------------
# Parser construction (separated so TUI can reuse the structure)
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="gallery",
description="Scientific Plot Gallery Generator",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--config",
type=str,
default=None,
metavar="FILE",
help="Path to config file (default: package bundled config)",
).completer = argcomplete.completers.FilesCompleter(["yaml", "yml"])
sub = parser.add_subparsers(dest="command", metavar="COMMAND")
# --- generate -----------------------------------------------------------
gen = sub.add_parser(
"generate",
help="Generate the gallery",
description="Generate scientific gallery from plot collections",
)
gen.add_argument("--clean", action="store_true", help="Clean gallery directory before generation")
gen.add_argument(
"--source",
type=str,
default=None,
metavar="DIR",
help="Only recompute a specific source directory (name defaults to dir name)",
).completer = argcomplete.completers.DirectoriesCompleter()
gen.add_argument("-v", "--verbose", action="store_true", help="Print verbose output")
# --- config -------------------------------------------------------------
cfg = sub.add_parser(
"config",
help="Read and write configuration",
description="Read and write gallery configuration",
)
cfg_sub = cfg.add_subparsers(dest="action", metavar="ACTION")
sub.add_parser("install-completion", help="Install shell tab-completion (bash/zsh)")
cfg_sub.add_parser("list", help="Print all config values")
cfg_sub.add_parser("path", help="Print the resolved config file path")
cfg_sub.add_parser("sources", help="List all configured sources")
p_get = cfg_sub.add_parser("get", help="Get a config value")
p_get.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi").completer = _config_keys
p_set = cfg_sub.add_parser("set", help="Set a config value")
p_set.add_argument("key", help="Dot-separated key, e.g. gallery.png_dpi").completer = _config_keys
p_set.add_argument("value", help="New value (YAML-parsed: use true/false for bools)")
p_add = cfg_sub.add_parser("add-source", help="Add a plot source")
p_add.add_argument("--name", default=None, help="Source name (default: bottom-level directory name)")
p_add.add_argument("--path", required=True, metavar="DIR",
help="Path to source directory").completer = argcomplete.completers.DirectoriesCompleter()
p_rm = cfg_sub.add_parser("remove-source", help="Remove a plot source by name")
p_rm.add_argument("name", help="Source name to remove").completer = _source_names
return parser
# ---------------------------------------------------------------------------
# Command handlers
# ---------------------------------------------------------------------------
def _run_generate(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
try:
config = GalleryConfig.from_yaml(config_path)
source_to_update = None
if args.source:
source_path = Path(args.source).resolve()
matching = next((s for s in config.sources if Path(s.path).resolve() == source_path), None)
if matching is None:
source_to_update = GallerySource(name=source_path.name, path=source_path)
config.sources.append(source_to_update)
if args.verbose:
print(f"Source {args.source} not in config. Adding temporarily as '{source_path.name}'")
else:
source_to_update = matching
success = generate(config=config, clean_first=args.clean, verbose=args.verbose,
source_to_update=source_to_update)
return 0 if success else 1
except (FileNotFoundError, ValueError) as e:
print(f"Error: {e}", file=sys.stderr)
return 1
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
def _run_config(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
mgr = ConfigManager(config_path)
if args.action == "path":
print(mgr.path)
elif args.action == "list":
import yaml
print(yaml.dump(mgr.list_all(), default_flow_style=False).rstrip())
elif args.action == "sources":
sources = mgr.list_sources()
if not sources:
print("No sources configured.")
else:
for s in sources:
print(f" {s['name']}: {s['path']}")
elif args.action == "get":
try:
print(mgr.get(args.key))
except KeyError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
elif args.action == "set":
try:
mgr.set(args.key, args.value)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
elif args.action == "add-source":
try:
name = args.name or Path(args.path).resolve().name
mgr.add_source(name, args.path)
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
elif args.action == "remove-source":
try:
mgr.remove_source(args.name)
except KeyError as e:
print(f"Error: {e}", file=sys.stderr)
return 1
else:
# `gallery config` with no action → show config help
build_parser().parse_args(["config", "--help"])
return 0
# ---------------------------------------------------------------------------
# install-completion handler
# ---------------------------------------------------------------------------
_COMPLETION_LINE = 'eval "$(register-python-argcomplete gallery)"'
_SHELL_RC = {
"zsh": ".zshrc",
"bash": ".bashrc",
"fish": ".config/fish/config.fish",
}
def _run_install_completion() -> int:
shell = Path(os.environ.get("SHELL", "")).name # e.g. "bash", "zsh"
rc_name = _SHELL_RC.get(shell, ".bashrc")
rc = Path.home() / rc_name
if rc.exists() and _COMPLETION_LINE in rc.read_text():
print(f"Shell completion already configured in {rc}")
return 0
with open(rc, "a") as f:
f.write(f"\n# gallery shell completion\n{_COMPLETION_LINE}\n")
print(f"Shell completion installed to {rc}")
print(f"Reload with: source {rc}")
return 0
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
parser = build_parser()
argcomplete.autocomplete(parser) # no-op when not completing; exits during completion
args = parser.parse_args()
if args.command == "generate":
sys.exit(_run_generate(args))
elif args.command == "config":
sys.exit(_run_config(args))
elif args.command == "install-completion":
sys.exit(_run_install_completion())
else:
print(_WELCOME.format(config_path=default_config_path()))
sys.exit(0)
if __name__ == "__main__":
main()