""" 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 Get a single value (e.g. gallery.png_dpi) gallery config set 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 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()