diff --git a/deadsands/site_tools/build_system.py b/deadsands/site_tools/build_system.py new file mode 100644 index 0000000..de0151e --- /dev/null +++ b/deadsands/site_tools/build_system.py @@ -0,0 +1,136 @@ +from collections import defaultdict +from pathlib import Path +from time import sleep + +import os +import subprocess +import shlex +import shutil +import webbrowser + +from pelican import main as pelican_main +from livereload import Server +from livereload.watcher import INotifyWatcher + +import site_tools as st + +CONFIG = defaultdict(dict) +CONFIG.update( + { + "settings_base": st.DEV_SETTINGS_FILE_BASE, + "settings_publish": st.PUB_SETTINGS_FILE_BASE, + # Output path. Can be absolute or relative to tasks.py. Default: 'output' + "deploy_path": st.SETTINGS["OUTPUT_PATH"], + # Remote server configuration + "ssh_user": "greg", + "ssh_host": "froghat.club", + "ssh_port": "22", + "ssh_path": "/usr/local/deploy/deadsands/", + # Host and port for `serve` + "host": "localhost", + "port": 8000, + # content manager config + "templates_path": "markdown-templates", + # directory to watch for new assets + "import_path": "imports", + # where new asseets will be made available + "production_host": "deadsands.froghat.club", + # where to find roll table sources + "table_sources_path": "sources", + } +) + + +def pelican_run(cmd: list = [], publish=False) -> None: + settings = CONFIG["settings_publish" if publish else "settings_base"] + pelican_main(["-s", settings] + cmd) + + +def clean(): + if os.path.isdir(CONFIG["deploy_path"]): + shutil.rmtree(CONFIG["deploy_path"]) + os.makedirs(CONFIG["deploy_path"]) + + +def build(): + subprocess.run(shlex.split("git submodule update --remote --merge")) + pelican_run() + + +def watch(): + import_path = Path(CONFIG["import_path"]) + content_path = Path(st.SETTINGS["PATH"]) + + def do_import(): + assets = [] + for src in import_path.rglob("*"): + relpath = src.relative_to(import_path) + target = content_path / relpath + if src.is_dir(): + target.mkdir(parents=True, exist_ok=True) + continue + if target.exists(): + print(f"{target}: exists; skipping.") + continue + print(f"{target}: importing...") + src.link_to(target) + subprocess.run(shlex.split(f"git add {target}")) + uri = target.relative_to("content") + assets.append(f"https://{CONFIG['production_host']}/{uri}") + src.unlink() + if assets: + publish() + print("\n\t".join(["\nImported Asset URLS:"] + assets)) + print("\n") + watcher = INotifyWatcher() + watcher.watch(import_path, do_import) + watcher.start(do_import) + print(f"Watching {import_path}. CTRL+C to exit.") + while True: + watcher.examine() + sleep(5) + + +def serve(): + url = "http://{host}:{port}/".format(**CONFIG) + + def cached_build(): + pelican_run(["-ve", "CACHE_CONTENT=true", "LOAD_CONTENT_CACHE=true", "SHOW_DRAFTS=true", f'SITEURL="{url}"']) + clean() + cached_build() + server = Server() + theme_path = st.SETTINGS["THEME"] + watched_globs = [ + CONFIG["settings_base"], + "{}/templates/**/*.html".format(theme_path), + ] + + content_file_extensions = [".md", ".rst"] + for extension in content_file_extensions: + content_glob = "{0}/**/*{1}".format(st.SETTINGS["PATH"], extension) + watched_globs.append(content_glob) + + static_file_extensions = [".css", ".js"] + for extension in static_file_extensions: + static_file_glob = "{0}/static/**/*{1}".format(theme_path, extension) + watched_globs.append(static_file_glob) + + for glob in watched_globs: + server.watch(glob, cached_build) + + if st.OPEN_BROWSER_ON_SERVE: + webbrowser.open(url) + + server.serve(host=CONFIG["host"], port=CONFIG["port"], root=CONFIG["deploy_path"]) + + +def publish(): + clean() + pelican_run(publish=True) + subprocess.run( + shlex.split( + 'rsync --delete --exclude ".DS_Store" -pthrvz -c ' + '-e "ssh -p {ssh_port}" ' + "{} {ssh_user}@{ssh_host}:{ssh_path}".format(CONFIG["deploy_path"].rstrip("/") + "/", **CONFIG) + ) + ) diff --git a/deadsands/site_tools/cli.py b/deadsands/site_tools/cli.py index f9ea09e..1d44ae2 100644 --- a/deadsands/site_tools/cli.py +++ b/deadsands/site_tools/cli.py @@ -1,55 +1,18 @@ import asyncio -import os -import shlex -import shutil -import subprocess import sys import termios -import webbrowser -from collections import defaultdict from enum import Enum from pathlib import Path -from time import sleep import click import typer -from livereload import Server -from livereload.watcher import INotifyWatcher -from pelican import main as pelican_main from rolltable.tables import RollTable from typing_extensions import Annotated -import site_tools as st from site_tools.content_manager import create from site_tools.shell.interactive_shell import DMShell +from site_tools import build_system -CONFIG = defaultdict(dict) -CONFIG.update( - { - "settings_base": st.DEV_SETTINGS_FILE_BASE, - "settings_publish": st.PUB_SETTINGS_FILE_BASE, - # Output path. Can be absolute or relative to tasks.py. Default: 'output' - "deploy_path": st.SETTINGS["OUTPUT_PATH"], - # Remote server configuration - "ssh_user": "greg", - "ssh_host": "froghat.club", - "ssh_port": "22", - "ssh_path": "/usr/local/deploy/deadsands/", - # Host and port for `serve` - "host": "localhost", - "port": 8000, - # content manager config - "templates_path": "markdown-templates", - # directory to watch for new assets - "import_path": "imports", - # where new asseets will be made available - "production_host": "deadsands.froghat.club", - # where to find roll table sources - "table_sources_path": "sources", - } -) - -app = typer.Typer() class ContentType(str, Enum): post = "post" @@ -59,6 +22,7 @@ class ContentType(str, Enum): location = "location" page = "page" + class Die(str, Enum): d100 = "100" d20 = "20" @@ -67,101 +31,41 @@ class Die(str, Enum): d6 = "6" d4 = "4" -def pelican_run(cmd: list = [], publish=False) -> None: - settings = CONFIG["settings_publish" if publish else "settings_base"] - pelican_main(["-s", settings] + cmd) -@app.command() +# 'site' ENTRY POINT +site_app = typer.Typer() + + +# BUILD SYSTEM SUBCOMMANDS + +@site_app.command() def clean() -> None: - if os.path.isdir(CONFIG["deploy_path"]): - shutil.rmtree(CONFIG["deploy_path"]) - os.makedirs(CONFIG["deploy_path"]) + build_system.clean() -@app.command() + +@site_app.command() def build() -> None: - subprocess.run(shlex.split("git submodule update --remote --merge")) - pelican_run() + build_system.build() -@app.command() + +@site_app.command() def watch() -> None: - import_path = Path(CONFIG["import_path"]) - content_path = Path(st.SETTINGS["PATH"]) + build_system.watch() - def do_import(): - assets = [] - for src in import_path.rglob("*"): - relpath = src.relative_to(import_path) - target = content_path / relpath - if src.is_dir(): - target.mkdir(parents=True, exist_ok=True) - continue - if target.exists(): - print(f"{target}: exists; skipping.") - continue - print(f"{target}: importing...") - src.link_to(target) - subprocess.run(shlex.split(f"git add {target}")) - uri = target.relative_to("content") - assets.append(f"https://{CONFIG['production_host']}/{uri}") - src.unlink() - if assets: - publish() - print("\n\t".join(["\nImported Asset URLS:"] + assets)) - print("\n") - watcher = INotifyWatcher() - watcher.watch(import_path, do_import) - watcher.start(do_import) - print(f"Watching {import_path}. CTRL+C to exit.") - while True: - watcher.examine() - sleep(5) -@app.command() +@site_app.command() def serve() -> None: - url = "http://{host}:{port}/".format(**CONFIG) + build_system.serve() - def cached_build(): - pelican_run(["-ve", "CACHE_CONTENT=true", "LOAD_CONTENT_CACHE=true", "SHOW_DRAFTS=true", f'SITEURL="{url}"']) - clean() - cached_build() - server = Server() - theme_path = st.SETTINGS["THEME"] - watched_globs = [ - CONFIG["settings_base"], - "{}/templates/**/*.html".format(theme_path), - ] - content_file_extensions = [".md", ".rst"] - for extension in content_file_extensions: - content_glob = "{0}/**/*{1}".format(st.SETTINGS["PATH"], extension) - watched_globs.append(content_glob) - - static_file_extensions = [".css", ".js"] - for extension in static_file_extensions: - static_file_glob = "{0}/static/**/*{1}".format(theme_path, extension) - watched_globs.append(static_file_glob) - - for glob in watched_globs: - server.watch(glob, cached_build) - - if st.OPEN_BROWSER_ON_SERVE: - webbrowser.open(url) - - server.serve(host=CONFIG["host"], port=CONFIG["port"], root=CONFIG["deploy_path"]) - -@app.command() +@site_app.command() def publish() -> None: - clean() - pelican_run(publish=True) - subprocess.run( - shlex.split( - 'rsync --delete --exclude ".DS_Store" -pthrvz -c ' - '-e "ssh -p {ssh_port}" ' - "{} {ssh_user}@{ssh_host}:{ssh_path}".format(CONFIG["deploy_path"].rstrip("/") + "/", **CONFIG) - ) - ) + build_system.publish() -@app.command() + +# CONTENT MANAGEMENT COMMANDS + +@site_app.command() def restock( source: str = typer.Argument(..., help="The source file for the store."), frequency: str = Annotated[str, typer.Option("default", help="use the specified frequency from the source file")], @@ -169,7 +73,7 @@ def restock( template_dir: str = Annotated[ str, typer.Argument( - CONFIG["templates_path"], + build_system.CONFIG["templates_path"], help="Override the default location for markdown templates.", ), ], @@ -188,7 +92,8 @@ def restock( ) ) -@app.command() + +@site_app.command() def new( content_type: ContentType = typer.Argument( ..., @@ -207,7 +112,7 @@ def new( help="Override the default template for the content_type.", ), template_dir: str = typer.Argument( - CONFIG["templates_path"], + build_system.CONFIG["templates_path"], help="Override the default location for markdown templates.", ), ) -> None: @@ -228,12 +133,16 @@ def new( category = content_type.value click.edit(filename=create(content_type.value, title, template_dir, category, template or content_type.value)) + +# STANDALONE ENTRY POINTS + def dmsh(): old_attrs = termios.tcgetattr(sys.stdin) try: - asyncio.run(DMShell(CONFIG).start()) + asyncio.run(DMShell(build_system.CONFIG).start()) finally: termios.tcsetattr(sys.stdin, termios.TCSANOW, old_attrs) + if __name__ == "__main__": - app() + site_app()