feat: add Gallery TUI for interactive configuration and generation

- Implemented a terminal user interface (TUI) for configuring the gallery generator using Textual.
- Included functionality to manage sources with add/remove
- Integrated a log pane to display generation output from subprocess calls.
- Implemented save functionality to persist configuration changes.
This commit is contained in:
Kylian Schmidt
2026-05-08 15:47:02 +02:00
parent 17acd478d3
commit 19fc15abb4
8 changed files with 843 additions and 41 deletions
+34 -10
View File
@@ -14,7 +14,10 @@ from pathlib import Path
import argcomplete
from gallery import generate
from gallery.config import ConfigManager, GalleryConfig, GallerySource, default_config_path
from gallery.config import (
ConfigManager, GalleryConfig, GallerySource,
default_config_path, get_active_config_path, ensure_user_config,
)
_WELCOME = """\
Gallery - Scientific Plot Gallery Generator
@@ -67,7 +70,7 @@ def _is_configured(config_path: Path) -> bool:
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()
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else get_active_config_path()
return [s["name"] for s in ConfigManager(config_path).list_sources()]
except Exception:
return []
@@ -76,7 +79,7 @@ def _source_names(prefix, parsed_args, **kwargs):
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()
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else get_active_config_path()
data = ConfigManager(config_path).list_all()
keys = []
for section, values in data.items():
@@ -94,6 +97,7 @@ def _config_keys(prefix, parsed_args, **kwargs):
# Parser construction (separated so TUI can reuse the structure)
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="gallery",
@@ -135,6 +139,7 @@ def build_parser() -> argparse.ArgumentParser:
cfg_sub = cfg.add_subparsers(dest="action", metavar="ACTION")
sub.add_parser("install-completion", help="Install shell tab-completion (bash/zsh)")
sub.add_parser("tui", help="Launch the interactive TUI")
cfg_sub.add_parser("list", help="Print all config values")
cfg_sub.add_parser("path", help="Print the resolved config file path")
@@ -149,8 +154,9 @@ def build_parser() -> argparse.ArgumentParser:
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_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
@@ -162,8 +168,9 @@ def build_parser() -> argparse.ArgumentParser:
# Command handlers
# ---------------------------------------------------------------------------
def _run_generate(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
config_path = Path(args.config) if args.config else get_active_config_path()
try:
config = GalleryConfig.from_yaml(config_path)
@@ -179,8 +186,12 @@ def _run_generate(args: argparse.Namespace) -> int:
else:
source_to_update = matching
success = generate(config=config, clean_first=args.clean, verbose=args.verbose,
source_to_update=source_to_update)
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:
@@ -192,7 +203,13 @@ def _run_generate(args: argparse.Namespace) -> int:
def _run_config(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
if args.config:
config_path = Path(args.config)
elif args.action in ("set", "add-source", "remove-source"):
# Writes go to the user config; create it from the template if needed
config_path = ensure_user_config()
else:
config_path = get_active_config_path()
mgr = ConfigManager(config_path)
if args.action == "path":
@@ -200,6 +217,7 @@ def _run_config(args: argparse.Namespace) -> int:
elif args.action == "list":
import yaml
print(yaml.dump(mgr.list_all(), default_flow_style=False).rstrip())
elif args.action == "sources":
@@ -280,6 +298,7 @@ def _run_install_completion() -> int:
# Entry point
# ---------------------------------------------------------------------------
def main():
parser = build_parser()
argcomplete.autocomplete(parser) # no-op when not completing; exits during completion
@@ -291,8 +310,13 @@ def main():
sys.exit(_run_config(args))
elif args.command == "install-completion":
sys.exit(_run_install_completion())
elif args.command == "tui":
from gallery.tui import GalleryTUI
config_path = Path(args.config) if args.config else None
GalleryTUI(config_path=config_path).run()
sys.exit(0)
else:
print(_WELCOME.format(config_path=default_config_path()))
print(_WELCOME.format(config_path=get_active_config_path()))
sys.exit(0)